Skip to content

Commit 9819e3a

Browse files
committed
expires and refactor
1 parent 12fc02c commit 9819e3a

File tree

19 files changed

+1029
-169
lines changed

19 files changed

+1029
-169
lines changed

packages/create-gen-app-test/src/__tests__/cached-template.test.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as path from 'path';
44

55
import { appstash, resolve } from 'appstash';
66

7-
import { createFromCachedTemplate, getCachedRepo, cloneToCache } from '../index';
7+
import { createFromCachedTemplate, TemplateCache } from '../index';
88

99
const DEFAULT_TEMPLATE_URL = 'https://github.com/launchql/pgpm-boilerplates';
1010

@@ -35,15 +35,23 @@ describe('cached template integration tests', () => {
3535

3636
describe('cache functionality', () => {
3737
let sharedCachePath: string;
38+
let templateCache: TemplateCache;
39+
40+
beforeAll(() => {
41+
templateCache = new TemplateCache({
42+
enabled: true,
43+
toolName: testCacheTool,
44+
});
45+
});
3846

3947
it('should return null when cache does not exist for new URL', () => {
4048
const nonExistentUrl = 'https://github.com/nonexistent/repo-test-123456';
41-
const cachedRepo = getCachedRepo(nonExistentUrl, testCacheTool);
49+
const cachedRepo = templateCache.get(nonExistentUrl);
4250
expect(cachedRepo).toBeNull();
4351
});
4452

4553
it('should clone repository to cache', () => {
46-
const cachePath = cloneToCache(DEFAULT_TEMPLATE_URL, testCacheTool);
54+
const cachePath = templateCache.set(DEFAULT_TEMPLATE_URL);
4755
sharedCachePath = cachePath;
4856

4957
expect(fs.existsSync(cachePath)).toBe(true);
@@ -54,7 +62,7 @@ describe('cached template integration tests', () => {
5462
}, 60000);
5563

5664
it('should retrieve cached repository', () => {
57-
const cachedRepo = getCachedRepo(DEFAULT_TEMPLATE_URL, testCacheTool);
65+
const cachedRepo = templateCache.get(DEFAULT_TEMPLATE_URL);
5866
expect(cachedRepo).not.toBeNull();
5967
expect(cachedRepo).toBe(sharedCachePath);
6068
expect(fs.existsSync(cachedRepo!)).toBe(true);
@@ -120,7 +128,11 @@ describe('cached template integration tests', () => {
120128
});
121129

122130
it('should verify template cache was created', () => {
123-
const cachedRepo = getCachedRepo(DEFAULT_TEMPLATE_URL, testCacheTool);
131+
const templateCache = new TemplateCache({
132+
enabled: true,
133+
toolName: testCacheTool,
134+
});
135+
const cachedRepo = templateCache.get(DEFAULT_TEMPLATE_URL);
124136
expect(cachedRepo).not.toBeNull();
125137
expect(fs.existsSync(cachedRepo!)).toBe(true);
126138
});

packages/create-gen-app-test/src/cli.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import { Inquirerer, ListQuestion } from "inquirerer";
77
import minimist, { ParsedArgs } from "minimist";
88

99
import { cloneRepo, createGen } from "create-gen-app";
10-
import createGenPackageJson from "create-gen-app/package.json";
1110

1211
const DEFAULT_REPO = "https://github.com/launchql/pgpm-boilerplates.git";
1312
const DEFAULT_PATH = ".";
1413
const DEFAULT_OUTPUT_FALLBACK = "create-gen-app-output";
1514

15+
// Use require for package.json to avoid module resolution issues
16+
const createGenPackageJson = require("create-gen-app/package.json");
1617
const PACKAGE_VERSION =
1718
(createGenPackageJson as { version?: string }).version ?? "0.0.0";
1819

Lines changed: 18 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import { execSync } from 'child_process';
2-
import * as crypto from 'crypto';
3-
import * as fs from 'fs';
4-
import * as path from 'path';
5-
6-
import { appstash, resolve } from 'appstash';
7-
import { replaceVariables, extractVariables } from 'create-gen-app';
1+
import { replaceVariables, extractVariables, TemplateCache } from 'create-gen-app';
82

93
export interface CachedTemplateOptions {
104
templateUrl: string;
115
outputDir: string;
126
answers: Record<string, any>;
137
cacheTool?: string;
8+
branch?: string;
9+
ttl?: number;
10+
maxAge?: number;
1411
}
1512

1613
export interface CachedTemplateResult {
@@ -20,82 +17,25 @@ export interface CachedTemplateResult {
2017
}
2118

2219
/**
23-
* Get cached repository from appstash cache directory
24-
* @param templateUrl - Repository URL
25-
* @param cacheTool - Tool name for appstash (default: 'mymodule')
26-
* @returns Cached repository path or null if not found
27-
*/
28-
export function getCachedRepo(templateUrl: string, cacheTool: string = 'mymodule'): string | null {
29-
const dirs = appstash(cacheTool, { ensure: true });
30-
const repoHash = crypto.createHash('md5').update(templateUrl).digest('hex');
31-
const cachePath = resolve(dirs, 'cache', 'repos', repoHash);
32-
33-
if (fs.existsSync(cachePath)) {
34-
return cachePath;
35-
}
36-
37-
return null;
38-
}
39-
40-
/**
41-
* Clone repository to cache
42-
* @param templateUrl - Repository URL
43-
* @param cacheTool - Tool name for appstash (default: 'mymodule')
44-
* @returns Path to cached repository
45-
*/
46-
export function cloneToCache(templateUrl: string, cacheTool: string = 'mymodule'): string {
47-
const dirs = appstash(cacheTool, { ensure: true });
48-
const repoHash = crypto.createHash('md5').update(templateUrl).digest('hex');
49-
const cachePath = resolve(dirs, 'cache', 'repos', repoHash);
50-
51-
if (!fs.existsSync(path.dirname(cachePath))) {
52-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
53-
}
54-
55-
const gitUrl = normalizeGitUrl(templateUrl);
56-
57-
execSync(`git clone ${gitUrl} ${cachePath}`, {
58-
stdio: 'inherit'
59-
});
60-
61-
const gitDir = path.join(cachePath, '.git');
62-
if (fs.existsSync(gitDir)) {
63-
fs.rmSync(gitDir, { recursive: true, force: true });
64-
}
65-
66-
return cachePath;
67-
}
68-
69-
/**
70-
* Normalize a URL to a git-cloneable format
71-
* @param url - Input URL
72-
* @returns Normalized git URL
73-
*/
74-
function normalizeGitUrl(url: string): string {
75-
if (url.startsWith('git@') || url.startsWith('https://') || url.startsWith('http://')) {
76-
return url;
77-
}
78-
79-
if (/^[\w-]+\/[\w-]+$/.test(url)) {
80-
return `https://github.com/${url}.git`;
81-
}
82-
83-
return url;
84-
}
85-
86-
/**
87-
* Create project from cached template
20+
* Create project from cached template using the shared TemplateCache
8821
* @param options - Options for creating from cached template
8922
* @returns Result with output directory and cache information
9023
*/
9124
export async function createFromCachedTemplate(options: CachedTemplateOptions): Promise<CachedTemplateResult> {
92-
const { templateUrl, outputDir, answers, cacheTool = 'mymodule' } = options;
25+
const { templateUrl, outputDir, answers, cacheTool = 'mymodule', branch, ttl, maxAge } = options;
26+
27+
const templateCache = new TemplateCache({
28+
enabled: true,
29+
toolName: cacheTool,
30+
ttl,
31+
maxAge,
32+
});
9333

9434
let templateDir: string;
9535
let cacheUsed = false;
9636
let cachePath: string | undefined;
9737

98-
const cachedRepo = getCachedRepo(templateUrl, cacheTool);
38+
const cachedRepo = templateCache.get(templateUrl, branch);
9939

10040
if (cachedRepo) {
10141
console.log(`Using cached template from ${cachedRepo}`);
@@ -104,7 +44,7 @@ export async function createFromCachedTemplate(options: CachedTemplateOptions):
10444
cachePath = cachedRepo;
10545
} else {
10646
console.log(`Cloning template to cache from ${templateUrl}`);
107-
templateDir = cloneToCache(templateUrl, cacheTool);
47+
templateDir = templateCache.set(templateUrl, branch);
10848
cachePath = templateDir;
10949
}
11050

@@ -118,3 +58,6 @@ export async function createFromCachedTemplate(options: CachedTemplateOptions):
11858
cachePath
11959
};
12060
}
61+
62+
// Re-export TemplateCache for convenience
63+
export { TemplateCache } from 'create-gen-app';

packages/create-gen-app/__tests__/cache.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,69 @@ describe("template caching (appstash)", () => {
8282
cleanupWorkspace(secondWorkspace);
8383
}
8484
});
85+
86+
it("invalidates cache when TTL expires", async () => {
87+
const shortTtl = 1000; // 1 second
88+
const cacheOptions = {
89+
toolName: `${cacheTool}-ttl`,
90+
baseDir: tempBaseDir,
91+
enabled: true,
92+
ttl: shortTtl,
93+
};
94+
95+
const firstWorkspace = createTempWorkspace("ttl-first");
96+
const firstAnswers = buildAnswers("ttl-first");
97+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
98+
99+
try {
100+
await createGen({
101+
templateUrl: TEST_REPO,
102+
fromBranch: TEST_BRANCH,
103+
fromPath: TEST_TEMPLATE,
104+
outputDir: firstWorkspace.outputDir,
105+
argv: firstAnswers,
106+
noTty: true,
107+
cache: cacheOptions,
108+
});
109+
110+
expect(
111+
logSpy.mock.calls.some(([message]) =>
112+
typeof message === "string" && message.includes("Caching repository")
113+
)
114+
).toBe(true);
115+
} finally {
116+
cleanupWorkspace(firstWorkspace);
117+
}
118+
119+
// Wait for TTL to expire
120+
await new Promise((resolve) => setTimeout(resolve, shortTtl + 200));
121+
122+
logSpy.mockClear();
123+
const secondWorkspace = createTempWorkspace("ttl-second");
124+
const secondAnswers = buildAnswers("ttl-second");
125+
126+
try {
127+
await createGen({
128+
templateUrl: TEST_REPO,
129+
fromBranch: TEST_BRANCH,
130+
fromPath: TEST_TEMPLATE,
131+
outputDir: secondWorkspace.outputDir,
132+
argv: secondAnswers,
133+
noTty: true,
134+
cache: cacheOptions,
135+
});
136+
137+
// Should re-cache since TTL expired
138+
expect(
139+
logSpy.mock.calls.some(([message]) =>
140+
typeof message === "string" && message.includes("Caching repository")
141+
)
142+
).toBe(true);
143+
} finally {
144+
logSpy.mockRestore();
145+
cleanupWorkspace(secondWorkspace);
146+
}
147+
});
85148
});
86149

87150

0 commit comments

Comments
 (0)