Skip to content

Commit b3eda6d

Browse files
Tommypop2claude
andcommitted
fix: cache template tarballs by resolved HEAD sha
Resolving HEAD to a commit sha lets begit key its cache on it and skip the download entirely on later scaffolds — a warm run is roughly twice as fast. The lookup goes through api.github.com (60 requests/hour unauthenticated), so it degrades instead of failing: a throw, a missing sha, or a lookup slower than 2s all fall back to fetching HEAD from the archive endpoint uncached. The live-download tests get a 60s budget. Their earlier intermittent failures were vitest's 5s default expiring on a cold-cache ~3MB tarball, not a defect in the code under test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e067e1b commit b3eda6d

5 files changed

Lines changed: 103 additions & 25 deletions

File tree

packages/create-solid/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
- Template downloads are no longer pinned to a baked-in solidjs/templates commit. Scaffolds come from live HEAD of the default branch, matching the `templates.json` manifest, so upstream template changes and dependency repins reach users without a CLI release.
1010
- Removed the `SOLID_CLI_TEMPLATES_REF` environment override that shipped in 0.9.0, along with the pin it existed to escape.
11-
- Scaffolding still avoids api.github.com, which is rate limited to 60 requests/hour for unauthenticated users: templates are fetched straight from the archive endpoint rather than resolving HEAD to a commit sha first.
11+
- HEAD is resolved to a commit sha before downloading so the tarball can be cached under it and reused by later scaffolds. That lookup goes through api.github.com, which is rate limited to 60 requests/hour for unauthenticated users, so any failure — rate limited, offline, slow — falls back to fetching HEAD from the archive endpoint directly, uncached.
1212
- Solid 2.0 is labelled "Solid 2.0 (RC)" in the project picker now that Solid 2.0 core is a release candidate. It stays listed first but not preselected.
1313

1414
## 0.9.0

packages/create/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
- Template downloads are no longer pinned to a baked-in solidjs/templates commit. Scaffolds come from live HEAD of the default branch, matching the `templates.json` manifest, so upstream template changes and dependency repins reach users without a CLI release.
1010
- Removed the `SOLID_CLI_TEMPLATES_REF` environment override that shipped in 0.9.0, along with the pin it existed to escape.
11-
- Scaffolding still avoids api.github.com, which is rate limited to 60 requests/hour for unauthenticated users: templates are fetched straight from the archive endpoint rather than resolving HEAD to a commit sha first.
11+
- HEAD is resolved to a commit sha before downloading so the tarball can be cached under it and reused by later scaffolds. That lookup goes through api.github.com, which is rate limited to 60 requests/hour for unauthenticated users, so any failure — rate limited, offline, slow — falls back to fetching HEAD from the archive endpoint directly, uncached.
1212
- Solid 2.0 is labelled "Solid 2.0 (RC)" in the project picker now that Solid 2.0 core is a release candidate. It stays listed first but not preselected.
1313

1414
## 0.9.0

packages/create/src/utils/download.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,53 @@ import { downloadRepo, GithubFetcher } from "@begit/core";
22

33
export const TEMPLATES_REPO = { owner: "solidjs", name: "templates" } as const;
44

5+
/** A slow commit lookup must not hold up a scaffold — it is only there to enable caching */
6+
const LATEST_COMMIT_TIMEOUT_MS = 2000;
7+
8+
/**
9+
* Resolves HEAD of the templates repo to a commit sha, so begit can key its
10+
* tarball cache on it and reuse the download across scaffolds.
11+
*
12+
* This goes through api.github.com, which is rate limited to 60 requests/hour
13+
* for unauthenticated users (the archive endpoint the tarball itself comes from
14+
* is not). Every failure — rate limited, offline, timeout, unexpected payload —
15+
* resolves to `undefined`, and the caller falls back to fetching HEAD directly.
16+
*/
17+
export const latestTemplatesCommit = async (): Promise<string | undefined> => {
18+
const lookup = (async () => {
19+
try {
20+
return await GithubFetcher.fetchLatestCommit(TEMPLATES_REPO);
21+
} catch {
22+
return undefined;
23+
}
24+
})();
25+
let timer: ReturnType<typeof setTimeout>;
26+
const timeout = new Promise<undefined>((resolve) => {
27+
timer = setTimeout(() => resolve(undefined), LATEST_COMMIT_TIMEOUT_MS);
28+
});
29+
return Promise.race([lookup, timeout]).finally(() => clearTimeout(timer));
30+
};
31+
532
/**
633
* Downloads `subdir` of the solidjs/templates repo into `destination`.
734
*
835
* No ref is pinned: scaffolds always come from live HEAD of the default branch,
936
* matching the `templates.json` manifest, so template updates reach users
10-
* without a CLI release.
11-
*
12-
* `fetch_latest_commit` is off because resolving HEAD to a sha goes through
13-
* api.github.com, which is rate limited to 60 requests/hour for unauthenticated
14-
* users — the archive download itself is not. Without a sha there is nothing to
15-
* key a cache on, so caching is off too and every scaffold fetches a fresh
16-
* tarball, which is what tracking HEAD means.
37+
* without a CLI release. HEAD is resolved to a sha first so begit can cache the
38+
* tarball under it and skip the download next time; when that lookup fails we
39+
* fetch HEAD directly instead, uncached — slower, but never a broken scaffold.
1740
*/
18-
export const downloadTemplate = (subdir: string, destination: string) =>
19-
downloadRepo(
41+
export const downloadTemplate = async (subdir: string, destination: string) => {
42+
const hash = await latestTemplatesCommit();
43+
return downloadRepo(
2044
{
21-
repo: { ...TEMPLATES_REPO, subdir },
45+
repo: { ...TEMPLATES_REPO, subdir, hash },
2246
dest: destination,
23-
opts: { cache: false, fetch_latest_commit: false },
47+
// HEAD is already resolved (or deliberately left unresolved) above, so begit
48+
// must not call the API itself. Without a sha there is nothing to key a cache
49+
// entry on, so the tarball is discarded rather than left behind unkeyed.
50+
opts: { cache: !!hash, fetch_latest_commit: false },
2451
},
2552
GithubFetcher,
2653
);
54+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { afterEach, expect, it, vi } from "vitest";
2+
import { GithubFetcher } from "@begit/core";
3+
import { latestTemplatesCommit } from "../src/utils/download";
4+
5+
afterEach(() => {
6+
vi.restoreAllMocks();
7+
vi.useRealTimers();
8+
});
9+
10+
it("resolves HEAD to a sha so begit can cache the tarball under it", async () => {
11+
vi.spyOn(GithubFetcher, "fetchLatestCommit").mockResolvedValue("c3032d9");
12+
13+
expect(await latestTemplatesCommit()).toBe("c3032d9");
14+
});
15+
16+
it("falls back to no sha when the commit lookup throws", async () => {
17+
// Offline, DNS failure, or a non-JSON response from the API
18+
vi.spyOn(GithubFetcher, "fetchLatestCommit").mockRejectedValue(new Error("fetch failed"));
19+
20+
expect(await latestTemplatesCommit()).toBeUndefined();
21+
});
22+
23+
it("falls back to no sha when the API answers without one", async () => {
24+
// What a rate-limited response looks like by the time begit is done with it
25+
vi.spyOn(GithubFetcher, "fetchLatestCommit").mockResolvedValue(undefined);
26+
27+
expect(await latestTemplatesCommit()).toBeUndefined();
28+
});
29+
30+
it("falls back to no sha rather than letting a hanging lookup block the scaffold", async () => {
31+
vi.useFakeTimers();
32+
vi.spyOn(GithubFetcher, "fetchLatestCommit").mockReturnValue(new Promise(() => {}));
33+
34+
const resolved = latestTemplatesCommit();
35+
await vi.advanceTimersByTimeAsync(2000);
36+
37+
expect(await resolved).toBeUndefined();
38+
});

packages/create/tests/template.test.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,32 @@ const expectScaffold = (destination: string) => {
4646
expect(readdirSync(join(destination, "src")).length).toBeGreaterThan(0);
4747
};
4848

49-
it("downloads and extracts the vanilla template", async () => {
50-
const destination = scratch("vanilla");
51-
const { path, template } = await liveTarget("vanilla");
49+
// A cold begit cache means pulling the whole templates tarball (~3MB), which does not
50+
// reliably fit in vitest's 5s default on a slow or contended connection
51+
const DOWNLOAD_TIMEOUT_MS = 60_000;
5252

53-
await createVanilla({ template, destination, path }, false);
53+
it(
54+
"downloads and extracts the vanilla template",
55+
async () => {
56+
const destination = scratch("vanilla");
57+
const { path, template } = await liveTarget("vanilla");
5458

55-
expectScaffold(destination);
56-
});
59+
await createVanilla({ template, destination, path }, false);
5760

58-
it("downloads and extracts the solid-v2 template", async () => {
59-
const destination = scratch("solid-v2");
60-
const { path, template } = await liveTarget("solid");
61+
expectScaffold(destination);
62+
},
63+
DOWNLOAD_TIMEOUT_MS,
64+
);
6165

62-
await createSolidV2({ template, destination, path }, false);
66+
it(
67+
"downloads and extracts the solid-v2 template",
68+
async () => {
69+
const destination = scratch("solid-v2");
70+
const { path, template } = await liveTarget("solid");
6371

64-
expectScaffold(destination);
65-
});
72+
await createSolidV2({ template, destination, path }, false);
73+
74+
expectScaffold(destination);
75+
},
76+
DOWNLOAD_TIMEOUT_MS,
77+
);

0 commit comments

Comments
 (0)