From 9adfb0b004ef41a144ce3849f3f33c2e326febb4 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 10:12:04 +0200 Subject: [PATCH 01/26] chore: add clack prompts --- packages/cli/package.json | 1 + pnpm-lock.yaml | 24 +++++++++++++++--------- pnpm-workspace.yaml | 3 ++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 31f5f9115..0c9c99ff6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,6 +42,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@clack/prompts": "catalog:prod", "@luxass/unicode-utils": "catalog:prod", "@luxass/utils": "catalog:prod", "@ucdjs/schema-gen": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8ae03251..80314e854 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,12 @@ catalogs: '@ai-sdk/openai': specifier: ^1.3.23 version: 1.3.23 - '@luxass/unicode-utils': + '@clack/prompts': specifier: ^0.11.0 version: 0.11.0 + '@luxass/unicode-utils': + specifier: ^0.9.0 + version: 0.9.0 '@luxass/unicode-utils-new': specifier: npm:@luxass/unicode-utils@0.12.0-beta.9 version: 0.12.0-beta.9 @@ -371,9 +374,12 @@ importers: packages/cli: dependencies: - '@luxass/unicode-utils': + '@clack/prompts': specifier: catalog:prod version: 0.11.0 + '@luxass/unicode-utils': + specifier: catalog:prod + version: 0.9.0 '@luxass/utils': specifier: catalog:prod version: 2.6.2 @@ -548,7 +554,7 @@ importers: version: 1.3.23(zod@4.0.13) '@luxass/unicode-utils': specifier: catalog:prod - version: 0.11.0 + version: 0.9.0 '@luxass/utils': specifier: catalog:prod version: 2.6.2 @@ -1705,12 +1711,12 @@ packages: '@luxass/spectral-ruleset@1.1.0': resolution: {integrity: sha512-Dm4n7LuyI3nInZ9kzytOF3bTIIw2zFdBKDhOvPP2QCzNXLX2iL6BhlZX1XFcflMIARQG2h2WwP+UaFcV+0/9aQ==} - '@luxass/unicode-utils@0.11.0': - resolution: {integrity: sha512-SjXHMnldUuqYY4NcyD8peL3HJglYxSgGHzpjqkAzG6gwyvlJSnZgmoz5UcFuuxpk0aM9flY5QcWOnx/080zDfA==} - '@luxass/unicode-utils@0.12.0-beta.9': resolution: {integrity: sha512-KX++zXKY1vGVoxT6VQNXz1OdKPSWZ/4p+ahiYGPPd+brDnjRaoAP3odaMKL7J+CRFM8RjKJk8yU9Qg9udmM1jQ==} + '@luxass/unicode-utils@0.9.0': + resolution: {integrity: sha512-RNhEJ2pXiIRhmr/KGtAbl4jy2uwQjXMdRQKHZE1M1IgjkXa4/d8is5HtwxdKKcL0CCO3f4lGAeLOIhR6ACGNfg==} + '@luxass/utils@2.6.2': resolution: {integrity: sha512-SK+bJyuH109psfzpHStTttbTPqKE2Pml9gkFH58PI6ssB2JxVC5Wg1eZLC+Kkw/3UXWCBQRP5RT+yXuaFWWYBg==} engines: {node: '>=20'} @@ -6702,14 +6708,14 @@ snapshots: '@luxass/spectral-ruleset@1.1.0': {} - '@luxass/unicode-utils@0.11.0': + '@luxass/unicode-utils@0.12.0-beta.9': dependencies: '@luxass/utils': 2.6.2 + defu: 6.1.4 - '@luxass/unicode-utils@0.12.0-beta.9': + '@luxass/unicode-utils@0.9.0': dependencies: '@luxass/utils': 2.6.2 - defu: 6.1.4 '@luxass/utils@2.6.2': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5973b2e59..77d2805b7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -35,7 +35,7 @@ catalogs: prod: # merge these together when heading inference is implemented better - "@luxass/unicode-utils": ^0.11.0 + "@luxass/unicode-utils": ^0.9.0 "@luxass/unicode-utils-new": npm:@luxass/unicode-utils@0.12.0-beta.9 "@luxass/utils": ^2.6.2 farver: ^0.4.2 @@ -52,6 +52,7 @@ catalogs: apache-autoindex-parse: ^2.4.0 openapi-fetch: ^0.14.0 pathe: ^2.0.3 + "@clack/prompts": ^0.11.0 dev: "@types/picomatch": ^4.0.2 From 27db542d96bd9b9f4f64dcecdf0bad52ff864bf1 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 10:12:20 +0200 Subject: [PATCH 02/26] feat(cli): enhance CLI store command with version selection - Added `versions` property to `CLIStoreCmdSharedFlags` interface. - Implemented `createStoreFromFlags` function to handle both remote and local UCD store creation. - Introduced `runVersionPrompt` function for selecting Unicode versions during store initialization. - Updated error handling to ensure either `--remote` or `--store-dir` is specified. --- packages/cli/src/cmd/store/_shared.ts | 59 ++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/cmd/store/_shared.ts b/packages/cli/src/cmd/store/_shared.ts index 0b43c3bcc..5ac794f1b 100644 --- a/packages/cli/src/cmd/store/_shared.ts +++ b/packages/cli/src/cmd/store/_shared.ts @@ -1,9 +1,14 @@ +import type { UCDStore } from "@ucdjs/ucd-store"; +import { isCancel, multiselect } from "@clack/prompts"; +import { UNICODE_VERSION_METADATA } from "@luxass/unicode-utils"; +import { createHTTPUCDStore, createNodeUCDStore } from "@ucdjs/ucd-store"; + export interface CLIStoreCmdSharedFlags { storeDir?: string; remote?: boolean; patterns?: string[]; - baseUrl?: string; + versions?: string[]; } export const SHARED_FLAGS = [ @@ -12,3 +17,55 @@ export const SHARED_FLAGS = [ ["--patterns", "Patterns to filter files in the store."], ["--base-url", "Base URL for the UCD Store."], ] as [string, string][]; + +export function assertRemoteOrStoreDir(flags: CLIStoreCmdSharedFlags): asserts flags is CLIStoreCmdSharedFlags & { remote: true } | { storeDir: string } { + if (!flags.remote && !flags.storeDir) { + throw new Error("Either --remote or --store-dir must be specified."); + } +} + +/** + * Creates a UCD store instance based on the provided CLI flags. + * + * @param {CLIStoreCmdSharedFlags} flags - Configuration flags for creating the store + * @returns {Promise} A promise that resolves to a UCDStore instance or null + * @throws {Error} When store directory is not specified for local stores + */ +export async function createStoreFromFlags(flags: CLIStoreCmdSharedFlags): Promise { + const { storeDir, remote, baseUrl, patterns } = flags; + + if (remote) { + return createHTTPUCDStore({ + baseUrl, + globalFilters: patterns, + }); + } + + if (!storeDir) { + throw new Error("Store directory must be specified when not using remote store."); + } + + return createNodeUCDStore({ + basePath: storeDir, + baseUrl, + globalFilters: patterns, + versions: flags.versions || [], + }); +} + +export async function runVersionPrompt(): Promise { + const selectedVersions = await multiselect({ + options: UNICODE_VERSION_METADATA.map(({ version }) => ({ + value: version, + label: version, + })), + message: "Select Unicode versions to initialize the store with:", + required: true, + }); + + if (isCancel(selectedVersions)) { + return []; + } + + return selectedVersions; +} From be5bd5eca4eea0c214faad42655694bc652fbc09 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 10:12:39 +0200 Subject: [PATCH 03/26] refactor(cli): improve error handling and version selection in init command * Enhanced error messages for unsupported features and store initialization failures. * Updated version selection logic to prompt users when no versions are specified. * Refined command usage documentation for clarity. --- packages/cli/src/cmd/store/init.ts | 82 +++++++++++++++++++----------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/packages/cli/src/cmd/store/init.ts b/packages/cli/src/cmd/store/init.ts index ea6d38585..38da8902d 100644 --- a/packages/cli/src/cmd/store/init.ts +++ b/packages/cli/src/cmd/store/init.ts @@ -1,10 +1,10 @@ -/* eslint-disable unused-imports/no-unused-vars */ import type { Prettify } from "@luxass/utils"; import type { CLIArguments } from "../../cli-utils"; import type { CLIStoreCmdSharedFlags } from "./_shared"; -// import { createLocalUCDStore, createRemoteUCDStore } from "@ucdjs/ucd-store"; +import { UCDStoreUnsupportedFeature } from "@ucdjs/ucd-store"; +import { red } from "farver/fast"; import { printHelp } from "../../cli-utils"; -import { SHARED_FLAGS } from "./_shared"; +import { assertRemoteOrStoreDir, createStoreFromFlags, runVersionPrompt, SHARED_FLAGS } from "./_shared"; export interface CLIStoreInitCmdOptions { flags: CLIArguments [...flags]", + usage: "[...versions] [...flags]", tables: { Flags: [ ...SHARED_FLAGS, @@ -31,40 +31,62 @@ export async function runInitStore({ flags, versions }: CLIStoreInitCmdOptions) return; } - if (!versions || versions.length === 0) { - console.error("Error: At least one Unicode version must be specified."); - console.error("Usage: ucd store init "); - return; - } - const { storeDir, - dryRun: _dryRun, + // TODO: handle force flag force: _force, remote, baseUrl, patterns, } = flags; - // let store: UCDStore | null = null; - // if (remote) { - // store = await createRemoteUCDStore({ - // baseUrl, - // globalFilters: patterns, - // }); - // } else { - // store = await createLocalUCDStore({ - // basePath: storeDir, - // baseUrl, - // globalFilters: patterns, - // }); - // } + if (!versions || versions.length === 0) { + const pickedVersions = await runVersionPrompt(); + + if (pickedVersions.length === 0) { + console.error("No versions selected. Operation cancelled."); + return; + } + + versions = pickedVersions; + } + + try { + assertRemoteOrStoreDir(flags); + + const store = await createStoreFromFlags({ + baseUrl, + storeDir, + remote, + patterns, + }); - // if (store == null) { - // console.error("Error: Failed to create UCD store."); - // return; - // } + if (store == null) { + console.error("Error: Failed to create UCD store."); + } - // eslint-disable-next-line no-console - console.log("Initializing UCD Store..."); + // TODO: expose a getter to see if the store has been initialized. + } catch (err) { + if (err instanceof UCDStoreUnsupportedFeature) { + console.error(red(`\n❌ Error: Unsupported feature:`)); + console.error(` ${err.message}`); + console.error(""); + console.error("This store does not support the clean operation."); + console.error("Please check the store capabilities or use a different store type."); + return; + } + + let message = "Unknown error"; + if (err instanceof Error) { + message = err.message; + } else if (typeof err === "string") { + message = err; + } + + console.error(red(`\n❌ Error cleaning store:`)); + console.error(` ${message}`); + console.error("Please check the store configuration and try again."); + console.error("If the issue persists, consider running with --dry-run to see more details."); + console.error("If you believe this is a bug, please report it at https://github.com/ucdjs/ucd/issues"); + } } From 4d5cfb2a024dc612418e99c5eedd2b1ad27e5e57 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 10:12:50 +0200 Subject: [PATCH 04/26] refactor(ucd-store): reorganize error exports for clarity * Moved error exports to improve structure and readability. * Ensures better organization of error handling in the UCD store module. --- packages/ucd-store/src/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ucd-store/src/index.ts b/packages/ucd-store/src/index.ts index 934bbc245..d4ae92ea7 100644 --- a/packages/ucd-store/src/index.ts +++ b/packages/ucd-store/src/index.ts @@ -1,3 +1,10 @@ +export { + UCDStoreError, + UCDStoreFileNotFoundError, + UCDStoreUnsupportedFeature, + UCDStoreVersionNotFoundError, +} from "./errors"; + export { createHTTPUCDStore, createNodeUCDStore, From 1cd8547429725e0f659ca9892cb5904e519e1c56 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 10:13:14 +0200 Subject: [PATCH 05/26] feat(store): add analyze method for version analysis - Introduced `analyze` method to perform analysis on specified Unicode versions. - Added `AnalyzeOptions` interface to define options for analysis, including orphaned file checks. - Implemented error handling for version validation and analysis process. - Created `VersionAnalysis` interface to structure the results of the analysis. --- packages/ucd-store/src/store.ts | 42 +++++++++++++++++++++++++--- packages/ucd-store/src/types.ts | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/packages/ucd-store/src/store.ts b/packages/ucd-store/src/store.ts index a93d73579..a157cf0f6 100644 --- a/packages/ucd-store/src/store.ts +++ b/packages/ucd-store/src/store.ts @@ -2,7 +2,7 @@ import type { UCDClient } from "@ucdjs/fetch"; import type { FileSystemBridgeOperationsWithSymbol } from "@ucdjs/fs-bridge"; import type { UCDStoreManifest } from "@ucdjs/schemas"; import type { PathFilter } from "@ucdjs/utils"; -import type { StoreCapabilities, UCDStoreOptions } from "./types"; +import type { AnalyzeOptions, StoreCapabilities, UCDStoreOptions, VersionAnalysis } from "./types"; import { invariant, prependLeadingSlash } from "@luxass/utils"; import { UCDJS_API_BASE_URL } from "@ucdjs/env"; import { createClient, isApiError } from "@ucdjs/fetch"; @@ -10,8 +10,8 @@ import { UCDStoreManifestSchema } from "@ucdjs/schemas"; import { createPathFilter, safeJsonParse } from "@ucdjs/utils"; import defu from "defu"; import { join } from "pathe"; -import { UCDStoreError } from "./errors"; -import { inferStoreCapabilities } from "./internal/capabilities"; +import { UCDStoreError, UCDStoreVersionNotFoundError } from "./errors"; +import { assertCapabilities, inferStoreCapabilities, requiresCapabilities } from "./internal/capabilities"; export class UCDStore { /** @@ -66,7 +66,7 @@ export class UCDStore { * allowing the store to work with different storage backends (local filesystem, * remote HTTP, in-memory, etc.) through a unified interface. * - * @returns {FileSystemBridgeOperations} The FileSystemBridge instance configured for this store + * @returns {FileSystemBridgeOperationsWithSymbol} The FileSystemBridge instance configured for this store */ get fs(): FileSystemBridgeOperationsWithSymbol { return this.#fs; @@ -142,6 +142,40 @@ export class UCDStore { } } + @requiresCapabilities("analyze") + async analyze(options: AnalyzeOptions): Promise { + const { + checkOrphaned = false, + versions = this.#versions, + } = options; + + let versionAnalyses: VersionAnalysis[] = []; + + try { + const promises = versions.map(async (version) => { + if (!this.versions.includes(version)) { + throw new UCDStoreVersionNotFoundError(version); + } + + return this.#analyzeVersion(version, { + checkOrphaned, + }); + }); + + versionAnalyses = await Promise.all(promises); + + return versionAnalyses; + } catch (err) { + console.error(`Error during store analysis: ${err instanceof Error ? err.message : String(err)}`); + return versionAnalyses; + } + } + + async #analyzeVersion(version: string, options: AnalyzeOptions): Promise { + assertCapabilities("analyze", this.#fs); + throw new UCDStoreError("Method not implemented: #analyzeVersion"); + } + async #loadVersionsFromStore(): Promise { try { const manifestContent = await this.#fs.read(this.#manifestPath); diff --git a/packages/ucd-store/src/types.ts b/packages/ucd-store/src/types.ts index 495028d94..2bf7f04c2 100644 --- a/packages/ucd-store/src/types.ts +++ b/packages/ucd-store/src/types.ts @@ -58,3 +58,52 @@ export interface StoreCapabilities { */ repair: boolean; } + +export interface AnalyzeOptions { + /** + * Whether to check for orphaned files in the store. + * Orphaned files are those that are not referenced by any Unicode version or data. + * This can help identify files that are no longer needed. + */ + checkOrphaned?: boolean; + + /** + * Specific versions to analyze (if not provided, analyzes all) + */ + versions?: string[]; +} + +export interface VersionAnalysis { + /** + * Analyzed Unicode version + * This should be in the format "major.minor.patch" (e.g., "15.0.0") + */ + version: string; + + /** + * List of orphaned files (files that exist but shouldn't) + */ + orphanedFiles: string[]; + + /** + * List of missing files (if any) + */ + missingFiles: string[]; + + /** + * Total number of files expected for this version + */ + totalFileCount: number; + + /** + * Number of files found for this version + */ + fileCount: number; + + /** + * Whether the version is complete + * This means all expected files are present and no orphaned files exist. + * If this is false, it indicates that some files are missing or there are orphaned files. + */ + isComplete: boolean; +} From b22886ade9f28bc6f0a8e54f29328376a0a53eec Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 13:07:11 +0200 Subject: [PATCH 06/26] feat(cli): add analyze command for UCD store * Introduced `runAnalyzeStore` function to analyze UCD store versions. * Added flags for JSON output and orphaned file checks. * Updated `runStoreRoot` to include the new `analyze` subcommand. * Removed obsolete `status` command and its related files. --- packages/cli/src/cmd/store/analyze.ts | 115 ++++++++++++++++++++++++++ packages/cli/src/cmd/store/root.ts | 8 +- packages/cli/src/cmd/store/status.ts | 79 ------------------ 3 files changed, 119 insertions(+), 83 deletions(-) create mode 100644 packages/cli/src/cmd/store/analyze.ts delete mode 100644 packages/cli/src/cmd/store/status.ts diff --git a/packages/cli/src/cmd/store/analyze.ts b/packages/cli/src/cmd/store/analyze.ts new file mode 100644 index 000000000..390ba9cab --- /dev/null +++ b/packages/cli/src/cmd/store/analyze.ts @@ -0,0 +1,115 @@ +/* eslint-disable no-console */ +import type { Prettify } from "@luxass/utils"; +import type { CLIArguments } from "../../cli-utils"; +import type { CLIStoreCmdSharedFlags } from "./_shared"; +import { UCDStoreUnsupportedFeature } from "@ucdjs/ucd-store"; +import { green, red } from "farver/fast"; +import { printHelp } from "../../cli-utils"; +import { assertRemoteOrStoreDir, createStoreFromFlags, SHARED_FLAGS } from "./_shared"; + +export interface CLIStoreAnalyzeCmdOptions { + flags: CLIArguments>; + versions?: string[]; +} + +export async function runAnalyzeStore({ flags, versions }: CLIStoreAnalyzeCmdOptions) { + if (flags?.help || flags?.h) { + printHelp({ + headline: "Analyze UCD Store", + commandName: "ucd store analyze", + usage: "[...versions] [...flags]", + tables: { + Flags: [ + ...SHARED_FLAGS, + ["--check-orphaned", "Check for orphaned files in the store."], + ["--json", "Output analyze information in JSON format."], + ["--help (-h)", "See all available flags."], + ], + }, + }); + return; + } + + if (!versions || versions.length === 0) { + console.info("No specific versions provided. Cleaning all versions in the store."); + } + + const { + storeDir, + json, + remote, + baseUrl, + patterns, + checkOrphaned, + } = flags; + + try { + assertRemoteOrStoreDir(flags); + + const store = await createStoreFromFlags({ + baseUrl, + storeDir, + remote, + patterns, + }); + + if (store == null) { + console.error("Error: Failed to create UCD store."); + return; + } + + const result = await store.analyze({ + checkOrphaned: !!checkOrphaned, + versions: versions || [], + }); + + if (json) { + console.info(JSON.stringify(result, null, 2)); + return; + } + + for (const { version, fileCount, isComplete, missingFiles, orphanedFiles, totalFileCount } of result) { + console.info(`Version: ${version}`); + if (isComplete) { + console.info(` Status: ${green("complete")}`); + } else { + console.warn(` Status: ${red("incomplete")}`); + } + console.info(` Files: ${fileCount}`); + if (missingFiles && missingFiles.length > 0) { + console.warn(` Missing files: ${missingFiles.length}`); + } + if (orphanedFiles && orphanedFiles.length > 0) { + console.warn(` Orphaned files: ${orphanedFiles.length}`); + } + + if (totalFileCount) { + console.info(` Total files expected: ${totalFileCount}`); + } + } + } catch (err) { + if (err instanceof UCDStoreUnsupportedFeature) { + console.error(red(`\n❌ Error: Unsupported feature:`)); + console.error(` ${err.message}`); + console.error(""); + console.error("This store does not support the analyze operation."); + console.error("Please check the store capabilities or use a different store type."); + return; + } + + let message = "Unknown error"; + if (err instanceof Error) { + message = err.message; + } else if (typeof err === "string") { + message = err; + } + + console.error(red(`\n❌ Error analyzing store:`)); + console.error(` ${message}`); + console.error("Please check the store configuration and try again."); + console.error("If you believe this is a bug, please report it at https://github.com/ucdjs/ucd/issues"); + } +} diff --git a/packages/cli/src/cmd/store/root.ts b/packages/cli/src/cmd/store/root.ts index 2a315cad3..fe5a3fa6c 100644 --- a/packages/cli/src/cmd/store/root.ts +++ b/packages/cli/src/cmd/store/root.ts @@ -14,7 +14,7 @@ const CODEGEN_SUBCOMMANDS = [ "init", "repair", "clean", - "status", + "analyze", ] as const; export type Subcommand = (typeof CODEGEN_SUBCOMMANDS)[number]; @@ -70,9 +70,9 @@ export async function runStoreRoot(subcommand: string, { flags }: CLIStoreCmdOpt return; } - if (subcommand === "status") { - const { runStatusStore } = await import("./status"); - await runStatusStore({ flags }); + if (subcommand === "analyze") { + const { runAnalyzeStore } = await import("./analyze"); + await runAnalyzeStore({ flags }); return; } diff --git a/packages/cli/src/cmd/store/status.ts b/packages/cli/src/cmd/store/status.ts deleted file mode 100644 index 3cc8eaa5d..000000000 --- a/packages/cli/src/cmd/store/status.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable unused-imports/no-unused-vars */ - -import type { Prettify } from "@luxass/utils"; -import type { CLIArguments } from "../../cli-utils"; -import type { CLIStoreCmdSharedFlags } from "./_shared"; -// import { createLocalUCDStore, createRemoteUCDStore } from "@ucdjs/ucd-store"; -import { printHelp } from "../../cli-utils"; -import { SHARED_FLAGS } from "./_shared"; - -export interface CLIStoreStatusCmdOptions { - flags: CLIArguments>; -} - -export async function runStatusStore({ flags }: CLIStoreStatusCmdOptions) { - if (flags?.help || flags?.h) { - printHelp({ - headline: "Show UCD Store Status", - commandName: "ucd store status", - usage: "[...flags]", - tables: { - Flags: [ - ...SHARED_FLAGS, - ["--json", "Output status information in JSON format."], - ["--help (-h)", "See all available flags."], - ], - }, - }); - return; - } - - const { - storeDir, - json, - remote, - baseUrl, - patterns, - } = flags; - - // let store: UCDStore | null = null; - // if (remote) { - // store = await createRemoteUCDStore({ - // baseUrl, - // globalFilters: patterns, - // }); - // } else { - // store = await createLocalUCDStore({ - // basePath: storeDir, - // baseUrl, - // globalFilters: patterns, - // }); - // } - - // if (store == null) { - // console.error("Error: Failed to create UCD store."); - // return; - // } - - // const result = await store.analyze(); - - // if (!result.success) { - // console.error("Error: Failed to analyze UCD store."); - // console.error(result.error); - // return; - // } - - // if (json) { - // console.log(JSON.stringify(result, null, 2)); - // } else { - // console.log("UCD Store Status:"); - // console.log(`Total files: ${result.totalFiles}`); - // for (const [version, info] of Object.entries(result.versions)) { - // console.log(`Version: ${version}`); - // console.log(` Total files: ${info.fileCount}`); - // console.log(` Is Complete: ${info.isComplete}`); - // } - // } -} From 54af41b92e2849e1b5eae3d39296ab733af21418 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 13:07:36 +0200 Subject: [PATCH 07/26] fix(utils): ensure 'entries' is an array before processing * Added a type check to validate that 'entries' is an array of UnicodeTreeNode. * Throws a TypeError if the validation fails, improving error handling. --- packages/utils/src/flatten.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/utils/src/flatten.ts b/packages/utils/src/flatten.ts index 4e4060ffc..4eab583e6 100644 --- a/packages/utils/src/flatten.ts +++ b/packages/utils/src/flatten.ts @@ -23,6 +23,10 @@ import { prependLeadingSlash } from "@luxass/utils"; export function flattenFilePaths(entries: UnicodeTreeNode[], prefix: string = ""): string[] { const paths: string[] = []; + if (!Array.isArray(entries)) { + throw new TypeError("Expected 'entries' to be an array of UnicodeTreeNode"); + } + for (const file of entries) { const fullPath = prefix ? `${prefix}${prependLeadingSlash(file.path ?? file.name)}` From a626eacf517f09ae5568bfb010aeb168a08365d1 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 13:08:41 +0200 Subject: [PATCH 08/26] feat(ucd-store): implement #analyzeVersion method for version analysis - Added functionality to analyze a specific version of files in the store. - Checks for orphaned files if the `checkOrphaned` option is enabled. - Throws an error if the specified version is not found in the store. - Returns an analysis object containing details about orphaned and missing files. --- packages/ucd-store/src/store.ts | 35 ++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/ucd-store/src/store.ts b/packages/ucd-store/src/store.ts index a157cf0f6..b2a52ee8c 100644 --- a/packages/ucd-store/src/store.ts +++ b/packages/ucd-store/src/store.ts @@ -12,6 +12,7 @@ import defu from "defu"; import { join } from "pathe"; import { UCDStoreError, UCDStoreVersionNotFoundError } from "./errors"; import { assertCapabilities, inferStoreCapabilities, requiresCapabilities } from "./internal/capabilities"; +import { getExpectedFilePaths } from "./internal/files"; export class UCDStore { /** @@ -173,7 +174,39 @@ export class UCDStore { async #analyzeVersion(version: string, options: AnalyzeOptions): Promise { assertCapabilities("analyze", this.#fs); - throw new UCDStoreError("Method not implemented: #analyzeVersion"); + const { checkOrphaned } = options; + + if (!this.#versions.includes(version)) { + throw new UCDStoreVersionNotFoundError(version); + } + + // get the expected files for this version + const expectedFiles = await getExpectedFilePaths(this.#client, version); + + // get the actual files from the store + const actualFiles = await this.getFilePaths(version); + + const orphanedFiles: string[] = []; + const missingFiles: string[] = []; + + if (checkOrphaned) { + for (const file of actualFiles) { + // if file is not in expected files, it's orphaned + if (!expectedFiles.includes(file)) { + orphanedFiles.push(file); + } + } + } + + const isComplete = orphanedFiles.length === 0 && missingFiles.length === 0; + return { + version, + orphanedFiles, + missingFiles, + totalFileCount: expectedFiles.length, + fileCount: actualFiles.length, + isComplete, + }; } async #loadVersionsFromStore(): Promise { From c4eb53972670c860a13825ed1b2b371bac5dd074 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 13:08:57 +0200 Subject: [PATCH 09/26] feat(ucd-store): add getExpectedFilePaths function to retrieve file paths for Unicode versions - Implements `getExpectedFilePaths` to fetch expected file paths from the API. - Handles API errors by throwing `UCDStoreError` with a descriptive message. - Includes tests for valid version retrieval and error handling scenarios. --- packages/ucd-store/src/internal/files.ts | 38 ++++++ .../ucd-store/test/internal/files.test.ts | 126 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 packages/ucd-store/src/internal/files.ts create mode 100644 packages/ucd-store/test/internal/files.test.ts diff --git a/packages/ucd-store/src/internal/files.ts b/packages/ucd-store/src/internal/files.ts new file mode 100644 index 000000000..47a0af9b4 --- /dev/null +++ b/packages/ucd-store/src/internal/files.ts @@ -0,0 +1,38 @@ +import type { UCDClient } from "@ucdjs/fetch"; +import { isApiError } from "@ucdjs/fetch"; +import { flattenFilePaths } from "@ucdjs/utils"; +import { UCDStoreError } from "../errors"; + +/** + * Retrieves the expected file paths for a specific Unicode version from the API. + * + * This method fetches the canonical list of files that should exist for a given + * Unicode version by making an API call to the UCD service. The returned file + * paths represent the complete set of files that should be present in a properly + * synchronized store for the specified version. + * + * @param {UCDClient} client - The UCD client instance for making API requests + * @param {string} version - The Unicode version to get expected file paths for + * @returns {Promise} A promise that resolves to an array of file paths that should exist for the version + * + * @throws {UCDStoreError} When the API request fails or returns an error + */ +export async function getExpectedFilePaths( + client: UCDClient, + version: string, +): Promise { + // fetch the expected files for this version from the API + const { data, error } = await client.GET("/api/v1/versions/{version}/file-tree", { + params: { + path: { + version, + }, + }, + }); + + if (isApiError(error)) { + throw new UCDStoreError(`Failed to fetch expected files for version '${version}': ${error.message}`); + } + + return flattenFilePaths(data!, `/${version}`); +} diff --git a/packages/ucd-store/test/internal/files.test.ts b/packages/ucd-store/test/internal/files.test.ts new file mode 100644 index 000000000..f00ae4334 --- /dev/null +++ b/packages/ucd-store/test/internal/files.test.ts @@ -0,0 +1,126 @@ +import type { UnicodeTree } from "@ucdjs/fetch"; +import { HttpResponse, mockFetch } from "#msw-utils"; +import { UCDJS_API_BASE_URL } from "@ucdjs/env"; +import { client } from "@ucdjs/fetch"; +import { describe, expect, it, vi } from "vitest"; +import { UCDStoreError, UCDStoreVersionNotFoundError } from "../../src/errors"; +import { getExpectedFilePaths } from "../../src/internal/files"; + +describe("getExpectedFilePaths", () => { + it("should return flattened file paths for valid version", async () => { + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/:version/file-tree`, () => { + return HttpResponse.json([ + { + type: "file", + name: "ReadMe.txt", + path: "/ReadMe.txt", + lastModified: Date.now(), + }, + { + type: "file", + name: "UnicodeData.txt", + path: "/UnicodeData.txt", + lastModified: Date.now(), + }, + { + type: "directory", + name: "ucd", + path: "/ucd/", + lastModified: Date.now(), + children: [ + { + type: "file", + name: "emoji-data.txt", + path: "/emoji-data.txt", + lastModified: Date.now(), + }, + ], + }, + ] satisfies UnicodeTree); + }], + ]); + + const result = await getExpectedFilePaths(client, "15.0.0"); + + expect(result).toEqual([ + "/15.0.0/ReadMe.txt", + "/15.0.0/UnicodeData.txt", + "/15.0.0/ucd/emoji-data.txt", + ]); + }); + + // it("should throw UCDStoreVersionNotFoundError for unavailable version", async () => { + // const version = "99.0.0"; + // const availableVersions = ["14.0.0", "15.0.0", "16.0.0"]; + + // await expect( + // getExpectedFilePaths(mockClient, version, availableVersions), + // ).rejects.toThrow(UCDStoreVersionNotFoundError); + + // expect(mockClient.GET).not.toHaveBeenCalled(); + // }); + + // it("should throw UCDStoreError when API returns error", async () => { + // const version = "15.0.0"; + // const availableVersions = ["14.0.0", "15.0.0", "16.0.0"]; + // const mockError = { message: "API endpoint not found" }; + + // mockClient.GET.mockResolvedValue({ + // data: null, + // error: mockError, + // }); + // vi.mocked(isApiError).mockReturnValue(true); + + // await expect( + // getExpectedFilePaths(mockClient, version, availableVersions), + // ).rejects.toThrow(UCDStoreError); + // await expect( + // getExpectedFilePaths(mockClient, version, availableVersions), + // ).rejects.toThrow("Failed to fetch expected files for version '15.0.0': API endpoint not found"); + // }); + + // it("should handle empty file tree", async () => { + // const version = "15.0.0"; + // const availableVersions = ["15.0.0"]; + // const mockFileTree = {}; + // const expectedPaths: string[] = []; + + // mockClient.GET.mockResolvedValue({ + // data: mockFileTree, + // error: null, + // }); + // vi.mocked(isApiError).mockReturnValue(false); + // vi.mocked(flattenFilePaths).mockReturnValue(expectedPaths); + + // const result = await getExpectedFilePaths(mockClient, version, availableVersions); + + // expect(result).toEqual([]); + // expect(flattenFilePaths).toHaveBeenCalledWith(mockFileTree, "/15.0.0"); + // }); + + // it("should handle version with special characters", async () => { + // const version = "15.0.0-beta"; + // const availableVersions = ["15.0.0-beta"]; + // const mockFileTree = { files: ["test.txt"] }; + // const expectedPaths = ["/15.0.0-beta/test.txt"]; + + // mockClient.GET.mockResolvedValue({ + // data: mockFileTree, + // error: null, + // }); + // vi.mocked(isApiError).mockReturnValue(false); + // vi.mocked(flattenFilePaths).mockReturnValue(expectedPaths); + + // const result = await getExpectedFilePaths(mockClient, version, availableVersions); + + // expect(mockClient.GET).toHaveBeenCalledWith("/api/v1/versions/{version}/file-tree", { + // params: { + // path: { + // version: "15.0.0-beta", + // }, + // }, + // }); + // expect(result).toEqual(expectedPaths); + // }); +}); From 1b2dc8dbf4cf2ea4dc9928855f03d965f752ffae Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 30 Jul 2025 13:32:20 +0200 Subject: [PATCH 10/26] feat(ucd-store): enhance error handling in getExpectedFilePaths function * Improved error handling to account for additional error scenarios. * Added new methods `getFileTree` and `getFilePaths` for better file management. * Updated tests to cover new error handling and file retrieval logic. --- packages/ucd-store/src/internal/files.ts | 4 +- packages/ucd-store/src/store.ts | 28 +++++- .../ucd-store/test/internal/files.test.ts | 96 +++++-------------- packages/ucd-store/tsconfig.json | 3 + 4 files changed, 54 insertions(+), 77 deletions(-) diff --git a/packages/ucd-store/src/internal/files.ts b/packages/ucd-store/src/internal/files.ts index 47a0af9b4..62579f6f2 100644 --- a/packages/ucd-store/src/internal/files.ts +++ b/packages/ucd-store/src/internal/files.ts @@ -30,8 +30,8 @@ export async function getExpectedFilePaths( }, }); - if (isApiError(error)) { - throw new UCDStoreError(`Failed to fetch expected files for version '${version}': ${error.message}`); + if (isApiError(error) || error != null || (data == null && error == null)) { + throw new UCDStoreError(`Failed to fetch expected files for version '${version}': ${error?.message}`); } return flattenFilePaths(data!, `/${version}`); diff --git a/packages/ucd-store/src/store.ts b/packages/ucd-store/src/store.ts index b2a52ee8c..3e1148211 100644 --- a/packages/ucd-store/src/store.ts +++ b/packages/ucd-store/src/store.ts @@ -1,15 +1,15 @@ -import type { UCDClient } from "@ucdjs/fetch"; +import type { UCDClient, UnicodeTreeNode } from "@ucdjs/fetch"; import type { FileSystemBridgeOperationsWithSymbol } from "@ucdjs/fs-bridge"; import type { UCDStoreManifest } from "@ucdjs/schemas"; import type { PathFilter } from "@ucdjs/utils"; import type { AnalyzeOptions, StoreCapabilities, UCDStoreOptions, VersionAnalysis } from "./types"; -import { invariant, prependLeadingSlash } from "@luxass/utils"; +import { invariant, prependLeadingSlash, trimLeadingSlash } from "@luxass/utils"; import { UCDJS_API_BASE_URL } from "@ucdjs/env"; import { createClient, isApiError } from "@ucdjs/fetch"; import { UCDStoreManifestSchema } from "@ucdjs/schemas"; -import { createPathFilter, safeJsonParse } from "@ucdjs/utils"; +import { createPathFilter, flattenFilePaths, safeJsonParse } from "@ucdjs/utils"; import defu from "defu"; -import { join } from "pathe"; +import { basename, join } from "pathe"; import { UCDStoreError, UCDStoreVersionNotFoundError } from "./errors"; import { assertCapabilities, inferStoreCapabilities, requiresCapabilities } from "./internal/capabilities"; import { getExpectedFilePaths } from "./internal/files"; @@ -112,6 +112,26 @@ export class UCDStore { return Object.freeze([...this.#versions]); } + async getFileTree(version: string, extraFilters?: string[]): Promise { + if (!this.#versions.includes(version)) { + throw new UCDStoreVersionNotFoundError(version); + } + + const files = await this.#fs.listdir(join(this.basePath, version), true); + + return files.filter(({ path }) => !this.#filter(trimLeadingSlash(path), extraFilters)); + } + + async getFilePaths(version: string, extraFilters?: string[]): Promise { + if (!this.#versions.includes(version)) { + throw new UCDStoreVersionNotFoundError(version); + } + + const tree = await this.getFileTree(version, extraFilters); + + return flattenFilePaths(tree); + } + /** * Initialize the store - loads existing data or creates new structure */ diff --git a/packages/ucd-store/test/internal/files.test.ts b/packages/ucd-store/test/internal/files.test.ts index f00ae4334..917734fa0 100644 --- a/packages/ucd-store/test/internal/files.test.ts +++ b/packages/ucd-store/test/internal/files.test.ts @@ -1,4 +1,4 @@ -import type { UnicodeTree } from "@ucdjs/fetch"; +import type { ApiError, UnicodeTree } from "@ucdjs/fetch"; import { HttpResponse, mockFetch } from "#msw-utils"; import { UCDJS_API_BASE_URL } from "@ucdjs/env"; import { client } from "@ucdjs/fetch"; @@ -26,7 +26,7 @@ describe("getExpectedFilePaths", () => { { type: "directory", name: "ucd", - path: "/ucd/", + path: "/ucd", lastModified: Date.now(), children: [ { @@ -50,77 +50,31 @@ describe("getExpectedFilePaths", () => { ]); }); - // it("should throw UCDStoreVersionNotFoundError for unavailable version", async () => { - // const version = "99.0.0"; - // const availableVersions = ["14.0.0", "15.0.0", "16.0.0"]; - - // await expect( - // getExpectedFilePaths(mockClient, version, availableVersions), - // ).rejects.toThrow(UCDStoreVersionNotFoundError); - - // expect(mockClient.GET).not.toHaveBeenCalled(); - // }); - - // it("should throw UCDStoreError when API returns error", async () => { - // const version = "15.0.0"; - // const availableVersions = ["14.0.0", "15.0.0", "16.0.0"]; - // const mockError = { message: "API endpoint not found" }; - - // mockClient.GET.mockResolvedValue({ - // data: null, - // error: mockError, - // }); - // vi.mocked(isApiError).mockReturnValue(true); - - // await expect( - // getExpectedFilePaths(mockClient, version, availableVersions), - // ).rejects.toThrow(UCDStoreError); - // await expect( - // getExpectedFilePaths(mockClient, version, availableVersions), - // ).rejects.toThrow("Failed to fetch expected files for version '15.0.0': API endpoint not found"); - // }); - - // it("should handle empty file tree", async () => { - // const version = "15.0.0"; - // const availableVersions = ["15.0.0"]; - // const mockFileTree = {}; - // const expectedPaths: string[] = []; - - // mockClient.GET.mockResolvedValue({ - // data: mockFileTree, - // error: null, - // }); - // vi.mocked(isApiError).mockReturnValue(false); - // vi.mocked(flattenFilePaths).mockReturnValue(expectedPaths); - - // const result = await getExpectedFilePaths(mockClient, version, availableVersions); - - // expect(result).toEqual([]); - // expect(flattenFilePaths).toHaveBeenCalledWith(mockFileTree, "/15.0.0"); - // }); + it("should throw UCDStoreError when API returns error", async () => { + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/:version/file-tree`, () => { + return HttpResponse.json({ + message: "Version not found", + status: 404, + timestamp: new Date().toISOString(), + } satisfies ApiError, { status: 404 }); + }], + ]); - // it("should handle version with special characters", async () => { - // const version = "15.0.0-beta"; - // const availableVersions = ["15.0.0-beta"]; - // const mockFileTree = { files: ["test.txt"] }; - // const expectedPaths = ["/15.0.0-beta/test.txt"]; + await expect( + getExpectedFilePaths(client, "15.0.0"), + ).rejects.toThrow(UCDStoreError); + }); - // mockClient.GET.mockResolvedValue({ - // data: mockFileTree, - // error: null, - // }); - // vi.mocked(isApiError).mockReturnValue(false); - // vi.mocked(flattenFilePaths).mockReturnValue(expectedPaths); + it("should handle empty file tree", async () => { + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/:version/file-tree`, () => { + return HttpResponse.json([], { status: 200 }); + }], + ]); - // const result = await getExpectedFilePaths(mockClient, version, availableVersions); + const result = await getExpectedFilePaths(client, "15.0.0"); - // expect(mockClient.GET).toHaveBeenCalledWith("/api/v1/versions/{version}/file-tree", { - // params: { - // path: { - // version: "15.0.0-beta", - // }, - // }, - // }); - // expect(result).toEqual(expectedPaths); - // }); + expect(result).toEqual([]); + }); }); diff --git a/packages/ucd-store/tsconfig.json b/packages/ucd-store/tsconfig.json index c25d7149d..d280772d4 100644 --- a/packages/ucd-store/tsconfig.json +++ b/packages/ucd-store/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "@ucdjs/tsconfig/base", + "compilerOptions": { + "experimentalDecorators": true + }, "include": [ "src", "test", From 3bd4c5fc2046eb6b364d3e2c34e18dcf72045697 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 08:16:30 +0200 Subject: [PATCH 11/26] chore: dump old test file --- packages/ucd-store/test/store-analyze.test.ts | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 packages/ucd-store/test/store-analyze.test.ts diff --git a/packages/ucd-store/test/store-analyze.test.ts b/packages/ucd-store/test/store-analyze.test.ts new file mode 100644 index 000000000..307d1f149 --- /dev/null +++ b/packages/ucd-store/test/store-analyze.test.ts @@ -0,0 +1,368 @@ +import { HttpResponse, mockFetch } from "#msw-utils"; +import { UCDJS_API_BASE_URL } from "@ucdjs/env"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { testdir } from "vitest-testdirs"; +import { createHTTPUCDStore, createNodeUCDStore, createUCDStore } from "../src/factory"; +import { createMemoryMockFS } from "./__shared"; + +describe("analyze operations", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + }); + + const mockFiles = [ + { + type: "file", + name: "ArabicShaping.txt", + path: "/ArabicShaping.txt", + lastModified: 1644920820000, + }, + { + type: "file", + name: "BidiBrackets.txt", + path: "/BidiBrackets.txt", + lastModified: 1651584360000, + }, + { + type: "directory", + name: "extracted", + path: "/extracted/", + lastModified: 1724676960000, + children: [ + { + type: "file", + name: "DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", + lastModified: 1724609100000, + }, + ], + }, + ]; + + describe("local store analyze operations", () => { + it("should analyze local store with complete files", async () => { + const storeStructure = { + "15.0.0": { + "ArabicShaping.txt": "Arabic shaping data", + "BidiBrackets.txt": "Bidi brackets data", + "extracted": { + "DerivedBidiClass.txt": "Derived bidi class data", + }, + }, + ".ucd-store.json": JSON.stringify([ + { version: "15.0.0", path: "15.0.0" }, + ]), + }; + + const storeDir = await testdir(storeStructure); + + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + return HttpResponse.json(mockFiles); + }], + ]); + + const store = await createNodeUCDStore({ + basePath: storeDir, + }); + + const result = await store.analyze({ checkOrphaned: false }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toHaveLength(1); + expect(result.versions[0]?.version).toBe("15.0.0"); + expect(result.versions[0]?.isComplete).toBe(true); + expect(result.versions[0]?.fileCount).toBe(3); + expect(result.versions[0]?.orphanedFiles).toEqual([]); + expect(result.versions[0]?.missingFiles).toEqual([]); + }); + + it("should analyze local store with orphaned files", async () => { + const storeStructure = { + "15.0.0": { + "ArabicShaping.txt": "Arabic shaping data", + "BidiBrackets.txt": "Bidi brackets data", + "extracted": { + "DerivedBidiClass.txt": "Derived bidi class data", + }, + "OrphanedFile.txt": "This shouldn't be here", + }, + ".ucd-store.json": JSON.stringify([ + { version: "15.0.0", path: "15.0.0" }, + ]), + }; + + const storeDir = await testdir(storeStructure); + + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + return HttpResponse.json(mockFiles); + }], + ]); + + const store = await createNodeUCDStore({ + basePath: storeDir, + }); + + const result = await store.analyze({ checkOrphaned: true }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toHaveLength(1); + expect(result.versions[0]?.version).toBe("15.0.0"); + expect(result.versions[0]?.isComplete).toBe(false); + expect(result.versions[0]?.fileCount).toBe(4); + expect(result.versions[0]?.orphanedFiles).toContain("OrphanedFile.txt"); + expect(result.versions[0]?.missingFiles).toEqual([]); + }); + + it("should analyze multiple versions", async () => { + const storeStructure = { + "15.0.0": { + "ArabicShaping.txt": "Arabic shaping data v15.0.0", + }, + "15.1.0": { + "BidiBrackets.txt": "Bidi brackets data v15.1.0", + }, + ".ucd-store.json": JSON.stringify([ + { version: "15.0.0", path: "15.0.0" }, + { version: "15.1.0", path: "15.1.0" }, + ]), + }; + + const storeDir = await testdir(storeStructure); + + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/:version`, ({ params }) => { + const { version } = params; + if (version === "15.0.0") { + return HttpResponse.json([mockFiles[0]]); + } + if (version === "15.1.0") { + return HttpResponse.json([mockFiles[1]]); + } + return HttpResponse.json([]); + }], + ]); + + const store = await createNodeUCDStore({ + basePath: storeDir, + }); + + const result = await store.analyze({ checkOrphaned: false }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toHaveLength(2); + + const v15_0_0 = result.versions.find((v) => v.version === "15.0.0"); + const v15_1_0 = result.versions.find((v) => v.version === "15.1.0"); + + expect(v15_0_0?.isComplete).toBe(true); + expect(v15_0_0?.fileCount).toBe(1); + expect(v15_1_0?.isComplete).toBe(true); + expect(v15_1_0?.fileCount).toBe(1); + }); + + it("should analyze specific versions only", async () => { + const storeStructure = { + "15.0.0": { + "ArabicShaping.txt": "Arabic shaping data", + }, + "15.1.0": { + "BidiBrackets.txt": "Bidi brackets data", + }, + ".ucd-store.json": JSON.stringify([ + { version: "15.0.0", path: "15.0.0" }, + { version: "15.1.0", path: "15.1.0" }, + ]), + }; + + const storeDir = await testdir(storeStructure); + + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + return HttpResponse.json([mockFiles[0]]); + }], + ]); + + const store = await createNodeUCDStore({ + basePath: storeDir, + }); + + const result = await store.analyze({ + versions: ["15.0.0"], + checkOrphaned: false, + }); + + expect(result.versions).toHaveLength(1); + expect(result.versions[0]?.version).toBe("15.0.0"); + }); + + it("should handle version not found error", async () => { + const storeStructure = { + "15.0.0": { + "ArabicShaping.txt": "Arabic shaping data", + }, + ".ucd-store.json": JSON.stringify([ + { version: "15.0.0", path: "15.0.0" }, + ]), + }; + + const storeDir = await testdir(storeStructure); + + const store = await createNodeUCDStore({ + basePath: storeDir, + }); + + const result = await store.analyze({ + versions: ["99.99.99"], + checkOrphaned: false, + }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toEqual([]); + }); + + it("should handle API errors gracefully", async () => { + const storeStructure = { + "15.0.0": { + "ArabicShaping.txt": "Arabic shaping data", + }, + ".ucd-store.json": JSON.stringify([ + { version: "15.0.0", path: "15.0.0" }, + ]), + }; + + const storeDir = await testdir(storeStructure); + + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + return new Response(null, { status: 500 }); + }], + ]); + + const store = await createNodeUCDStore({ + basePath: storeDir, + }); + + const result = await store.analyze({ checkOrphaned: false }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toEqual([]); + }); + }); + + describe("remote store analyze operations", () => { + it("should analyze remote store with complete files", async () => { + mockFetch([ + [["GET", "HEAD"], `${UCDJS_API_BASE_URL}/api/v1/unicode-proxy/.ucd-store.json`, () => { + return HttpResponse.json([{ version: "15.0.0", path: "/15.0.0" }]); + }], + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + return HttpResponse.json(mockFiles); + }], + ]); + + const store = await createHTTPUCDStore(); + + const result = await store.analyze({ checkOrphaned: false }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toHaveLength(1); + expect(result.versions[0]?.version).toBe("15.0.0"); + expect(result.versions[0]?.isComplete).toBe(true); + expect(result.versions[0]?.fileCount).toBe(3); + }); + + it("should handle remote store with no versions", async () => { + mockFetch([ + [["GET", "HEAD"], `${UCDJS_API_BASE_URL}/api/v1/unicode-proxy/.ucd-store.json`, () => { + return HttpResponse.json([]); + }], + ]); + + const store = await createHTTPUCDStore(); + + const result = await store.analyze({ checkOrphaned: false }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toEqual([]); + expect(result.totalFiles).toBe(0); + }); + }); + + describe("custom store analyze operations", () => { + it("should analyze store with custom filesystem bridge", async () => { + const customFS = createMemoryMockFS(); + await customFS.write("/.ucd-store.json", JSON.stringify([ + { version: "15.0.0", path: "15.0.0" }, + ])); + await customFS.write("/15.0.0/ArabicShaping.txt", "Arabic shaping data"); + + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + return HttpResponse.json([mockFiles[0]]); + }], + ]); + + const store = await createUCDStore({ + basePath: "/", + fs: customFS, + }); + + const result = await store.analyze({ checkOrphaned: false }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toHaveLength(1); + expect(result.versions[0]?.isComplete).toBe(true); + }); + }); + + describe("analyze edge cases", () => { + it("should handle empty store", async () => { + const storeStructure = { + ".ucd-store.json": JSON.stringify([]), + }; + + const storeDir = await testdir(storeStructure); + + const store = await createNodeUCDStore({ + basePath: storeDir, + }); + + const result = await store.analyze({ checkOrphaned: false }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toEqual([]); + expect(result.totalFiles).toBe(0); + }); + + it("should handle store with empty version directory", async () => { + const storeStructure = { + "15.0.0": {}, + ".ucd-store.json": JSON.stringify([ + { version: "15.0.0", path: "15.0.0" }, + ]), + }; + + const storeDir = await testdir(storeStructure); + + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + return HttpResponse.json([]); + }], + ]); + + const store = await createNodeUCDStore({ + basePath: storeDir, + }); + + const result = await store.analyze({ checkOrphaned: false }); + + expect(result.storeHealth).toBe("healthy"); + expect(result.versions).toHaveLength(1); + expect(result.versions[0]?.fileCount).toBe(0); + expect(result.versions[0]?.isComplete).toBe(true); + }); + }); +}); From 651babe3c71fad6352353744ecc8acf7b5906992 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 12:21:23 +0200 Subject: [PATCH 12/26] chore: add experimental decorators to compiler options * Enables the use of experimental decorators in the TypeScript configuration. --- packages/ucd-store/tsconfig.build.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ucd-store/tsconfig.build.json b/packages/ucd-store/tsconfig.build.json index ec6cc954e..088a39580 100644 --- a/packages/ucd-store/tsconfig.build.json +++ b/packages/ucd-store/tsconfig.build.json @@ -1,5 +1,8 @@ { "extends": "@ucdjs/tsconfig/base.build", + "compilerOptions": { + "experimentalDecorators": true + }, "include": ["src"], "exclude": ["dist"] } From 508e1bd03705d7ebdc445836ba15c817396fdcb4 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 12:22:25 +0200 Subject: [PATCH 13/26] fix(ucd-store): set default basePath to './' in createNodeUCDStore This is because when using the Node FS Bridge we are using a base path, and the base path that is being used by the different features inside the ucd-store will also use this basePath. So in the end the base path will be duplicated. --- packages/ucd-store/src/factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ucd-store/src/factory.ts b/packages/ucd-store/src/factory.ts index c3c317d5f..276cf3729 100644 --- a/packages/ucd-store/src/factory.ts +++ b/packages/ucd-store/src/factory.ts @@ -36,7 +36,7 @@ export async function createNodeUCDStore(options: Omit = const store = new UCDStore({ ...options, fs: fs({ - basePath: options.basePath || "", + basePath: "./", }), }); From 1aa8ea49fd0c93964d8111324c642992a91f7a0b Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 12:22:39 +0200 Subject: [PATCH 14/26] fix(cli): handle version selection more robustly * Moved the version selection logic inside the try block to ensure that the remote or store directory is validated before prompting for versions. * This change prevents potential errors when no versions are provided and improves error handling during the initialization process. --- packages/cli/src/cmd/store/init.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/cmd/store/init.ts b/packages/cli/src/cmd/store/init.ts index 38da8902d..6f5421ac0 100644 --- a/packages/cli/src/cmd/store/init.ts +++ b/packages/cli/src/cmd/store/init.ts @@ -40,25 +40,26 @@ export async function runInitStore({ flags, versions }: CLIStoreInitCmdOptions) patterns, } = flags; - if (!versions || versions.length === 0) { - const pickedVersions = await runVersionPrompt(); + try { + assertRemoteOrStoreDir(flags); - if (pickedVersions.length === 0) { - console.error("No versions selected. Operation cancelled."); - return; - } + if (!versions || versions.length === 0) { + const pickedVersions = await runVersionPrompt(); - versions = pickedVersions; - } + if (pickedVersions.length === 0) { + console.error("No versions selected. Operation cancelled."); + return; + } - try { - assertRemoteOrStoreDir(flags); + versions = pickedVersions; + } const store = await createStoreFromFlags({ baseUrl, storeDir, remote, patterns, + versions, }); if (store == null) { From 8aacb20120de7eafbf232165d1a0bd59de83bcee Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 12:22:52 +0200 Subject: [PATCH 15/26] feat(ucd-store): add support for versions in UCDStore constructor - Updated the constructor to accept a `versions` parameter. - Initialized the private `#versions` property with the provided versions. --- packages/ucd-store/src/store.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ucd-store/src/store.ts b/packages/ucd-store/src/store.ts index 3e1148211..f47213e49 100644 --- a/packages/ucd-store/src/store.ts +++ b/packages/ucd-store/src/store.ts @@ -9,7 +9,7 @@ import { createClient, isApiError } from "@ucdjs/fetch"; import { UCDStoreManifestSchema } from "@ucdjs/schemas"; import { createPathFilter, flattenFilePaths, safeJsonParse } from "@ucdjs/utils"; import defu from "defu"; -import { basename, join } from "pathe"; +import { join } from "pathe"; import { UCDStoreError, UCDStoreVersionNotFoundError } from "./errors"; import { assertCapabilities, inferStoreCapabilities, requiresCapabilities } from "./internal/capabilities"; import { getExpectedFilePaths } from "./internal/files"; @@ -40,10 +40,11 @@ export class UCDStore { }; constructor(options: UCDStoreOptions) { - const { baseUrl, globalFilters, fs, basePath } = defu(options, { + const { baseUrl, globalFilters, fs, basePath, versions } = defu(options, { baseUrl: UCDJS_API_BASE_URL, globalFilters: [], basePath: "", + versions: [], }); if (fs == null) { @@ -56,6 +57,7 @@ export class UCDStore { this.#filter = createPathFilter(globalFilters); this.#fs = fs as FileSystemBridgeOperationsWithSymbol; this.#capabilities = inferStoreCapabilities(this.#fs); + this.#versions = versions; this.#manifestPath = join(this.basePath, ".ucd-store.json"); } From 2dd1e822174c5480b4ad12e3bb861cbfa3cee275 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 12:22:58 +0200 Subject: [PATCH 16/26] fix(ucd-store): remove unused error import in files.test.ts * Eliminated the import of `UCDStoreVersionNotFoundError` as it was not utilized in the test file. --- packages/ucd-store/test/internal/files.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ucd-store/test/internal/files.test.ts b/packages/ucd-store/test/internal/files.test.ts index 917734fa0..fb330d701 100644 --- a/packages/ucd-store/test/internal/files.test.ts +++ b/packages/ucd-store/test/internal/files.test.ts @@ -3,7 +3,7 @@ import { HttpResponse, mockFetch } from "#msw-utils"; import { UCDJS_API_BASE_URL } from "@ucdjs/env"; import { client } from "@ucdjs/fetch"; import { describe, expect, it, vi } from "vitest"; -import { UCDStoreError, UCDStoreVersionNotFoundError } from "../../src/errors"; +import { UCDStoreError } from "../../src/errors"; import { getExpectedFilePaths } from "../../src/internal/files"; describe("getExpectedFilePaths", () => { From 3d586c5b14d8f0e49c4fc107a0a4fb4f674ba239 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 13:57:24 +0200 Subject: [PATCH 17/26] refactor(store): improve file path handling and analysis logic * Updated `listdir` calls to use `joinURL` for correct path resolution. * Simplified return values in `getExpectedFilePaths` by removing unnecessary version prefix. * Enhanced filtering logic in `analyze` methods to improve clarity and correctness. * Introduced `stripChildrenFromEntries` utility function for cleaner entry processing in tests. * Adjusted test cases to reflect changes in expected file paths and analysis results. --- packages/fs-bridge/src/bridges/http.ts | 2 +- packages/ucd-store/src/internal/files.ts | 2 +- packages/ucd-store/src/store.ts | 18 +- packages/ucd-store/test/__shared.ts | 7 + .../ucd-store/test/internal/files.test.ts | 6 +- packages/ucd-store/test/store-analyze.test.ts | 158 +++++++++--------- 6 files changed, 105 insertions(+), 88 deletions(-) diff --git a/packages/fs-bridge/src/bridges/http.ts b/packages/fs-bridge/src/bridges/http.ts index eac5847a8..c357b97a3 100644 --- a/packages/fs-bridge/src/bridges/http.ts +++ b/packages/fs-bridge/src/bridges/http.ts @@ -90,7 +90,7 @@ const HTTPFileSystemBridge = defineFileSystemBridge({ const entries: FSEntry[] = []; for (const entry of data) { if (entry.type === "directory") { - const children = await this.listdir(entry.path, true); + const children = await this.listdir(joinURL(path, entry.path), true); entries.push({ type: "directory", name: entry.name, diff --git a/packages/ucd-store/src/internal/files.ts b/packages/ucd-store/src/internal/files.ts index 62579f6f2..d2748ecd1 100644 --- a/packages/ucd-store/src/internal/files.ts +++ b/packages/ucd-store/src/internal/files.ts @@ -34,5 +34,5 @@ export async function getExpectedFilePaths( throw new UCDStoreError(`Failed to fetch expected files for version '${version}': ${error?.message}`); } - return flattenFilePaths(data!, `/${version}`); + return flattenFilePaths(data!); } diff --git a/packages/ucd-store/src/store.ts b/packages/ucd-store/src/store.ts index f47213e49..5a11a1482 100644 --- a/packages/ucd-store/src/store.ts +++ b/packages/ucd-store/src/store.ts @@ -121,7 +121,7 @@ export class UCDStore { const files = await this.#fs.listdir(join(this.basePath, version), true); - return files.filter(({ path }) => !this.#filter(trimLeadingSlash(path), extraFilters)); + return files.filter(({ path }) => this.#filter(trimLeadingSlash(path), extraFilters)); } async getFilePaths(version: string, extraFilters?: string[]): Promise { @@ -211,12 +211,16 @@ export class UCDStore { const orphanedFiles: string[] = []; const missingFiles: string[] = []; - if (checkOrphaned) { - for (const file of actualFiles) { - // if file is not in expected files, it's orphaned - if (!expectedFiles.includes(file)) { - orphanedFiles.push(file); - } + for (const expectedFile of expectedFiles) { + if (!actualFiles.includes(expectedFile)) { + missingFiles.push(expectedFile); + } + } + + for (const actualFile of actualFiles) { + // if file is not in expected files, it's orphaned + if (checkOrphaned && !expectedFiles.includes(actualFile)) { + orphanedFiles.push(actualFile); } } diff --git a/packages/ucd-store/test/__shared.ts b/packages/ucd-store/test/__shared.ts index 661e095b7..7319fe9d8 100644 --- a/packages/ucd-store/test/__shared.ts +++ b/packages/ucd-store/test/__shared.ts @@ -81,3 +81,10 @@ export const createMemoryMockFS = defineFileSystemBridge({ }; }, }); + +export function stripChildrenFromEntries(entries: TObj[]): Omit[] { + return entries.map((entry) => { + const { children: _children, ...rest } = entry; + return rest; + }); +} diff --git a/packages/ucd-store/test/internal/files.test.ts b/packages/ucd-store/test/internal/files.test.ts index fb330d701..c7163d168 100644 --- a/packages/ucd-store/test/internal/files.test.ts +++ b/packages/ucd-store/test/internal/files.test.ts @@ -44,9 +44,9 @@ describe("getExpectedFilePaths", () => { const result = await getExpectedFilePaths(client, "15.0.0"); expect(result).toEqual([ - "/15.0.0/ReadMe.txt", - "/15.0.0/UnicodeData.txt", - "/15.0.0/ucd/emoji-data.txt", + "/ReadMe.txt", + "/UnicodeData.txt", + "/ucd/emoji-data.txt", ]); }); diff --git a/packages/ucd-store/test/store-analyze.test.ts b/packages/ucd-store/test/store-analyze.test.ts index 307d1f149..b636244d0 100644 --- a/packages/ucd-store/test/store-analyze.test.ts +++ b/packages/ucd-store/test/store-analyze.test.ts @@ -3,7 +3,7 @@ import { UCDJS_API_BASE_URL } from "@ucdjs/env"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { testdir } from "vitest-testdirs"; import { createHTTPUCDStore, createNodeUCDStore, createUCDStore } from "../src/factory"; -import { createMemoryMockFS } from "./__shared"; +import { createMemoryMockFS, stripChildrenFromEntries } from "./__shared"; describe("analyze operations", () => { beforeEach(() => { @@ -27,13 +27,13 @@ describe("analyze operations", () => { { type: "directory", name: "extracted", - path: "/extracted/", + path: "/extracted", lastModified: 1724676960000, children: [ { type: "file", name: "DerivedBidiClass.txt", - path: "/extracted/DerivedBidiClass.txt", + path: "/DerivedBidiClass.txt", lastModified: 1724609100000, }, ], @@ -50,15 +50,15 @@ describe("analyze operations", () => { "DerivedBidiClass.txt": "Derived bidi class data", }, }, - ".ucd-store.json": JSON.stringify([ - { version: "15.0.0", path: "15.0.0" }, - ]), + ".ucd-store.json": JSON.stringify({ + "15.0.0": "/15.0.0", + }), }; const storeDir = await testdir(storeStructure); mockFetch([ - ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/15.0.0/file-tree`, () => { return HttpResponse.json(mockFiles); }], ]); @@ -67,15 +67,14 @@ describe("analyze operations", () => { basePath: storeDir, }); - const result = await store.analyze({ checkOrphaned: false }); + const analyzeResult = await store.analyze({ checkOrphaned: false }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toHaveLength(1); - expect(result.versions[0]?.version).toBe("15.0.0"); - expect(result.versions[0]?.isComplete).toBe(true); - expect(result.versions[0]?.fileCount).toBe(3); - expect(result.versions[0]?.orphanedFiles).toEqual([]); - expect(result.versions[0]?.missingFiles).toEqual([]); + expect(analyzeResult).toHaveLength(1); + expect(analyzeResult[0]?.version).toBe("15.0.0"); + expect(analyzeResult[0]?.isComplete).toBe(true); + expect(analyzeResult[0]?.fileCount).toBe(3); + expect(analyzeResult[0]?.orphanedFiles).toEqual([]); + expect(analyzeResult[0]?.missingFiles).toEqual([]); }); it("should analyze local store with orphaned files", async () => { @@ -88,15 +87,15 @@ describe("analyze operations", () => { }, "OrphanedFile.txt": "This shouldn't be here", }, - ".ucd-store.json": JSON.stringify([ - { version: "15.0.0", path: "15.0.0" }, - ]), + ".ucd-store.json": JSON.stringify({ + "15.0.0": "/15.0.0", + }), }; const storeDir = await testdir(storeStructure); mockFetch([ - ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/15.0.0/file-tree`, () => { return HttpResponse.json(mockFiles); }], ]); @@ -105,15 +104,14 @@ describe("analyze operations", () => { basePath: storeDir, }); - const result = await store.analyze({ checkOrphaned: true }); + const analysisResult = await store.analyze({ checkOrphaned: true }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toHaveLength(1); - expect(result.versions[0]?.version).toBe("15.0.0"); - expect(result.versions[0]?.isComplete).toBe(false); - expect(result.versions[0]?.fileCount).toBe(4); - expect(result.versions[0]?.orphanedFiles).toContain("OrphanedFile.txt"); - expect(result.versions[0]?.missingFiles).toEqual([]); + expect(analysisResult).toHaveLength(1); + expect(analysisResult[0]?.version).toBe("15.0.0"); + expect(analysisResult[0]?.isComplete).toBe(false); + expect(analysisResult[0]?.fileCount).toBe(4); + expect(analysisResult[0]?.orphanedFiles).toContain("/OrphanedFile.txt"); + expect(analysisResult[0]?.missingFiles).toEqual([]); }); it("should analyze multiple versions", async () => { @@ -124,16 +122,16 @@ describe("analyze operations", () => { "15.1.0": { "BidiBrackets.txt": "Bidi brackets data v15.1.0", }, - ".ucd-store.json": JSON.stringify([ - { version: "15.0.0", path: "15.0.0" }, - { version: "15.1.0", path: "15.1.0" }, - ]), + ".ucd-store.json": JSON.stringify({ + "15.0.0": "/15.0.0", + "15.1.0": "/15.1.0", + }), }; const storeDir = await testdir(storeStructure); mockFetch([ - ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/:version`, ({ params }) => { + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/:version/file-tree`, ({ params }) => { const { version } = params; if (version === "15.0.0") { return HttpResponse.json([mockFiles[0]]); @@ -149,13 +147,12 @@ describe("analyze operations", () => { basePath: storeDir, }); - const result = await store.analyze({ checkOrphaned: false }); + const analysisResult = await store.analyze({ checkOrphaned: false }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toHaveLength(2); + expect(analysisResult).toHaveLength(2); - const v15_0_0 = result.versions.find((v) => v.version === "15.0.0"); - const v15_1_0 = result.versions.find((v) => v.version === "15.1.0"); + const v15_0_0 = analysisResult.find((v) => v.version === "15.0.0"); + const v15_1_0 = analysisResult.find((v) => v.version === "15.1.0"); expect(v15_0_0?.isComplete).toBe(true); expect(v15_0_0?.fileCount).toBe(1); @@ -171,16 +168,16 @@ describe("analyze operations", () => { "15.1.0": { "BidiBrackets.txt": "Bidi brackets data", }, - ".ucd-store.json": JSON.stringify([ - { version: "15.0.0", path: "15.0.0" }, - { version: "15.1.0", path: "15.1.0" }, - ]), + ".ucd-store.json": JSON.stringify({ + "15.0.0": "/15.0.0", + "15.1.0": "/15.1.0", + }), }; const storeDir = await testdir(storeStructure); mockFetch([ - ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/15.0.0/file-tree`, () => { return HttpResponse.json([mockFiles[0]]); }], ]); @@ -189,13 +186,13 @@ describe("analyze operations", () => { basePath: storeDir, }); - const result = await store.analyze({ + const analysisResult = await store.analyze({ versions: ["15.0.0"], checkOrphaned: false, }); - expect(result.versions).toHaveLength(1); - expect(result.versions[0]?.version).toBe("15.0.0"); + expect(analysisResult).toHaveLength(1); + expect(analysisResult[0]?.version).toBe("15.0.0"); }); it("should handle version not found error", async () => { @@ -203,9 +200,9 @@ describe("analyze operations", () => { "15.0.0": { "ArabicShaping.txt": "Arabic shaping data", }, - ".ucd-store.json": JSON.stringify([ - { version: "15.0.0", path: "15.0.0" }, - ]), + ".ucd-store.json": JSON.stringify({ + "15.0.0": "/15.0.0", + }), }; const storeDir = await testdir(storeStructure); @@ -214,13 +211,12 @@ describe("analyze operations", () => { basePath: storeDir, }); - const result = await store.analyze({ + const analysisResult = await store.analyze({ versions: ["99.99.99"], checkOrphaned: false, }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toEqual([]); + expect(analysisResult).toEqual([]); }); it("should handle API errors gracefully", async () => { @@ -228,15 +224,15 @@ describe("analyze operations", () => { "15.0.0": { "ArabicShaping.txt": "Arabic shaping data", }, - ".ucd-store.json": JSON.stringify([ - { version: "15.0.0", path: "15.0.0" }, - ]), + ".ucd-store.json": JSON.stringify({ + "15.0.0": "/15.0.0", + }), }; const storeDir = await testdir(storeStructure); mockFetch([ - ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/15.0.0/file-tree`, () => { return new Response(null, { status: 500 }); }], ]); @@ -245,58 +241,68 @@ describe("analyze operations", () => { basePath: storeDir, }); - const result = await store.analyze({ checkOrphaned: false }); + const analysisResult = await store.analyze({ checkOrphaned: false }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toEqual([]); + expect(analysisResult).toEqual([]); }); }); describe("remote store analyze operations", () => { it("should analyze remote store with complete files", async () => { mockFetch([ - [["GET", "HEAD"], `${UCDJS_API_BASE_URL}/api/v1/unicode-proxy/.ucd-store.json`, () => { - return HttpResponse.json([{ version: "15.0.0", path: "/15.0.0" }]); + [["GET", "HEAD"], `${UCDJS_API_BASE_URL}/api/v1/files/.ucd-store.json`, () => { + return HttpResponse.json({ + "15.0.0": "/15.0.0", + }); }], - ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/15.0.0/file-tree`, () => { return HttpResponse.json(mockFiles); }], + ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0/:file?`, ({ params }) => { + const file = params.file; + + if (file === "extracted") { + return HttpResponse.json(mockFiles[2]?.children); + } + + return HttpResponse.json(stripChildrenFromEntries(mockFiles)); + }], ]); const store = await createHTTPUCDStore(); - const result = await store.analyze({ checkOrphaned: false }); + const analysisResult = await store.analyze({ checkOrphaned: false }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toHaveLength(1); - expect(result.versions[0]?.version).toBe("15.0.0"); - expect(result.versions[0]?.isComplete).toBe(true); - expect(result.versions[0]?.fileCount).toBe(3); + expect(analysisResult).toHaveLength(1); + expect(analysisResult[0]?.version).toBe("15.0.0"); + expect(analysisResult[0]?.isComplete).toBe(true); + expect(analysisResult[0]?.fileCount).toBe(3); }); it("should handle remote store with no versions", async () => { mockFetch([ - [["GET", "HEAD"], `${UCDJS_API_BASE_URL}/api/v1/unicode-proxy/.ucd-store.json`, () => { - return HttpResponse.json([]); + [["GET", "HEAD"], `${UCDJS_API_BASE_URL}/api/v1/files/.ucd-store.json`, () => { + return HttpResponse.json({}); }], ]); const store = await createHTTPUCDStore(); - const result = await store.analyze({ checkOrphaned: false }); + const analysisResult = await store.analyze({ checkOrphaned: false }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toEqual([]); - expect(result.totalFiles).toBe(0); + expect(analysisResult).toEqual([]); + + const totalFileCount = analysisResult.reduce((sum, { fileCount }) => sum + fileCount, 0); + expect(totalFileCount).toBe(0); }); }); describe("custom store analyze operations", () => { it("should analyze store with custom filesystem bridge", async () => { const customFS = createMemoryMockFS(); - await customFS.write("/.ucd-store.json", JSON.stringify([ - { version: "15.0.0", path: "15.0.0" }, - ])); + await customFS.write("/.ucd-store.json", JSON.stringify({ + "15.0.0": "/15.0.0", + })); await customFS.write("/15.0.0/ArabicShaping.txt", "Arabic shaping data"); mockFetch([ From 190846efa5c2c06220d25238131056b3f824503a Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 15:07:28 +0200 Subject: [PATCH 18/26] test: update custom store analyze operations and improve assertions * Changed `describe` to `describe.todo` for custom store analyze operations. * Enhanced assertions in the test for analyzing a store with no files. * Improved clarity and consistency in the test structure. --- packages/ucd-store/test/store-analyze.test.ts | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/packages/ucd-store/test/store-analyze.test.ts b/packages/ucd-store/test/store-analyze.test.ts index b636244d0..2d6df39bc 100644 --- a/packages/ucd-store/test/store-analyze.test.ts +++ b/packages/ucd-store/test/store-analyze.test.ts @@ -297,7 +297,7 @@ describe("analyze operations", () => { }); }); - describe("custom store analyze operations", () => { + describe.todo("custom store analyze operations", () => { it("should analyze store with custom filesystem bridge", async () => { const customFS = createMemoryMockFS(); await customFS.write("/.ucd-store.json", JSON.stringify({ @@ -311,64 +311,62 @@ describe("analyze operations", () => { }], ]); + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/15.0.0/file-tree`, () => { + return HttpResponse.json(mockFiles); + }], + ]); + const store = await createUCDStore({ basePath: "/", fs: customFS, }); - const result = await store.analyze({ checkOrphaned: false }); + const analysisResult = await store.analyze({ checkOrphaned: false }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toHaveLength(1); - expect(result.versions[0]?.isComplete).toBe(true); + expect(analysisResult).toHaveLength(1); + expect(analysisResult[0]?.isComplete).toBe(true); }); }); - describe("analyze edge cases", () => { - it("should handle empty store", async () => { - const storeStructure = { - ".ucd-store.json": JSON.stringify([]), - }; + it("should handle empty store", async () => { + const storeDir = await testdir({ + ".ucd-store.json": JSON.stringify({}), + }); - const storeDir = await testdir(storeStructure); + const store = await createNodeUCDStore({ + basePath: storeDir, + }); - const store = await createNodeUCDStore({ - basePath: storeDir, - }); + const analysisResult = await store.analyze({ checkOrphaned: false }); - const result = await store.analyze({ checkOrphaned: false }); + expect(analysisResult).toEqual([]); + expect(analysisResult.length).toBe(0); + }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toEqual([]); - expect(result.totalFiles).toBe(0); + it("should analyse store with no files", async () => { + const storeDir = await testdir({ + "15.0.0": {}, + ".ucd-store.json": JSON.stringify({ + "15.0.0": "/15.0.0", + }), }); - it("should handle store with empty version directory", async () => { - const storeStructure = { - "15.0.0": {}, - ".ucd-store.json": JSON.stringify([ - { version: "15.0.0", path: "15.0.0" }, - ]), - }; + mockFetch([ + ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/15.0.0/file-tree`, () => { + return HttpResponse.json([]); + }], + ]); - const storeDir = await testdir(storeStructure); - - mockFetch([ - ["GET", `${UCDJS_API_BASE_URL}/api/v1/files/15.0.0`, () => { - return HttpResponse.json([]); - }], - ]); - - const store = await createNodeUCDStore({ - basePath: storeDir, - }); + const store = await createNodeUCDStore({ + basePath: storeDir, + }); - const result = await store.analyze({ checkOrphaned: false }); + const analysisResult = await store.analyze({ checkOrphaned: false }); - expect(result.storeHealth).toBe("healthy"); - expect(result.versions).toHaveLength(1); - expect(result.versions[0]?.fileCount).toBe(0); - expect(result.versions[0]?.isComplete).toBe(true); - }); + expect(analysisResult).toHaveLength(1); + expect(analysisResult[0]?.version).toBe("15.0.0"); + expect(analysisResult[0]?.fileCount).toBe(0); + expect(analysisResult[0]?.isComplete).toBe(true); }); }); From 1b539ab2b8c4511c446ed41114b850f5350358e7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 15:30:48 +0200 Subject: [PATCH 19/26] test(ucd-store): integrate `memfs` for enhanced filesystem operations * Added `memfs` as a dependency for improved memory filesystem handling. * Updated `createMemoryMockFS` to utilize `memfs` for file operations. * Enhanced `listdir` functionality to support recursive directory listing. * Refactored tests to ensure compatibility with the new filesystem bridge. --- packages/ucd-store/package.json | 1 + packages/ucd-store/test/__shared.ts | 92 ++++++++++++++++--- packages/ucd-store/test/store-analyze.test.ts | 4 +- packages/utils/package.json | 1 - pnpm-lock.yaml | 18 ++-- pnpm-workspace.yaml | 2 +- 6 files changed, 91 insertions(+), 27 deletions(-) diff --git a/packages/ucd-store/package.json b/packages/ucd-store/package.json index dc970856b..c94d6f892 100644 --- a/packages/ucd-store/package.json +++ b/packages/ucd-store/package.json @@ -58,6 +58,7 @@ "@ucdjs/tsconfig": "workspace:*", "@ucdjs/tsdown-config": "workspace:*", "eslint": "catalog:linting", + "memfs": "catalog:dev", "publint": "catalog:dev", "tsdown": "catalog:dev", "tsx": "catalog:dev", diff --git a/packages/ucd-store/test/__shared.ts b/packages/ucd-store/test/__shared.ts index 7319fe9d8..1363c3691 100644 --- a/packages/ucd-store/test/__shared.ts +++ b/packages/ucd-store/test/__shared.ts @@ -1,4 +1,9 @@ +import type { FSEntry } from "@ucdjs/fs-bridge"; +import type Dirent from "memfs/lib/Dirent"; +import { dirname, join, relative } from "node:path"; +import { prependLeadingSlash, trimTrailingSlash } from "@luxass/utils"; import { defineFileSystemBridge } from "@ucdjs/fs-bridge"; +import { memfs } from "memfs"; export const createReadOnlyMockFS = defineFileSystemBridge({ capabilities: { @@ -46,37 +51,96 @@ export const createMemoryMockFS = defineFileSystemBridge({ capabilities: { read: true, write: true, - listdir: false, + listdir: true, mkdir: false, exists: true, rm: false, }, state: { - map: new Map(), + fs: memfs().fs, }, setup({ state }) { return { async read(path) { - const content = state.map.get(path); - if (content === undefined) { - throw new Error(`File not found: ${path}`); - } - return content; + return state.fs.readFileSync(path, "utf8"); }, async exists(path) { - return state.map.has(path); + return state.fs.existsSync(path); }, - async listdir() { - throw new Error("listdir not supported"); + async listdir(path, recursive = false) { + function createFSEntry(entry: Dirent): FSEntry { + const pathFromName = prependLeadingSlash(trimTrailingSlash(entry.name)); + return entry.isDirectory() + ? { + type: "directory", + name: entry.name, + path: pathFromName, + children: [], + } + : { + type: "file", + name: entry.name, + path: pathFromName, + }; + } + + if (!recursive) { + const entries = state.fs.readdirSync(path, { withFileTypes: true }) as Dirent[]; + return entries.map((entry) => createFSEntry(entry)); + } + + const allEntries = state.fs.readdirSync(path, { + withFileTypes: true, + recursive: true, + }) as Dirent[]; + + const entryMap = new Map(); + const rootEntries: FSEntry[] = []; + + for (const entry of allEntries) { + const entryPath = entry.parentPath || entry.path; + const relativeToTarget = relative(path, entryPath); + const fsEntry = createFSEntry(entry); + + const entryRelativePath = relativeToTarget + ? join(relativeToTarget, entry.name) + : entry.name; + + entryMap.set(entryRelativePath, fsEntry); + + if (!relativeToTarget) { + rootEntries.push(fsEntry); + } + } + + for (const [entryPath, entry] of entryMap) { + const parentPath = dirname(entryPath); + if (parentPath && parentPath !== ".") { + const parent = entryMap.get(parentPath); + if (parent?.type === "directory") { + parent.children!.push(entry); + } + } + } + + return rootEntries; }, async mkdir(path) { - state.map.set(path, ""); + state.fs.mkdirSync(path); }, async rm(path) { - state.map.delete(path); + state.fs.rmSync(path); }, - async write(path, data) { - state.map.set(path, data); + async write(path, data, encoding = "utf8") { + // ensure the directory exists + const dir = dirname(path); + if (!state.fs.existsSync(dir)) { + state.fs.mkdirSync(dir, { recursive: true }); + } + + state.fs.writeFileSync(path, data, { + encoding, + }); }, }; }, diff --git a/packages/ucd-store/test/store-analyze.test.ts b/packages/ucd-store/test/store-analyze.test.ts index 2d6df39bc..a69808702 100644 --- a/packages/ucd-store/test/store-analyze.test.ts +++ b/packages/ucd-store/test/store-analyze.test.ts @@ -297,7 +297,7 @@ describe("analyze operations", () => { }); }); - describe.todo("custom store analyze operations", () => { + describe("custom store analyze operations", () => { it("should analyze store with custom filesystem bridge", async () => { const customFS = createMemoryMockFS(); await customFS.write("/.ucd-store.json", JSON.stringify({ @@ -313,7 +313,7 @@ describe("analyze operations", () => { mockFetch([ ["GET", `${UCDJS_API_BASE_URL}/api/v1/versions/15.0.0/file-tree`, () => { - return HttpResponse.json(mockFiles); + return HttpResponse.json([mockFiles[0]]); }], ]); diff --git a/packages/utils/package.json b/packages/utils/package.json index 10d6a38f4..a5adfcc2f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -44,7 +44,6 @@ "@ucdjs/env": "workspace:*", "@ucdjs/fetch": "workspace:*", "defu": "catalog:prod", - "memfs": "catalog:prod", "picomatch": "catalog:prod", "zod": "catalog:prod" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79ee0de4c..618029766 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ catalogs: '@types/yargs-parser': specifier: ^21.0.3 version: 21.0.3 + memfs: + specifier: ^4.23.0 + version: 4.25.0 nanotar: specifier: ^0.2.0 version: 0.2.0 @@ -102,9 +105,6 @@ catalogs: knitwork: specifier: ^1.2.0 version: 1.2.0 - memfs: - specifier: ^4.23.0 - version: 4.23.0 openapi-fetch: specifier: ^0.14.0 version: 0.14.0 @@ -672,6 +672,9 @@ importers: eslint: specifier: catalog:linting version: 9.32.0(jiti@2.4.2) + memfs: + specifier: catalog:dev + version: 4.25.0 publint: specifier: catalog:dev version: 0.3.12 @@ -705,9 +708,6 @@ importers: defu: specifier: catalog:prod version: 6.1.4 - memfs: - specifier: catalog:prod - version: 4.23.0 picomatch: specifier: catalog:prod version: 4.0.3 @@ -4186,8 +4186,8 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - memfs@4.23.0: - resolution: {integrity: sha512-SucHN2lcWf0jrnw+jP6FoVW6l/zGJiXfNMdApZzG0x/0mAIMdwAeR5mjfsCH5U3BoqpUEtqzz+dSQSO0H/eqxg==} + memfs@4.25.0: + resolution: {integrity: sha512-VZhVrmQJFuWhjGAye01xO2N7SQZ507j3rq5mfebhcXbYTj7b4iapuA1w+LPJEp+uGKtDTYmBWZa9BNZDem1yuA==} engines: {node: '>= 4.0.0'} merge2@1.4.1: @@ -9616,7 +9616,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - memfs@4.23.0: + memfs@4.25.0: dependencies: '@jsonjoy.com/json-pack': 1.2.0(tslib@2.8.1) '@jsonjoy.com/util': 1.6.0(tslib@2.8.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6ebda849f..e81e141be 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -48,7 +48,6 @@ catalogs: p-limit: ^6.2.0 knitwork: ^1.2.0 picomatch: ^4.0.3 - memfs: ^4.23.0 apache-autoindex-parse: ^3.0.0 openapi-fetch: ^0.14.0 pathe: ^2.0.3 @@ -64,6 +63,7 @@ catalogs: nanotar: ^0.2.0 openapi-typescript: ^7.8.0 tsx: ^4.20.3 + memfs: ^4.23.0 workers: wrangler: ^4.26.1 From 28c3338fbe59474a2dca4f65e21598a5fece83e2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 15:59:38 +0200 Subject: [PATCH 20/26] chore(eslint): update `@luxass/eslint-config` to version 5.2.1 * Added `GLOB_TESTS` to the ignores list in ESLint configuration for better file handling. * Updated `createMemoryMockFS` to ensure type safety by casting return values. * Improved path handling in `createFSEntry` and `entryMap` for consistency. --- packages/fs-bridge/eslint.config.js | 4 +- packages/ucd-store/eslint.config.js | 4 +- packages/ucd-store/test/__shared.ts | 13 +++--- pnpm-lock.yaml | 65 +++++++++++++---------------- pnpm-workspace.yaml | 2 +- 5 files changed, 42 insertions(+), 46 deletions(-) diff --git a/packages/fs-bridge/eslint.config.js b/packages/fs-bridge/eslint.config.js index c1bf22109..ecc434a96 100644 --- a/packages/fs-bridge/eslint.config.js +++ b/packages/fs-bridge/eslint.config.js @@ -1,11 +1,11 @@ // @ts-check -import { luxass } from "@luxass/eslint-config"; +import { GLOB_TESTS, luxass } from "@luxass/eslint-config"; export default luxass({ type: "lib", pnpm: true, }).append({ - ignores: ["src/bridges/node.ts"], + ignores: ["src/bridges/node.ts", ...GLOB_TESTS], rules: { "no-restricted-imports": ["error", { patterns: [ diff --git a/packages/ucd-store/eslint.config.js b/packages/ucd-store/eslint.config.js index dd1f5bfbb..c3c48ce4e 100644 --- a/packages/ucd-store/eslint.config.js +++ b/packages/ucd-store/eslint.config.js @@ -1,11 +1,11 @@ // @ts-check -import { luxass } from "@luxass/eslint-config"; +import { GLOB_TESTS, luxass } from "@luxass/eslint-config"; export default luxass({ type: "lib", pnpm: true, }).append({ - ignores: ["playgrounds/node-playground.ts"], + ignores: ["playgrounds/node-playground.ts", ...GLOB_TESTS], rules: { "no-restricted-imports": ["error", { patterns: [ diff --git a/packages/ucd-store/test/__shared.ts b/packages/ucd-store/test/__shared.ts index 1363c3691..493f88f72 100644 --- a/packages/ucd-store/test/__shared.ts +++ b/packages/ucd-store/test/__shared.ts @@ -62,24 +62,25 @@ export const createMemoryMockFS = defineFileSystemBridge({ setup({ state }) { return { async read(path) { - return state.fs.readFileSync(path, "utf8"); + return state.fs.readFileSync(path, "utf8") as string; }, async exists(path) { return state.fs.existsSync(path); }, async listdir(path, recursive = false) { function createFSEntry(entry: Dirent): FSEntry { - const pathFromName = prependLeadingSlash(trimTrailingSlash(entry.name)); + const name = entry.name.toString(); + const pathFromName = prependLeadingSlash(trimTrailingSlash(name)); return entry.isDirectory() ? { type: "directory", - name: entry.name, + name, path: pathFromName, children: [], } : { type: "file", - name: entry.name, + name, path: pathFromName, }; } @@ -103,8 +104,8 @@ export const createMemoryMockFS = defineFileSystemBridge({ const fsEntry = createFSEntry(entry); const entryRelativePath = relativeToTarget - ? join(relativeToTarget, entry.name) - : entry.name; + ? join(relativeToTarget, entry.name.toString()) + : entry.name.toString(); entryMap.set(entryRelativePath, fsEntry); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 618029766..ba02bfa27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,8 +41,8 @@ catalogs: specifier: ^1.52.3 version: 1.52.3 '@luxass/eslint-config': - specifier: ^5.2.0 - version: 5.2.0 + specifier: ^5.2.1 + version: 5.2.1 '@luxass/spectral-ruleset': specifier: ^1.1.0 version: 1.1.0 @@ -273,7 +273,7 @@ importers: version: 0.8.58(@cloudflare/workers-types@4.20250726.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4) '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@luxass/spectral-ruleset': specifier: catalog:linting version: 1.1.0 @@ -334,7 +334,7 @@ importers: version: 1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@tailwindcss/vite': specifier: catalog:web version: 4.1.11(vite@7.0.6(@types/node@24.0.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) @@ -401,7 +401,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@types/yargs-parser': specifier: catalog:dev version: 21.0.3 @@ -435,7 +435,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@ucdjs/tsconfig': specifier: workspace:* version: link:../../tooling/tsconfig @@ -466,7 +466,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@luxass/utils': specifier: catalog:prod version: 2.6.2 @@ -521,7 +521,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@ucdjs/tsconfig': specifier: workspace:* version: link:../../tooling/tsconfig @@ -573,7 +573,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@ucdjs/tsconfig': specifier: workspace:* version: link:../../tooling/tsconfig @@ -607,7 +607,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@ucdjs/tsconfig': specifier: workspace:* version: link:../../tooling/tsconfig @@ -662,7 +662,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@ucdjs/tsconfig': specifier: workspace:* version: link:../../tooling/tsconfig @@ -717,7 +717,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@types/picomatch': specifier: catalog:dev version: 4.0.2 @@ -763,7 +763,7 @@ importers: version: 4.20250726.0 '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@ucdjs/tsconfig': specifier: workspace:* version: link:../../tooling/tsconfig @@ -781,7 +781,7 @@ importers: devDependencies: '@luxass/eslint-config': specifier: catalog:linting - version: 5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) + version: 5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@typescript-eslint/utils': specifier: catalog:linting version: 8.38.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) @@ -1674,8 +1674,8 @@ packages: peerDependencies: tslib: '2' - '@luxass/eslint-config@5.2.0': - resolution: {integrity: sha512-iCo+MP8V8qHVpVg+PqDgg/K+DeVyIvWRgG/vhulyZ8MPMyG5cBz+gax1rciw1zBz3mrOFmzIOXWP2K3TN770cA==} + '@luxass/eslint-config@5.2.1': + resolution: {integrity: sha512-EIUzYEiThpc+OmmJqVl4NCIarpg3gUM9lrQOB5ZIMfjWRt/ltoeqcFex9LJwbfG7aW8udKAbS1XLZcrCpi5i+g==} engines: {node: '>=20'} peerDependencies: '@eslint-react/eslint-plugin': ^1.19.0 @@ -3148,8 +3148,8 @@ packages: peerDependencies: eslint: ^9.5.0 - eslint-flat-config-utils@2.1.0: - resolution: {integrity: sha512-6fjOJ9tS0k28ketkUcQ+kKptB4dBZY2VijMZ9rGn8Cwnn1SH0cZBoPXT8AHBFHxmHcLFQK9zbELDinZ2Mr1rng==} + eslint-flat-config-utils@2.1.1: + resolution: {integrity: sha512-K8eaPkBemHkfbYsZH7z4lZ/tt6gNSsVh535Wh9W9gQBS2WjvfUbbVr2NZR3L1yiRCLuOEimYfPxCxODczD4Opg==} eslint-formatting-reporter@0.0.0: resolution: {integrity: sha512-k9RdyTqxqN/wNYVaTk/ds5B5rA8lgoAmvceYN7bcZMBwU7TuXx5ntewJv81eF3pIL/CiJE+pJZm36llG8yhyyw==} @@ -3201,8 +3201,8 @@ packages: typescript: optional: true - eslint-plugin-jsdoc@52.0.0: - resolution: {integrity: sha512-KZjaoTWWUIml6K6zyPvwCYlLoMDQ69taSdTcdTIavBUoJCIWUfYcsRIw4n9dzllMouqdxiFfKW33EAbBLBu1HA==} + eslint-plugin-jsdoc@52.0.1: + resolution: {integrity: sha512-zJdjpC9z4x28BBdCoxH+/h0BdDLZ9KXQGVKU9gHt3TQJ9kMraep+DgfTIVaXu9u1wy0HyhK2eFMp3icQBV4dkw==} engines: {node: '>=20.11.0'} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 @@ -3331,8 +3331,8 @@ packages: '@typescript-eslint/eslint-plugin': optional: true - eslint-plugin-vue@10.3.0: - resolution: {integrity: sha512-A0u9snqjCfYaPnqqOaH6MBLVWDUIN4trXn8J3x67uDcXvR7X6Ut8p16N+nYhMCQ9Y7edg2BIRGzfyZsY0IdqoQ==} + eslint-plugin-vue@10.4.0: + resolution: {integrity: sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 @@ -3426,9 +3426,6 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} - exsolve@1.0.5: - resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} - exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -6659,7 +6656,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@luxass/eslint-config@5.2.0(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4)': + '@luxass/eslint-config@5.2.1(@eslint-react/eslint-plugin@1.52.3(eslint@9.32.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@1.0.1(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.20(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 @@ -6671,11 +6668,11 @@ snapshots: '@vitest/eslint-plugin': 1.3.4(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) eslint: 9.32.0(jiti@2.4.2) eslint-config-flat-gitignore: 2.1.0(eslint@9.32.0(jiti@2.4.2)) - eslint-flat-config-utils: 2.1.0 + eslint-flat-config-utils: 2.1.1 eslint-merge-processors: 2.0.0(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-antfu: 3.1.1(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-import-lite: 0.3.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-jsdoc: 52.0.0(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-jsdoc: 52.0.1(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-jsonc: 2.20.1(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-n: 17.21.3(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint-plugin-perfectionist: 4.15.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) @@ -6684,7 +6681,7 @@ snapshots: eslint-plugin-toml: 0.12.0(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-unicorn: 60.0.0(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-vue: 10.3.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.32.0(jiti@2.4.2))) + eslint-plugin-vue: 10.4.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.32.0(jiti@2.4.2))) eslint-plugin-yml: 1.18.0(eslint@9.32.0(jiti@2.4.2)) eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.32.0(jiti@2.4.2)) globals: 16.3.0 @@ -8402,7 +8399,7 @@ snapshots: '@eslint/compat': 1.2.8(eslint@9.32.0(jiti@2.4.2)) eslint: 9.32.0(jiti@2.4.2) - eslint-flat-config-utils@2.1.0: + eslint-flat-config-utils@2.1.1: dependencies: pathe: 2.0.3 @@ -8453,7 +8450,7 @@ snapshots: optionalDependencies: typescript: 5.8.3 - eslint-plugin-jsdoc@52.0.0(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-jsdoc@52.0.1(eslint@9.32.0(jiti@2.4.2)): dependencies: '@es-joy/jsdoccomment': 0.52.0 are-docs-informative: 0.0.2 @@ -8697,7 +8694,7 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.32.0(jiti@2.4.2))): + eslint-plugin-vue@10.4.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.32.0(jiti@2.4.2))): dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.4.2)) eslint: 9.32.0(jiti@2.4.2) @@ -8817,8 +8814,6 @@ snapshots: expect-type@1.2.1: {} - exsolve@1.0.5: {} - exsolve@1.0.7: {} extendable-error@0.1.7: {} @@ -10133,7 +10128,7 @@ snapshots: pkg-types@2.1.0: dependencies: confbox: 0.2.2 - exsolve: 1.0.5 + exsolve: 1.0.7 pathe: 2.0.3 pluralize@8.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e81e141be..3445b500d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,7 +23,7 @@ catalogs: "@cloudflare/vitest-pool-workers": ^0.8.58 linting: - "@luxass/eslint-config": ^5.2.0 + "@luxass/eslint-config": ^5.2.1 "@stoplight/spectral-cli": ^6.15.0 eslint: ^9.32.0 eslint-plugin-format: ^1.0.1 From defc116f57b09ad6147314305a7be9f39f302e34 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 16:33:54 +0200 Subject: [PATCH 21/26] test: remove unused import from `files.test.ts` * Cleaned up the test file by removing the unused `vi` import. * This improves code readability and maintains a cleaner codebase. --- packages/ucd-store/test/internal/files.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ucd-store/test/internal/files.test.ts b/packages/ucd-store/test/internal/files.test.ts index c7163d168..c45d4d953 100644 --- a/packages/ucd-store/test/internal/files.test.ts +++ b/packages/ucd-store/test/internal/files.test.ts @@ -2,7 +2,7 @@ import type { ApiError, UnicodeTree } from "@ucdjs/fetch"; import { HttpResponse, mockFetch } from "#msw-utils"; import { UCDJS_API_BASE_URL } from "@ucdjs/env"; import { client } from "@ucdjs/fetch"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { UCDStoreError } from "../../src/errors"; import { getExpectedFilePaths } from "../../src/internal/files"; From ef9cf9a5e59990c4d310e92b5643648f9feecdd0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 16:40:47 +0200 Subject: [PATCH 22/26] chore: update build scripts to include custom TypeScript configuration * Modified the `build` script in multiple package.json files to use `tsdown --tsconfig=./tsconfig.build.json`. * Removed experimental decorators from TypeScript configuration files. * Improved consistency across packages regarding build configurations. --- packages/cli/package.json | 2 +- packages/env/package.json | 2 +- packages/fetch/package.json | 2 +- packages/fs-bridge/package.json | 2 +- packages/schema-gen/package.json | 2 +- packages/schemas/package.json | 2 +- packages/ucd-store/package.json | 2 +- .../ucd-store/src/internal/capabilities.ts | 29 ------------------- packages/ucd-store/src/store.ts | 4 +-- packages/ucd-store/tsconfig.build.json | 3 -- packages/ucd-store/tsconfig.json | 3 -- packages/utils/package.json | 2 +- 12 files changed, 10 insertions(+), 45 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0c9c99ff6..67e5a32d0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,7 +36,7 @@ "node": ">=22.17" }, "scripts": { - "build": "tsdown", + "build": "tsdown --tsconfig=./tsconfig.build.json", "clean": "git clean -xdf dist node_modules", "lint": "eslint .", "typecheck": "tsc --noEmit" diff --git a/packages/env/package.json b/packages/env/package.json index dd6fa1fbc..32a89dbc1 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -32,7 +32,7 @@ "node": ">=22.17" }, "scripts": { - "build": "tsdown", + "build": "tsdown --tsconfig=./tsconfig.build.json", "dev": "tsdown --watch", "clean": "git clean -xdf dist node_modules", "lint": "eslint .", diff --git a/packages/fetch/package.json b/packages/fetch/package.json index f97163380..cb28722f4 100644 --- a/packages/fetch/package.json +++ b/packages/fetch/package.json @@ -32,7 +32,7 @@ "node": ">=22.17" }, "scripts": { - "build": "tsdown", + "build": "tsdown --tsconfig=./tsconfig.build.json", "dev": "tsdown --watch", "clean": "git clean -xdf dist node_modules", "lint": "eslint .", diff --git a/packages/fs-bridge/package.json b/packages/fs-bridge/package.json index cc7590598..2458475ba 100644 --- a/packages/fs-bridge/package.json +++ b/packages/fs-bridge/package.json @@ -35,7 +35,7 @@ "node": ">=22.17" }, "scripts": { - "build": "tsdown", + "build": "tsdown --tsconfig=./tsconfig.build.json", "dev": "tsdown --watch", "clean": "git clean -xdf dist node_modules", "lint": "eslint .", diff --git a/packages/schema-gen/package.json b/packages/schema-gen/package.json index 6eb5c8590..b68a59629 100644 --- a/packages/schema-gen/package.json +++ b/packages/schema-gen/package.json @@ -32,7 +32,7 @@ "node": ">=22.17" }, "scripts": { - "build": "tsdown", + "build": "tsdown --tsconfig=./tsconfig.build.json", "dev": "tsdown --watch", "clean": "git clean -xdf dist node_modules", "lint": "eslint .", diff --git a/packages/schemas/package.json b/packages/schemas/package.json index cb6c5c32a..1a93df6bf 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -32,7 +32,7 @@ "node": ">=22.17" }, "scripts": { - "build": "tsdown", + "build": "tsdown --tsconfig=./tsconfig.build.json", "dev": "tsdown --watch", "clean": "git clean -xdf dist node_modules", "lint": "eslint .", diff --git a/packages/ucd-store/package.json b/packages/ucd-store/package.json index c94d6f892..655bdfdc2 100644 --- a/packages/ucd-store/package.json +++ b/packages/ucd-store/package.json @@ -32,7 +32,7 @@ "node": ">=22.17" }, "scripts": { - "build": "tsdown", + "build": "tsdown --tsconfig=./tsconfig.build.json", "dev": "tsdown --watch", "clean": "git clean -xdf dist node_modules", "lint": "eslint .", diff --git a/packages/ucd-store/src/internal/capabilities.ts b/packages/ucd-store/src/internal/capabilities.ts index 63dd5602f..2b56468db 100644 --- a/packages/ucd-store/src/internal/capabilities.ts +++ b/packages/ucd-store/src/internal/capabilities.ts @@ -85,32 +85,3 @@ function getRequiredCapabilities(feature: keyof StoreCapabilities): FileSystemBr } return capabilities; } - -interface HasFileSystemBridge { - fs: FileSystemBridgeOperationsWithSymbol; -} - -export function requiresCapabilities(capability?: K) { - return function < - T extends HasFileSystemBridge, - M extends (...args: any[]) => Promise, - >( - target: T, - propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor, - ): TypedPropertyDescriptor { - const originalMethod = descriptor.value!; - - const _capability = capability || propertyKey as K; - if (!_capability || !CAPABILITY_REQUIREMENTS[_capability]) { - throw new Error(`Invalid capability: ${_capability}`); - } - - descriptor.value = async function (this: T, ...args: Parameters): Promise>> { - assertCapabilities(_capability, this.fs); - return await originalMethod.apply(this, args); - } as M; - - return descriptor; - }; -} diff --git a/packages/ucd-store/src/store.ts b/packages/ucd-store/src/store.ts index 5a11a1482..950937d8f 100644 --- a/packages/ucd-store/src/store.ts +++ b/packages/ucd-store/src/store.ts @@ -11,7 +11,7 @@ import { createPathFilter, flattenFilePaths, safeJsonParse } from "@ucdjs/utils" import defu from "defu"; import { join } from "pathe"; import { UCDStoreError, UCDStoreVersionNotFoundError } from "./errors"; -import { assertCapabilities, inferStoreCapabilities, requiresCapabilities } from "./internal/capabilities"; +import { assertCapabilities, inferStoreCapabilities } from "./internal/capabilities"; import { getExpectedFilePaths } from "./internal/files"; export class UCDStore { @@ -165,8 +165,8 @@ export class UCDStore { } } - @requiresCapabilities("analyze") async analyze(options: AnalyzeOptions): Promise { + assertCapabilities("analyze", this.#fs); const { checkOrphaned = false, versions = this.#versions, diff --git a/packages/ucd-store/tsconfig.build.json b/packages/ucd-store/tsconfig.build.json index 088a39580..ec6cc954e 100644 --- a/packages/ucd-store/tsconfig.build.json +++ b/packages/ucd-store/tsconfig.build.json @@ -1,8 +1,5 @@ { "extends": "@ucdjs/tsconfig/base.build", - "compilerOptions": { - "experimentalDecorators": true - }, "include": ["src"], "exclude": ["dist"] } diff --git a/packages/ucd-store/tsconfig.json b/packages/ucd-store/tsconfig.json index d280772d4..c25d7149d 100644 --- a/packages/ucd-store/tsconfig.json +++ b/packages/ucd-store/tsconfig.json @@ -1,8 +1,5 @@ { "extends": "@ucdjs/tsconfig/base", - "compilerOptions": { - "experimentalDecorators": true - }, "include": [ "src", "test", diff --git a/packages/utils/package.json b/packages/utils/package.json index a5adfcc2f..02bcd321e 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -32,7 +32,7 @@ "node": ">=22.17" }, "scripts": { - "build": "tsdown", + "build": "tsdown --tsconfig=./tsconfig.build.json", "dev": "tsdown --watch", "clean": "git clean -xdf dist node_modules", "lint": "eslint .", From 51a64936556a9a8d684261bc235e76b683ff8888 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 16:45:22 +0200 Subject: [PATCH 23/26] chore: fix test --- packages/fs-bridge/test/bridges/http.test.ts | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/fs-bridge/test/bridges/http.test.ts b/packages/fs-bridge/test/bridges/http.test.ts index ef63db973..25949913d 100644 --- a/packages/fs-bridge/test/bridges/http.test.ts +++ b/packages/fs-bridge/test/bridges/http.test.ts @@ -120,19 +120,19 @@ describe("http fs-bridge", () => { { type: "file" as const, name: "file1.txt", - path: "/dir/file1.txt", + path: "/file1.txt", lastModified: Date.now(), }, { type: "file" as const, name: "file2.txt", - path: "/dir/file2.txt", + path: "/file2.txt", lastModified: Date.now(), }, { type: "directory" as const, name: "subdir", - path: "/dir/subdir", + path: "/subdir", lastModified: Date.now(), }, ] satisfies FileEntry[]; @@ -154,18 +154,18 @@ describe("http fs-bridge", () => { expect(files).toEqual([ { name: "file1.txt", - path: "/dir/file1.txt", + path: "/file1.txt", type: "file", }, { name: "file2.txt", - path: "/dir/file2.txt", + path: "/file2.txt", type: "file", }, { children: [], name: "subdir", - path: "/dir/subdir", + path: "/subdir", type: "directory", }, @@ -177,7 +177,7 @@ describe("http fs-bridge", () => { { type: "file" as const, name: "nested.txt", - path: "/dir/subdir/nested.txt", + path: "/nested.txt", lastModified: Date.now(), }, ] satisfies FileEntry[]; @@ -199,14 +199,14 @@ describe("http fs-bridge", () => { const files = await bridge.listdir("dir", true); expect(files).toEqual([ - { type: "file", name: "file1.txt", path: "/dir/file1.txt" }, - { type: "file", name: "file2.txt", path: "/dir/file2.txt" }, + { type: "file", name: "file1.txt", path: "/file1.txt" }, + { type: "file", name: "file2.txt", path: "/file2.txt" }, { type: "directory", name: "subdir", - path: "/dir/subdir", + path: "/subdir", children: [ - { type: "file", name: "nested.txt", path: "/dir/subdir/nested.txt" }, + { type: "file", name: "nested.txt", path: "/nested.txt" }, ], }, ]); @@ -260,13 +260,13 @@ describe("http fs-bridge", () => { { type: "file" as const, name: "accessible.txt", - path: "/dir/accessible.txt", + path: "/accessible.txt", lastModified: Date.now(), }, { type: "directory" as const, name: "inaccessible", - path: "/dir/inaccessible", + path: "/inaccessible", lastModified: Date.now(), }, ] satisfies FileEntry[]), { @@ -290,10 +290,10 @@ describe("http fs-bridge", () => { const files = await bridge.listdir("dir", true); expect(files).toEqual([ - { type: "file", name: "accessible.txt", path: "/dir/accessible.txt" }, - { type: "directory", name: "inaccessible", path: "/dir/inaccessible", children: [] }, + { type: "file", name: "accessible.txt", path: "/accessible.txt" }, + { type: "directory", name: "inaccessible", path: "/inaccessible", children: [] }, ]); - expect(flattenFilePaths(files)).not.toContain("/dir/inaccessible/another-file.txt"); + expect(flattenFilePaths(files)).not.toContain("/inaccessible/another-file.txt"); }); }); From 5f7ad5389c0806e895936fd377110ad9972563db Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 16:57:57 +0200 Subject: [PATCH 24/26] chore: fix typo --- packages/ucd-store/test/store-analyze.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ucd-store/test/store-analyze.test.ts b/packages/ucd-store/test/store-analyze.test.ts index a69808702..4af79b724 100644 --- a/packages/ucd-store/test/store-analyze.test.ts +++ b/packages/ucd-store/test/store-analyze.test.ts @@ -344,7 +344,7 @@ describe("analyze operations", () => { expect(analysisResult.length).toBe(0); }); - it("should analyse store with no files", async () => { + it("should analyze store with no files", async () => { const storeDir = await testdir({ "15.0.0": {}, ".ucd-store.json": JSON.stringify({ From 8404d3bf46277df6e9330c88d8ec62bda076cead Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 16:59:44 +0200 Subject: [PATCH 25/26] fix(analyze): correct log message for analyzing versions * Updated the log message to accurately reflect that all versions will be analyzed when no specific versions are provided. --- packages/cli/src/cmd/store/analyze.ts | 2 +- packages/ucd-store/src/store.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/cmd/store/analyze.ts b/packages/cli/src/cmd/store/analyze.ts index 390ba9cab..84d6336ec 100644 --- a/packages/cli/src/cmd/store/analyze.ts +++ b/packages/cli/src/cmd/store/analyze.ts @@ -34,7 +34,7 @@ export async function runAnalyzeStore({ flags, versions }: CLIStoreAnalyzeCmdOpt } if (!versions || versions.length === 0) { - console.info("No specific versions provided. Cleaning all versions in the store."); + console.info("No specific versions provided. Analyzing all versions in the store."); } const { diff --git a/packages/ucd-store/src/store.ts b/packages/ucd-store/src/store.ts index 950937d8f..90fc7402c 100644 --- a/packages/ucd-store/src/store.ts +++ b/packages/ucd-store/src/store.ts @@ -121,6 +121,7 @@ export class UCDStore { const files = await this.#fs.listdir(join(this.basePath, version), true); + // TODO: handle the cases where we wanna filter child files. return files.filter(({ path }) => this.#filter(trimLeadingSlash(path), extraFilters)); } From 6a432841e12d6e5783822cc8fe2586ae2b5ab4e1 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 31 Jul 2025 17:05:23 +0200 Subject: [PATCH 26/26] chore: add changeset --- .changeset/proud-mangos-look.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/proud-mangos-look.md diff --git a/.changeset/proud-mangos-look.md b/.changeset/proud-mangos-look.md new file mode 100644 index 000000000..6767bb4cb --- /dev/null +++ b/.changeset/proud-mangos-look.md @@ -0,0 +1,5 @@ +--- +"@ucdjs/ucd-store": minor +--- + +implement analyze on ucd-store