From e6099804e99077fce2f977f87006775bef578a50 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 1 Oct 2024 16:33:12 +0200 Subject: [PATCH 01/10] remove `file-system-cache` by embedding it into our source --- code/core/package.json | 2 +- code/core/src/common/utils/file-cache.ts | 472 ++++++++++++++++++++- code/core/src/types/modules/core-common.ts | 2 +- code/yarn.lock | 77 +--- 4 files changed, 472 insertions(+), 81 deletions(-) diff --git a/code/core/package.json b/code/core/package.json index 17ec07a2124..c502e9ac845 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -365,7 +365,6 @@ "express": "^4.19.2", "fd-package-json": "^1.2.0", "fetch-retry": "^6.0.0", - "file-system-cache": "^2.4.4", "find-cache-dir": "^5.0.0", "find-up": "^7.0.0", "flush-promises": "^1.0.2", @@ -390,6 +389,7 @@ "prettier": "^3.2.5", "pretty-hrtime": "^1.0.3", "prompts": "^2.4.0", + "ramda": "^0.30.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-draggable": "^4.4.5", diff --git a/code/core/src/common/utils/file-cache.ts b/code/core/src/common/utils/file-cache.ts index de65cc98838..a788150c4a6 100644 --- a/code/core/src/common/utils/file-cache.ts +++ b/code/core/src/common/utils/file-cache.ts @@ -1,11 +1,469 @@ -import * as fsc from 'file-system-cache'; +import * as crypto from 'node:crypto'; +import * as fs from 'node:fs'; +import { existsSync, lstatSync } from 'node:fs'; +import * as fsp from 'node:fs/promises'; +import { mkdir, rm, stat } from 'node:fs/promises'; +import * as fsPath from 'node:path'; -// @ts-expect-error (needed due to it's use of `exports.default`) -const Cache = (fsc.default.default || fsc.default) as typeof fsc.default; +import * as R from 'ramda'; -export type Options = Parameters['0']; -export type FileSystemCache = ReturnType; +const pathExists = async (path: string) => { + return stat(path) + .then(() => true) + .catch(() => false); +}; -export function createFileSystemCache(options: Options): FileSystemCache { - return Cache(options); +async function ensureDir(dirPath: string): Promise { + try { + // Attempt to create the directory recursively + await mkdir(dirPath, { recursive: true }); + } catch (err: any) { + // If the error is something other than the directory already existing, throw the error + if (err.code !== 'EEXIST') { + throw err; + } + } +} + +async function remove(path: string): Promise { + try { + // Attempt to remove the file or directory recursively + await rm(path, { recursive: true, force: true }); + } catch (err: any) { + // If the error code is anything other than the path not existing, rethrow the error + if (err.code !== 'ENOENT') { + throw err; + } + } +} + +export type FileSystemCacheOptions = { + basePath?: string; + ns?: any; + ttl?: number; + hash?: HashAlgorithm; + extension?: string; +}; + +export const isNothing = (value: any) => R.isNil(value) || R.isEmpty(value); +export const isString = R.is(String); + +export const toAbsolutePath = (path: string) => { + return path.startsWith('.') ? fsPath.resolve(path) : path; +}; + +export const ensureString = (defaultValue: string, text?: string): string => { + return typeof text === 'string' ? text : defaultValue; +}; + +export const compact = (input: any[]): string[] => { + const flat = [].concat(...input); + return flat.filter((value) => !R.isNil(value)); +}; + +export const toStringArray = R.pipe(compact, R.map(R.toString)); + +export const isFileSync = (path: string) => { + return existsSync(path) ? lstatSync(path).isFile() : false; +}; + +export const readFileSync = (path: string) => { + return existsSync(path) ? fs.readFileSync(path).toString() : undefined; +}; + +export const filePathsP = async (basePath: string, ns: string): Promise => { + if (!(await pathExists(basePath))) { + return []; + } + return (await fsp.readdir(basePath)) + .filter(Boolean) + .filter((name) => (ns ? name.startsWith(ns) : true)) + .filter((name) => (!ns ? !name.includes('-') : true)) + .map((name) => `${basePath}/${name}`); +}; + +/** + * Turns a set of values into a HEX hash code. + * + * @param values: The set of values to hash. + */ +export const hash = (algorithm: HashAlgorithm, ...values: any[]) => { + if (R.pipe(compact, R.isEmpty)(values)) { + return undefined; + } + const resultHash = crypto.createHash(algorithm); + const addValue = (value: any) => resultHash.update(value); + const addValues = R.forEach(addValue); + R.pipe(toStringArray, addValues)(values); + return resultHash.digest('hex'); +}; + +export const hashExists = (algorithm: HashAlgorithm) => { + return crypto.getHashes().includes(algorithm); +}; + +/** Retrieve a value from the given path. */ +export async function getValueP(path: string, defaultValue?: any) { + const exists = await pathExists(path); + + if (!exists) { + return defaultValue; + } + try { + const content = await fsp.readFile(path, 'utf8'); + return toGetValue(JSON.parse(content)); + } catch (error: any) { + if (error.code === 'ENOENT') { + return defaultValue; + } + if (error.message === 'Cache item has expired.') { + fs.rmSync(path); + return defaultValue; + } + throw new Error(`Failed to read cache value at: ${path}. ${error.message}`); + } +} + +/** Format value structure. */ +export const toGetValue = (data: any) => { + if (isExpired(data)) { + return undefined; + } + + if (data.type === 'Date') { + return new Date(data.value); + } + return data.value; +}; + +/** Stringify a value into JSON. */ +export const toJson = (value: any, ttl: number) => + JSON.stringify({ value, type: R.type(value), created: new Date(), ttl }); + +/** Check's a cache item to see if it has expired. */ +export const isExpired = (data: any) => { + const timeElapsed = (new Date().getTime() - new Date(data.created).getTime()) / 1000; + return timeElapsed > data.ttl && data.ttl > 0; +}; + +export type HashAlgorithm = + | 'RSA-MD5' + | 'RSA-RIPEMD160' + | 'RSA-SHA1' + | 'RSA-SHA1-2' + | 'RSA-SHA224' + | 'RSA-SHA256' + | 'RSA-SHA3-224' + | 'RSA-SHA3-256' + | 'RSA-SHA3-384' + | 'RSA-SHA3-512' + | 'RSA-SHA384' + | 'RSA-SHA512' + | 'RSA-SHA512/224' + | 'RSA-SHA512/256' + | 'RSA-SM3' + | 'blake2b512' + | 'blake2s256' + | 'id-rsassa-pkcs1-v1_5-with-sha3-224' + | 'id-rsassa-pkcs1-v1_5-with-sha3-256' + | 'id-rsassa-pkcs1-v1_5-with-sha3-384' + | 'id-rsassa-pkcs1-v1_5-with-sha3-512' + | 'md5' + | 'md5-sha1' + | 'md5WithRSAEncryption' + | 'ripemd' + | 'ripemd160' + | 'ripemd160WithRSA' + | 'rmd160' + | 'sha1' + | 'sha1WithRSAEncryption' + | 'sha224' + | 'sha224WithRSAEncryption' + | 'sha256' + | 'sha256WithRSAEncryption' + | 'sha3-224' + | 'sha3-256' + | 'sha3-384' + | 'sha3-512' + | 'sha384' + | 'sha384WithRSAEncryption' + | 'sha512' + | 'sha512-224' + | 'sha512-224WithRSAEncryption' + | 'sha512-256' + | 'sha512-256WithRSAEncryption' + | 'sha512WithRSAEncryption' + | 'shake128' + | 'shake256' + | 'sm3' + | 'sm3WithRSAEncryption' + | 'ssl3-md5' + | 'ssl3-sha1'; + +export const hashAlgorithms: HashAlgorithm[] = [ + 'RSA-MD5', + 'RSA-RIPEMD160', + 'RSA-SHA1', + 'RSA-SHA1-2', + 'RSA-SHA224', + 'RSA-SHA256', + 'RSA-SHA3-224', + 'RSA-SHA3-256', + 'RSA-SHA3-384', + 'RSA-SHA3-512', + 'RSA-SHA384', + 'RSA-SHA512', + 'RSA-SHA512/224', + 'RSA-SHA512/256', + 'RSA-SM3', + 'blake2b512', + 'blake2s256', + 'id-rsassa-pkcs1-v1_5-with-sha3-224', + 'id-rsassa-pkcs1-v1_5-with-sha3-256', + 'id-rsassa-pkcs1-v1_5-with-sha3-384', + 'id-rsassa-pkcs1-v1_5-with-sha3-512', + 'md5', + 'md5-sha1', + 'md5WithRSAEncryption', + 'ripemd', + 'ripemd160', + 'ripemd160WithRSA', + 'rmd160', + 'sha1', + 'sha1WithRSAEncryption', + 'sha224', + 'sha224WithRSAEncryption', + 'sha256', + 'sha256WithRSAEncryption', + 'sha3-224', + 'sha3-256', + 'sha3-384', + 'sha3-512', + 'sha384', + 'sha384WithRSAEncryption', + 'sha512', + 'sha512-224', + 'sha512-224WithRSAEncryption', + 'sha512-256', + 'sha512-256WithRSAEncryption', + 'sha512WithRSAEncryption', + 'shake128', + 'shake256', + 'sm3', + 'sm3WithRSAEncryption', + 'ssl3-md5', + 'ssl3-sha1', +]; +/** A cache that read/writes to a specific part of the file-system. */ +export class FileSystemCache { + /** The list of all available hash algorithms. */ + static hashAlgorithms: HashAlgorithm[] = hashAlgorithms; + + /** Instance. */ + readonly basePath: string; + + readonly ns?: any; + + readonly extension?: string; + + readonly hash: HashAlgorithm; + + readonly ttl: number; + + basePathExists?: boolean; + + /** + * Constructor. + * + * @param options - BasePath: The folder path to read/write to. Default: './build' - ns: A single + * value, or array, that represents a a unique namespace within which values for this store are + * cached. - extension: An optional file-extension for paths. - ttl: The default time-to-live + * for cached values in seconds. Default: 0 (never expires) - hash: The hashing algorithm to use + * when generating cache keys. Default: "sha1" + */ + constructor(options: FileSystemCacheOptions = {}) { + this.basePath = formatPath(options.basePath); + this.hash = options.hash ?? 'sha1'; + this.ns = hash(this.hash, options.ns); + this.ttl = options.ttl ?? 0; + + if (isString(options.extension)) { + this.extension = options.extension; + } + + if (isFileSync(this.basePath)) { + throw new Error(`The basePath '${this.basePath}' is a file. It should be a folder.`); + } + + if (!hashExists(this.hash)) { + throw new Error(`Hash does not exist: ${this.hash}`); + } + } + + /** + * Generates the path to the cached files. + * + * @param {string} key: The key of the cache item. + */ + public path(key: string): string { + if (isNothing(key)) { + throw new Error(`Path requires a cache key.`); + } + let name = hash(this.hash, key); + + if (this.ns) { + name = `${this.ns}-${name}`; + } + + if (this.extension) { + name = `${name}.${this.extension.replace(/^\./, '')}`; + } + return `${this.basePath}/${name}`; + } + + /** + * Determines whether the file exists. + * + * @param {string} key: The key of the cache item. + */ + public fileExists(key: string) { + return pathExists(this.path(key)); + } + + /** Ensure that the base path exists. */ + public async ensureBasePath() { + if (!this.basePathExists) { + await ensureDir(this.basePath); + } + this.basePathExists = true; + } + + /** + * Gets the contents of the file with the given key. + * + * @param {string} key: The key of the cache item. + * @param defaultValue: Optional. A default value to return if the value does not exist in cache. + * @returns File contents, or undefined if the file does not exis + */ + public get(key: string, defaultValue?: any) { + return getValueP(this.path(key), defaultValue); + } + + /** + * Gets the contents of the file with the given key. + * + * @param {string} key: The key of the cache item. + * @param defaultValue: Optional. A default value to return if the value does not exist in cache. + * @returns The cached value, or undefined. + */ + public getSync(key: string, defaultValue?: any) { + const path = this.path(key); + const content = readFileSync(path) || ''; + return fs.existsSync(path) ? toGetValue(JSON.parse(content)) : defaultValue; + } + + /** + * Writes the given value to the file-system. + * + * @param {string} key: The key of the cache item. + * @param value: The value to write (Primitive or Object). + */ + public async set(key: string, value: any, ttl?: number) { + const path = this.path(key); + ttl = typeof ttl === 'number' ? ttl : this.ttl; + await this.ensureBasePath(); + await fsp.writeFile(path, toJson(value, ttl)); + return { path }; + } + + /** + * Writes the given value to the file-system and memory cache. + * + * @param {string} key: The key of the cache item. + * @param value: The value to write (Primitive or Object). + * @returns The cache. + */ + public setSync(key: string, value: any, ttl?: number) { + ttl = typeof ttl === 'number' ? ttl : this.ttl; + fs.writeFileSync(this.path(key), toJson(value, ttl)); + return this; + } + + /** + * Removes the item from the file-system. + * + * @param {string} key: The key of the cache item. + */ + public remove(key: string) { + return remove(this.path(key)); + } + + /** Removes all items from the cache. */ + public async clear() { + const paths = await filePathsP(this.basePath, this.ns); + await Promise.all(paths.map((path) => remove(path))); + console.groupEnd(); + } + + /** + * Saves several items to the cache in one operation. + * + * @param {array} items: An array of objects of the form { key, value }. + */ + public async save( + input: ({ key: string; value: any } | null | undefined)[] + ): Promise<{ paths: string[] }> { + type Item = { key: string; value: any }; + let items = (Array.isArray(input) ? input : [input]) as Item[]; + + const isValid = (item: any) => { + if (!R.is(Object, item)) { + return false; + } + return item.key && item.value; + }; + + items = items.filter((item) => Boolean(item)); + items + .filter((item) => !isValid(item)) + .forEach(() => { + const err = `Save items not valid, must be an array of {key, value} objects.`; + throw new Error(err); + }); + + if (items.length === 0) { + return { paths: [] }; + } + + const paths = await Promise.all( + items.map(async (item) => (await this.set(item.key, item.value)).path) + ); + + return { paths }; + } + + /** Loads all files within the cache's namespace. */ + public async load(): Promise<{ files: { path: string; value: any }[] }> { + const paths = await filePathsP(this.basePath, this.ns); + + if (paths.length === 0) { + return { files: [] }; + } + const files = await Promise.all( + paths.map(async (path) => ({ path, value: await getValueP(path) })) + ); + return { files }; + } +} + +/** Helpers */ + +function formatPath(path?: string) { + path = ensureString('./.cache', path); + path = toAbsolutePath(path); + return path; +} + +export function createFileSystemCache(options: FileSystemCacheOptions): FileSystemCache { + return new FileSystemCache(options); } diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 8e71a4cadb2..82fbfb8c766 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { Router } from 'express'; -import type { FileSystemCache } from 'file-system-cache'; // should be node:http, but that caused the ui/manager to fail to build, might be able to switch this back once ui/manager is in the core import type { Server } from 'http'; import type * as telejson from 'telejson'; import type { PackageJson as PackageJsonFromTypeFest } from 'type-fest'; +import type { FileSystemCache } from '../../common/utils/file-cache'; import type { Indexer, StoriesEntry } from './indexer'; /** ⚠️ This file contains internal WIP types they MUST NOT be exported outside this package for now! */ diff --git a/code/yarn.lock b/code/yarn.lock index a3ab66666dd..6fdf68f6070 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6078,7 +6078,6 @@ __metadata: express: "npm:^4.19.2" fd-package-json: "npm:^1.2.0" fetch-retry: "npm:^6.0.0" - file-system-cache: "npm:^2.4.4" find-cache-dir: "npm:^5.0.0" find-up: "npm:^7.0.0" flush-promises: "npm:^1.0.2" @@ -6105,6 +6104,7 @@ __metadata: pretty-hrtime: "npm:^1.0.3" process: "npm:^0.11.10" prompts: "npm:^2.4.0" + ramda: "npm:^0.30.1" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-draggable: "npm:^4.4.5" @@ -7844,16 +7844,6 @@ __metadata: languageName: node linkType: hard -"@types/fs-extra@npm:11.0.1": - version: 11.0.1 - resolution: "@types/fs-extra@npm:11.0.1" - dependencies: - "@types/jsonfile": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/a65f1fae47849fe1a17441dcabc9400390303405972ff3cbb3578746cea8916b23d5e7652bf57a87767f75a9b2f37caac499b78b5230ae08fef0ba58b34c3a85 - languageName: node - linkType: hard - "@types/fs-extra@npm:^5.0.5": version: 5.1.0 resolution: "@types/fs-extra@npm:5.1.0" @@ -7988,15 +7978,6 @@ __metadata: languageName: node linkType: hard -"@types/jsonfile@npm:*": - version: 6.1.2 - resolution: "@types/jsonfile@npm:6.1.2" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/c2943f9bfa7867b33fb362b88a932efdc00e9e5f2762b6ef912617cb0a3e3221a98920f8976a4cf817aa576e03d28a25391236e9644e2ebe648081b08df62ef5 - languageName: node - linkType: hard - "@types/loader-utils@npm:^2.0.5": version: 2.0.6 resolution: "@types/loader-utils@npm:2.0.6" @@ -8164,15 +8145,6 @@ __metadata: languageName: node linkType: hard -"@types/ramda@npm:0.29.3": - version: 0.29.3 - resolution: "@types/ramda@npm:0.29.3" - dependencies: - types-ramda: "npm:^0.29.4" - checksum: 10c0/9c62a4600f5df5e65a01ffe4a470500c98f7c0d093fde47e0d4257675f1ec50effe4696cb004a6b53227948db67ea26a2345dbc91819ecc868105c0f64cecd1e - languageName: node - linkType: hard - "@types/range-parser@npm:*": version: 1.2.5 resolution: "@types/range-parser@npm:1.2.5" @@ -15268,18 +15240,6 @@ __metadata: languageName: node linkType: hard -"file-system-cache@npm:^2.4.4": - version: 2.4.4 - resolution: "file-system-cache@npm:2.4.4" - dependencies: - "@types/fs-extra": "npm:11.0.1" - "@types/ramda": "npm:0.29.3" - fs-extra: "npm:11.1.1" - ramda: "npm:0.29.0" - checksum: 10c0/274bd9c2f8f81d0c3b2cc0d077807c969b48cac4857ae77f87b4b480548252aa42d3a43b3e9d4bb54df567eb70f0c384782514fcea74b78765543e9496e27e2d - languageName: node - linkType: hard - "filelist@npm:^1.0.4": version: 1.0.4 resolution: "filelist@npm:1.0.4" @@ -15723,17 +15683,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:11.1.1": - version: 11.1.1 - resolution: "fs-extra@npm:11.1.1" - dependencies: - graceful-fs: "npm:^4.2.0" - jsonfile: "npm:^6.0.1" - universalify: "npm:^2.0.0" - checksum: 10c0/a2480243d7dcfa7d723c5f5b24cf4eba02a6ccece208f1524a2fbde1c629492cfb9a59e4b6d04faff6fbdf71db9fdc8ef7f396417a02884195a625f5d8dc9427 - languageName: node - linkType: hard - "fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -23614,10 +23563,10 @@ __metadata: languageName: node linkType: hard -"ramda@npm:0.29.0": - version: 0.29.0 - resolution: "ramda@npm:0.29.0" - checksum: 10c0/b00eaaf1c62b06a99affa1d583e256bd65ad27ab9d0ef512f55d7d93b842e7cd244a4a09179f61fdd8548362e409323867a2b0477cbd0626b5644eb6ac7c53da +"ramda@npm:^0.30.1": + version: 0.30.1 + resolution: "ramda@npm:0.30.1" + checksum: 10c0/3ea3e35c80e1a1b78c23de0c72d3382c3446f42052b113b851f1b7fc421e33a45ce92e7aef3c705cc6de3812a209d03417af5c264f67126cda539fd66c8bea71 languageName: node linkType: hard @@ -27377,13 +27326,6 @@ __metadata: languageName: node linkType: hard -"ts-toolbelt@npm:^9.6.0": - version: 9.6.0 - resolution: "ts-toolbelt@npm:9.6.0" - checksum: 10c0/838f9a2f0fe881d5065257a23b402c41315b33ff987b73db3e2b39fcb70640c4c7220e1ef118ed5676763543724fdbf4eda7b0e2c17acb667ed1401336af9f8c - languageName: node - linkType: hard - "tsconfig-paths-webpack-plugin@npm:^4.0.1": version: 4.1.0 resolution: "tsconfig-paths-webpack-plugin@npm:4.1.0" @@ -27567,15 +27509,6 @@ __metadata: languageName: node linkType: hard -"types-ramda@npm:^0.29.4": - version: 0.29.10 - resolution: "types-ramda@npm:0.29.10" - dependencies: - ts-toolbelt: "npm:^9.6.0" - checksum: 10c0/cc6439341a60a4f2b49e1ac447c8a0279f161464fd0a204abaa57e90e101772c0b1adc185a7c0715c3836c19594a9ec268c1e5c4394d0e409cb71d141def3963 - languageName: node - linkType: hard - "typescript@npm:^3.8.3": version: 3.9.10 resolution: "typescript@npm:3.9.10" From 94a0bf62437249af606e2923c21ef4fbd6a87210 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 1 Oct 2024 17:07:22 +0200 Subject: [PATCH 02/10] fix dep missing --- code/core/package.json | 1 + code/yarn.lock | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/code/core/package.json b/code/core/package.json index c502e9ac845..fc1fce7cc28 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -329,6 +329,7 @@ "@types/prettier": "^3.0.0", "@types/pretty-hrtime": "^1.0.0", "@types/prompts": "^2.0.9", + "@types/ramda": "^0.30.2", "@types/react-syntax-highlighter": "11.0.5", "@types/react-transition-group": "^4", "@types/semver": "^7.5.8", diff --git a/code/yarn.lock b/code/yarn.lock index 6fdf68f6070..495364337ab 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6039,6 +6039,7 @@ __metadata: "@types/prettier": "npm:^3.0.0" "@types/pretty-hrtime": "npm:^1.0.0" "@types/prompts": "npm:^2.0.9" + "@types/ramda": "npm:^0.30.2" "@types/react-syntax-highlighter": "npm:11.0.5" "@types/react-transition-group": "npm:^4" "@types/semver": "npm:^7.5.8" @@ -8145,6 +8146,15 @@ __metadata: languageName: node linkType: hard +"@types/ramda@npm:^0.30.2": + version: 0.30.2 + resolution: "@types/ramda@npm:0.30.2" + dependencies: + types-ramda: "npm:^0.30.1" + checksum: 10c0/dda95008860f594eb7b4fd9819adeb2dcd4d4e2baca6cb33f692f6f8ea76d04a7fd81f9a057c5c67555612769e5592cb15f91de6c9f8b619b8b1d806d19dc9ea + languageName: node + linkType: hard + "@types/range-parser@npm:*": version: 1.2.5 resolution: "@types/range-parser@npm:1.2.5" @@ -27326,6 +27336,13 @@ __metadata: languageName: node linkType: hard +"ts-toolbelt@npm:^9.6.0": + version: 9.6.0 + resolution: "ts-toolbelt@npm:9.6.0" + checksum: 10c0/838f9a2f0fe881d5065257a23b402c41315b33ff987b73db3e2b39fcb70640c4c7220e1ef118ed5676763543724fdbf4eda7b0e2c17acb667ed1401336af9f8c + languageName: node + linkType: hard + "tsconfig-paths-webpack-plugin@npm:^4.0.1": version: 4.1.0 resolution: "tsconfig-paths-webpack-plugin@npm:4.1.0" @@ -27509,6 +27526,15 @@ __metadata: languageName: node linkType: hard +"types-ramda@npm:^0.30.1": + version: 0.30.1 + resolution: "types-ramda@npm:0.30.1" + dependencies: + ts-toolbelt: "npm:^9.6.0" + checksum: 10c0/4a8b230ae9772e6534f65b1a154dd5604bcd1d74e27b49686337a215e83aa8fc93e49f8c49af395418d2950cb9fb9b900662077c1d4b73ff6fe4f4bcb83ab2d6 + languageName: node + linkType: hard + "typescript@npm:^3.8.3": version: 3.9.10 resolution: "typescript@npm:3.9.10" From eb504091eb80b2cca7b52ebea4cd41cda318fe31 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 2 Oct 2024 13:15:50 +0200 Subject: [PATCH 03/10] replace for https://git.nfp.is/TheThing/fs-cache-fast/src/branch/master/index.mjs --- code/core/package.json | 2 - code/core/src/common/utils/file-cache.ts | 535 +++++------------------ code/yarn.lock | 34 -- 3 files changed, 116 insertions(+), 455 deletions(-) diff --git a/code/core/package.json b/code/core/package.json index fc1fce7cc28..3f404f11fb2 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -329,7 +329,6 @@ "@types/prettier": "^3.0.0", "@types/pretty-hrtime": "^1.0.0", "@types/prompts": "^2.0.9", - "@types/ramda": "^0.30.2", "@types/react-syntax-highlighter": "11.0.5", "@types/react-transition-group": "^4", "@types/semver": "^7.5.8", @@ -390,7 +389,6 @@ "prettier": "^3.2.5", "pretty-hrtime": "^1.0.3", "prompts": "^2.4.0", - "ramda": "^0.30.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-draggable": "^4.4.5", diff --git a/code/core/src/common/utils/file-cache.ts b/code/core/src/common/utils/file-cache.ts index a788150c4a6..6e2a9d545bf 100644 --- a/code/core/src/common/utils/file-cache.ts +++ b/code/core/src/common/utils/file-cache.ts @@ -1,469 +1,166 @@ -import * as crypto from 'node:crypto'; -import * as fs from 'node:fs'; -import { existsSync, lstatSync } from 'node:fs'; -import * as fsp from 'node:fs/promises'; -import { mkdir, rm, stat } from 'node:fs/promises'; -import * as fsPath from 'node:path'; - -import * as R from 'ramda'; - -const pathExists = async (path: string) => { - return stat(path) - .then(() => true) - .catch(() => false); -}; - -async function ensureDir(dirPath: string): Promise { - try { - // Attempt to create the directory recursively - await mkdir(dirPath, { recursive: true }); - } catch (err: any) { - // If the error is something other than the directory already existing, throw the error - if (err.code !== 'EEXIST') { - throw err; - } - } +import { createHash, randomBytes } from 'node:crypto'; +import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { promises as fsPromises } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +interface FileSystemCacheOptions { + ns?: string; + prefix?: string; + hash_alg?: string; + basePath?: string; + ttl?: number; } -async function remove(path: string): Promise { - try { - // Attempt to remove the file or directory recursively - await rm(path, { recursive: true, force: true }); - } catch (err: any) { - // If the error code is anything other than the path not existing, rethrow the error - if (err.code !== 'ENOENT') { - throw err; - } - } +interface CacheItem { + key: string; + content?: any; + value?: any; } -export type FileSystemCacheOptions = { - basePath?: string; - ns?: any; +interface CacheSetOptions { ttl?: number; - hash?: HashAlgorithm; - extension?: string; -}; - -export const isNothing = (value: any) => R.isNil(value) || R.isEmpty(value); -export const isString = R.is(String); - -export const toAbsolutePath = (path: string) => { - return path.startsWith('.') ? fsPath.resolve(path) : path; -}; - -export const ensureString = (defaultValue: string, text?: string): string => { - return typeof text === 'string' ? text : defaultValue; -}; + encoding?: BufferEncoding; +} -export const compact = (input: any[]): string[] => { - const flat = [].concat(...input); - return flat.filter((value) => !R.isNil(value)); -}; +export class FileSystemCache { + private id = randomBytes(15).toString('base64').replace(/\//g, '-'); -export const toStringArray = R.pipe(compact, R.map(R.toString)); + private prefix: string; -export const isFileSync = (path: string) => { - return existsSync(path) ? lstatSync(path).isFile() : false; -}; + private hash_alg: string; -export const readFileSync = (path: string) => { - return existsSync(path) ? fs.readFileSync(path).toString() : undefined; -}; + private cache_dir: string; -export const filePathsP = async (basePath: string, ns: string): Promise => { - if (!(await pathExists(basePath))) { - return []; - } - return (await fsp.readdir(basePath)) - .filter(Boolean) - .filter((name) => (ns ? name.startsWith(ns) : true)) - .filter((name) => (!ns ? !name.includes('-') : true)) - .map((name) => `${basePath}/${name}`); -}; + private ttl: number; -/** - * Turns a set of values into a HEX hash code. - * - * @param values: The set of values to hash. - */ -export const hash = (algorithm: HashAlgorithm, ...values: any[]) => { - if (R.pipe(compact, R.isEmpty)(values)) { - return undefined; + constructor(options: FileSystemCacheOptions = {}) { + this.prefix = (options.ns || options.prefix || '') + '-'; + this.hash_alg = options.hash_alg || 'md5'; + this.cache_dir = options.basePath || join(tmpdir(), this.id); + this.ttl = options.ttl || 0; + createHash(this.hash_alg); // Verifies hash algorithm is available + mkdirSync(this.cache_dir, { recursive: true }); } - const resultHash = crypto.createHash(algorithm); - const addValue = (value: any) => resultHash.update(value); - const addValues = R.forEach(addValue); - R.pipe(toStringArray, addValues)(values); - return resultHash.digest('hex'); -}; - -export const hashExists = (algorithm: HashAlgorithm) => { - return crypto.getHashes().includes(algorithm); -}; - -/** Retrieve a value from the given path. */ -export async function getValueP(path: string, defaultValue?: any) { - const exists = await pathExists(path); - if (!exists) { - return defaultValue; + private generateHash(name: string): string { + return join(this.cache_dir, this.prefix + createHash(this.hash_alg).update(name).digest('hex')); } - try { - const content = await fsp.readFile(path, 'utf8'); - return toGetValue(JSON.parse(content)); - } catch (error: any) { - if (error.code === 'ENOENT') { - return defaultValue; - } - if (error.message === 'Cache item has expired.') { - fs.rmSync(path); - return defaultValue; - } - throw new Error(`Failed to read cache value at: ${path}. ${error.message}`); - } -} -/** Format value structure. */ -export const toGetValue = (data: any) => { - if (isExpired(data)) { - return undefined; + private isExpired(parsed: { ttl?: number }, now: number): boolean { + return parsed.ttl != null && now > parsed.ttl; } - if (data.type === 'Date') { - return new Date(data.value); + private parseCacheData( + data: string, + fallback: T | null, + opts: CacheSetOptions = {} + ): T | null { + const parsed = JSON.parse(data); + return this.isExpired(parsed, Date.now()) ? fallback : (parsed.content as T); } - return data.value; -}; - -/** Stringify a value into JSON. */ -export const toJson = (value: any, ttl: number) => - JSON.stringify({ value, type: R.type(value), created: new Date(), ttl }); - -/** Check's a cache item to see if it has expired. */ -export const isExpired = (data: any) => { - const timeElapsed = (new Date().getTime() - new Date(data.created).getTime()) / 1000; - return timeElapsed > data.ttl && data.ttl > 0; -}; - -export type HashAlgorithm = - | 'RSA-MD5' - | 'RSA-RIPEMD160' - | 'RSA-SHA1' - | 'RSA-SHA1-2' - | 'RSA-SHA224' - | 'RSA-SHA256' - | 'RSA-SHA3-224' - | 'RSA-SHA3-256' - | 'RSA-SHA3-384' - | 'RSA-SHA3-512' - | 'RSA-SHA384' - | 'RSA-SHA512' - | 'RSA-SHA512/224' - | 'RSA-SHA512/256' - | 'RSA-SM3' - | 'blake2b512' - | 'blake2s256' - | 'id-rsassa-pkcs1-v1_5-with-sha3-224' - | 'id-rsassa-pkcs1-v1_5-with-sha3-256' - | 'id-rsassa-pkcs1-v1_5-with-sha3-384' - | 'id-rsassa-pkcs1-v1_5-with-sha3-512' - | 'md5' - | 'md5-sha1' - | 'md5WithRSAEncryption' - | 'ripemd' - | 'ripemd160' - | 'ripemd160WithRSA' - | 'rmd160' - | 'sha1' - | 'sha1WithRSAEncryption' - | 'sha224' - | 'sha224WithRSAEncryption' - | 'sha256' - | 'sha256WithRSAEncryption' - | 'sha3-224' - | 'sha3-256' - | 'sha3-384' - | 'sha3-512' - | 'sha384' - | 'sha384WithRSAEncryption' - | 'sha512' - | 'sha512-224' - | 'sha512-224WithRSAEncryption' - | 'sha512-256' - | 'sha512-256WithRSAEncryption' - | 'sha512WithRSAEncryption' - | 'shake128' - | 'shake256' - | 'sm3' - | 'sm3WithRSAEncryption' - | 'ssl3-md5' - | 'ssl3-sha1'; - -export const hashAlgorithms: HashAlgorithm[] = [ - 'RSA-MD5', - 'RSA-RIPEMD160', - 'RSA-SHA1', - 'RSA-SHA1-2', - 'RSA-SHA224', - 'RSA-SHA256', - 'RSA-SHA3-224', - 'RSA-SHA3-256', - 'RSA-SHA3-384', - 'RSA-SHA3-512', - 'RSA-SHA384', - 'RSA-SHA512', - 'RSA-SHA512/224', - 'RSA-SHA512/256', - 'RSA-SM3', - 'blake2b512', - 'blake2s256', - 'id-rsassa-pkcs1-v1_5-with-sha3-224', - 'id-rsassa-pkcs1-v1_5-with-sha3-256', - 'id-rsassa-pkcs1-v1_5-with-sha3-384', - 'id-rsassa-pkcs1-v1_5-with-sha3-512', - 'md5', - 'md5-sha1', - 'md5WithRSAEncryption', - 'ripemd', - 'ripemd160', - 'ripemd160WithRSA', - 'rmd160', - 'sha1', - 'sha1WithRSAEncryption', - 'sha224', - 'sha224WithRSAEncryption', - 'sha256', - 'sha256WithRSAEncryption', - 'sha3-224', - 'sha3-256', - 'sha3-384', - 'sha3-512', - 'sha384', - 'sha384WithRSAEncryption', - 'sha512', - 'sha512-224', - 'sha512-224WithRSAEncryption', - 'sha512-256', - 'sha512-256WithRSAEncryption', - 'sha512WithRSAEncryption', - 'shake128', - 'shake256', - 'sm3', - 'sm3WithRSAEncryption', - 'ssl3-md5', - 'ssl3-sha1', -]; -/** A cache that read/writes to a specific part of the file-system. */ -export class FileSystemCache { - /** The list of all available hash algorithms. */ - static hashAlgorithms: HashAlgorithm[] = hashAlgorithms; - - /** Instance. */ - readonly basePath: string; - - readonly ns?: any; - readonly extension?: string; - - readonly hash: HashAlgorithm; - - readonly ttl: number; - - basePathExists?: boolean; - - /** - * Constructor. - * - * @param options - BasePath: The folder path to read/write to. Default: './build' - ns: A single - * value, or array, that represents a a unique namespace within which values for this store are - * cached. - extension: An optional file-extension for paths. - ttl: The default time-to-live - * for cached values in seconds. Default: 0 (never expires) - hash: The hashing algorithm to use - * when generating cache keys. Default: "sha1" - */ - constructor(options: FileSystemCacheOptions = {}) { - this.basePath = formatPath(options.basePath); - this.hash = options.hash ?? 'sha1'; - this.ns = hash(this.hash, options.ns); - this.ttl = options.ttl ?? 0; - - if (isString(options.extension)) { - this.extension = options.extension; - } - - if (isFileSync(this.basePath)) { - throw new Error(`The basePath '${this.basePath}' is a file. It should be a folder.`); - } - - if (!hashExists(this.hash)) { - throw new Error(`Hash does not exist: ${this.hash}`); - } + private parseSetData(key: string, data: T, opts: CacheSetOptions = {}): string { + const ttl = opts.ttl ?? this.ttl; + return JSON.stringify({ key, content: data, ...(ttl && { ttl: Date.now() + ttl * 1000 }) }); } - /** - * Generates the path to the cached files. - * - * @param {string} key: The key of the cache item. - */ - public path(key: string): string { - if (isNothing(key)) { - throw new Error(`Path requires a cache key.`); - } - let name = hash(this.hash, key); - - if (this.ns) { - name = `${this.ns}-${name}`; - } - - if (this.extension) { - name = `${name}.${this.extension.replace(/^\./, '')}`; + public async get( + name: string, + fallback: T | null = null, + opts?: CacheSetOptions + ): Promise { + try { + const data = await fsPromises.readFile(this.generateHash(name), 'utf8'); + return this.parseCacheData(data, fallback, opts); + } catch { + return fallback; } - return `${this.basePath}/${name}`; } - /** - * Determines whether the file exists. - * - * @param {string} key: The key of the cache item. - */ - public fileExists(key: string) { - return pathExists(this.path(key)); - } - - /** Ensure that the base path exists. */ - public async ensureBasePath() { - if (!this.basePathExists) { - await ensureDir(this.basePath); + public getSync(name: string, fallback: T | null = null, opts?: CacheSetOptions): T | null { + try { + const data = readFileSync(this.generateHash(name), 'utf8'); + return this.parseCacheData(data, fallback, opts); + } catch { + return fallback; } - this.basePathExists = true; } - /** - * Gets the contents of the file with the given key. - * - * @param {string} key: The key of the cache item. - * @param defaultValue: Optional. A default value to return if the value does not exist in cache. - * @returns File contents, or undefined if the file does not exis - */ - public get(key: string, defaultValue?: any) { - return getValueP(this.path(key), defaultValue); + public async set( + name: string, + data: T, + orgOpts: CacheSetOptions | number = {} + ): Promise { + const opts: CacheSetOptions = typeof orgOpts === 'number' ? { ttl: orgOpts } : orgOpts; + await fsPromises.writeFile(this.generateHash(name), this.parseSetData(name, data, opts), { + encoding: opts.encoding || 'utf8', + }); } - /** - * Gets the contents of the file with the given key. - * - * @param {string} key: The key of the cache item. - * @param defaultValue: Optional. A default value to return if the value does not exist in cache. - * @returns The cached value, or undefined. - */ - public getSync(key: string, defaultValue?: any) { - const path = this.path(key); - const content = readFileSync(path) || ''; - return fs.existsSync(path) ? toGetValue(JSON.parse(content)) : defaultValue; + public setSync(name: string, data: T, orgOpts: CacheSetOptions | number = {}): void { + const opts: CacheSetOptions = typeof orgOpts === 'number' ? { ttl: orgOpts } : orgOpts; + writeFileSync(this.generateHash(name), this.parseSetData(name, data, opts), { + encoding: opts.encoding || 'utf8', + }); } - /** - * Writes the given value to the file-system. - * - * @param {string} key: The key of the cache item. - * @param value: The value to write (Primitive or Object). - */ - public async set(key: string, value: any, ttl?: number) { - const path = this.path(key); - ttl = typeof ttl === 'number' ? ttl : this.ttl; - await this.ensureBasePath(); - await fsp.writeFile(path, toJson(value, ttl)); - return { path }; + public async setMany(items: CacheItem[], options?: CacheSetOptions): Promise { + await Promise.all(items.map((item) => this.set(item.key, item.content ?? item.value, options))); } - /** - * Writes the given value to the file-system and memory cache. - * - * @param {string} key: The key of the cache item. - * @param value: The value to write (Primitive or Object). - * @returns The cache. - */ - public setSync(key: string, value: any, ttl?: number) { - ttl = typeof ttl === 'number' ? ttl : this.ttl; - fs.writeFileSync(this.path(key), toJson(value, ttl)); - return this; + public setManySync(items: CacheItem[], options?: CacheSetOptions): void { + items.forEach((item) => this.setSync(item.key, item.content ?? item.value, options)); } - /** - * Removes the item from the file-system. - * - * @param {string} key: The key of the cache item. - */ - public remove(key: string) { - return remove(this.path(key)); + public async remove(name: string): Promise { + await fsPromises.rm(this.generateHash(name), { force: true }); } - /** Removes all items from the cache. */ - public async clear() { - const paths = await filePathsP(this.basePath, this.ns); - await Promise.all(paths.map((path) => remove(path))); - console.groupEnd(); + public removeSync(name: string): void { + rmSync(this.generateHash(name), { force: true }); } - /** - * Saves several items to the cache in one operation. - * - * @param {array} items: An array of objects of the form { key, value }. - */ - public async save( - input: ({ key: string; value: any } | null | undefined)[] - ): Promise<{ paths: string[] }> { - type Item = { key: string; value: any }; - let items = (Array.isArray(input) ? input : [input]) as Item[]; - - const isValid = (item: any) => { - if (!R.is(Object, item)) { - return false; - } - return item.key && item.value; - }; - - items = items.filter((item) => Boolean(item)); - items - .filter((item) => !isValid(item)) - .forEach(() => { - const err = `Save items not valid, must be an array of {key, value} objects.`; - throw new Error(err); - }); - - if (items.length === 0) { - return { paths: [] }; - } - - const paths = await Promise.all( - items.map(async (item) => (await this.set(item.key, item.value)).path) + public async clear(): Promise { + const files = await fsPromises.readdir(this.cache_dir); + await Promise.all( + files + .filter((f) => f.startsWith(this.prefix)) + .map((f) => fsPromises.rm(join(this.cache_dir, f), { force: true })) ); - - return { paths }; } - /** Loads all files within the cache's namespace. */ - public async load(): Promise<{ files: { path: string; value: any }[] }> { - const paths = await filePathsP(this.basePath, this.ns); + public clearSync(): void { + readdirSync(this.cache_dir) + .filter((f) => f.startsWith(this.prefix)) + .forEach((f) => rmSync(join(this.cache_dir, f), { force: true })); + } - if (paths.length === 0) { - return { files: [] }; - } - const files = await Promise.all( - paths.map(async (path) => ({ path, value: await getValueP(path) })) + public async getAll(): Promise { + const now = Date.now(); + const files = await fsPromises.readdir(this.cache_dir); + const items = await Promise.all( + files + .filter((f) => f.startsWith(this.prefix)) + .map((f) => fsPromises.readFile(join(this.cache_dir, f), 'utf8')) ); - return { files }; + return items + .map((data) => JSON.parse(data)) + .filter((entry) => entry.content && !this.isExpired(entry, now)); + } + + public async load(): Promise<{ files: CacheItem[] }> { + const res = await this.getAll(); + return { + files: res.map((entry) => ({ + path: this.generateHash(entry.key), + value: entry.content, + key: entry.key, + })), + }; } } -/** Helpers */ - -function formatPath(path?: string) { - path = ensureString('./.cache', path); - path = toAbsolutePath(path); - return path; -} - export function createFileSystemCache(options: FileSystemCacheOptions): FileSystemCache { return new FileSystemCache(options); } diff --git a/code/yarn.lock b/code/yarn.lock index 495364337ab..f258f17c2b2 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6039,7 +6039,6 @@ __metadata: "@types/prettier": "npm:^3.0.0" "@types/pretty-hrtime": "npm:^1.0.0" "@types/prompts": "npm:^2.0.9" - "@types/ramda": "npm:^0.30.2" "@types/react-syntax-highlighter": "npm:11.0.5" "@types/react-transition-group": "npm:^4" "@types/semver": "npm:^7.5.8" @@ -6105,7 +6104,6 @@ __metadata: pretty-hrtime: "npm:^1.0.3" process: "npm:^0.11.10" prompts: "npm:^2.4.0" - ramda: "npm:^0.30.1" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-draggable: "npm:^4.4.5" @@ -8146,15 +8144,6 @@ __metadata: languageName: node linkType: hard -"@types/ramda@npm:^0.30.2": - version: 0.30.2 - resolution: "@types/ramda@npm:0.30.2" - dependencies: - types-ramda: "npm:^0.30.1" - checksum: 10c0/dda95008860f594eb7b4fd9819adeb2dcd4d4e2baca6cb33f692f6f8ea76d04a7fd81f9a057c5c67555612769e5592cb15f91de6c9f8b619b8b1d806d19dc9ea - languageName: node - linkType: hard - "@types/range-parser@npm:*": version: 1.2.5 resolution: "@types/range-parser@npm:1.2.5" @@ -23573,13 +23562,6 @@ __metadata: languageName: node linkType: hard -"ramda@npm:^0.30.1": - version: 0.30.1 - resolution: "ramda@npm:0.30.1" - checksum: 10c0/3ea3e35c80e1a1b78c23de0c72d3382c3446f42052b113b851f1b7fc421e33a45ce92e7aef3c705cc6de3812a209d03417af5c264f67126cda539fd66c8bea71 - languageName: node - linkType: hard - "randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -27336,13 +27318,6 @@ __metadata: languageName: node linkType: hard -"ts-toolbelt@npm:^9.6.0": - version: 9.6.0 - resolution: "ts-toolbelt@npm:9.6.0" - checksum: 10c0/838f9a2f0fe881d5065257a23b402c41315b33ff987b73db3e2b39fcb70640c4c7220e1ef118ed5676763543724fdbf4eda7b0e2c17acb667ed1401336af9f8c - languageName: node - linkType: hard - "tsconfig-paths-webpack-plugin@npm:^4.0.1": version: 4.1.0 resolution: "tsconfig-paths-webpack-plugin@npm:4.1.0" @@ -27526,15 +27501,6 @@ __metadata: languageName: node linkType: hard -"types-ramda@npm:^0.30.1": - version: 0.30.1 - resolution: "types-ramda@npm:0.30.1" - dependencies: - ts-toolbelt: "npm:^9.6.0" - checksum: 10c0/4a8b230ae9772e6534f65b1a154dd5604bcd1d74e27b49686337a215e83aa8fc93e49f8c49af395418d2950cb9fb9b900662077c1d4b73ff6fe4f4bcb83ab2d6 - languageName: node - linkType: hard - "typescript@npm:^3.8.3": version: 3.9.10 resolution: "typescript@npm:3.9.10" From a4e35ec921a948026e6cc63d2db2f9e1db2c05f5 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 2 Oct 2024 13:33:39 +0200 Subject: [PATCH 04/10] fix --- code/core/src/common/utils/file-cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/common/utils/file-cache.ts b/code/core/src/common/utils/file-cache.ts index 6e2a9d545bf..c44e4dc6603 100644 --- a/code/core/src/common/utils/file-cache.ts +++ b/code/core/src/common/utils/file-cache.ts @@ -65,7 +65,7 @@ export class FileSystemCache { return JSON.stringify({ key, content: data, ...(ttl && { ttl: Date.now() + ttl * 1000 }) }); } - public async get( + public async get( name: string, fallback: T | null = null, opts?: CacheSetOptions From 9e050395f156dffdb8ee4f75207755d80a0f1b1e Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 2 Oct 2024 13:42:44 +0200 Subject: [PATCH 05/10] fix --- code/__mocks__/fs.ts | 2 ++ code/core/src/cli/detect.test.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/code/__mocks__/fs.ts b/code/__mocks__/fs.ts index 50e34e8e4ca..f617d31141f 100644 --- a/code/__mocks__/fs.ts +++ b/code/__mocks__/fs.ts @@ -19,6 +19,7 @@ export const realpathSync = vi.fn(); export const readdir = vi.fn(); export const readdirSync = vi.fn(); export const readlinkSync = vi.fn(); +export const mkdirSync = vi.fn(); export default { __setMockFiles, @@ -29,4 +30,5 @@ export default { readdir, readdirSync, readlinkSync, + mkdirSync, }; diff --git a/code/core/src/cli/detect.test.ts b/code/core/src/cli/detect.test.ts index 95c1b126dca..ea7f8139fda 100644 --- a/code/core/src/cli/detect.test.ts +++ b/code/core/src/cli/detect.test.ts @@ -24,6 +24,7 @@ vi.mock('fs', () => ({ readdirSync: vi.fn(), readlinkSync: vi.fn(), default: vi.fn(), + mkdirSync: vi.fn(), })); vi.mock('@storybook/core/node-logger'); From c36ee50b67be7917b46c7acc5e63e1915abbf671 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 2 Oct 2024 15:17:40 +0200 Subject: [PATCH 06/10] simplifications, fixes, and make webpack progress actually represent the modules, instead of entrypoints... --- code/builders/builder-webpack5/src/index.ts | 4 +- .../src/preview/iframe-webpack.config.ts | 4 +- code/core/src/common/utils/file-cache.ts | 38 ++++++++----------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index ffc11812562..e35ced7be07 100644 --- a/code/builders/builder-webpack5/src/index.ts +++ b/code/builders/builder-webpack5/src/index.ts @@ -137,7 +137,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ } yield; - const modulesCount = (await options.cache?.get('modulesCount').catch(() => {})) || 1000; + const modulesCount = await options.cache?.get('modulesCount', 1000); let totalModules: number; let value = 0; @@ -147,7 +147,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ const progress = { value, message: message.charAt(0).toUpperCase() + message.slice(1) }; if (message === 'building') { // arg3 undefined in webpack5 - const counts = (arg3 && arg3.match(/(\d+)\/(\d+)/)) || []; + const counts = (arg3 && arg3.match(/entries (\d+)\/(\d+)/)) || []; const complete = parseInt(counts[1], 10); const total = parseInt(counts[2], 10); if (!Number.isNaN(complete) && !Number.isNaN(total)) { diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index ea8b55d4973..763f2bf1564 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -72,7 +72,7 @@ export default async ( docsOptions, entries, nonNormalizedStories, - modulesCount = 1000, + modulesCount, build, tagsOptions, ] = await Promise.all([ @@ -86,7 +86,7 @@ export default async ( presets.apply('docs'), presets.apply('entries', []), presets.apply('stories', []), - options.cache?.get('modulesCount').catch(() => {}), + options.cache?.get('modulesCount', 1000), options.presets.apply('build'), presets.apply('tags', {}), ]); diff --git a/code/core/src/common/utils/file-cache.ts b/code/core/src/common/utils/file-cache.ts index c44e4dc6603..3dd7849abd6 100644 --- a/code/core/src/common/utils/file-cache.ts +++ b/code/core/src/common/utils/file-cache.ts @@ -1,6 +1,6 @@ import { createHash, randomBytes } from 'node:crypto'; import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; -import { promises as fsPromises } from 'node:fs'; +import { readFile, readdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -51,11 +51,7 @@ export class FileSystemCache { return parsed.ttl != null && now > parsed.ttl; } - private parseCacheData( - data: string, - fallback: T | null, - opts: CacheSetOptions = {} - ): T | null { + private parseCacheData(data: string, fallback: T | null): T | null { const parsed = JSON.parse(data); return this.isExpired(parsed, Date.now()) ? fallback : (parsed.content as T); } @@ -65,25 +61,21 @@ export class FileSystemCache { return JSON.stringify({ key, content: data, ...(ttl && { ttl: Date.now() + ttl * 1000 }) }); } - public async get( - name: string, - fallback: T | null = null, - opts?: CacheSetOptions - ): Promise { + public async get(name: string, fallback?: T): Promise { try { - const data = await fsPromises.readFile(this.generateHash(name), 'utf8'); - return this.parseCacheData(data, fallback, opts); + const data = await readFile(this.generateHash(name), 'utf8'); + return this.parseCacheData(data, fallback) as T; } catch { - return fallback; + return fallback as T; } } - public getSync(name: string, fallback: T | null = null, opts?: CacheSetOptions): T | null { + public getSync(name: string, fallback?: T): T { try { const data = readFileSync(this.generateHash(name), 'utf8'); - return this.parseCacheData(data, fallback, opts); + return this.parseCacheData(data, fallback) as T; } catch { - return fallback; + return fallback as T; } } @@ -93,7 +85,7 @@ export class FileSystemCache { orgOpts: CacheSetOptions | number = {} ): Promise { const opts: CacheSetOptions = typeof orgOpts === 'number' ? { ttl: orgOpts } : orgOpts; - await fsPromises.writeFile(this.generateHash(name), this.parseSetData(name, data, opts), { + await writeFile(this.generateHash(name), this.parseSetData(name, data, opts), { encoding: opts.encoding || 'utf8', }); } @@ -114,7 +106,7 @@ export class FileSystemCache { } public async remove(name: string): Promise { - await fsPromises.rm(this.generateHash(name), { force: true }); + await rm(this.generateHash(name), { force: true }); } public removeSync(name: string): void { @@ -122,11 +114,11 @@ export class FileSystemCache { } public async clear(): Promise { - const files = await fsPromises.readdir(this.cache_dir); + const files = await readdir(this.cache_dir); await Promise.all( files .filter((f) => f.startsWith(this.prefix)) - .map((f) => fsPromises.rm(join(this.cache_dir, f), { force: true })) + .map((f) => rm(join(this.cache_dir, f), { force: true })) ); } @@ -138,11 +130,11 @@ export class FileSystemCache { public async getAll(): Promise { const now = Date.now(); - const files = await fsPromises.readdir(this.cache_dir); + const files = await readdir(this.cache_dir); const items = await Promise.all( files .filter((f) => f.startsWith(this.prefix)) - .map((f) => fsPromises.readFile(join(this.cache_dir, f), 'utf8')) + .map((f) => readFile(join(this.cache_dir, f), 'utf8')) ); return items .map((data) => JSON.parse(data)) From 0080cf2b450a30ca07fdd4999dee92be235263e1 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 2 Oct 2024 15:58:19 +0200 Subject: [PATCH 07/10] fix typing issue --- code/core/src/common/utils/file-cache.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/core/src/common/utils/file-cache.ts b/code/core/src/common/utils/file-cache.ts index 3dd7849abd6..3c9fd7d1078 100644 --- a/code/core/src/common/utils/file-cache.ts +++ b/code/core/src/common/utils/file-cache.ts @@ -24,8 +24,6 @@ interface CacheSetOptions { } export class FileSystemCache { - private id = randomBytes(15).toString('base64').replace(/\//g, '-'); - private prefix: string; private hash_alg: string; @@ -37,7 +35,8 @@ export class FileSystemCache { constructor(options: FileSystemCacheOptions = {}) { this.prefix = (options.ns || options.prefix || '') + '-'; this.hash_alg = options.hash_alg || 'md5'; - this.cache_dir = options.basePath || join(tmpdir(), this.id); + this.cache_dir = + options.basePath || join(tmpdir(), randomBytes(15).toString('base64').replace(/\//g, '-')); this.ttl = options.ttl || 0; createHash(this.hash_alg); // Verifies hash algorithm is available mkdirSync(this.cache_dir, { recursive: true }); From 219b7d7cb1662799b9a7af0c25e86830c3e192c2 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 2 Oct 2024 16:10:10 +0200 Subject: [PATCH 08/10] fix typing issues proper --- code/core/src/cli/dev.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/core/src/cli/dev.ts b/code/core/src/cli/dev.ts index 6d05f140fa3..26ff6250f3a 100644 --- a/code/core/src/cli/dev.ts +++ b/code/core/src/cli/dev.ts @@ -44,14 +44,16 @@ export const dev = async (cliOptions: CLIOptions) => { const packageJson = await findPackage(__dirname); invariant(packageJson, 'Failed to find the closest package.json file.'); + type Options = Parameters[0]; + const options = { ...cliOptions, configDir: cliOptions.configDir || './.storybook', configType: 'DEVELOPMENT', ignorePreview: !!cliOptions.previewUrl && !cliOptions.forceBuildPreview, - cache, + cache: cache as any, packageJson, - } as Parameters[0]; + } as Options; await withTelemetry( 'dev', From ca2a0698e5ba92155c8df1dffc0e28e9ae470aa8 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 7 Oct 2024 13:46:04 +0200 Subject: [PATCH 09/10] add comment --- code/core/src/common/utils/file-cache.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/core/src/common/utils/file-cache.ts b/code/core/src/common/utils/file-cache.ts index 3c9fd7d1078..74bc27d87b2 100644 --- a/code/core/src/common/utils/file-cache.ts +++ b/code/core/src/common/utils/file-cache.ts @@ -1,3 +1,7 @@ +/** + * This file is a modified copy from https://git.nfp.is/TheThing/fs-cache-fast + */ + import { createHash, randomBytes } from 'node:crypto'; import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { readFile, readdir, rm, writeFile } from 'node:fs/promises'; From fbf7a926cdef8fc77acae6a44b854f8e78e1e37c Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 7 Oct 2024 14:00:51 +0200 Subject: [PATCH 10/10] fix linting --- code/core/src/common/utils/file-cache.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/code/core/src/common/utils/file-cache.ts b/code/core/src/common/utils/file-cache.ts index 74bc27d87b2..590ddf60d60 100644 --- a/code/core/src/common/utils/file-cache.ts +++ b/code/core/src/common/utils/file-cache.ts @@ -1,7 +1,4 @@ -/** - * This file is a modified copy from https://git.nfp.is/TheThing/fs-cache-fast - */ - +/** This file is a modified copy from https://git.nfp.is/TheThing/fs-cache-fast */ import { createHash, randomBytes } from 'node:crypto'; import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { readFile, readdir, rm, writeFile } from 'node:fs/promises';