Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/snaps-cli/src/commands/build/implementation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ describe('build', () => {

await build(config);

// Manifest checksum mismatch is the warning
expect(warn).toHaveBeenCalledWith(
expect.stringMatching(/Compiled 1 file in \d+ms with 1 warning\./u),
expect.stringMatching(
/Compiled \d+ files? in \d+ms with \d+ warnings?\./u,
),
);

const output = await fs.readFile('/snap/output.js', 'utf8');
Expand Down Expand Up @@ -140,9 +141,10 @@ describe('build', () => {

await build(config);

// Manifest checksum mismatch is the warning
expect(warn).toHaveBeenCalledWith(
expect.stringMatching(/Compiled 1 file in \d+ms with 1 warning\./u),
expect.stringMatching(
/Compiled \d+ files? in \d+ms with \d+ warnings?\./u,
),
);

const output = await fs.readFile('/snap/output.js', 'utf8');
Expand Down
8 changes: 5 additions & 3 deletions packages/snaps-cli/src/commands/watch/watch.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ describe('mm-snap watch', () => {
async (command) => {
runner = getCommandRunner(command, ['--port', '0']);
await runner.waitForStderr(
/Compiled \d+ files? in \d+ms with 1 warning\./u,
/Compiled \d+ files? in \d+ms with \d+ warnings?\./u,
);

await fs.writeFile(SNAP_FILE, originalFile);
await runner.waitForStdout(/Changes detected in .+, recompiling\./u);
await runner.waitForStderr(
/Compiled \d+ files? in \d+ms with 1 warning\./u,
/Compiled \d+ files? in \d+ms with \d+ warnings?\./u,
);

expect(runner.stdout).toContainEqual(
Expand All @@ -50,7 +50,9 @@ describe('mm-snap watch', () => {
expect.stringMatching(/Building the Snap bundle\./u),
);
expect(runner.stderr).toContainEqual(
expect.stringMatching(/Compiled \d+ files? in \d+ms with 1 warning\./u),
expect.stringMatching(
/Compiled \d+ files? in \d+ms with \d+ warnings?\./u,
),
);
expect(runner.stderr).toContainEqual(
expect.stringContaining(
Expand Down
6 changes: 3 additions & 3 deletions packages/snaps-utils/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 99.76,
"functions": 99.01,
"lines": 98.62,
"statements": 97.18
"functions": 99.02,
"lines": 98.66,
"statements": 97.25
}
3 changes: 3 additions & 0 deletions packages/snaps-utils/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ module.exports = deepmerge(baseConfig, {
// https://github.com/facebook/jest/issues/5274
'./src/eval-worker.ts',
],

// This is required for `jest-fetch-mock` to work.
resetMocks: false,
});
1 change: 1 addition & 0 deletions packages/snaps-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"istanbul-lib-report": "^3.0.0",
"istanbul-reports": "^3.1.5",
"jest": "^29.0.2",
"jest-fetch-mock": "^3.0.3",
"jest-silent-reporter": "^0.6.0",
"memfs": "^3.4.13",
"prettier": "^3.3.3",
Expand Down
97 changes: 96 additions & 1 deletion packages/snaps-utils/src/fs.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import { join } from 'path';
import { join, dirname } from 'path';

import {
getOutfilePath,
isDirectory,
isFile,
readJsonFile,
useFileSystemCache,
useTemporaryFile,
validateDirPath,
validateFilePath,
Expand All @@ -20,13 +21,15 @@ jest.mock('fs');

const BASE_PATH = '/snap';
const MANIFEST_PATH = join(BASE_PATH, NpmSnapFileNames.Manifest);
const CACHE_PATH = join(process.cwd(), 'node_modules/.cache/snaps');

/**
* Clears out all the files in the in-memory file system, and writes the default
* files to the `BASE_PATH` folder, including sub-folders.
*/
async function resetFileSystem() {
await fs.rm(BASE_PATH, { recursive: true, force: true });
await fs.rm(CACHE_PATH, { recursive: true, force: true });

// Create `dist` folder.
await fs.mkdir(join(BASE_PATH, 'dist'), { recursive: true });
Expand Down Expand Up @@ -262,3 +265,95 @@ describe('useTemporaryFile', () => {
expect(await isFile(filePath)).toBe(false);
});
});

describe('useFileSystemCache', () => {
beforeEach(async () => {
await resetFileSystem();
});

const cachedFunction = useFileSystemCache('foo', 5000, async () => {
return 'foo';
});

const cachedFilePath = join(CACHE_PATH, 'foo.json');

it('writes cached value to the file system', async () => {
const spy = jest.spyOn(fs, 'writeFile');
expect(await cachedFunction()).toBe('foo');

expect(spy).toHaveBeenCalledTimes(1);

const cacheValue = await fs.readFile(cachedFilePath, 'utf8');
const cacheJson = JSON.parse(cacheValue);

expect(cacheJson).toStrictEqual({
timestamp: expect.any(Number),
value: 'foo',
});
});

it('reads cached value from the file system', async () => {
const readSpy = jest.spyOn(fs, 'readFile');
const writeSpy = jest.spyOn(fs, 'writeFile');

expect(await cachedFunction()).toBe('foo');
expect(await cachedFunction()).toBe('foo');

expect(readSpy).toHaveBeenCalledTimes(2);
expect(writeSpy).toHaveBeenCalledTimes(1);

const cacheValue = await fs.readFile(cachedFilePath, 'utf8');
const cacheJson = JSON.parse(cacheValue);

expect(cacheJson).toStrictEqual({
timestamp: expect.any(Number),
value: 'foo',
});
});

it('discards cached value if it is expired', async () => {
await fs.mkdir(dirname(cachedFilePath), { recursive: true });
await fs.writeFile(
cachedFilePath,
JSON.stringify({ timestamp: Date.now() - 6000, value: 'bar' }),
);

const readSpy = jest.spyOn(fs, 'readFile');
const writeSpy = jest.spyOn(fs, 'writeFile');

expect(await cachedFunction()).toBe('foo');

expect(readSpy).toHaveBeenCalledTimes(1);
expect(writeSpy).toHaveBeenCalledTimes(1);

const cacheValue = await fs.readFile(cachedFilePath, 'utf8');
const cacheJson = JSON.parse(cacheValue);

expect(cacheJson).toStrictEqual({
timestamp: expect.any(Number),
value: 'foo',
});
});

it('skips persisting undefined', async () => {
const fn = useFileSystemCache('foo', 5000, async () => {
return undefined;
});

const spy = jest.spyOn(fs, 'writeFile');
expect(await fn()).toBeUndefined();

expect(spy).toHaveBeenCalledTimes(0);
});

it('skips persisting null', async () => {
const fn = useFileSystemCache('foo', 5000, async () => {
return null;
});

const spy = jest.spyOn(fs, 'writeFile');
expect(await fn()).toBeNull();

expect(spy).toHaveBeenCalledTimes(0);
});
});
53 changes: 53 additions & 0 deletions packages/snaps-utils/src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,56 @@ export async function useTemporaryFile<Type = unknown>(
}
}
}

/**
* Use the file system to cache a return value with a given key and TTL.
*
* @param cacheKey - The key to use for the cache.
* @param ttl - The time-to-live in milliseconds.
* @param fn - The callback function to wrap.
* @returns The result from the callback.
*/
export function useFileSystemCache<Type = unknown>(
cacheKey: string,
ttl: number,
fn: () => Promise<Type>,
) {
return async () => {
const filePath = pathUtils.join(
process.cwd(),
'node_modules/.cache/snaps',
`${cacheKey}.json`,
);

try {
const cacheContents = await fs.readFile(filePath, 'utf8');
const json = JSON.parse(cacheContents);

if (json.timestamp + ttl > Date.now()) {
return json.value;
}
} catch {
// No-op
}

const value = await fn();

// Null or undefined is not persisted.
if (value === null || value === undefined) {
return value;
}

try {
await fs.mkdir(pathUtils.dirname(filePath), { recursive: true });

const json = { timestamp: Date.now(), value };
await fs.writeFile(filePath, JSON.stringify(json), {
encoding: 'utf8',
});
} catch {
// No-op
}

return value;
};
}
19 changes: 19 additions & 0 deletions packages/snaps-utils/src/manifest/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { promises as fs } from 'fs';
import fetchMock from 'jest-fetch-mock';
import { join } from 'path';

import {
Expand Down Expand Up @@ -34,6 +35,17 @@ const BASE_PATH = '/snap';
const MANIFEST_PATH = join(BASE_PATH, NpmSnapFileNames.Manifest);
const PACKAGE_JSON_PATH = join(BASE_PATH, NpmSnapFileNames.PackageJson);

const MOCK_GITHUB_RESPONSE = JSON.stringify({
// eslint-disable-next-line @typescript-eslint/naming-convention
target_commitish: '5fceb7ed2ef18a3984786db1161a76ca5c8e15b9',
});

const MOCK_PACKAGE_JSON = JSON.stringify({
dependencies: {
'@metamask/snaps-sdk': getPlatformVersion(),
},
});

/**
* Get the default manifest for the current platform version.
*
Expand Down Expand Up @@ -73,9 +85,16 @@ async function resetFileSystem() {

describe('checkManifest', () => {
beforeEach(async () => {
fetchMock.enableMocks();
fetchMock.mockResponses(MOCK_GITHUB_RESPONSE, MOCK_PACKAGE_JSON);

await resetFileSystem();
});

afterAll(() => {
fetchMock.disableMocks();
});

it('returns the status and warnings after processing', async () => {
const { updated, reports } = await checkManifest(BASE_PATH);
expect(reports).toHaveLength(0);
Expand Down
1 change: 1 addition & 0 deletions packages/snaps-utils/src/manifest/validators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './manifest-localization';
export * from './package-json-recommended-fields';
export * from './package-name-match';
export * from './platform-version';
export * from './production-platform-version';
export * from './repository-match';
export * from './version-match';
export * from './icon-declared';
Expand Down
Loading
Loading