From 9bc3c4edfb209771b5d5cf8988e885279085dc25 Mon Sep 17 00:00:00 2001
From: Cedric van Putten <github@cedric.dev>
Date: Sun, 31 Mar 2024 23:28:18 +0200
Subject: [PATCH] refactor: simplify filters by moving back to picomatch

---
 src/index.ts                                  |  1 -
 src/utils/search.ts                           | 41 ------------------
 .../api/stats/[entry]/modules/index+api.ts    |  4 +-
 webui/src/utils/search.ts                     | 42 +++++++++++++++++++
 4 files changed, 44 insertions(+), 44 deletions(-)
 delete mode 100644 src/utils/search.ts
 create mode 100644 webui/src/utils/search.ts

diff --git a/src/index.ts b/src/index.ts
index a6f840b..00357b0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,5 +6,4 @@ export { StatsFileSource } from './data/StatsFileSource';
 
 export { AtlasError, AtlasValidationError } from './utils/errors';
 export { createAtlasMiddleware } from './utils/middleware';
-export { fuzzyFilterModules } from './utils/search';
 export { createStatsFile, validateStatsFile, getStatsMetdata, getStatsPath } from './utils/stats';
diff --git a/src/utils/search.ts b/src/utils/search.ts
deleted file mode 100644
index bf8b7e9..0000000
--- a/src/utils/search.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import Fuse from 'fuse.js';
-
-import { type StatsModule } from '../data/types';
-
-type ModuleFilters = {
-  include?: string;
-  exclude?: string;
-};
-
-export function fuzzyFilterModules(items: StatsModule[], options: ModuleFilters) {
-  if (!options.include && !options.exclude) {
-    return items;
-  }
-
-  let results = items;
-  const fuse = new Fuse(items, {
-    keys: ['path'],
-    useExtendedSearch: true,
-    shouldSort: false,
-    findAllMatches: true,
-    threshold: 0.7,
-  });
-
-  if (options.include) {
-    results = fuse.search(sanitizePattern(options.include)).map((result) => result.item);
-  }
-
-  if (options.exclude) {
-    const excluded = new Set(
-      fuse.search(sanitizePattern(options.exclude)).map((result) => result.item.path)
-    );
-
-    results = results.filter((item) => !excluded.has(item.path));
-  }
-
-  return results;
-}
-
-function sanitizePattern(pattern: string) {
-  return pattern.replaceAll(/\s*,\s*/g, '|');
-}
diff --git a/webui/src/app/api/stats/[entry]/modules/index+api.ts b/webui/src/app/api/stats/[entry]/modules/index+api.ts
index bfbab70..4d47cf0 100644
--- a/webui/src/app/api/stats/[entry]/modules/index+api.ts
+++ b/webui/src/app/api/stats/[entry]/modules/index+api.ts
@@ -1,7 +1,7 @@
 import { statsModuleFiltersFromUrlParams } from '~/components/forms/StatsModuleFilter';
 import { getSource } from '~/utils/atlas';
+import { globFilterModules } from '~/utils/search';
 import { type StatsEntry, type StatsModule } from '~core/data/types';
-import { fuzzyFilterModules } from '~core/utils/search';
 
 /** The partial module data, when listing all available modules from a stats entry */
 export type ModuleMetadata = Omit<StatsModule, 'source' | 'output'> & {
@@ -62,7 +62,7 @@ function filterModules(request: Request, stats: StatsEntry): ModuleMetadata[] {
     modules = modules.filter((module) => !module.package);
   }
 
-  return fuzzyFilterModules(modules, filters).map((module) => ({
+  return globFilterModules(modules, stats.projectRoot, filters).map((module) => ({
     ...module,
     source: undefined,
     output: undefined,
diff --git a/webui/src/utils/search.ts b/webui/src/utils/search.ts
new file mode 100644
index 0000000..7196594
--- /dev/null
+++ b/webui/src/utils/search.ts
@@ -0,0 +1,42 @@
+import path from 'path';
+import picomatch from 'picomatch';
+
+import { type StatsModule } from '~core/data/types';
+
+type ModuleFilters = {
+  include?: string;
+  exclude?: string;
+};
+
+/**
+ * Filter the modules based on the include and exclude glob patterns.
+ * Note, you can provide multiple patterns using the comma separator.
+ * This also only searches the relative module path from project root, avoiding false positives.
+ */
+export function globFilterModules(
+  items: StatsModule[],
+  projectRoot: string,
+  options: ModuleFilters
+) {
+  if (!options.include && !options.exclude) {
+    return items;
+  }
+
+  const matcher = picomatch(options.include ? splitPattern(options.include) : '**', {
+    cwd: '',
+    dot: true,
+    nocase: true,
+    contains: true,
+    ignore: !options.exclude ? undefined : splitPattern(options.exclude),
+  });
+
+  return items.filter((item) => matcher(path.relative(projectRoot, item.path)));
+}
+
+/**
+ * Split the comma separated string into an array of separate patterns.
+ * This splits on any combination of `,` and whitespaces.
+ */
+function splitPattern(pattern: string) {
+  return pattern.split(/\s*,\s*/);
+}