From 84fb905368cafa51cc84701180fd96e9da1f26e2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 09:11:33 +0100 Subject: [PATCH 1/9] refactor(test-utils): simplify content handling in filesRoute --- packages/test-utils/src/mock-store/handlers/files.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/test-utils/src/mock-store/handlers/files.ts b/packages/test-utils/src/mock-store/handlers/files.ts index f999da526..251f20a99 100644 --- a/packages/test-utils/src/mock-store/handlers/files.ts +++ b/packages/test-utils/src/mock-store/handlers/files.ts @@ -142,19 +142,13 @@ export const filesRoute = defineMockRouteHandler({ // If it's a file with _content, return the content if (fileNode && "_content" in fileNode && typeof fileNode._content === "string") { - let content = fileNode._content; + const content = fileNode._content; const contentLength = new TextEncoder().encode(content).length; const headers: Record = { "Content-Type": "text/plain; charset=utf-8", [UCD_STAT_TYPE_HEADER]: "file", }; - // Check if the content ends with a newline; if not, add one for better terminal display - if (!content.endsWith("\n")) { - headers["X-Content-Warning"] = "Content did not end with a newline; added for display purposes."; - content += "\n"; - } - // Only include size headers for HEAD requests (buffered response) if (isHeadRequest) { headers["Content-Length"] = `${contentLength}`; From ffb9fb3254858eac1c33072ff372f05e30caba5d Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 09:13:15 +0100 Subject: [PATCH 2/9] chore(cli): remove debug log for store path in lockfile info --- packages/cli/src/cmd/lockfile/info.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/src/cmd/lockfile/info.ts b/packages/cli/src/cmd/lockfile/info.ts index aa532db41..7edc68736 100644 --- a/packages/cli/src/cmd/lockfile/info.ts +++ b/packages/cli/src/cmd/lockfile/info.ts @@ -40,7 +40,6 @@ export async function runLockfileInfo({ flags }: CLILockfileInfoCmdOptions) { const { storeDir, json } = flags; const storePath = storeDir ? resolve(storeDir) : process.cwd(); - console.error("Reading lockfile info from store at:", storePath); try { const fs = NodeFileSystemBridge({ basePath: storePath }); const lockfilePath = getLockfilePath(); From 203abcbfc4403368489985105d439da608a913a5 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 09:14:55 +0100 Subject: [PATCH 3/9] refactor(test-utils): standardize path formatting in memory fs bridge --- .../src/fs-bridges/memory-fs-bridge.ts | 23 +++++++++++----- .../test/fs-bridge/memory-fs-bridge.test.ts | 26 +++++++++---------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/packages/test-utils/src/fs-bridges/memory-fs-bridge.ts b/packages/test-utils/src/fs-bridges/memory-fs-bridge.ts index 1e62c697e..a4b1e6bea 100644 --- a/packages/test-utils/src/fs-bridges/memory-fs-bridge.ts +++ b/packages/test-utils/src/fs-bridges/memory-fs-bridge.ts @@ -1,5 +1,6 @@ import type { FileSystemBridgeOperations, FSEntry } from "@ucdjs/fs-bridge"; import { Buffer } from "node:buffer"; +import { appendTrailingSlash, prependLeadingSlash } from "@luxass/utils/path"; import { defineFileSystemBridge } from "@ucdjs/fs-bridge"; import { FileEntrySchema } from "@ucdjs/schemas"; import { z } from "zod"; @@ -12,6 +13,16 @@ function normalizeRootPath(path: string | undefined): string { return (!path || path === "." || path === "/") ? "" : path; } +/** + * Formats a relative path to match FSEntry schema requirements (parity with node/http bridges): + * - Leading slash required for all paths + * - Trailing slash required for directories + */ +function formatEntryPath(relativePath: string, isDirectory: boolean): string { + const withLeadingSlash = prependLeadingSlash(relativePath); + return isDirectory ? appendTrailingSlash(withLeadingSlash) : withLeadingSlash; +} + /** * Marker value for explicit directories in the flat Map storage. * Directories are stored as "path/" -> DIR_MARKER @@ -195,7 +206,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ entries.push({ type: "directory" as const, name: dirName, - path: dirName, + path: formatEntryPath(dirName, true), children: [], }); } @@ -216,7 +227,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ dirEntry = { type: "directory" as const, name: part, - path: partPath, + path: formatEntryPath(partPath, true), children: [], }; currentLevel.push(dirEntry); @@ -237,7 +248,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ entries.push({ type: "file" as const, name: parts[0], - path: relativePath, + path: formatEntryPath(relativePath, false), }); } else if (parts.length > 1 && parts[0]) { // Directory (implicit from file path) @@ -247,7 +258,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ entries.push({ type: "directory" as const, name: dirName, - path: dirName, + path: formatEntryPath(dirName, true), children: [], }); } @@ -267,7 +278,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ currentLevel.push({ type: "file" as const, name: part, - path: partPath, + path: formatEntryPath(partPath, false), }); } else { // It's a directory - find or create it @@ -279,7 +290,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ dirEntry = { type: "directory" as const, name: part, - path: partPath, + path: formatEntryPath(partPath, true), children: [], }; currentLevel.push(dirEntry); diff --git a/packages/test-utils/test/fs-bridge/memory-fs-bridge.test.ts b/packages/test-utils/test/fs-bridge/memory-fs-bridge.test.ts index aabab5031..0ebca6c22 100644 --- a/packages/test-utils/test/fs-bridge/memory-fs-bridge.test.ts +++ b/packages/test-utils/test/fs-bridge/memory-fs-bridge.test.ts @@ -133,8 +133,8 @@ describe("memory fs bridge", () => { const entries = await fs.listdir("", false); expect(entries).toHaveLength(2); expect(entries).toEqual(expect.arrayContaining([ - { type: "file", name: "file1.txt", path: "file1.txt" }, - { type: "file", name: "file2.txt", path: "file2.txt" }, + { type: "file", name: "file1.txt", path: "/file1.txt" }, + { type: "file", name: "file2.txt", path: "/file2.txt" }, ])); }); @@ -149,8 +149,8 @@ describe("memory fs bridge", () => { const entries = await fs.listdir("", false); expect(entries).toHaveLength(2); expect(entries).toEqual(expect.arrayContaining([ - { type: "file", name: "file.txt", path: "file.txt" }, - { type: "directory", name: "dir", path: "dir", children: [] }, + { type: "file", name: "file.txt", path: "/file.txt" }, + { type: "directory", name: "dir", path: "/dir/", children: [] }, ])); }); @@ -166,9 +166,9 @@ describe("memory fs bridge", () => { const entries = await fs.listdir("dir", false); expect(entries).toHaveLength(3); expect(entries).toEqual(expect.arrayContaining([ - { type: "file", name: "file1.txt", path: "file1.txt" }, - { type: "file", name: "file2.txt", path: "file2.txt" }, - { type: "directory", name: "nested", path: "nested", children: [] }, + { type: "file", name: "file1.txt", path: "/file1.txt" }, + { type: "file", name: "file2.txt", path: "/file2.txt" }, + { type: "directory", name: "nested", path: "/nested/", children: [] }, ])); }); @@ -191,19 +191,19 @@ describe("memory fs bridge", () => { expect(entries).toHaveLength(2); const fileEntry = entries.find((e) => e.type === "file"); - expect(fileEntry).toEqual({ type: "file", name: "file.txt", path: "file.txt" }); + expect(fileEntry).toEqual({ type: "file", name: "file.txt", path: "/file.txt" }); const dirEntry = entries.find((e) => e.type === "directory" && e.name === "dir"); expect(dirEntry).toBeDefined(); if (dirEntry?.type === "directory") { expect(dirEntry.children).toHaveLength(2); expect(dirEntry.children).toEqual(expect.arrayContaining([ - { type: "file", name: "nested.txt", path: "dir/nested.txt" }, + { type: "file", name: "nested.txt", path: "/dir/nested.txt" }, { type: "directory", name: "deep", - path: "dir/deep", - children: [{ type: "file", name: "file.txt", path: "dir/deep/file.txt" }], + path: "/dir/deep/", + children: [{ type: "file", name: "file.txt", path: "/dir/deep/file.txt" }], }, ])); } @@ -238,7 +238,7 @@ describe("memory fs bridge", () => { expect(cDir.name).toBe("c"); expect(cDir.children).toHaveLength(1); - expect(cDir.children[0]).toEqual({ type: "file", name: "file.txt", path: "a/b/c/file.txt" }); + expect(cDir.children[0]).toEqual({ type: "file", name: "file.txt", path: "/a/b/c/file.txt" }); }); }); @@ -266,7 +266,7 @@ describe("memory fs bridge", () => { assertCapability(fs, "mkdir"); await fs.mkdir("emptydir"); const entries = await fs.listdir("", false); - expect(entries).toEqual([{ type: "directory", name: "emptydir", path: "emptydir", children: [] }]); + expect(entries).toEqual([{ type: "directory", name: "emptydir", path: "/emptydir/", children: [] }]); }); it("should throw EISDIR when reading a directory", async () => { From 1dde1e18adca10a5a0513d11bc5ea4bacbc21adf Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 09:31:07 +0100 Subject: [PATCH 4/9] feat(shared): enhance path normalization for API file-tree --- packages/shared/src/files.ts | 83 +++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/files.ts b/packages/shared/src/files.ts index 6facd4ed1..e73fb8238 100644 --- a/packages/shared/src/files.ts +++ b/packages/shared/src/files.ts @@ -1,5 +1,86 @@ import type { UnicodeFileTreeNodeWithoutLastModified } from "@ucdjs/schemas"; -import { prependLeadingSlash } from "@luxass/utils"; +import { prependLeadingSlash, trimLeadingSlash, trimTrailingSlash } from "@luxass/utils"; +import { hasUCDFolderPath } from "@unicode-utils/core"; + +/** + * Normalizes an API file-tree path to a version-relative path suitable for filtering. + * + * This strips: + * - Leading/trailing slashes + * - Version prefix (e.g., "16.0.0/") + * - "ucd/" prefix for versions that have it + * + * @param {string} version - The Unicode version string + * @param {string} rawPath - The raw path from the API file tree (e.g., "/16.0.0/ucd/Blocks.txt") + * @returns {string} The normalized path (e.g., "Blocks.txt") + * + * @example + * ```typescript + * normalizePathForFiltering("16.0.0", "/16.0.0/ucd/Blocks.txt"); + * // Returns: "Blocks.txt" + * + * normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/GraphemeBreakProperty.txt"); + * // Returns: "auxiliary/GraphemeBreakProperty.txt" + * ``` + */ +export function normalizePathForFiltering(version: string, rawPath: string): string { + // Strip leading and trailing slashes + let path = trimTrailingSlash(trimLeadingSlash(rawPath)); + + // Strip version prefix if present + const versionPrefix = `${version}/`; + if (path.startsWith(versionPrefix)) { + path = path.slice(versionPrefix.length); + } + + // Strip "ucd/" prefix for versions that have it + if (hasUCDFolderPath(version) && path.startsWith("ucd/")) { + path = path.slice(4); + } + + return path; +} + +/** + * Creates a normalized view of a file tree for filtering purposes. + * + * This recursively maps all `path` properties to version-relative paths, + * so that filter patterns like "Blocks.txt" or "auxiliary/**" will match + * against paths like "/16.0.0/ucd/Blocks.txt". + * + * @template {UnicodeFileTreeNodeWithoutLastModified} T - A tree node type that extends the base TreeNode interface + * @param {string} version - The Unicode version string + * @param {T[]} entries - Array of file tree nodes from the API + * @returns {T[]} A new tree with normalized paths suitable for filtering + * + * @example + * ```typescript + * const apiTree = [{ type: "file", name: "Blocks.txt", path: "/16.0.0/ucd/Blocks.txt" }]; + * const normalizedTree = normalizeTreeForFiltering("16.0.0", apiTree); + * // Returns: [{ type: "file", name: "Blocks.txt", path: "Blocks.txt" }] + * ``` + */ +export function normalizeTreeForFiltering( + version: string, + entries: T[], +): T[] { + return entries.map((entry) => { + const normalizedPath = normalizePathForFiltering(version, entry.path); + + if (entry.type === "directory" && entry.children) { + return { + ...entry, + path: normalizedPath, + children: normalizeTreeForFiltering(version, entry.children), + }; + } + + return { + ...entry, + path: normalizedPath, + }; + }); +} /** * Recursively find a node (file or directory) by its path in the tree. From d2cc2d2db6f101dd9cc569e2c495f7f072398937 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 09:31:22 +0100 Subject: [PATCH 5/9] refactor(shared): enhance file exports in index.ts --- packages/shared/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9ddf52cc6..86e36a264 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -5,7 +5,7 @@ export * from "./debugger"; export { customFetch } from "./fetch/fetch"; export type { FetchOptions, FetchResponse, SafeFetchResponse } from "./fetch/types"; -export { findFileByPath, flattenFilePaths } from "./files"; +export { findFileByPath, flattenFilePaths, normalizePathForFiltering, normalizeTreeForFiltering } from "./files"; export { createPathFilter, From 3549914765e86a22ce0901583c40fa07dd09a78f Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 13:20:44 +0100 Subject: [PATCH 6/9] feat(shared): enhance path normalization in createPathFilter Added functions to normalize paths and patterns for matching, ensuring leading slashes are handled correctly. Updated tests to verify behavior with leading slashes and typical project filters. --- packages/shared/src/filter.ts | 35 +++++++- packages/shared/test/filter.test.ts | 120 +++++++++++++++------------- 2 files changed, 96 insertions(+), 59 deletions(-) diff --git a/packages/shared/src/filter.ts b/packages/shared/src/filter.ts index 1ff402d17..d6e1d91fa 100644 --- a/packages/shared/src/filter.ts +++ b/packages/shared/src/filter.ts @@ -141,6 +141,32 @@ export function createPathFilter(options: PathFilterOptions = {}): PathFilter { return filterFn; } +function normalizeForMatching(value: string): string { + // 1) unify slashes + let normalized = value.replace(/\\/g, "/"); + + // 2) drop leading "./" segments + normalized = normalized.replace(/^\.\/+/, ""); + + // 3) drop leading slash to let relative globs match absolute-style inputs + normalized = normalized.replace(/^\//, ""); + + // 4) drop trailing slash so directory paths don't require it in patterns + normalized = normalized.replace(/\/$/, ""); + + return normalized; +} + +function normalizePatterns(patterns: string[]): string[] { + const normalized: string[] = []; + + for (let i = 0; i < patterns.length; i += 1) { + normalized.push(normalizeForMatching(patterns[i]!)); + } + + return normalized; +} + function internal__createFilterFunction(config: PathFilterOptions): PathFilterFn { // If include is empty or not set, include everything using "**" pattern const includePatterns = config.include && config.include.length > 0 ? config.include : ["**"]; @@ -153,14 +179,17 @@ function internal__createFilterFunction(config: PathFilterOptions): PathFilterFn ...(config.exclude || []), ]; + const normalizedIncludePatterns = normalizePatterns(includePatterns); + const normalizedExcludePatterns = normalizePatterns(rawExcludePatterns); + // Transform directory-only patterns to include their contents // e.g., "**/extracted" becomes both "**/extracted" and "**/extracted/**" - const excludePatterns = expandDirectoryPatterns(rawExcludePatterns); + const excludePatterns = expandDirectoryPatterns(normalizedExcludePatterns); return (path: string): boolean => { - const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, ""); + const normalizedPath = normalizeForMatching(path); - return picomatch.isMatch(normalizedPath, includePatterns, { + return picomatch.isMatch(normalizedPath, normalizedIncludePatterns, { ...DEFAULT_PICOMATCH_OPTIONS, ignore: excludePatterns, } satisfies PicomatchOptions); diff --git a/packages/shared/test/filter.test.ts b/packages/shared/test/filter.test.ts index a838e4972..23960ddfd 100644 --- a/packages/shared/test/filter.test.ts +++ b/packages/shared/test/filter.test.ts @@ -35,6 +35,9 @@ describe("createPathFilter", () => { [["src/**", "lib/**"], "src/file.txt", true], [["src/**", "lib/**"], "lib/file.txt", true], [["src/**", "lib/**"], "test/file.txt", false], + // leading slash paths should still match relative patterns + [["src/**"], "/src/file.txt", true], + [["src/**"], "/src/", true], ])("with multiple include patterns %j, path \"%s\" should return %s", (include, path, expected) => { const filter = createPathFilter({ include }); expect(filter(path)).toBe(expected); @@ -79,6 +82,7 @@ describe("createPathFilter", () => { }); it.each([ + // typical project filter ["src/components/Button.tsx", true, "includes source tsx file"], ["src/utils/helper.js", true, "includes source js file"], ["src/components/Button.vue", true, "includes vue file"], @@ -89,6 +93,9 @@ describe("createPathFilter", () => { ["src/utils/helper.spec.js", false, "excludes spec files"], ["README.md", false, "excludes non-matching extensions"], ["package.json", false, "excludes json files"], + ["/src/utils/helper.js", true, "leading slash should still match"], + ["/src/", false, "leading slash directory should not match file-only pattern"], + ["/node_modules/react/index.js", false, "leading slash excluded node_modules"], ])("typical project filter: path \"%s\" should return %s (%s)", (path, expected) => { const filter = createPathFilter({ include: ["**/*.{js,ts,jsx,tsx,vue,svelte}"], @@ -100,7 +107,6 @@ describe("createPathFilter", () => { "**/*.spec.*", ], }); - expect(filter(path)).toBe(expected); }); @@ -675,42 +681,42 @@ describe("filterTreeStructure", () => { { type: "file" as const, name: "root-file.txt", - path: "root-file.txt", + path: "/root-file.txt", }, { type: "file" as const, name: "root-config.json", - path: "root-config.json", + path: "/root-config.json", }, { type: "directory" as const, name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file" as const, name: "DerivedBidiClass.txt", - path: "extracted/DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", }, { type: "file" as const, name: "config.json", - path: "extracted/config.json", + path: "/extracted/config.json", }, { type: "directory" as const, name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file" as const, name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, { type: "file" as const, name: "debug.log", - path: "extracted/nested/debug.log", + path: "/extracted/nested/debug.log", }, ], }, @@ -721,30 +727,32 @@ describe("filterTreeStructure", () => { describe("basic filtering", () => { it("should filter files based on extension", () => { const filter = createPathFilter({ include: ["**/*.json"] }); - const result = filterTreeStructure(filter, tree); - - expect(result).toEqual([ - { - type: "file", - name: "root-config.json", - path: "root-config.json", - }, - { - type: "directory", - name: "extracted", - path: "extracted", - children: [ - { - type: "file", - name: "config.json", - path: "extracted/config.json", - }, - ], - }, - ]); - }); + const result = filterTreeStructure(filter, tree); + + expect(result).toEqual([ + { + type: "file", + name: "root-config.json", + path: "/root-config.json", + }, + { + type: "directory", + name: "extracted", + path: "/extracted/", + children: [ + { + type: "file", + name: "config.json", + path: "/extracted/config.json", + }, + ], + }, + ]); + }); + + + it("should include all items when no filters are applied", () => { - it("should include all items when no filters are applied", () => { const filter = createPathFilter(); const result = filterTreeStructure(filter, tree); @@ -761,27 +769,27 @@ describe("filterTreeStructure", () => { { type: "file", name: "root-file.txt", - path: "root-file.txt", + path: "/root-file.txt", }, { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file", name: "DerivedBidiClass.txt", - path: "extracted/DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", }, { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, ], }, @@ -800,17 +808,17 @@ describe("filterTreeStructure", () => { { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, ], }, @@ -834,32 +842,32 @@ describe("filterTreeStructure", () => { { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file", name: "DerivedBidiClass.txt", - path: "extracted/DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", }, { type: "file", name: "config.json", - path: "extracted/config.json", + path: "/extracted/config.json", }, { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, { type: "file", name: "debug.log", - path: "extracted/nested/debug.log", + path: "/extracted/nested/debug.log", }, ], }, @@ -964,17 +972,17 @@ describe("filterTreeStructure", () => { { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, ], }, @@ -994,37 +1002,37 @@ describe("filterTreeStructure", () => { { type: "file", name: "root-file.txt", - path: "root-file.txt", + path: "/root-file.txt", }, { type: "file", name: "root-config.json", - path: "root-config.json", + path: "/root-config.json", }, { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file", name: "DerivedBidiClass.txt", - path: "extracted/DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", }, { type: "file", name: "config.json", - path: "extracted/config.json", + path: "/extracted/config.json", }, { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, // debug.log should be excluded ], From 10028cec87f3b66eef5cb1c49133b66e47e55b44 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 13:30:15 +0100 Subject: [PATCH 7/9] feat(shared): add path normalization functions for filtering Introduces `normalizePathForFiltering` and `normalizeTreeForFiltering` to standardize file paths and maintain directory structure. Enhances test coverage for these new functionalities. --- packages/shared/test/files.test.ts | 130 ++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/packages/shared/test/files.test.ts b/packages/shared/test/files.test.ts index 580dd8257..3af042b4d 100644 --- a/packages/shared/test/files.test.ts +++ b/packages/shared/test/files.test.ts @@ -1,6 +1,11 @@ import type { UnicodeFileTreeNode } from "@ucdjs/schemas"; import { describe, expect, it } from "vitest"; -import { findFileByPath, flattenFilePaths } from "../src/files"; +import { + findFileByPath, + flattenFilePaths, + normalizePathForFiltering, + normalizeTreeForFiltering, +} from "../src/files"; describe("findFileByPath", () => { it("should return undefined for empty input", () => { @@ -154,6 +159,129 @@ describe("findFileByPath", () => { lastModified: null, }], }); + + describe("normalizePathForFiltering", () => { + it("strips leading and trailing slashes", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/")).toBe("auxiliary"); + expect(normalizePathForFiltering("16.0.0", "16.0.0/ucd/Blocks.txt")).toBe("Blocks.txt"); + }); + + it("strips version prefix regardless of leading slash", () => { + expect(normalizePathForFiltering("17.0.0", "/17.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + expect(normalizePathForFiltering("17.0.0", "17.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + }); + + it("strips ucd/ when version uses UCD folder", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/Blocks.txt")).toBe("Blocks.txt"); + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + }); + + it("keeps ucd/ when version does not use UCD folder", () => { + expect(normalizePathForFiltering("1.1.0", "/1.1.0/ucd/Blocks.txt")).toBe("ucd/Blocks.txt"); + }); + + it("handles nested paths with all prefixes", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/LineBreak.txt")).toBe("auxiliary/LineBreak.txt"); + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/emoji/emoji-data.txt")).toBe("emoji/emoji-data.txt"); + }); + + it("no-ops when already normalized", () => { + expect(normalizePathForFiltering("16.0.0", "Blocks.txt")).toBe("Blocks.txt"); + expect(normalizePathForFiltering("16.0.0", "auxiliary/LineBreak.txt")).toBe("auxiliary/LineBreak.txt"); + }); + }); + + describe("normalizeTreeForFiltering", () => { + it("normalizes file paths and preserves structure", () => { + const tree: UnicodeFileTreeNode[] = [ + { type: "file", name: "Blocks.txt", path: "/16.0.0/ucd/Blocks.txt", lastModified: null }, + { + type: "directory", + name: "auxiliary", + path: "/16.0.0/ucd/auxiliary/", + lastModified: null, + children: [ + { type: "file", name: "Grapheme.txt", path: "/16.0.0/ucd/auxiliary/Grapheme.txt", lastModified: null }, + { + type: "directory", + name: "nested", + path: "/16.0.0/ucd/auxiliary/nested/", + lastModified: null, + children: [ + { type: "file", name: "Deep.txt", path: "/16.0.0/ucd/auxiliary/nested/Deep.txt", lastModified: null }, + ], + }, + ], + }, + ]; + + const normalized = normalizeTreeForFiltering("16.0.0", tree); + + expect(normalized).toEqual([ + { type: "file", name: "Blocks.txt", path: "Blocks.txt", lastModified: null }, + { + type: "directory", + name: "auxiliary", + path: "auxiliary", + lastModified: null, + children: [ + { type: "file", name: "Grapheme.txt", path: "auxiliary/Grapheme.txt", lastModified: null }, + { + type: "directory", + name: "nested", + path: "auxiliary/nested", + lastModified: null, + children: [ + { type: "file", name: "Deep.txt", path: "auxiliary/nested/Deep.txt", lastModified: null }, + ], + }, + ], + }, + ]); + }); + + it("retains ucd/ prefix when version lacks UCD folder", () => { + const tree: UnicodeFileTreeNode[] = [ + { type: "file", name: "Blocks.txt", path: "/1.1.0/ucd/Blocks.txt", lastModified: null }, + { type: "file", name: "ReadMe.txt", path: "/1.1.0/ReadMe.txt", lastModified: null }, + ]; + + const normalized = normalizeTreeForFiltering("1.1.0", tree); + + expect(normalized).toEqual([ + { type: "file", name: "Blocks.txt", path: "ucd/Blocks.txt", lastModified: null }, + { type: "file", name: "ReadMe.txt", path: "ReadMe.txt", lastModified: null }, + ]); + }); + + it("handles trailing slashes on directories", () => { + const tree: UnicodeFileTreeNode[] = [ + { + type: "directory", + name: "emoji", + path: "/17.0.0/ucd/emoji/", + lastModified: null, + children: [ + { type: "file", name: "emoji-data.txt", path: "/17.0.0/ucd/emoji/emoji-data.txt", lastModified: null }, + ], + }, + ]; + + const normalized = normalizeTreeForFiltering("17.0.0", tree); + + expect(normalized).toEqual([ + { + type: "directory", + name: "emoji", + path: "emoji", + lastModified: null, + children: [ + { type: "file", name: "emoji-data.txt", path: "emoji/emoji-data.txt", lastModified: null }, + ], + }, + ]); + }); + }); }); it("should handle mixed files and directories", () => { From b0051a64b186a71ba324c01dce40e62e8913bd74 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 13:41:11 +0100 Subject: [PATCH 8/9] fix bad test --- packages/shared/test/files.test.ts | 246 ++++++++++++++--------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/packages/shared/test/files.test.ts b/packages/shared/test/files.test.ts index 3af042b4d..a3b6021df 100644 --- a/packages/shared/test/files.test.ts +++ b/packages/shared/test/files.test.ts @@ -159,129 +159,6 @@ describe("findFileByPath", () => { lastModified: null, }], }); - - describe("normalizePathForFiltering", () => { - it("strips leading and trailing slashes", () => { - expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/")).toBe("auxiliary"); - expect(normalizePathForFiltering("16.0.0", "16.0.0/ucd/Blocks.txt")).toBe("Blocks.txt"); - }); - - it("strips version prefix regardless of leading slash", () => { - expect(normalizePathForFiltering("17.0.0", "/17.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); - expect(normalizePathForFiltering("17.0.0", "17.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); - }); - - it("strips ucd/ when version uses UCD folder", () => { - expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/Blocks.txt")).toBe("Blocks.txt"); - expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); - }); - - it("keeps ucd/ when version does not use UCD folder", () => { - expect(normalizePathForFiltering("1.1.0", "/1.1.0/ucd/Blocks.txt")).toBe("ucd/Blocks.txt"); - }); - - it("handles nested paths with all prefixes", () => { - expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/LineBreak.txt")).toBe("auxiliary/LineBreak.txt"); - expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/emoji/emoji-data.txt")).toBe("emoji/emoji-data.txt"); - }); - - it("no-ops when already normalized", () => { - expect(normalizePathForFiltering("16.0.0", "Blocks.txt")).toBe("Blocks.txt"); - expect(normalizePathForFiltering("16.0.0", "auxiliary/LineBreak.txt")).toBe("auxiliary/LineBreak.txt"); - }); - }); - - describe("normalizeTreeForFiltering", () => { - it("normalizes file paths and preserves structure", () => { - const tree: UnicodeFileTreeNode[] = [ - { type: "file", name: "Blocks.txt", path: "/16.0.0/ucd/Blocks.txt", lastModified: null }, - { - type: "directory", - name: "auxiliary", - path: "/16.0.0/ucd/auxiliary/", - lastModified: null, - children: [ - { type: "file", name: "Grapheme.txt", path: "/16.0.0/ucd/auxiliary/Grapheme.txt", lastModified: null }, - { - type: "directory", - name: "nested", - path: "/16.0.0/ucd/auxiliary/nested/", - lastModified: null, - children: [ - { type: "file", name: "Deep.txt", path: "/16.0.0/ucd/auxiliary/nested/Deep.txt", lastModified: null }, - ], - }, - ], - }, - ]; - - const normalized = normalizeTreeForFiltering("16.0.0", tree); - - expect(normalized).toEqual([ - { type: "file", name: "Blocks.txt", path: "Blocks.txt", lastModified: null }, - { - type: "directory", - name: "auxiliary", - path: "auxiliary", - lastModified: null, - children: [ - { type: "file", name: "Grapheme.txt", path: "auxiliary/Grapheme.txt", lastModified: null }, - { - type: "directory", - name: "nested", - path: "auxiliary/nested", - lastModified: null, - children: [ - { type: "file", name: "Deep.txt", path: "auxiliary/nested/Deep.txt", lastModified: null }, - ], - }, - ], - }, - ]); - }); - - it("retains ucd/ prefix when version lacks UCD folder", () => { - const tree: UnicodeFileTreeNode[] = [ - { type: "file", name: "Blocks.txt", path: "/1.1.0/ucd/Blocks.txt", lastModified: null }, - { type: "file", name: "ReadMe.txt", path: "/1.1.0/ReadMe.txt", lastModified: null }, - ]; - - const normalized = normalizeTreeForFiltering("1.1.0", tree); - - expect(normalized).toEqual([ - { type: "file", name: "Blocks.txt", path: "ucd/Blocks.txt", lastModified: null }, - { type: "file", name: "ReadMe.txt", path: "ReadMe.txt", lastModified: null }, - ]); - }); - - it("handles trailing slashes on directories", () => { - const tree: UnicodeFileTreeNode[] = [ - { - type: "directory", - name: "emoji", - path: "/17.0.0/ucd/emoji/", - lastModified: null, - children: [ - { type: "file", name: "emoji-data.txt", path: "/17.0.0/ucd/emoji/emoji-data.txt", lastModified: null }, - ], - }, - ]; - - const normalized = normalizeTreeForFiltering("17.0.0", tree); - - expect(normalized).toEqual([ - { - type: "directory", - name: "emoji", - path: "emoji", - lastModified: null, - children: [ - { type: "file", name: "emoji-data.txt", path: "emoji/emoji-data.txt", lastModified: null }, - ], - }, - ]); - }); - }); }); it("should handle mixed files and directories", () => { @@ -563,3 +440,126 @@ describe("flattenFilePaths", () => { expect(result).toEqual(["/file1.txt", "/folder/nested.txt"]); }); }); + +describe("normalizePathForFiltering", () => { + it("strips leading and trailing slashes", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/")).toBe("auxiliary"); + expect(normalizePathForFiltering("16.0.0", "16.0.0/ucd/Blocks.txt")).toBe("Blocks.txt"); + }); + + it("strips version prefix regardless of leading slash", () => { + expect(normalizePathForFiltering("17.0.0", "/17.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + expect(normalizePathForFiltering("17.0.0", "17.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + }); + + it("strips ucd/ when version uses UCD folder", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/Blocks.txt")).toBe("Blocks.txt"); + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + }); + + it("keeps ucd/ when version does not use UCD folder", () => { + expect(normalizePathForFiltering("1.1.0", "/1.1.0/ucd/Blocks.txt")).toBe("ucd/Blocks.txt"); + }); + + it("handles nested paths with all prefixes", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/LineBreak.txt")).toBe("auxiliary/LineBreak.txt"); + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/emoji/emoji-data.txt")).toBe("emoji/emoji-data.txt"); + }); + + it("no-ops when already normalized", () => { + expect(normalizePathForFiltering("16.0.0", "Blocks.txt")).toBe("Blocks.txt"); + expect(normalizePathForFiltering("16.0.0", "auxiliary/LineBreak.txt")).toBe("auxiliary/LineBreak.txt"); + }); +}); + +describe("normalizeTreeForFiltering", () => { + it("normalizes file paths and preserves structure", () => { + const tree: UnicodeFileTreeNode[] = [ + { type: "file", name: "Blocks.txt", path: "/16.0.0/ucd/Blocks.txt", lastModified: null }, + { + type: "directory", + name: "auxiliary", + path: "/16.0.0/ucd/auxiliary/", + lastModified: null, + children: [ + { type: "file", name: "Grapheme.txt", path: "/16.0.0/ucd/auxiliary/Grapheme.txt", lastModified: null }, + { + type: "directory", + name: "nested", + path: "/16.0.0/ucd/auxiliary/nested/", + lastModified: null, + children: [ + { type: "file", name: "Deep.txt", path: "/16.0.0/ucd/auxiliary/nested/Deep.txt", lastModified: null }, + ], + }, + ], + }, + ]; + + const normalized = normalizeTreeForFiltering("16.0.0", tree); + + expect(normalized).toEqual([ + { type: "file", name: "Blocks.txt", path: "Blocks.txt", lastModified: null }, + { + type: "directory", + name: "auxiliary", + path: "auxiliary", + lastModified: null, + children: [ + { type: "file", name: "Grapheme.txt", path: "auxiliary/Grapheme.txt", lastModified: null }, + { + type: "directory", + name: "nested", + path: "auxiliary/nested", + lastModified: null, + children: [ + { type: "file", name: "Deep.txt", path: "auxiliary/nested/Deep.txt", lastModified: null }, + ], + }, + ], + }, + ]); + }); + + it("retains ucd/ prefix when version lacks UCD folder", () => { + const tree: UnicodeFileTreeNode[] = [ + { type: "file", name: "Blocks.txt", path: "/1.1.0/ucd/Blocks.txt", lastModified: null }, + { type: "file", name: "ReadMe.txt", path: "/1.1.0/ReadMe.txt", lastModified: null }, + ]; + + const normalized = normalizeTreeForFiltering("1.1.0", tree); + + expect(normalized).toEqual([ + { type: "file", name: "Blocks.txt", path: "ucd/Blocks.txt", lastModified: null }, + { type: "file", name: "ReadMe.txt", path: "ReadMe.txt", lastModified: null }, + ]); + }); + + it("handles trailing slashes on directories", () => { + const tree: UnicodeFileTreeNode[] = [ + { + type: "directory", + name: "emoji", + path: "/17.0.0/ucd/emoji/", + lastModified: null, + children: [ + { type: "file", name: "emoji-data.txt", path: "/17.0.0/ucd/emoji/emoji-data.txt", lastModified: null }, + ], + }, + ]; + + const normalized = normalizeTreeForFiltering("17.0.0", tree); + + expect(normalized).toEqual([ + { + type: "directory", + name: "emoji", + path: "emoji", + lastModified: null, + children: [ + { type: "file", name: "emoji-data.txt", path: "emoji/emoji-data.txt", lastModified: null }, + ], + }, + ]); + }); +}); From e77d460e2ffc91eff8bc9d95e45f217d2e102c6d Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 10 Jan 2026 14:14:13 +0100 Subject: [PATCH 9/9] chore: lint --- packages/shared/test/filter.test.ts | 48 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/packages/shared/test/filter.test.ts b/packages/shared/test/filter.test.ts index 23960ddfd..8eac7da9e 100644 --- a/packages/shared/test/filter.test.ts +++ b/packages/shared/test/filter.test.ts @@ -727,32 +727,30 @@ describe("filterTreeStructure", () => { describe("basic filtering", () => { it("should filter files based on extension", () => { const filter = createPathFilter({ include: ["**/*.json"] }); - const result = filterTreeStructure(filter, tree); - - expect(result).toEqual([ - { - type: "file", - name: "root-config.json", - path: "/root-config.json", - }, - { - type: "directory", - name: "extracted", - path: "/extracted/", - children: [ - { - type: "file", - name: "config.json", - path: "/extracted/config.json", - }, - ], - }, - ]); - }); - - - it("should include all items when no filters are applied", () => { + const result = filterTreeStructure(filter, tree); + + expect(result).toEqual([ + { + type: "file", + name: "root-config.json", + path: "/root-config.json", + }, + { + type: "directory", + name: "extracted", + path: "/extracted/", + children: [ + { + type: "file", + name: "config.json", + path: "/extracted/config.json", + }, + ], + }, + ]); + }); + it("should include all items when no filters are applied", () => { const filter = createPathFilter(); const result = filterTreeStructure(filter, tree);