add test case and update usage

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
aramissennyeydd
2025-05-08 21:40:41 -04:00
parent 82a46e979d
commit 1bab3f6e6f
4 changed files with 185 additions and 22 deletions
@@ -16,22 +16,163 @@
import { Lockfile } from '@backstage/cli-node';
import { PackageDocsCache } from './Cache';
import { join as joinPath } from 'path';
import {
createMockDirectory,
MockDirectory,
} from '@backstage/backend-test-utils';
import { readFile } from 'fs/promises';
import { join as joinPath } from 'path';
jest.mock('crypto', () => {
const hash = {
update: jest.fn(),
digest: jest.fn().mockReturnValue('test'),
};
return {
createHash: jest.fn().mockReturnValue(hash),
};
});
describe('PackageDocsCache', () => {
let testDir: MockDirectory;
beforeEach(async () => {
beforeAll(async () => {
testDir = createMockDirectory();
});
afterEach(async () => {
testDir.clear();
});
it('should be able to parse cache files', async () => {
const cache = await PackageDocsCache.loadAsync(
joinPath(__dirname, 'test-cache'),
new Lockfile(),
testDir.addContent({
'.cache': {
'package-docs': {
test: {
'cache.json': JSON.stringify({
hash: 'test',
packageName: 'test',
restoreTo: 'test',
version: '1',
}),
},
},
},
test: {
'package.json': JSON.stringify({
name: '@test/test',
}),
},
});
const lockfile = {
getDependencyTreeHash: () => 'test',
} as any as Lockfile;
const cache = await PackageDocsCache.loadAsync(testDir.path, lockfile);
expect(await cache.has('test')).toBe(true);
});
it('should be able to restore cache', async () => {
testDir.addContent({
'.cache': {
'package-docs': {
test: {
'cache.json': JSON.stringify({
hash: 'test',
packageName: 'test',
restoreTo: 'test',
version: '1',
}),
contents: {
'src/index.ts': 'export const test = "test";',
},
},
},
},
test: {
'package.json': JSON.stringify({
name: '@test/test',
}),
},
});
const lockfile = {
getDependencyTreeHash: () => 'test',
} as any as Lockfile;
const cache = await PackageDocsCache.loadAsync(testDir.path, lockfile);
await cache.restore('test');
expect(
await readFile(joinPath(testDir.path, 'test', 'src/index.ts'), 'utf-8'),
).toBe('export const test = "test";');
});
it('should be able to write cache', async () => {
testDir.addContent({
'.cache': {},
test: {
'package.json': JSON.stringify({
name: '@test/test',
}),
'src/index.ts': 'export const test = "test";',
},
});
const lockfile = {
getDependencyTreeHash: () => 'test',
} as any as Lockfile;
const cache = await PackageDocsCache.loadAsync(testDir.path, lockfile);
await cache.write('test', joinPath(testDir.path, 'test'));
expect(
await readFile(
joinPath(testDir.path, '.cache', 'package-docs', 'test', 'cache.json'),
'utf-8',
),
).toBe(
JSON.stringify({
hash: 'test',
packageName: '@test/test',
restoreTo: 'test',
version: '1',
}),
);
cache.add('test', 'test');
expect(
await readFile(
joinPath(
testDir.path,
'.cache',
'package-docs',
'test',
'contents',
'src/index.ts',
),
'utf-8',
),
).toBe('export const test = "test";');
});
it.each([
{
content: JSON.stringify({
hash: 'test',
packageName: 'test',
restoreTo: 'test',
version: '2',
}),
},
{
content: JSON.stringify({
hash: 'test',
packageName: 1,
}),
},
])('should skip invalid cache files', async content => {
testDir.addContent({
'.cache': {},
test: {
'package.json': JSON.stringify({
name: '@test/test',
}),
},
'cache.json': content,
});
const lockfile = {
getDependencyTreeHash: () => 'test',
} as any as Lockfile;
const cache = await PackageDocsCache.loadAsync(testDir.path, lockfile);
expect(await cache.has('test')).toBe(false);
});
});
@@ -18,12 +18,11 @@ import globby from 'globby';
import { dirname, join as joinPath, relative } from 'path';
import crypto from 'crypto';
import { Lockfile } from '@backstage/cli-node';
import { paths as cliPaths } from '../../lib/paths';
import { mkdirp } from 'fs-extra';
import { z } from 'zod';
import { CACHE_DIR, CACHE_FILE } from './constants';
const version = '1';
const CACHE_FILE = 'cache.json';
interface CacheEntry {
hash: string;
@@ -46,11 +45,13 @@ export class PackageDocsCache {
private readonly lockfile: Lockfile,
// A map of package directory to cache entry.
private readonly cache: Map<string, CacheEntry>,
private readonly cacheDir: string,
private readonly baseDirectory: string,
) {
this.keyCache = new Map();
}
static async loadAsync(cacheDir: string, lockfile: Lockfile) {
static async loadAsync(baseDirectory: string, lockfile: Lockfile) {
const cacheDir = joinPath(baseDirectory, CACHE_DIR);
await mkdirp(cacheDir);
const cacheFiles = await globby(`**/${CACHE_FILE}`, {
cwd: cacheDir,
});
@@ -61,23 +62,29 @@ export class PackageDocsCache {
try {
const cacheJson = JSON.parse(cache);
const parsed = cacheEntrySchema.parse(cacheJson);
if (parsed.version !== version) {
console.warn(
`Skipping cache file ${file} due to version mismatch: ${parsed.version} !== ${version}`,
);
continue;
}
map.set(pkg, parsed);
} catch (e) {
console.error(`Skipping unparseable cache file ${file}: ${e}`);
}
}
return new PackageDocsCache(lockfile, map, cacheDir);
return new PackageDocsCache(lockfile, map, baseDirectory);
}
async directoryToName(directory: string) {
const packageJson = await readFile(
joinPath(directory, 'package.json'),
joinPath(this.baseDirectory, directory, 'package.json'),
'utf-8',
);
return JSON.parse(packageJson).name;
}
async toKey(pkg: string) {
private async toKey(pkg: string) {
if (this.keyCache.has(pkg)) {
return this.keyCache.get(pkg)!;
}
@@ -93,7 +100,7 @@ export class PackageDocsCache {
hash.update('\0');
for (const path of result.sort()) {
const absPath = cliPaths.resolveTargetRoot(pkg, path);
const absPath = joinPath(this.baseDirectory, pkg, path);
const pathInPackage = joinPath(absPath, path);
hash.update(pathInPackage);
hash.update('\0');
@@ -122,16 +129,16 @@ export class PackageDocsCache {
}
const cacheEntry = this.cache.get(pkg);
const restoreTo = cacheEntry!.restoreTo;
const cacheDir = joinPath(this.cacheDir, pkg);
const cacheDir = joinPath(this.baseDirectory, CACHE_DIR, pkg);
const contentsDir = joinPath(cacheDir, 'contents');
const targetDir = cliPaths.resolveTargetRoot(restoreTo);
const targetDir = joinPath(this.baseDirectory, restoreTo);
await mkdirp(targetDir);
await cp(contentsDir, targetDir, { recursive: true });
}
async write(pkg: string, contentDirectory: string) {
const cacheDir = joinPath(this.cacheDir, pkg);
const cacheDir = joinPath(this.baseDirectory, CACHE_DIR, pkg);
const contentsDir = joinPath(cacheDir, 'contents');
await mkdirp(contentsDir);
const hashString = await this.toKey(pkg);
@@ -139,7 +146,7 @@ export class PackageDocsCache {
const cacheEntry: CacheEntry = {
hash: hashString,
packageName: await this.directoryToName(pkg),
restoreTo: relative(cliPaths.resolveTargetRoot(), contentDirectory),
restoreTo: relative(this.baseDirectory, contentDirectory),
version,
};
await writeFile(joinPath(cacheDir, CACHE_FILE), JSON.stringify(cacheEntry));
@@ -58,8 +58,6 @@ const HIGHLIGHT_LANGUAGES = [
'json',
];
const CACHE_DIR = '.cache/package-docs';
function getExports(packageJson: any) {
if (packageJson.exports) {
return Object.values(packageJson.exports).filter(
@@ -120,9 +118,8 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
exclude: opts.exclude,
});
await mkdirp(CACHE_DIR);
const cache = await PackageDocsCache.loadAsync(
CACHE_DIR,
cliPaths.resolveTargetRoot(),
await Lockfile.load(cliPaths.resolveTargetRoot('yarn.lock')),
);
@@ -0,0 +1,18 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const CACHE_FILE = 'cache.json';
export const CACHE_DIR = '.cache/package-docs';