deslop-js 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,326 +1,19 @@
1
+ import { A as STANDALONE_PROJECT_LOCKFILES, C as RESOLVER_EXTENSIONS, D as SCRIPT_FILE_PATTERN, E as SCRIPT_EXTENSIONLESS_FILE_PATTERN, O as SEMANTIC_MAX_PROGRAM_FILES, S as REACT_NATIVE_PLATFORM_EXTENSIONS, T as SCRIPT_ENTRY_PATTERNS, _ as MINIFIED_DETECTION_MIN_BYTES, a as DEFAULT_EXCLUSIONS, b as OUTPUT_DIRECTORIES, c as DEFAULT_SEMANTIC_TSCONFIG_NAMES, d as IMPLICIT_DEPENDENCIES, f as KNOWN_CONFIG_PREFIXES, g as MAX_PARSE_FILE_SIZE_BYTES, h as MAX_ERROR_DETAIL_LENGTH, i as DEFAULT_ENTRY_GLOBS, j as SUMMARY_CACHE_MAX_BYTES, k as SOURCE_EXTENSIONS$3, l as GIT_CHECK_IGNORE_MAX_BUFFER_BYTES, m as MAX_ANALYSIS_ERRORS, n as BINARY_DETECTION_SAMPLE_BYTES, o as DEFAULT_EXTENSIONS, p as LOCKFILE_MARKERS, r as BUILTIN_MODULES, s as DEFAULT_SEMANTIC_DECORATOR_ALLOWLIST, t as ANALYZED_MANIFEST_FILENAMES, u as HIDDEN_DIRECTORY_ALLOWLIST, v as MIN_NUMERIC_LITERAL_MAGNITUDE_FOR_DUPLICATE, w as SCRIPT_CONFIG_FILE_PATTERN, x as PLATFORM_SUFFIXES, y as MONOREPO_ROOT_MARKERS } from "./constants-oYYofMhb.mjs";
2
+ import { createRequire } from "node:module";
1
3
  import path, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
- import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
3
5
  import fg from "fast-glob";
4
6
  import { readFile } from "node:fs/promises";
5
7
  import { parseSync } from "oxc-parser";
6
8
  import ts from "typescript";
7
9
  import { ResolverFactory } from "oxc-resolver";
10
+ import os, { devNull } from "node:os";
8
11
  import { Worker } from "node:worker_threads";
9
12
  import { fileURLToPath } from "node:url";
10
- import os, { devNull } from "node:os";
11
- import { minimatch } from "minimatch";
13
+ import { Minimatch, minimatch } from "minimatch";
14
+ import crypto from "node:crypto";
12
15
  import { spawnSync } from "node:child_process";
13
16
 
14
- //#region src/constants.ts
15
- const DEFAULT_EXTENSIONS = [
16
- ".ts",
17
- ".tsx",
18
- ".js",
19
- ".jsx",
20
- ".mts",
21
- ".mjs",
22
- ".cts",
23
- ".cjs",
24
- ".mdx",
25
- ".astro",
26
- ".graphql",
27
- ".gql",
28
- ".css",
29
- ".scss",
30
- ".vue",
31
- ".svelte"
32
- ];
33
- const HIDDEN_DIRECTORY_ALLOWLIST = [
34
- ".storybook",
35
- ".vitepress",
36
- ".well-known",
37
- ".changeset",
38
- ".github",
39
- ".client",
40
- ".server"
41
- ];
42
- const OUTPUT_DIRECTORIES = [
43
- "dist",
44
- "build",
45
- "out",
46
- "esm",
47
- "cjs"
48
- ];
49
- const SOURCE_EXTENSIONS$3 = [
50
- "ts",
51
- "tsx",
52
- "mts",
53
- "cts",
54
- "js",
55
- "jsx",
56
- "mjs",
57
- "cjs"
58
- ];
59
- const DEFAULT_EXCLUSIONS = [
60
- "**/node_modules/**",
61
- "**/.git/**",
62
- "**/coverage/**",
63
- "**/*.min.js",
64
- "**/*.min.mjs",
65
- "**/mockServiceWorker.js"
66
- ];
67
- const SCRIPT_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|tsc|npx|bun|esr|esno|jiti|babel-node|zx)\s+(?:\S+\s+)*?([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:\s|$)/;
68
- const SCRIPT_EXTENSIONLESS_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|bun|esr|esno|jiti|babel-node|zx)\s+(?:\S+\s+)*?((?:[./]|[\w@][\w@-]*\/)[\w./@-]+)(?:\s|$)/;
69
- const SCRIPT_CONFIG_FILE_PATTERN = /--config\s+([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))/;
70
- const SCRIPT_ENTRY_PATTERNS = [];
71
- const DEFAULT_ENTRY_GLOBS = [
72
- "src/index.{ts,tsx,js,jsx}",
73
- "src/main.{ts,tsx,js,jsx}",
74
- "index.{ts,tsx,js,jsx}",
75
- "main.{ts,tsx,js,jsx}"
76
- ];
77
- const KNOWN_CONFIG_PREFIXES = [
78
- "babel.config.",
79
- "rollup.config.",
80
- "webpack.config.",
81
- "postcss.config.",
82
- "stencil.config.",
83
- "remotion.config.",
84
- "metro.config.",
85
- "tsup.config.",
86
- "tsdown.config.",
87
- "unbuild.config.",
88
- "esbuild.config.",
89
- "swc.config.",
90
- "turbo.",
91
- "jest.config.",
92
- "jest.setup.",
93
- "vitest.config.",
94
- "vitest.ci.config.",
95
- "vitest.setup.",
96
- "vitest.workspace.",
97
- "playwright.config.",
98
- "cypress.config.",
99
- "karma.conf.",
100
- "eslint.config.",
101
- "prettier.config.",
102
- "stylelint.config.",
103
- "lint-staged.config.",
104
- "commitlint.config.",
105
- "next.config.",
106
- "next-sitemap.config.",
107
- "nuxt.config.",
108
- "astro.config.",
109
- "sanity.config.",
110
- "vite.config.",
111
- "tailwind.config.",
112
- "drizzle.config.",
113
- "knexfile.",
114
- "sentry.client.config.",
115
- "sentry.server.config.",
116
- "sentry.edge.config.",
117
- "react-router.config.",
118
- "typedoc.",
119
- "deslop.config.",
120
- "i18next-parser.config.",
121
- "codegen.config.",
122
- "graphql.config.",
123
- "npmpackagejsonlint.config.",
124
- "release-it.",
125
- "release.config.",
126
- "contentlayer.config.",
127
- "rspack.config.",
128
- "rsbuild.config.",
129
- "module-federation.config.",
130
- "vercel.",
131
- "next-env.d.",
132
- "env.d.",
133
- "vite-env.d."
134
- ];
135
- const IMPLICIT_DEPENDENCIES = new Set([
136
- "typescript",
137
- "@types/node",
138
- "@types/react",
139
- "@types/react-dom",
140
- "eslint",
141
- "prettier",
142
- "husky",
143
- "lint-staged",
144
- "tslib",
145
- "@babel/core",
146
- "@babel/runtime",
147
- "babel-core",
148
- "babel-jest",
149
- "babel-loader",
150
- "postcss",
151
- "cross-env",
152
- "sass",
153
- "node-sass",
154
- "less",
155
- "oxlint",
156
- "biome",
157
- "@biomejs/biome",
158
- "patch-package",
159
- "simple-git-hooks",
160
- "lefthook",
161
- "ts-node",
162
- "ts-jest",
163
- "tsx",
164
- "jsdom",
165
- "rimraf",
166
- "concurrently",
167
- "npm-run-all",
168
- "npm-run-all2",
169
- "dotenv-cli",
170
- "webpack",
171
- "rollup",
172
- "terser",
173
- "autoprefixer",
174
- "tailwindcss",
175
- "react-test-renderer",
176
- "esbuild",
177
- "typedoc",
178
- "commitizen",
179
- "cz-conventional-changelog"
180
- ]);
181
- const BUILTIN_MODULES = new Set([
182
- "assert",
183
- "async_hooks",
184
- "buffer",
185
- "child_process",
186
- "cluster",
187
- "console",
188
- "constants",
189
- "crypto",
190
- "dgram",
191
- "diagnostics_channel",
192
- "dns",
193
- "domain",
194
- "events",
195
- "fs",
196
- "http",
197
- "http2",
198
- "https",
199
- "inspector",
200
- "module",
201
- "net",
202
- "os",
203
- "path",
204
- "perf_hooks",
205
- "process",
206
- "punycode",
207
- "querystring",
208
- "readline",
209
- "repl",
210
- "stream",
211
- "string_decoder",
212
- "sys",
213
- "timers",
214
- "tls",
215
- "trace_events",
216
- "tty",
217
- "url",
218
- "util",
219
- "v8",
220
- "vm",
221
- "wasi",
222
- "worker_threads",
223
- "zlib"
224
- ]);
225
- const PLATFORM_SUFFIXES = [
226
- ".web",
227
- ".react-native",
228
- ".native",
229
- ".ios",
230
- ".android",
231
- ".desktop",
232
- ".windows",
233
- ".macos",
234
- ".any",
235
- ".react-server",
236
- ".server",
237
- ".client"
238
- ];
239
- const REACT_NATIVE_PLATFORM_EXTENSIONS = [
240
- ".web.ts",
241
- ".web.tsx",
242
- ".web.js",
243
- ".web.jsx",
244
- ".native.ts",
245
- ".native.tsx",
246
- ".native.js",
247
- ".native.jsx",
248
- ".ios.ts",
249
- ".ios.tsx",
250
- ".ios.js",
251
- ".ios.jsx",
252
- ".android.ts",
253
- ".android.tsx",
254
- ".android.js",
255
- ".android.jsx"
256
- ];
257
- const RESOLVER_EXTENSIONS = [
258
- ...DEFAULT_EXTENSIONS,
259
- ".d.ts",
260
- ".d.mts",
261
- ".d.cts",
262
- ".json",
263
- ".node",
264
- ".css",
265
- ".scss",
266
- ".less",
267
- ".svg",
268
- ".png",
269
- ".jpg",
270
- ".graphql",
271
- ".gql"
272
- ];
273
- const SEMANTIC_MAX_PROGRAM_FILES = 5e3;
274
- const MAX_PARSE_FILE_SIZE_BYTES = 2e6;
275
- const MAX_ANALYSIS_ERRORS = 5e3;
276
- const MAX_ERROR_DETAIL_LENGTH = 1e3;
277
- const BINARY_DETECTION_SAMPLE_BYTES = 2048;
278
- const MINIFIED_DETECTION_MIN_BYTES = 5e3;
279
- /**
280
- * Numeric literals below 1000 are dominated by indices, counters, small
281
- * ranges, ports, percentages, and array sizes that coincide by accident
282
- * (every `MAX_RETRIES = 3` is not a duplicate of every `LIMIT = 3`).
283
- * 1000 admits real shared constants (timeouts in ms, byte sizes, polling
284
- * intervals) without producing the noise floor that smaller magnitudes do.
285
- * NOTE: even at 1000, the rule still produces medium-confidence false
286
- * positives when constants share a value coincidentally with different
287
- * names (e.g. `STEP_DELAY_MS` vs `MINIMUM_TOKENS`); the report explicitly
288
- * downgrades those to `confidence: "medium"`.
289
- */
290
- const MIN_NUMERIC_LITERAL_MAGNITUDE_FOR_DUPLICATE = 1e3;
291
- const DEFAULT_SEMANTIC_DECORATOR_ALLOWLIST = [
292
- "Component",
293
- "Injectable",
294
- "NgModule",
295
- "Pipe",
296
- "Directive",
297
- "Controller",
298
- "Module",
299
- "Resolver",
300
- "Query",
301
- "Mutation",
302
- "Get",
303
- "Post",
304
- "Put",
305
- "Patch",
306
- "Delete",
307
- "Head",
308
- "Options",
309
- "All",
310
- "Sse",
311
- "WebSocketGateway",
312
- "SubscribeMessage"
313
- ];
314
- const DEFAULT_SEMANTIC_TSCONFIG_NAMES = [
315
- "tsconfig.json",
316
- "tsconfig.app.json",
317
- "tsconfig.build.json",
318
- "tsconfig.src.json",
319
- "jsconfig.json"
320
- ];
321
- const GIT_CHECK_IGNORE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
322
-
323
- //#endregion
324
17
  //#region src/errors.ts
325
18
  const truncateDetail = (text) => {
326
19
  if (text.length <= 1e3) return text;
@@ -2612,12 +2305,6 @@ const resolveWorkspaces = (rootDir) => {
2612
2305
  };
2613
2306
  };
2614
2307
  const IMPLICIT_SUB_PROJECT_SEARCH_DEPTH = 3;
2615
- const STANDALONE_PROJECT_LOCKFILES = [
2616
- "package-lock.json",
2617
- "yarn.lock",
2618
- "pnpm-lock.yaml",
2619
- "bun.lockb"
2620
- ];
2621
2308
  const isStandaloneProject = (directory) => STANDALONE_PROJECT_LOCKFILES.some((lockfile) => existsSync(join(directory, lockfile)));
2622
2309
  const discoverImplicitSubProjects = (rootDir, alreadyDiscoveredDirectories) => {
2623
2310
  const knownDirectories = new Set(alreadyDiscoveredDirectories);
@@ -3444,21 +3131,6 @@ const resolveWithIndexPrefix = (stemPath, directory) => {
3444
3131
 
3445
3132
  //#endregion
3446
3133
  //#region src/utils/find-monorepo-root.ts
3447
- const MONOREPO_ROOT_MARKERS = [
3448
- "pnpm-workspace.yaml",
3449
- "pnpm-workspace.yml",
3450
- "lerna.json",
3451
- "nx.json",
3452
- "turbo.json",
3453
- "rush.json"
3454
- ];
3455
- const LOCKFILE_MARKERS = [
3456
- "pnpm-lock.yaml",
3457
- "yarn.lock",
3458
- "package-lock.json",
3459
- "bun.lockb",
3460
- "bun.lock"
3461
- ];
3462
3134
  const MAX_MONOREPO_WALK_DEPTH = 5;
3463
3135
  const findMonorepoRoot = (rootDir) => {
3464
3136
  let currentDirectory = resolve(rootDir);
@@ -7224,6 +6896,18 @@ const resolveAvailableConcurrency = () => {
7224
6896
  return clampParseConcurrency(available);
7225
6897
  };
7226
6898
 
6899
+ //#endregion
6900
+ //#region src/collect/launch-worker.ts
6901
+ /**
6902
+ * Launches a worker module that sits next to `fromModuleUrl`: the TypeScript
6903
+ * source (via tsx) when the caller itself runs from source — tests and direct
6904
+ * `tsx` execution — and the built `.mjs` sibling when running from dist.
6905
+ */
6906
+ const launchSiblingWorker = (fromModuleUrl, workerBaseName) => {
6907
+ const isTypeScriptSource = fromModuleUrl.endsWith(".ts");
6908
+ return new Worker(fileURLToPath(new URL(isTypeScriptSource ? `./${workerBaseName}.ts` : `./${workerBaseName}.mjs`, fromModuleUrl)), { ...isTypeScriptSource ? { execArgv: ["--import", "tsx"] } : {} });
6909
+ };
6910
+
7227
6911
  //#endregion
7228
6912
  //#region src/collect/parallel-parse.ts
7229
6913
  const deserializeErrors = (serializedErrors) => serializedErrors.map((errorJson) => new DeslopError({
@@ -7250,14 +6934,6 @@ const deserializeParsedSource = (serialized) => ({
7250
6934
  duplicateConstantCandidates: serialized.duplicateConstantCandidates,
7251
6935
  errors: deserializeErrors(serialized.errors)
7252
6936
  });
7253
- const resolveWorkerPath = () => {
7254
- const currentUrl = import.meta.url;
7255
- if (currentUrl.endsWith(".ts")) return fileURLToPath(new URL("./parse-worker.ts", currentUrl));
7256
- return fileURLToPath(new URL("./parse-worker.mjs", currentUrl));
7257
- };
7258
- const createWorker = (workerPath) => {
7259
- return new Worker(workerPath, { ...workerPath.endsWith(".ts") ? { execArgv: ["--import", "tsx"] } : {} });
7260
- };
7261
6937
  const waitForReady = (worker) => new Promise((resolve, reject) => {
7262
6938
  const onMessage = (message) => {
7263
6939
  if (message.type === "ready") {
@@ -7275,11 +6951,10 @@ const waitForReady = (worker) => new Promise((resolve, reject) => {
7275
6951
  worker.on("error", onError);
7276
6952
  });
7277
6953
  const parseFilesWithWorkerPool = async (files, workerCount) => {
7278
- const workerPath = resolveWorkerPath();
7279
6954
  const results = new Array(files.length);
7280
6955
  const workers = [];
7281
6956
  try {
7282
- for (let workerIndex = 0; workerIndex < workerCount; workerIndex++) workers.push(createWorker(workerPath));
6957
+ for (let workerIndex = 0; workerIndex < workerCount; workerIndex++) workers.push(launchSiblingWorker(import.meta.url, "parse-worker"));
7283
6958
  await Promise.all(workers.map(waitForReady));
7284
6959
  } catch {
7285
6960
  for (const worker of workers) worker.terminate();
@@ -7799,10 +7474,16 @@ const detectDeadExports = (graph, config) => {
7799
7474
  const buildUsageMap = (graph) => {
7800
7475
  const usedExportKeys = /* @__PURE__ */ new Set();
7801
7476
  const sourceToTargetMap = buildSourceToTargetsMap(graph);
7477
+ const reExportEdgesBySource = /* @__PURE__ */ new Map();
7478
+ for (const edge of graph.edges) {
7479
+ if (!edge.isReExportEdge) continue;
7480
+ const existingEdges = reExportEdgesBySource.get(edge.source);
7481
+ if (existingEdges) existingEdges.push(edge);
7482
+ else reExportEdgesBySource.set(edge.source, [edge]);
7483
+ }
7802
7484
  for (const module of graph.modules) {
7803
7485
  if (!module.isEntryPoint) continue;
7804
- for (const edge of graph.edges) {
7805
- if (edge.source !== module.fileId.index || !edge.isReExportEdge) continue;
7486
+ for (const edge of reExportEdgesBySource.get(module.fileId.index) ?? []) {
7806
7487
  const targetModule = graph.modules[edge.target];
7807
7488
  if (!targetModule) continue;
7808
7489
  if (edge.reExportedNames.includes("*")) markAllExportsUsedRecursive(targetModule, graph, sourceToTargetMap, usedExportKeys, /* @__PURE__ */ new Set());
@@ -8049,12 +7730,29 @@ const matchesPackageTokenReference = (command, packageName) => {
8049
7730
 
8050
7731
  //#endregion
8051
7732
  //#region src/report/packages.ts
8052
- const discoverAllPackageJsonPaths = (rootDir) => {
7733
+ const globPackageFiles = (cwd, patterns, options, summaryCache) => summaryCache?.matchWalkedFiles({
7734
+ cwd,
7735
+ patterns,
7736
+ ...options
7737
+ }) ?? fg.sync([...patterns], {
7738
+ cwd,
7739
+ absolute: true,
7740
+ onlyFiles: true,
7741
+ ignore: [...options.ignore],
7742
+ deep: options.deep,
7743
+ ...options.dot === void 0 ? {} : { dot: options.dot }
7744
+ });
7745
+ const containsPackageName = (content, packageName) => content.includes(packageName);
7746
+ const matchPackageNamesInFile = (filePath, kind, names, matcher, summaryCache) => {
7747
+ if (summaryCache !== void 0) return summaryCache.matchPackageNames(filePath, kind, names, matcher);
7748
+ const content = readFileSync(filePath, "utf-8");
7749
+ const matchedNames = [];
7750
+ for (const packageName of names) if (matcher(content, packageName)) matchedNames.push(packageName);
7751
+ return matchedNames;
7752
+ };
7753
+ const discoverAllPackageJsonPaths = (rootDir, summaryCache) => {
8053
7754
  const paths = [join(rootDir, "package.json")];
8054
- const workspacePackageJsons = fg.sync("**/package.json", {
8055
- cwd: rootDir,
8056
- absolute: true,
8057
- onlyFiles: true,
7755
+ const workspacePackageJsons = globPackageFiles(rootDir, ["**/package.json"], {
8058
7756
  ignore: [
8059
7757
  "**/node_modules/**",
8060
7758
  "**/dist/**",
@@ -8062,11 +7760,11 @@ const discoverAllPackageJsonPaths = (rootDir) => {
8062
7760
  "**/.git/**"
8063
7761
  ],
8064
7762
  deep: 5
8065
- });
7763
+ }, summaryCache);
8066
7764
  for (const workspacePath of workspacePackageJsons) if (workspacePath !== paths[0] && !paths.includes(workspacePath)) paths.push(workspacePath);
8067
7765
  return paths;
8068
7766
  };
8069
- const detectStalePackages = (graph, config) => {
7767
+ const detectStalePackages = (graph, config, summaryCache) => {
8070
7768
  const packageJsonPath = resolve(config.rootDir, "package.json");
8071
7769
  let packageJson;
8072
7770
  try {
@@ -8084,7 +7782,7 @@ const detectStalePackages = (graph, config) => {
8084
7782
  const usedPackageNames = collectUsedPackages(graph);
8085
7783
  const monorepoRoot = findMonorepoRoot(config.rootDir);
8086
7784
  const nodeModulesSearchRoots = monorepoRoot && monorepoRoot !== config.rootDir ? [config.rootDir, monorepoRoot] : [config.rootDir];
8087
- const allPackageJsonPaths = discoverAllPackageJsonPaths(config.rootDir);
7785
+ const allPackageJsonPaths = discoverAllPackageJsonPaths(config.rootDir, summaryCache);
8088
7786
  if (monorepoRoot) {
8089
7787
  const monorepoPackageJson = join(monorepoRoot, "package.json");
8090
7788
  if (!allPackageJsonPaths.includes(monorepoPackageJson) && existsSync(monorepoPackageJson)) allPackageJsonPaths.push(monorepoPackageJson);
@@ -8097,13 +7795,13 @@ const detectStalePackages = (graph, config) => {
8097
7795
  const packageJsonConfigReferenced = collectPackageJsonConfigReferences(workspacePackageJsonPath, declaredNames);
8098
7796
  for (const packageName of packageJsonConfigReferenced) usedPackageNames.add(packageName);
8099
7797
  }
8100
- const nxProjectReferenced = collectNxProjectJsonReferences(config.rootDir, declaredNames, binToPackage);
7798
+ const nxProjectReferenced = collectNxProjectJsonReferences(config.rootDir, declaredNames, binToPackage, summaryCache);
8101
7799
  for (const packageName of nxProjectReferenced) usedPackageNames.add(packageName);
8102
7800
  const configSearchRoots = monorepoRoot && monorepoRoot !== config.rootDir ? [config.rootDir, monorepoRoot] : [config.rootDir];
8103
7801
  for (const configSearchRoot of configSearchRoots) {
8104
- const configReferenced = collectConfigReferencedPackages(configSearchRoot, graph, declaredNames);
7802
+ const configReferenced = collectConfigReferencedPackages(configSearchRoot, graph, declaredNames, summaryCache);
8105
7803
  for (const packageName of configReferenced) usedPackageNames.add(packageName);
8106
- const tsconfigReferenced = collectTsconfigReferencedPackages(configSearchRoot);
7804
+ const tsconfigReferenced = collectTsconfigReferencedPackages(configSearchRoot, summaryCache);
8107
7805
  for (const packageName of tsconfigReferenced) usedPackageNames.add(packageName);
8108
7806
  const { packageNames: expoPluginPackageNames } = extractExpoConfigPluginEntries(configSearchRoot, {
8109
7807
  ...dependencies,
@@ -8155,7 +7853,7 @@ const detectStalePackages = (graph, config) => {
8155
7853
  candidateUnused.add(dependencyName);
8156
7854
  }
8157
7855
  if (candidateUnused.size > 0) {
8158
- const sourceFileRescued = scanSourceFilesForPackageImports(config.rootDir, candidateUnused);
7856
+ const sourceFileRescued = scanSourceFilesForPackageImports(config.rootDir, candidateUnused, summaryCache);
8159
7857
  for (const packageName of sourceFileRescued) {
8160
7858
  usedPackageNames.add(packageName);
8161
7859
  candidateUnused.delete(packageName);
@@ -8323,35 +8021,26 @@ const CONFIG_FILE_GLOBS = [
8323
8021
  ".dumirc.js",
8324
8022
  "dumi.config.{ts,js}"
8325
8023
  ];
8326
- const collectConfigReferencedPackages = (rootDir, graph, declaredNames) => {
8024
+ const collectConfigReferencedPackages = (rootDir, graph, declaredNames, summaryCache) => {
8327
8025
  const referenced = /* @__PURE__ */ new Set();
8328
- for (const module of graph.modules) {
8329
- if (!module.isConfigFile) continue;
8026
+ const addMatchesFromFile = (filePath, kind, matcher) => {
8330
8027
  try {
8331
- const content = readFileSync(module.fileId.path, "utf-8");
8332
- for (const packageName of declaredNames) if (content.includes(packageName)) referenced.add(packageName);
8028
+ for (const packageName of matchPackageNamesInFile(filePath, kind, declaredNames, matcher, summaryCache)) referenced.add(packageName);
8333
8029
  } catch {
8334
- continue;
8030
+ return;
8335
8031
  }
8032
+ };
8033
+ for (const module of graph.modules) {
8034
+ if (!module.isConfigFile) continue;
8035
+ addMatchesFromFile(module.fileId.path, "substring", containsPackageName);
8336
8036
  }
8337
- const configFiles = fg.sync(CONFIG_FILE_GLOBS, {
8338
- cwd: rootDir,
8339
- absolute: true,
8340
- onlyFiles: true,
8037
+ const configFiles = globPackageFiles(rootDir, CONFIG_FILE_GLOBS, {
8341
8038
  ignore: ["**/node_modules/**"],
8342
8039
  dot: true,
8343
8040
  deep: 3
8344
- });
8345
- for (const configPath of configFiles) try {
8346
- const content = readFileSync(configPath, "utf-8");
8347
- for (const packageName of declaredNames) if (content.includes(packageName)) referenced.add(packageName);
8348
- } catch {
8349
- continue;
8350
- }
8351
- const documentationFiles = fg.sync(["**/*.{mdx,md}"], {
8352
- cwd: rootDir,
8353
- absolute: true,
8354
- onlyFiles: true,
8041
+ }, summaryCache);
8042
+ for (const configPath of configFiles) addMatchesFromFile(configPath, "substring", containsPackageName);
8043
+ const documentationFiles = globPackageFiles(rootDir, ["**/*.{mdx,md}"], {
8355
8044
  ignore: [
8356
8045
  "**/node_modules/**",
8357
8046
  "**/dist/**",
@@ -8359,13 +8048,8 @@ const collectConfigReferencedPackages = (rootDir, graph, declaredNames) => {
8359
8048
  "**/CHANGELOG.md"
8360
8049
  ],
8361
8050
  deep: 6
8362
- });
8363
- for (const documentationPath of documentationFiles) try {
8364
- const content = readFileSync(documentationPath, "utf-8");
8365
- for (const packageName of declaredNames) if (matchesPackageImportReference(content, packageName)) referenced.add(packageName);
8366
- } catch {
8367
- continue;
8368
- }
8051
+ }, summaryCache);
8052
+ for (const documentationPath of documentationFiles) addMatchesFromFile(documentationPath, "importReference", matchesPackageImportReference);
8369
8053
  return referenced;
8370
8054
  };
8371
8055
  const PACKAGE_JSON_CONFIG_SECTIONS = [
@@ -8436,19 +8120,16 @@ const collectPackageJsonConfigReferences = (packageJsonPath, declaredNames) => {
8436
8120
  }
8437
8121
  return referenced;
8438
8122
  };
8439
- const collectNxProjectJsonReferences = (rootDir, declaredNames, binToPackage) => {
8123
+ const collectNxProjectJsonReferences = (rootDir, declaredNames, binToPackage, summaryCache) => {
8440
8124
  const referenced = /* @__PURE__ */ new Set();
8441
- const projectJsonPaths = fg.sync(["project.json", "**/project.json"], {
8442
- cwd: rootDir,
8443
- absolute: true,
8444
- onlyFiles: true,
8125
+ const projectJsonPaths = globPackageFiles(rootDir, ["project.json", "**/project.json"], {
8445
8126
  ignore: [
8446
8127
  "**/node_modules/**",
8447
8128
  "**/dist/**",
8448
8129
  "**/build/**"
8449
8130
  ],
8450
8131
  deep: 5
8451
- });
8132
+ }, summaryCache);
8452
8133
  for (const projectJsonPath of projectJsonPaths) try {
8453
8134
  const content = readFileSync(projectJsonPath, "utf-8");
8454
8135
  const projectJson = JSON.parse(content);
@@ -8476,16 +8157,13 @@ const TSCONFIG_GLOBS = [
8476
8157
  "**/tsconfig.json",
8477
8158
  "**/tsconfig.*.json"
8478
8159
  ];
8479
- const collectTsconfigReferencedPackages = (rootDir) => {
8160
+ const collectTsconfigReferencedPackages = (rootDir, summaryCache) => {
8480
8161
  const referenced = /* @__PURE__ */ new Set();
8481
- const tsconfigFiles = fg.sync(TSCONFIG_GLOBS, {
8482
- cwd: rootDir,
8483
- absolute: true,
8484
- onlyFiles: true,
8162
+ const tsconfigFiles = globPackageFiles(rootDir, TSCONFIG_GLOBS, {
8485
8163
  ignore: ["**/node_modules/**"],
8486
8164
  dot: false,
8487
8165
  deep: 4
8488
- });
8166
+ }, summaryCache);
8489
8167
  for (const tsconfigPath of tsconfigFiles) try {
8490
8168
  const cleaned = readFileSync(tsconfigPath, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
8491
8169
  const parsed = JSON.parse(cleaned);
@@ -8531,16 +8209,22 @@ const SOURCE_FILE_IGNORES = [
8531
8209
  "**/*.min.js",
8532
8210
  "**/*.d.ts"
8533
8211
  ];
8534
- const scanSourceFilesForPackageImports = (rootDir, candidatePackages) => {
8212
+ const scanSourceFilesForPackageImports = (rootDir, candidatePackages, summaryCache) => {
8535
8213
  const found = /* @__PURE__ */ new Set();
8536
8214
  if (candidatePackages.size === 0) return found;
8537
- const sourceFiles = fg.sync(SOURCE_FILE_GLOBS, {
8538
- cwd: rootDir,
8539
- absolute: true,
8540
- onlyFiles: true,
8215
+ const sourceFiles = globPackageFiles(rootDir, SOURCE_FILE_GLOBS, {
8541
8216
  ignore: SOURCE_FILE_IGNORES,
8542
8217
  deep: 15
8543
- });
8218
+ }, summaryCache);
8219
+ if (summaryCache !== void 0) {
8220
+ for (const filePath of sourceFiles) try {
8221
+ for (const packageName of summaryCache.matchPackageNames(filePath, "importReference", candidatePackages, matchesPackageImportReference)) found.add(packageName);
8222
+ } catch {
8223
+ continue;
8224
+ }
8225
+ for (const packageName of found) candidatePackages.delete(packageName);
8226
+ return found;
8227
+ }
8544
8228
  for (const filePath of sourceFiles) {
8545
8229
  if (candidatePackages.size === 0) break;
8546
8230
  try {
@@ -12510,7 +12194,7 @@ const safeReportDetector = (detectorName, detector, fallback, errorSink) => runS
12510
12194
  module: "report",
12511
12195
  contextDescription: "while building findings"
12512
12196
  });
12513
- const generateReport = (graph, config) => {
12197
+ const generateReport = (graph, config, summaryCache) => {
12514
12198
  const analysisStartTime = performance.now();
12515
12199
  const errorSink = [];
12516
12200
  for (const module of graph.modules) {
@@ -12522,7 +12206,7 @@ const generateReport = (graph, config) => {
12522
12206
  }
12523
12207
  const unusedFiles = safeReportDetector("detectOrphanFiles", () => detectOrphanFiles(graph), [], errorSink);
12524
12208
  const unusedExports = safeReportDetector("detectDeadExports", () => detectDeadExports(graph, config), [], errorSink);
12525
- const unusedDependencies = safeReportDetector("detectStalePackages", () => detectStalePackages(graph, config), [], errorSink);
12209
+ const unusedDependencies = safeReportDetector("detectStalePackages", () => detectStalePackages(graph, config, summaryCache), [], errorSink);
12526
12210
  const circularDependencies = safeReportDetector("detectCycles", () => detectCycles(graph), [], errorSink);
12527
12211
  const syntacticRedundantAliases = config.reportRedundancy ? [...safeReportDetector("detectRedundantAliases", () => detectRedundantAliases(graph), [], errorSink), ...safeReportDetector("detectUselessAliasedReExports", () => detectUselessAliasedReExports(graph), [], errorSink)] : [];
12528
12212
  const duplicateExports = config.reportRedundancy ? safeReportDetector("detectDuplicateExports", () => detectDuplicateExports(graph), [], errorSink) : [];
@@ -12616,6 +12300,478 @@ const generateReport = (graph, config) => {
12616
12300
  };
12617
12301
  };
12618
12302
 
12303
+ //#endregion
12304
+ //#region src/collect/entries-in-worker.ts
12305
+ /**
12306
+ * Runs `resolveEntries` on a dedicated worker thread so its ~90%-synchronous
12307
+ * fs work overlaps the main-thread analysis phases — the incremental-cache
12308
+ * warm path has no long parse `await` left to hide it behind. Entry content
12309
+ * reads stay live (fresh every run) exactly as inline. Worker infrastructure
12310
+ * failures fall back to the inline call (same result, just serialized); an
12311
+ * error thrown by `resolveEntries` itself rejects with the same message an
12312
+ * inline throw would, so the caller's fallback-to-empty-entries handling is
12313
+ * unchanged.
12314
+ */
12315
+ const resolveEntriesInWorker = async (config) => {
12316
+ let worker;
12317
+ try {
12318
+ worker = launchSiblingWorker(import.meta.url, "entries-worker");
12319
+ } catch {
12320
+ return resolveEntries(config);
12321
+ }
12322
+ try {
12323
+ const outcome = await new Promise((resolveOutcome) => {
12324
+ worker.on("message", (message) => {
12325
+ if (message.type === "ready") worker.postMessage({
12326
+ type: "resolve-entries",
12327
+ config
12328
+ });
12329
+ else if (message.type === "result") resolveOutcome({
12330
+ kind: "result",
12331
+ entries: message.entries
12332
+ });
12333
+ else if (message.type === "error") resolveOutcome({
12334
+ kind: "analysis-error",
12335
+ errorMessage: message.errorMessage
12336
+ });
12337
+ });
12338
+ worker.on("error", () => resolveOutcome({ kind: "infra-failure" }));
12339
+ worker.on("exit", () => resolveOutcome({ kind: "infra-failure" }));
12340
+ });
12341
+ if (outcome.kind === "result") return outcome.entries;
12342
+ if (outcome.kind === "analysis-error") throw new Error(outcome.errorMessage);
12343
+ return resolveEntries(config);
12344
+ } finally {
12345
+ worker.terminate();
12346
+ }
12347
+ };
12348
+
12349
+ //#endregion
12350
+ //#region src/summary-cache.ts
12351
+ const WALK_SKIP_DIRECTORY_NAMES = new Set(["node_modules", ".git"]);
12352
+ const MANIFEST_LIKE_FILENAMES = new Set(ANALYZED_MANIFEST_FILENAMES);
12353
+ const isManifestLikeFileName = (fileName) => MANIFEST_LIKE_FILENAMES.has(fileName) || (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
12354
+ const isResolverConfigFileName = (fileName) => fileName.includes("webpack") || fileName.startsWith("vite.config.") || fileName.startsWith("vitest.config.") || fileName.startsWith("babel.config.") || fileName.startsWith(".babelrc") || fileName.startsWith("jest.config.");
12355
+ const sha1Hex = (text) => crypto.createHash("sha1").update(text).digest("hex");
12356
+ const sha1OfFileBytes = (filePath) => {
12357
+ try {
12358
+ return crypto.createHash("sha1").update(readFileSync(filePath)).digest("hex");
12359
+ } catch {
12360
+ return null;
12361
+ }
12362
+ };
12363
+ const fileNameOfPosixPath = (posixPath) => posixPath.slice(posixPath.lastIndexOf("/") + 1);
12364
+ const deslopRequire = createRequire(import.meta.url);
12365
+ const resolveOwnPackageVersion = () => {
12366
+ try {
12367
+ const packageJson = deslopRequire("deslop-js/package.json");
12368
+ return typeof packageJson?.version === "string" ? packageJson.version : "unknown";
12369
+ } catch {
12370
+ return "unknown";
12371
+ }
12372
+ };
12373
+ const walkTreeStats = (rootDirectory) => {
12374
+ const collected = /* @__PURE__ */ new Map();
12375
+ const followedLinkTargets = /* @__PURE__ */ new Set();
12376
+ const walk = (directory) => {
12377
+ let entries;
12378
+ try {
12379
+ entries = readdirSync(directory, { withFileTypes: true });
12380
+ } catch {
12381
+ return;
12382
+ }
12383
+ for (const entry of entries) {
12384
+ const entryPath = `${directory}/${entry.name}`;
12385
+ if (entry.isDirectory()) {
12386
+ if (!WALK_SKIP_DIRECTORY_NAMES.has(entry.name)) walk(entryPath);
12387
+ continue;
12388
+ }
12389
+ if (entry.isSymbolicLink()) {
12390
+ if (WALK_SKIP_DIRECTORY_NAMES.has(entry.name)) continue;
12391
+ try {
12392
+ const linkStat = statSync(entryPath);
12393
+ if (linkStat.isDirectory()) {
12394
+ const linkTarget = realpathSync(entryPath);
12395
+ if (!followedLinkTargets.has(linkTarget)) {
12396
+ followedLinkTargets.add(linkTarget);
12397
+ walk(entryPath);
12398
+ }
12399
+ } else if (linkStat.isFile()) collected.set(entryPath, {
12400
+ m: linkStat.mtimeMs,
12401
+ s: linkStat.size
12402
+ });
12403
+ } catch {
12404
+ continue;
12405
+ }
12406
+ continue;
12407
+ }
12408
+ if (!entry.isFile()) continue;
12409
+ try {
12410
+ const fileStat = statSync(entryPath);
12411
+ collected.set(entryPath, {
12412
+ m: fileStat.mtimeMs,
12413
+ s: fileStat.size
12414
+ });
12415
+ } catch {
12416
+ continue;
12417
+ }
12418
+ }
12419
+ };
12420
+ walk(toPosixPath(resolve(rootDirectory)));
12421
+ return collected;
12422
+ };
12423
+ const compileGlobAlternation = (patterns, matchDotNames) => {
12424
+ const regexSources = [];
12425
+ for (const pattern of patterns) {
12426
+ const compiled = new Minimatch(pattern, { dot: matchDotNames }).makeRe();
12427
+ if (compiled === false) return null;
12428
+ regexSources.push(compiled.source);
12429
+ }
12430
+ return regexSources.length === 0 ? null : new RegExp(regexSources.join("|"));
12431
+ };
12432
+ const countPathSegments = (relativePath) => {
12433
+ let segmentCount = 1;
12434
+ for (let charIndex = 0; charIndex < relativePath.length; charIndex++) if (relativePath.charCodeAt(charIndex) === 47) segmentCount++;
12435
+ return segmentCount;
12436
+ };
12437
+ const contentFingerprintOf = (filePath, fileStat) => `${filePath}:${fileStat.s}:${sha1OfFileBytes(filePath) ?? `stat-${fileStat.m}`}`;
12438
+ const computeCollectHash = (walkedStats) => {
12439
+ const fingerprintLines = [];
12440
+ for (const [filePath, fileStat] of walkedStats) fingerprintLines.push(isManifestLikeFileName(fileNameOfPosixPath(filePath)) ? contentFingerprintOf(filePath, fileStat) : filePath);
12441
+ fingerprintLines.sort();
12442
+ return sha1Hex(fingerprintLines.join("\n"));
12443
+ };
12444
+ const computeResolutionHash = (collectHash, walkedStats) => {
12445
+ const fingerprintLines = [];
12446
+ for (const [filePath, fileStat] of walkedStats) if (isResolverConfigFileName(fileNameOfPosixPath(filePath))) fingerprintLines.push(contentFingerprintOf(filePath, fileStat));
12447
+ fingerprintLines.sort();
12448
+ return sha1Hex(`${collectHash}\n${fingerprintLines.join("\n")}`);
12449
+ };
12450
+ const computeScopeHash = (config) => sha1Hex(JSON.stringify({
12451
+ deslopVersion: resolveOwnPackageVersion(),
12452
+ rootDir: toPosixPath(resolve(config.rootDir)),
12453
+ entryPatterns: config.entryPatterns,
12454
+ ignorePatterns: config.ignorePatterns,
12455
+ includeExtensions: config.includeExtensions,
12456
+ reportTypes: config.reportTypes,
12457
+ includeEntryExports: config.includeEntryExports,
12458
+ reportRedundancy: config.reportRedundancy,
12459
+ tsConfigPath: config.tsConfigPath ?? null,
12460
+ paths: config.paths ?? null
12461
+ }));
12462
+ const isRecordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12463
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
12464
+ const isOptionalArray = (value) => value === void 0 || Array.isArray(value);
12465
+ const emptyStore = (scopeHash) => ({
12466
+ version: 2,
12467
+ scopeHash,
12468
+ fileList: null,
12469
+ resolutions: null,
12470
+ summaries: {},
12471
+ packageFacts: {}
12472
+ });
12473
+ const readPersistedStore = (cachePath, scopeHash) => {
12474
+ try {
12475
+ const parsed = JSON.parse(readFileSync(cachePath, "utf-8"));
12476
+ if (isRecordValue(parsed) && parsed.version === 2 && parsed.scopeHash === scopeHash && isRecordValue(parsed.summaries) && isRecordValue(parsed.packageFacts)) {
12477
+ const persisted = parsed;
12478
+ return {
12479
+ version: 2,
12480
+ scopeHash,
12481
+ fileList: isRecordValue(persisted.fileList) ? persisted.fileList : null,
12482
+ resolutions: isRecordValue(persisted.resolutions) && typeof persisted.resolutions.hash === "string" && isRecordValue(persisted.resolutions.entries) ? persisted.resolutions : null,
12483
+ summaries: persisted.summaries,
12484
+ packageFacts: persisted.packageFacts
12485
+ };
12486
+ }
12487
+ } catch {}
12488
+ return emptyStore(scopeHash);
12489
+ };
12490
+ const intersectLocalReferencesWithOwnExports = (parsed) => {
12491
+ if (parsed.localIdentifierReferences.length === 0) return [];
12492
+ const ownExportNames = new Set(parsed.exports.map((exportInfo) => exportInfo.name));
12493
+ return parsed.localIdentifierReferences.filter((identifierName) => ownExportNames.has(identifierName));
12494
+ };
12495
+ const toPersistedArray = (values) => values.length > 0 ? values : void 0;
12496
+ const serializeParsedSource = (parsed, shouldPersistDryPatternFields) => ({
12497
+ imports: toPersistedArray(parsed.imports),
12498
+ exports: toPersistedArray(parsed.exports),
12499
+ memberAccesses: toPersistedArray(parsed.memberAccesses),
12500
+ wholeObjectUses: toPersistedArray(parsed.wholeObjectUses),
12501
+ localIdentifierReferences: toPersistedArray(intersectLocalReferencesWithOwnExports(parsed)),
12502
+ referencedFilenames: toPersistedArray(parsed.referencedFilenames),
12503
+ ...shouldPersistDryPatternFields ? {
12504
+ redundantTypePatterns: toPersistedArray(parsed.redundantTypePatterns),
12505
+ identityWrappers: toPersistedArray(parsed.identityWrappers),
12506
+ typeDefinitionHashes: toPersistedArray(parsed.typeDefinitionHashes),
12507
+ inlineTypeLiterals: toPersistedArray(parsed.inlineTypeLiterals),
12508
+ simplifiableFunctions: toPersistedArray(parsed.simplifiableFunctions),
12509
+ simplifiableExpressions: toPersistedArray(parsed.simplifiableExpressions),
12510
+ duplicateConstantCandidates: toPersistedArray(parsed.duplicateConstantCandidates)
12511
+ } : {},
12512
+ errors: toPersistedArray(parsed.errors.map((deslopError) => deslopError.toJSON()))
12513
+ });
12514
+ const isPersistedErrorJson = (value) => isRecordValue(value) && typeof value.code === "string" && typeof value.module === "string" && typeof value.message === "string";
12515
+ const PERSISTED_SOURCE_ARRAY_FIELDS = [
12516
+ "imports",
12517
+ "exports",
12518
+ "memberAccesses",
12519
+ "wholeObjectUses",
12520
+ "localIdentifierReferences",
12521
+ "referencedFilenames",
12522
+ "redundantTypePatterns",
12523
+ "identityWrappers",
12524
+ "typeDefinitionHashes",
12525
+ "inlineTypeLiterals",
12526
+ "simplifiableFunctions",
12527
+ "simplifiableExpressions",
12528
+ "duplicateConstantCandidates",
12529
+ "errors"
12530
+ ];
12531
+ const reviveParsedSource = (persisted) => {
12532
+ if (!isRecordValue(persisted)) return null;
12533
+ for (const fieldName of PERSISTED_SOURCE_ARRAY_FIELDS) if (!isOptionalArray(persisted[fieldName])) return null;
12534
+ const source = persisted;
12535
+ const persistedErrors = source.errors ?? [];
12536
+ if (!persistedErrors.every(isPersistedErrorJson)) return null;
12537
+ return {
12538
+ imports: source.imports ?? [],
12539
+ exports: source.exports ?? [],
12540
+ memberAccesses: source.memberAccesses ?? [],
12541
+ wholeObjectUses: source.wholeObjectUses ?? [],
12542
+ localIdentifierReferences: source.localIdentifierReferences ?? [],
12543
+ referencedFilenames: source.referencedFilenames ?? [],
12544
+ redundantTypePatterns: source.redundantTypePatterns ?? [],
12545
+ identityWrappers: source.identityWrappers ?? [],
12546
+ typeDefinitionHashes: source.typeDefinitionHashes ?? [],
12547
+ inlineTypeLiterals: source.inlineTypeLiterals ?? [],
12548
+ simplifiableFunctions: source.simplifiableFunctions ?? [],
12549
+ simplifiableExpressions: source.simplifiableExpressions ?? [],
12550
+ duplicateConstantCandidates: source.duplicateConstantCandidates ?? [],
12551
+ errors: persistedErrors.map((errorJson) => new DeslopError({
12552
+ code: errorJson.code,
12553
+ module: errorJson.module,
12554
+ severity: errorJson.severity,
12555
+ message: errorJson.message,
12556
+ path: errorJson.path,
12557
+ detail: errorJson.detail
12558
+ }))
12559
+ };
12560
+ };
12561
+ const atomicWriteFile = (filePath, contents) => {
12562
+ try {
12563
+ mkdirSync(dirname(filePath), { recursive: true });
12564
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
12565
+ writeFileSync(temporaryPath, contents);
12566
+ renameSync(temporaryPath, filePath);
12567
+ } catch {}
12568
+ };
12569
+ const createSummaryCache = (cachePath, config) => {
12570
+ const scopeHash = computeScopeHash(config);
12571
+ const store = readPersistedStore(cachePath, scopeHash);
12572
+ const walkRoot = toPosixPath(resolve(config.rootDir));
12573
+ const walkedStats = walkTreeStats(config.rootDir);
12574
+ const collectHash = computeCollectHash(walkedStats);
12575
+ const resolutionHash = computeResolutionHash(collectHash, walkedStats);
12576
+ const shouldPersistDryPatternFields = config.reportRedundancy;
12577
+ if (store.resolutions === null || store.resolutions.hash !== resolutionHash) store.resolutions = {
12578
+ hash: resolutionHash,
12579
+ entries: {}
12580
+ };
12581
+ const resolutionEntries = store.resolutions.entries;
12582
+ let isDirty = false;
12583
+ const activeSummaryPaths = /* @__PURE__ */ new Set();
12584
+ const activeResolutionKeys = /* @__PURE__ */ new Set();
12585
+ const activeFactPaths = /* @__PURE__ */ new Set();
12586
+ const sortedNameSetMemo = /* @__PURE__ */ new WeakMap();
12587
+ const statOfLive = (filePath) => {
12588
+ try {
12589
+ const fileStat = statSync(filePath);
12590
+ return {
12591
+ m: fileStat.mtimeMs,
12592
+ s: fileStat.size
12593
+ };
12594
+ } catch {
12595
+ return null;
12596
+ }
12597
+ };
12598
+ const statOf = (filePath) => walkedStats.get(filePath) ?? statOfLive(filePath);
12599
+ return {
12600
+ lookupFileList: () => {
12601
+ const cachedFileList = store.fileList;
12602
+ if (cachedFileList === null || cachedFileList.hash !== collectHash || !isStringArray(cachedFileList.files)) return null;
12603
+ return cachedFileList.files.map((filePath, fileIndex) => ({
12604
+ index: fileIndex,
12605
+ path: filePath
12606
+ }));
12607
+ },
12608
+ storeFileList: (files) => {
12609
+ store.fileList = {
12610
+ hash: collectHash,
12611
+ files: files.map((file) => file.path)
12612
+ };
12613
+ isDirty = true;
12614
+ },
12615
+ lookupSummary: (filePath) => {
12616
+ const cachedSummary = store.summaries[filePath];
12617
+ if (!isRecordValue(cachedSummary)) return null;
12618
+ const fileStat = statOf(filePath);
12619
+ if (!fileStat) return null;
12620
+ if (fileStat.m !== cachedSummary.m || fileStat.s !== cachedSummary.s) {
12621
+ if (fileStat.s !== cachedSummary.s || typeof cachedSummary.h !== "string") return null;
12622
+ const contentHash = sha1OfFileBytes(filePath);
12623
+ if (contentHash === null || contentHash !== cachedSummary.h) return null;
12624
+ cachedSummary.m = fileStat.m;
12625
+ cachedSummary.s = fileStat.s;
12626
+ isDirty = true;
12627
+ }
12628
+ const revived = reviveParsedSource(cachedSummary.p);
12629
+ if (revived === null) return null;
12630
+ activeSummaryPaths.add(filePath);
12631
+ return revived;
12632
+ },
12633
+ storeSummary: (filePath, parsed) => {
12634
+ const walkStat = statOf(filePath);
12635
+ if (!walkStat) return;
12636
+ const contentHash = sha1OfFileBytes(filePath);
12637
+ if (contentHash === null) return;
12638
+ const liveStat = statOfLive(filePath);
12639
+ if (liveStat === null || liveStat.m !== walkStat.m || liveStat.s !== walkStat.s) return;
12640
+ store.summaries[filePath] = {
12641
+ m: walkStat.m,
12642
+ s: walkStat.s,
12643
+ h: contentHash,
12644
+ p: serializeParsedSource(parsed, shouldPersistDryPatternFields)
12645
+ };
12646
+ activeSummaryPaths.add(filePath);
12647
+ isDirty = true;
12648
+ },
12649
+ lookupResolution: (specifier, fromFile) => {
12650
+ const resolutionKey = `${dirname(fromFile)}::${specifier}`;
12651
+ const cachedResolution = resolutionEntries[resolutionKey];
12652
+ if (!isRecordValue(cachedResolution) || typeof cachedResolution.e !== "boolean") return null;
12653
+ activeResolutionKeys.add(resolutionKey);
12654
+ return {
12655
+ resolvedPath: typeof cachedResolution.p === "string" ? cachedResolution.p : void 0,
12656
+ isExternal: cachedResolution.e,
12657
+ packageName: typeof cachedResolution.n === "string" ? cachedResolution.n : void 0
12658
+ };
12659
+ },
12660
+ storeResolution: (specifier, fromFile, resolved) => {
12661
+ const resolutionKey = `${dirname(fromFile)}::${specifier}`;
12662
+ resolutionEntries[resolutionKey] = {
12663
+ p: resolved.resolvedPath ?? null,
12664
+ e: resolved.isExternal,
12665
+ n: resolved.packageName ?? null
12666
+ };
12667
+ activeResolutionKeys.add(resolutionKey);
12668
+ isDirty = true;
12669
+ },
12670
+ matchPackageNames: (filePath, kind, names, matcher) => {
12671
+ let nameSet = sortedNameSetMemo.get(names);
12672
+ if (nameSet === void 0) {
12673
+ const sortedNames = [...names].sort();
12674
+ nameSet = {
12675
+ sortedNames,
12676
+ hash: sha1Hex(sortedNames.join("\n"))
12677
+ };
12678
+ sortedNameSetMemo.set(names, nameSet);
12679
+ }
12680
+ activeFactPaths.add(filePath);
12681
+ const fileStat = statOf(filePath);
12682
+ const existingEntry = isRecordValue(store.packageFacts[filePath]) ? store.packageFacts[filePath] : null;
12683
+ let validEntry = fileStat !== null && existingEntry !== null && existingEntry.m === fileStat.m && existingEntry.s === fileStat.s ? existingEntry : null;
12684
+ const existingMatch = validEntry?.[kind];
12685
+ if (isRecordValue(existingMatch) && existingMatch.h === nameSet.hash && isStringArray(existingMatch.matched)) return existingMatch.matched;
12686
+ const content = readFileSync(filePath, "utf-8");
12687
+ const contentHash = sha1Hex(content);
12688
+ if (validEntry === null && fileStat !== null && existingEntry?.h === contentHash) {
12689
+ existingEntry.m = fileStat.m;
12690
+ existingEntry.s = fileStat.s;
12691
+ isDirty = true;
12692
+ validEntry = existingEntry;
12693
+ const repairedMatch = validEntry[kind];
12694
+ if (isRecordValue(repairedMatch) && repairedMatch.h === nameSet.hash && isStringArray(repairedMatch.matched)) return repairedMatch.matched;
12695
+ }
12696
+ const matchedNames = nameSet.sortedNames.filter((packageName) => matcher(content, packageName));
12697
+ if (fileStat !== null) {
12698
+ const factEntry = validEntry ?? {
12699
+ m: fileStat.m,
12700
+ s: fileStat.s,
12701
+ h: contentHash
12702
+ };
12703
+ factEntry[kind] = {
12704
+ h: nameSet.hash,
12705
+ matched: matchedNames
12706
+ };
12707
+ store.packageFacts[filePath] = factEntry;
12708
+ isDirty = true;
12709
+ }
12710
+ return matchedNames;
12711
+ },
12712
+ matchWalkedFiles: (query) => {
12713
+ if (toPosixPath(resolve(query.cwd)) !== walkRoot) return null;
12714
+ const positiveMatcher = compileGlobAlternation(query.patterns, query.dot === true);
12715
+ if (positiveMatcher === null) return null;
12716
+ const ignoreMatcher = query.ignore.length > 0 ? compileGlobAlternation(query.ignore, true) : null;
12717
+ if (query.ignore.length > 0 && ignoreMatcher === null) return null;
12718
+ const rootPrefixLength = walkRoot.length + 1;
12719
+ const matchedPaths = [];
12720
+ for (const filePath of walkedStats.keys()) {
12721
+ const relativePath = filePath.slice(rootPrefixLength);
12722
+ if (countPathSegments(relativePath) > query.deep) continue;
12723
+ if (ignoreMatcher !== null && ignoreMatcher.test(relativePath)) continue;
12724
+ if (positiveMatcher.test(relativePath)) matchedPaths.push(filePath);
12725
+ }
12726
+ return matchedPaths.sort();
12727
+ },
12728
+ save: () => {
12729
+ if (!isDirty) return;
12730
+ const compactedSummaries = {};
12731
+ for (const filePath of activeSummaryPaths) {
12732
+ const summaryEntry = store.summaries[filePath];
12733
+ if (summaryEntry !== void 0) compactedSummaries[filePath] = summaryEntry;
12734
+ }
12735
+ const compactedFacts = {};
12736
+ for (const filePath of activeFactPaths) {
12737
+ const factEntry = store.packageFacts[filePath];
12738
+ if (factEntry !== void 0) compactedFacts[filePath] = factEntry;
12739
+ }
12740
+ const compactedResolutions = {};
12741
+ for (const resolutionKey of activeResolutionKeys) {
12742
+ const resolutionEntry = resolutionEntries[resolutionKey];
12743
+ if (resolutionEntry !== void 0) compactedResolutions[resolutionKey] = resolutionEntry;
12744
+ }
12745
+ const serialized = JSON.stringify({
12746
+ version: 2,
12747
+ scopeHash,
12748
+ fileList: store.fileList,
12749
+ resolutions: {
12750
+ hash: resolutionHash,
12751
+ entries: compactedResolutions
12752
+ },
12753
+ summaries: compactedSummaries,
12754
+ packageFacts: compactedFacts
12755
+ });
12756
+ if (serialized.length > 268435456) return;
12757
+ atomicWriteFile(cachePath, serialized);
12758
+ }
12759
+ };
12760
+ };
12761
+ /**
12762
+ * Loads the incremental cache configured at `config.incrementalCachePath`.
12763
+ * Returns `null` (analysis runs exactly as without a cache) when the path is
12764
+ * unset or anything about initialization fails.
12765
+ */
12766
+ const loadSummaryCache = (config) => {
12767
+ if (!config.incrementalCachePath) return null;
12768
+ try {
12769
+ return createSummaryCache(config.incrementalCachePath, config);
12770
+ } catch {
12771
+ return null;
12772
+ }
12773
+ };
12774
+
12619
12775
  //#endregion
12620
12776
  //#region src/utils/collect-git-ignored-paths.ts
12621
12777
  /**
@@ -12810,6 +12966,7 @@ const defineConfig = (options) => ({
12810
12966
  includeExtensions: options.includeExtensions ?? DEFAULT_EXTENSIONS,
12811
12967
  tsConfigPath: options.tsConfigPath,
12812
12968
  paths: options.paths,
12969
+ incrementalCachePath: options.incrementalCachePath,
12813
12970
  reportTypes: options.reportTypes ?? false,
12814
12971
  includeEntryExports: options.includeEntryExports ?? false,
12815
12972
  reportRedundancy: options.reportRedundancy ?? true,
@@ -12930,24 +13087,26 @@ const analyze = async (config) => {
12930
13087
  ...config,
12931
13088
  ignorePatterns: [...config.ignorePatterns, ...allExclusionPatterns]
12932
13089
  } : config;
13090
+ const entriesPromise = (configWithExclusions.incrementalCachePath ? resolveEntriesInWorker(configWithExclusions) : resolveEntries(configWithExclusions)).catch((entriesError) => {
13091
+ setupErrors.push(new WorkspaceError({
13092
+ code: "workspace-discovery-failed",
13093
+ message: "resolveEntries failed — defaulting to empty entry set",
13094
+ path: config.rootDir,
13095
+ detail: describeUnknownError(entriesError)
13096
+ }));
13097
+ return {
13098
+ productionEntries: [],
13099
+ testEntries: [],
13100
+ alwaysUsedFiles: []
13101
+ };
13102
+ });
13103
+ const summaryCache = loadSummaryCache(configWithExclusions);
12933
13104
  let files;
12934
- let discoveredEntries;
12935
- try {
12936
- const [collectedFiles, resolvedEntries] = await Promise.all([collectSourceFiles(configWithExclusions), resolveEntries(configWithExclusions).catch((entriesError) => {
12937
- setupErrors.push(new WorkspaceError({
12938
- code: "workspace-discovery-failed",
12939
- message: "resolveEntries failed — defaulting to empty entry set",
12940
- path: config.rootDir,
12941
- detail: describeUnknownError(entriesError)
12942
- }));
12943
- return {
12944
- productionEntries: [],
12945
- testEntries: [],
12946
- alwaysUsedFiles: []
12947
- };
12948
- })]);
12949
- files = collectedFiles;
12950
- discoveredEntries = resolvedEntries;
13105
+ const cachedFileList = summaryCache?.lookupFileList() ?? null;
13106
+ if (cachedFileList !== null) files = cachedFileList;
13107
+ else try {
13108
+ files = await collectSourceFiles(configWithExclusions);
13109
+ summaryCache?.storeFileList(files);
12951
13110
  } catch (collectError) {
12952
13111
  setupErrors.push(new WorkspaceError({
12953
13112
  code: "workspace-discovery-failed",
@@ -12958,9 +13117,6 @@ const analyze = async (config) => {
12958
13117
  }));
12959
13118
  return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
12960
13119
  }
12961
- const productionEntrySet = new Set(discoveredEntries.productionEntries);
12962
- const testEntrySet = new Set(discoveredEntries.testEntries);
12963
- const alwaysUsedFileSet = new Set(discoveredEntries.alwaysUsedFiles);
12964
13120
  const gitIgnoreResult = collectGitIgnoredPaths(resolve(config.rootDir), files.map((file) => file.path));
12965
13121
  const gitIgnoredFileSet = gitIgnoreResult.ignoredPaths;
12966
13122
  if (gitIgnoreResult.gitUnavailable) setupErrors.push(new WorkspaceError({
@@ -12992,7 +13148,41 @@ const analyze = async (config) => {
12992
13148
  }));
12993
13149
  return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
12994
13150
  }
12995
- const parsedModules = await parseFilesInParallel(files);
13151
+ const resolveModuleThroughCache = (specifier, fromFile) => {
13152
+ if (summaryCache === null) return moduleResolver.resolveModule(specifier, fromFile);
13153
+ const cachedResolution = summaryCache.lookupResolution(specifier, fromFile);
13154
+ if (cachedResolution !== null) return cachedResolution;
13155
+ const resolved = moduleResolver.resolveModule(specifier, fromFile);
13156
+ summaryCache.storeResolution(specifier, fromFile, resolved);
13157
+ return resolved;
13158
+ };
13159
+ let parsedModules;
13160
+ let summaryMissCount = 0;
13161
+ if (summaryCache === null) parsedModules = await parseFilesInParallel(files);
13162
+ else {
13163
+ parsedModules = new Array(files.length);
13164
+ const missedFiles = [];
13165
+ const missedPositions = [];
13166
+ for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
13167
+ const cachedSummary = summaryCache.lookupSummary(files[fileIndex].path);
13168
+ if (cachedSummary !== null) {
13169
+ parsedModules[fileIndex] = cachedSummary;
13170
+ continue;
13171
+ }
13172
+ missedFiles.push(files[fileIndex]);
13173
+ missedPositions.push(fileIndex);
13174
+ }
13175
+ summaryMissCount = missedFiles.length;
13176
+ const parsedMissedModules = await parseFilesInParallel(missedFiles);
13177
+ for (let missIndex = 0; missIndex < missedFiles.length; missIndex++) {
13178
+ parsedModules[missedPositions[missIndex]] = parsedMissedModules[missIndex];
13179
+ summaryCache.storeSummary(missedFiles[missIndex].path, parsedMissedModules[missIndex]);
13180
+ }
13181
+ }
13182
+ const discoveredEntries = await entriesPromise;
13183
+ const productionEntrySet = new Set(discoveredEntries.productionEntries);
13184
+ const testEntrySet = new Set(discoveredEntries.testEntries);
13185
+ const alwaysUsedFileSet = new Set(discoveredEntries.alwaysUsedFiles);
12996
13186
  const graphInputs = [];
12997
13187
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
12998
13188
  const file = files[fileIndex];
@@ -13000,7 +13190,7 @@ const analyze = async (config) => {
13000
13190
  const resolvedImportMap = /* @__PURE__ */ new Map();
13001
13191
  const safeResolveImport = (specifier) => {
13002
13192
  try {
13003
- return moduleResolver.resolveModule(specifier, file.path);
13193
+ return resolveModuleThroughCache(specifier, file.path);
13004
13194
  } catch (resolveError) {
13005
13195
  setupErrors.push(new ResolverError({
13006
13196
  severity: "warning",
@@ -13080,7 +13270,7 @@ const analyze = async (config) => {
13080
13270
  for (const importInfo of parsedStyleModule.imports) {
13081
13271
  let resolvedImport;
13082
13272
  try {
13083
- resolvedImport = moduleResolver.resolveModule(importInfo.specifier, styleFilePath);
13273
+ resolvedImport = resolveModuleThroughCache(importInfo.specifier, styleFilePath);
13084
13274
  } catch (styleResolveError) {
13085
13275
  setupErrors.push(new ResolverError({
13086
13276
  severity: "warning",
@@ -13144,7 +13334,7 @@ const analyze = async (config) => {
13144
13334
  }
13145
13335
  let analysisResult;
13146
13336
  try {
13147
- analysisResult = generateReport(moduleGraph, config);
13337
+ analysisResult = generateReport(moduleGraph, config, summaryCache ?? void 0);
13148
13338
  } catch (reportError) {
13149
13339
  setupErrors.push(new DetectorError({
13150
13340
  module: "report",
@@ -13154,6 +13344,11 @@ const analyze = async (config) => {
13154
13344
  }));
13155
13345
  return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
13156
13346
  }
13347
+ summaryCache?.save();
13348
+ if (summaryCache !== null) analysisResult.incrementalCacheStats = {
13349
+ summaryHits: files.length - summaryMissCount,
13350
+ summaryMisses: summaryMissCount
13351
+ };
13157
13352
  if (setupErrors.length > 0) analysisResult.analysisErrors = [...setupErrors, ...analysisResult.analysisErrors];
13158
13353
  analysisResult.analysisTimeMs = performance.now() - pipelineStartTime;
13159
13354
  return analysisResult;