deslop-js 0.0.25-dev.aeeb44a → 0.5.6
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/README.md +308 -0
- package/dist/index.cjs +37 -51
- package/dist/index.d.cts +10 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +37 -51
- package/dist/parse-worker.mjs +3 -3
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# deslop-js
|
|
2
|
+
|
|
3
|
+
[](https://npmjs.com/package/deslop-js)
|
|
4
|
+
[](https://npmjs.com/package/deslop-js)
|
|
5
|
+
|
|
6
|
+
Deslop JavaScript code.
|
|
7
|
+
|
|
8
|
+
Finds unused files, dead exports, dead dependencies, circular imports, redundant aliases, duplicate types, and other DRY violations. Each finding carries a confidence tier so you can gate CI on the high-signal ones and treat the rest as code-review prompts.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install deslop-js
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## CLI
|
|
17
|
+
|
|
18
|
+
The `deslop-cli` package provides a command-line interface:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g deslop-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Quick start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# scan the current directory
|
|
28
|
+
deslop
|
|
29
|
+
|
|
30
|
+
# scan a specific project
|
|
31
|
+
deslop ./my-project
|
|
32
|
+
|
|
33
|
+
# use the explicit analyze sub-command (equivalent to the above)
|
|
34
|
+
deslop analyze ./my-project
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Options
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
deslop [root] [options]
|
|
41
|
+
|
|
42
|
+
# custom entry points
|
|
43
|
+
deslop --entry src/main.ts --entry src/worker.ts
|
|
44
|
+
|
|
45
|
+
# ignore test files
|
|
46
|
+
deslop --ignore "**/*.test.ts" --ignore "**/__mocks__/**"
|
|
47
|
+
|
|
48
|
+
# only scan specific extensions
|
|
49
|
+
deslop --extensions .ts .tsx
|
|
50
|
+
|
|
51
|
+
# resolve path aliases via tsconfig
|
|
52
|
+
deslop --tsconfig ./tsconfig.json
|
|
53
|
+
|
|
54
|
+
# add explicit path aliases (in addition to the auto-detected ones)
|
|
55
|
+
deslop --paths "@app/*=src/*" --paths "@lib/*=packages/lib/*"
|
|
56
|
+
|
|
57
|
+
# include type-only exports in results
|
|
58
|
+
deslop --report-types
|
|
59
|
+
|
|
60
|
+
# report unused exports from entry files too
|
|
61
|
+
deslop --include-entry-exports
|
|
62
|
+
|
|
63
|
+
# output results as JSON (useful for CI or piping to other tools)
|
|
64
|
+
deslop --json
|
|
65
|
+
|
|
66
|
+
# exit with code 1 when unused code is found (for CI gates)
|
|
67
|
+
deslop --fail-on-issues
|
|
68
|
+
|
|
69
|
+
# exit with code 1 when circular imports are found
|
|
70
|
+
deslop --fail-on-cycles
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### CI example
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# fail the build if there are unused exports or circular imports
|
|
77
|
+
deslop ./src --fail-on-issues --fail-on-cycles --ignore "**/*.test.ts"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Programmatic Usage
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { analyze, defineConfig } from "deslop-js";
|
|
84
|
+
|
|
85
|
+
const config = defineConfig({ rootDir: "./my-project" });
|
|
86
|
+
const result = await analyze(config);
|
|
87
|
+
|
|
88
|
+
// unused-code findings (syntactic)
|
|
89
|
+
result.unusedFiles; // files unreachable from any entry point
|
|
90
|
+
result.unusedExports; // exported symbols never imported
|
|
91
|
+
result.unusedDependencies; // package.json deps not imported anywhere
|
|
92
|
+
result.circularDependencies; // import cycles
|
|
93
|
+
|
|
94
|
+
// redundancy / DRY findings (syntactic, on by default)
|
|
95
|
+
result.redundantAliases; // `import { x as x }`, useless re-export renames
|
|
96
|
+
result.duplicateExports; // same name exported twice from one module
|
|
97
|
+
result.duplicateImports; // same specifier imported multiple times
|
|
98
|
+
result.redundantTypePatterns; // `T & {}`, `Partial<Partial<T>>`, etc.
|
|
99
|
+
result.identityWrappers; // `const wrap = (x) => fn(x)`
|
|
100
|
+
result.duplicateTypeDefinitions; // same-shape type declared in N files
|
|
101
|
+
result.duplicateInlineTypes; // anonymous `{ a, b, c }` repeated across modules
|
|
102
|
+
result.simplifiableFunctions; // `() => { return x }`, `await x; return x`
|
|
103
|
+
result.simplifiableExpressions; // `!!x`, `x ? x : y`, `cond ? true : false`
|
|
104
|
+
result.duplicateConstants; // same literal value across files
|
|
105
|
+
result.crossFileDuplicateExports; // same export name shipped by 2+ files that share an importer
|
|
106
|
+
result.reExportCycles; // `export * from "./a"` cycles (self-loop or multi-node)
|
|
107
|
+
result.privateTypeLeaks; // exported signature references a non-exported local type
|
|
108
|
+
|
|
109
|
+
// duplicate-block detection (token-based copy-paste; on by default, disable via `duplicateBlocks.enabled: false`)
|
|
110
|
+
result.duplicateBlocks; // suffix-array + LCP detected duplicate code blocks
|
|
111
|
+
result.duplicateBlockClusters; // clones grouped by file set + refactoring suggestions
|
|
112
|
+
result.shadowedDirectoryPairs; // directory pairs with many identical files
|
|
113
|
+
|
|
114
|
+
// feature flag inventory (on by default, disable via `featureFlags.enabled: false`)
|
|
115
|
+
result.featureFlags; // LaunchDarkly/Statsig/Unleash/PostHog/Vercel Flags/process.env.* uses
|
|
116
|
+
|
|
117
|
+
// function complexity hotspots (on by default, disable via `complexity.enabled: false`)
|
|
118
|
+
result.complexFunctions; // McCabe cyclomatic + SonarSource cognitive per function
|
|
119
|
+
|
|
120
|
+
// TypeScript-specific smells (on by default)
|
|
121
|
+
result.unnecessaryAssertions; // `x as unknown as T`, `x as any`, `x!!`, `<T>x`, `"foo"!`
|
|
122
|
+
result.lazyImportsAtTopLevel; // top-level `await import(...)` / `.then(...)` that should be static
|
|
123
|
+
result.commonjsInEsm; // `require()`, `module.exports`, `exports.x` inside ESM modules
|
|
124
|
+
result.typeScriptEscapeHatches; // `// @ts-ignore`, `// @ts-nocheck`, undocumented `@ts-expect-error`
|
|
125
|
+
|
|
126
|
+
// semantic findings (type-aware; on by default, disable via `semantic: { enabled: false }`)
|
|
127
|
+
result.unusedTypes; // type aliases / interfaces never referenced
|
|
128
|
+
result.unusedEnumMembers; // enum members no reference site uses
|
|
129
|
+
result.unusedClassMembers; // class members no caller invokes (skips React/Angular lifecycle methods)
|
|
130
|
+
result.misclassifiedDependencies; // `dependencies` entries used only as types
|
|
131
|
+
|
|
132
|
+
// diagnostics
|
|
133
|
+
result.analysisErrors; // structured errors from any pipeline stage
|
|
134
|
+
result.totalFiles;
|
|
135
|
+
result.totalExports;
|
|
136
|
+
result.analysisTimeMs;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Programmatic Options
|
|
140
|
+
|
|
141
|
+
`defineConfig` accepts a required `rootDir` and optional overrides:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const config = defineConfig({
|
|
145
|
+
rootDir: "./my-project",
|
|
146
|
+
entryPatterns: ["src/main.ts"],
|
|
147
|
+
ignorePatterns: ["**/*.test.ts"],
|
|
148
|
+
tsConfigPath: "./tsconfig.json",
|
|
149
|
+
reportTypes: true,
|
|
150
|
+
includeEntryExports: true,
|
|
151
|
+
reportRedundancy: true,
|
|
152
|
+
semantic: { enabled: true },
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Option | Type | Default | Description |
|
|
157
|
+
| --------------------- | --------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `rootDir` | `string` | required | Project root directory |
|
|
159
|
+
| `entryPatterns` | `string[]` | auto-detected | Entry point glob patterns |
|
|
160
|
+
| `ignorePatterns` | `string[]` | `[]` | Glob patterns to exclude from analysis |
|
|
161
|
+
| `includeExtensions` | `string[]` | `[".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs", ".cts"]` | File extensions to scan |
|
|
162
|
+
| `tsConfigPath` | `string \| undefined` | `undefined` | Path to tsconfig.json for path alias resolution |
|
|
163
|
+
| `paths` | `Record<string, string[]> \| undefined` | `undefined` | Explicit path-alias mappings (e.g. `{ "@app/*": ["src/*"] }`), resolved alongside auto-detected aliases |
|
|
164
|
+
| `reportTypes` | `boolean` | `false` | Include type-only exports in `unusedExports` |
|
|
165
|
+
| `includeEntryExports` | `boolean` | `false` | Report unused exports from entry files |
|
|
166
|
+
| `reportRedundancy` | `boolean` | `true` | Emit the redundancy / DRY findings listed above |
|
|
167
|
+
| `semantic` | `SemanticConfig` | `undefined` | Opt-in TypeScript type-aware analysis (see below) |
|
|
168
|
+
|
|
169
|
+
Path aliases are auto-detected by default — from `tsconfig` `paths`, Vite (`resolve.alias`), webpack, Babel (`module-resolver`), and Jest (`moduleNameMapper`) configs, plus the workspace layout (a `@scope/<dir>` import resolves to the matching workspace package even when its `package.json` name differs). Use `paths` / `--paths` only for mappings none of those cover.
|
|
170
|
+
|
|
171
|
+
### Semantic (type-aware) analysis
|
|
172
|
+
|
|
173
|
+
On by default. Loads the TypeScript program when the project has a valid `tsconfig.json`; gracefully no-ops on JS-only projects. Disable with `semantic: { enabled: false }` to skip the ~1–3s program load.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const config = defineConfig({
|
|
177
|
+
rootDir: "./my-project",
|
|
178
|
+
semantic: {
|
|
179
|
+
enabled: true,
|
|
180
|
+
reportUnusedTypes: true,
|
|
181
|
+
reportUnusedEnumMembers: true,
|
|
182
|
+
reportUnusedClassMembers: false, // off by default, noisy on framework code
|
|
183
|
+
reportMisclassifiedDependencies: true,
|
|
184
|
+
reportRedundantVariableAliases: true,
|
|
185
|
+
reportRoundTripAliases: true,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Option | Default | Notes |
|
|
191
|
+
| --------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
192
|
+
| `enabled` | `false` | Master switch; semantic analysis loads the TS program and adds ~1–3s per scan |
|
|
193
|
+
| `reportUnusedTypes` | `true` | Type aliases / interfaces / type-only exports never referenced |
|
|
194
|
+
| `reportUnusedEnumMembers` | `true` | Enum members no reference site reads or writes |
|
|
195
|
+
| `reportUnusedClassMembers` | **`false`** | Subclass overrides, framework method-by-name invocation (`@HttpGet`, lifecycle hooks) produce too many stylistic FPs to enable by default. Opt in selectively. |
|
|
196
|
+
| `reportMisclassifiedDependencies` | `true` | `dependencies` packages used only via `import type` |
|
|
197
|
+
| `reportRedundantVariableAliases` | `true` | Local aliases like `const X = Y; export { X }` |
|
|
198
|
+
| `reportRoundTripAliases` | `true` | `import { X as Y } from "./a"; export { Y as X }` |
|
|
199
|
+
|
|
200
|
+
### Duplicate blocks (token-based copy-paste detection)
|
|
201
|
+
|
|
202
|
+
On by default. Detects maximal duplicated token sequences across files via a suffix array + LCP pass over a normalized AST token stream. Tune thresholds or disable entirely:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const config = defineConfig({
|
|
206
|
+
rootDir: "./my-project",
|
|
207
|
+
duplicateBlocks: {
|
|
208
|
+
enabled: true, // default
|
|
209
|
+
mode: "semantic", // "strict" preserves identifiers/literals; "semantic" (default) blinds them
|
|
210
|
+
minTokens: 50,
|
|
211
|
+
minLines: 5,
|
|
212
|
+
minOccurrences: 2,
|
|
213
|
+
skipLocal: false, // true => only report cross-directory duplicates
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Surfaces in three result fields:
|
|
219
|
+
|
|
220
|
+
- `result.duplicateBlocks` — every duplicated block group with all its occurrences
|
|
221
|
+
- `result.duplicateBlockClusters` — duplicate blocks sharing the same file set, plus an `extract-function` / `extract-module` refactoring hint
|
|
222
|
+
- `result.shadowedDirectoryPairs` — directory pairs (e.g. `src/` and `deno/lib/`) whose files mirror each other
|
|
223
|
+
|
|
224
|
+
### Feature flag inventory
|
|
225
|
+
|
|
226
|
+
On by default. Scans the codebase for every place a feature flag is _read_ and produces a finding per use. The detector recognizes three families:
|
|
227
|
+
|
|
228
|
+
1. **Env-var flags** — `process.env.X` whose name starts with one of the built-in prefixes `FEATURE_`, `NEXT_PUBLIC_FEATURE_`, `REACT_APP_FEATURE_`, `VITE_FEATURE_`, `NUXT_PUBLIC_FEATURE_`, `ENABLE_`, `FF_`, `FLAG_`, `TOGGLE_` (extend with `extraEnvPrefixes`).
|
|
229
|
+
2. **SDK calls** with provider attribution — LaunchDarkly (`useFlag`/`variation`/...), Statsig (`useGate`/`checkGate`/...), Unleash (`isEnabled`/`getVariant`), GrowthBook (`isOn`/`isOff`/`getFeatureValue`), Split (`getTreatment`), PostHog (`useFeatureFlagEnabled`/...), ConfigCat, Flagsmith, Optimizely, Eppo, and Vercel Flags (`flag()` / `evaluate()` from `flags` or `@vercel/flags`).
|
|
230
|
+
3. **Config-object access** — `config.features.X` style; off by default because it's heuristic. Opt in with `detectConfigObjects: true`.
|
|
231
|
+
|
|
232
|
+
Each finding carries `name`, `path`, `line`, `column`, `sdkProvider` (when known), and `kind: "env-var" | "sdk-call" | "config-object"`. The detector also tracks the surrounding `if` / ternary guard span and sets `guardsDeadCode: true` when an `unusedExports` finding falls inside that guard — so a flag whose enabled branch contains only dead code lights up immediately.
|
|
233
|
+
|
|
234
|
+
**The actionable angle**: cross-reference `result.featureFlags` with your live flag dashboard.
|
|
235
|
+
|
|
236
|
+
- Flags in the dashboard but missing from `result.featureFlags` → no longer read by the codebase, safe to retire from the platform.
|
|
237
|
+
- Flags in `result.featureFlags` with `guardsDeadCode: true` → the guarded code is unreachable, delete both the flag and its body.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const config = defineConfig({
|
|
241
|
+
rootDir: "./my-project",
|
|
242
|
+
featureFlags: {
|
|
243
|
+
enabled: true, // default
|
|
244
|
+
extraEnvPrefixes: ["MYAPP_FF_"],
|
|
245
|
+
extraSdkFunctionNames: ["myCustomFlag"],
|
|
246
|
+
detectConfigObjects: false, // heuristic config.features.x — opt in if you use that pattern
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Function complexity (cyclomatic + cognitive)
|
|
252
|
+
|
|
253
|
+
On by default. Reports per-function McCabe cyclomatic and SonarSource cognitive complexity, function size, and parameter count for every function that breaches at least one threshold. Tune the thresholds or disable entirely:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const config = defineConfig({
|
|
257
|
+
rootDir: "./my-project",
|
|
258
|
+
complexity: {
|
|
259
|
+
enabled: true, // default
|
|
260
|
+
cyclomaticThreshold: 10,
|
|
261
|
+
cognitiveThreshold: 15,
|
|
262
|
+
paramCountThreshold: 5,
|
|
263
|
+
functionLineThreshold: 80,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### TypeScript code smells
|
|
269
|
+
|
|
270
|
+
On by default. Four families of TypeScript-specific patterns surfaced at high or medium confidence — no extra config required.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
result.unnecessaryAssertions; // type assertions that drop type-safety or do nothing
|
|
274
|
+
result.lazyImportsAtTopLevel; // dynamic imports at the module top level
|
|
275
|
+
result.commonjsInEsm; // CommonJS forms inside ESM modules
|
|
276
|
+
result.typeScriptEscapeHatches; // @ts-ignore / @ts-nocheck / undocumented @ts-expect-error
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
| Finding | Kinds |
|
|
280
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
281
|
+
| `result.unnecessaryAssertions` | `redundant-double-assertion` (`x as unknown as T`), `assertion-to-any`, `redundant-non-null-on-literal` (`"foo"!`), `double-non-null` (`x!!`), `angle-bracket-assertion` (`<T>x`) |
|
|
282
|
+
| `result.lazyImportsAtTopLevel` | `top-level-await-import`, `top-level-then-import` |
|
|
283
|
+
| `result.commonjsInEsm` | `require`, `module-exports`, `exports-assignment` |
|
|
284
|
+
| `result.typeScriptEscapeHatches` | `ts-ignore`, `ts-nocheck`, `ts-expect-error-without-explanation` |
|
|
285
|
+
|
|
286
|
+
ESM detection follows the runtime rules: `.mts`/`.mjs` extensions are always ESM, `.cts`/`.cjs` are always CommonJS, and other files inherit from the nearest `package.json`'s `"type"` field.
|
|
287
|
+
|
|
288
|
+
## Findings have confidence tiers
|
|
289
|
+
|
|
290
|
+
Every redundancy / semantic finding carries `confidence: "high" | "medium" | "low"`. Use `"high"` for CI gates; `"medium"` and `"low"` are best treated as code-review prompts since intent is sometimes unknowable from syntax alone (e.g. `?? null` may be required by a typed callback signature).
|
|
291
|
+
|
|
292
|
+
## Error handling
|
|
293
|
+
|
|
294
|
+
`analyze()` never throws on a corrupted file, unparseable `tsconfig`, or missing dependency. Failures surface as `analysisErrors: DeslopError[]` with structured `code`, `module`, `severity`, and `path` fields. See `errors.ts` for the full taxonomy. Errors at `severity: "info"` (empty files, binary files, minified bundles skipped from redundancy analysis) are informational and do not indicate problems.
|
|
295
|
+
|
|
296
|
+
## Development
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
pnpm install
|
|
300
|
+
pnpm build
|
|
301
|
+
pnpm test
|
|
302
|
+
pnpm lint
|
|
303
|
+
pnpm format
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -96,8 +96,8 @@ const DEFAULT_EXCLUSIONS = [
|
|
|
96
96
|
"**/*.min.mjs",
|
|
97
97
|
"**/mockServiceWorker.js"
|
|
98
98
|
];
|
|
99
|
-
const SCRIPT_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|tsc|npx|bun|esr|esno|jiti|babel-node|zx)\s+(
|
|
100
|
-
const SCRIPT_EXTENSIONLESS_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|bun|esr|esno|jiti|babel-node|zx)\s+(
|
|
99
|
+
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|$)/;
|
|
100
|
+
const SCRIPT_EXTENSIONLESS_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|bun|esr|esno|jiti|babel-node|zx)\s+(?:\S+\s+)*?((?:[./]|[\w@][\w@-]*\/)[\w./@-]+)(?:\s|$)/;
|
|
101
101
|
const SCRIPT_CONFIG_FILE_PATTERN = /--config\s+([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))/;
|
|
102
102
|
const SCRIPT_ENTRY_PATTERNS = [];
|
|
103
103
|
const DEFAULT_ENTRY_GLOBS = [
|
|
@@ -268,13 +268,6 @@ const PLATFORM_SUFFIXES = [
|
|
|
268
268
|
".server",
|
|
269
269
|
".client"
|
|
270
270
|
];
|
|
271
|
-
const REACT_NATIVE_PLATFORM_SUFFIXES = [
|
|
272
|
-
".web",
|
|
273
|
-
".react-native",
|
|
274
|
-
".native",
|
|
275
|
-
".ios",
|
|
276
|
-
".android"
|
|
277
|
-
];
|
|
278
271
|
const REACT_NATIVE_PLATFORM_EXTENSIONS = [
|
|
279
272
|
".web.ts",
|
|
280
273
|
".web.tsx",
|
|
@@ -1589,7 +1582,7 @@ const extractMdxImportsExports = (sourceText) => {
|
|
|
1589
1582
|
return statements.join("\n");
|
|
1590
1583
|
};
|
|
1591
1584
|
const ASTRO_FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
1592
|
-
const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script
|
|
1585
|
+
const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1593
1586
|
const ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
|
|
1594
1587
|
const extractAstroSources = (sourceText) => {
|
|
1595
1588
|
const sections = [];
|
|
@@ -1608,7 +1601,7 @@ const extractAstroSources = (sourceText) => {
|
|
|
1608
1601
|
}
|
|
1609
1602
|
return sections.join("\n");
|
|
1610
1603
|
};
|
|
1611
|
-
const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script
|
|
1604
|
+
const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1612
1605
|
const extractVueScriptContent = (sourceText) => {
|
|
1613
1606
|
const scriptBlocks = [];
|
|
1614
1607
|
let scriptMatch;
|
|
@@ -1616,7 +1609,7 @@ const extractVueScriptContent = (sourceText) => {
|
|
|
1616
1609
|
while ((scriptMatch = VUE_SCRIPT_PATTERN.exec(sourceText)) !== null) if (scriptMatch[1]) scriptBlocks.push(scriptMatch[1]);
|
|
1617
1610
|
return scriptBlocks.join("\n");
|
|
1618
1611
|
};
|
|
1619
|
-
const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script
|
|
1612
|
+
const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1620
1613
|
const extractSvelteScriptContent = (sourceText) => {
|
|
1621
1614
|
const scriptBlocks = [];
|
|
1622
1615
|
let scriptMatch;
|
|
@@ -3853,7 +3846,7 @@ const matchCompiledMapping = (specifier, mappings) => {
|
|
|
3853
3846
|
matchedWildcard = specifier.slice(mapping.prefix.length, specifier.length - mapping.suffix.length);
|
|
3854
3847
|
} else if (specifier !== mapping.prefix) continue;
|
|
3855
3848
|
for (const target of mapping.targets) {
|
|
3856
|
-
const resolved = resolveAliasTarget(target.
|
|
3849
|
+
const resolved = resolveAliasTarget(target.replaceAll("*", matchedWildcard));
|
|
3857
3850
|
if (resolved) return resolved;
|
|
3858
3851
|
}
|
|
3859
3852
|
}
|
|
@@ -3903,11 +3896,11 @@ const BABEL_CONFIG_GLOBS = [
|
|
|
3903
3896
|
];
|
|
3904
3897
|
const JEST_CONFIG_GLOBS = ["jest.config.{js,ts,mjs,cjs,json}", "**/jest.config.{js,ts,mjs,cjs,json}"];
|
|
3905
3898
|
const ALIAS_BLOCK_PATTERN = /alias\s*:\s*\{([\s\S]*?)\}/g;
|
|
3906
|
-
const ALIAS_ENTRY_PATTERN = /["']?([@\w$./-]+)["']?\s*:\s*(?:path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["']\s
|
|
3899
|
+
const ALIAS_ENTRY_PATTERN = /["']?([@\w$./-]+)["']?\s*:\s*(?:path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["'][\s,]*)+)\)|fileURLToPath\(\s*new URL\(\s*["']([^"']+)["']\s*,\s*import\.meta\.url\s*\)\s*\)|["']([^"']+)["'])/g;
|
|
3907
3900
|
const JEST_MODULE_NAME_MAPPER_BLOCK_PATTERN = /moduleNameMapper\s*:\s*\{([\s\S]*?)\}/g;
|
|
3908
3901
|
const JEST_MODULE_NAME_MAPPER_ENTRY_PATTERN = /["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
|
|
3909
3902
|
const WEBPACK_MODULES_BLOCK_PATTERN = /modules\s*:\s*\[([\s\S]*?)\]/g;
|
|
3910
|
-
const WEBPACK_PATH_CALL_PATTERN = /path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["']\s
|
|
3903
|
+
const WEBPACK_PATH_CALL_PATTERN = /path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["'][\s,]*)+)\)/g;
|
|
3911
3904
|
const STRING_LITERAL_PATTERN$1 = /["']([^"']+)["']/g;
|
|
3912
3905
|
const TSCONFIG_FILENAMES = [
|
|
3913
3906
|
"tsconfig.json",
|
|
@@ -5512,7 +5505,7 @@ const extractBundlerConfigEntryPoints = (directory) => {
|
|
|
5512
5505
|
};
|
|
5513
5506
|
const WEBPACK_ENTRY_BLOCK_PATTERN = /entry\s*:\s*(?:\{[^}]*\}|\[[^\]]*\]|['"][^'"]+['"]|path\.(?:join|resolve)\([^)]*\))/gs;
|
|
5514
5507
|
const WEBPACK_ENTRY_FILE_PATTERN = /['"]([^'"]+)['"]/g;
|
|
5515
|
-
const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"]\s
|
|
5508
|
+
const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"][\s,]*)+)\)/g;
|
|
5516
5509
|
const REQUIRE_RESOLVE_PATTERN = /require\.resolve\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
5517
5510
|
const extractWebpackEntryPoints = (directory) => {
|
|
5518
5511
|
const entries = [];
|
|
@@ -7231,10 +7224,13 @@ const discoverToolingEntryPoints = (rootDir, workspacePackages) => {
|
|
|
7231
7224
|
|
|
7232
7225
|
//#endregion
|
|
7233
7226
|
//#region src/utils/resolve-available-concurrency.ts
|
|
7227
|
+
const clampParseConcurrency = (value) => Math.max(1, Math.min(Math.floor(value), 16));
|
|
7234
7228
|
const resolveAvailableConcurrency = () => {
|
|
7229
|
+
const requestedConcurrency = Number(process.env["DESLOP_PARSE_CONCURRENCY"]);
|
|
7230
|
+
if (Number.isFinite(requestedConcurrency) && requestedConcurrency >= 1) return clampParseConcurrency(requestedConcurrency);
|
|
7235
7231
|
const available = node_os.default.availableParallelism();
|
|
7236
7232
|
if (!Number.isFinite(available) || available < 1) return 1;
|
|
7237
|
-
return
|
|
7233
|
+
return clampParseConcurrency(available);
|
|
7238
7234
|
};
|
|
7239
7235
|
|
|
7240
7236
|
//#endregion
|
|
@@ -7778,21 +7774,10 @@ const hasReachableDirectImporter = (targetModuleIndex, graph) => {
|
|
|
7778
7774
|
return false;
|
|
7779
7775
|
};
|
|
7780
7776
|
|
|
7781
|
-
//#endregion
|
|
7782
|
-
//#region src/utils/platform-stripped-base-path.ts
|
|
7783
|
-
const platformStrippedBasePath = (modulePath, suffixes = PLATFORM_SUFFIXES) => {
|
|
7784
|
-
const extensionIndex = modulePath.lastIndexOf(".");
|
|
7785
|
-
if (extensionIndex === -1) return modulePath;
|
|
7786
|
-
const withoutExtension = modulePath.slice(0, extensionIndex);
|
|
7787
|
-
for (const suffix of suffixes) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
|
|
7788
|
-
return modulePath;
|
|
7789
|
-
};
|
|
7790
|
-
|
|
7791
7777
|
//#endregion
|
|
7792
7778
|
//#region src/report/exports.ts
|
|
7793
7779
|
const detectDeadExports = (graph, config) => {
|
|
7794
7780
|
const usageMap = buildUsageMap(graph);
|
|
7795
|
-
const platformSiblingUsageKeys = graph.hasReactNative ? buildPlatformSiblingUsageKeys(usageMap) : void 0;
|
|
7796
7781
|
const unusedExports = [];
|
|
7797
7782
|
for (const module of graph.modules) {
|
|
7798
7783
|
if (!module.isReachable) continue;
|
|
@@ -7807,7 +7792,6 @@ const detectDeadExports = (graph, config) => {
|
|
|
7807
7792
|
if (!config.reportTypes && exportInfo.isTypeOnly) continue;
|
|
7808
7793
|
const usageKey = `${module.fileId.path}::${exportInfo.name}`;
|
|
7809
7794
|
if (usageMap.has(usageKey)) continue;
|
|
7810
|
-
if (platformSiblingUsageKeys?.has(`${platformStrippedBasePath(module.fileId.path, REACT_NATIVE_PLATFORM_SUFFIXES)}::${exportInfo.name}`)) continue;
|
|
7811
7795
|
if (module.localIdentifierReferences.includes(exportInfo.name)) continue;
|
|
7812
7796
|
if (!exportInfo.isDefault && defaultExportLinkedNames.has(exportInfo.name)) continue;
|
|
7813
7797
|
unusedExports.push({
|
|
@@ -7821,18 +7805,6 @@ const detectDeadExports = (graph, config) => {
|
|
|
7821
7805
|
}
|
|
7822
7806
|
return unusedExports;
|
|
7823
7807
|
};
|
|
7824
|
-
const buildPlatformSiblingUsageKeys = (usageMap) => {
|
|
7825
|
-
const baseKeys = /* @__PURE__ */ new Set();
|
|
7826
|
-
for (const usageKey of usageMap) {
|
|
7827
|
-
const separatorIndex = usageKey.lastIndexOf("::");
|
|
7828
|
-
if (separatorIndex === -1) continue;
|
|
7829
|
-
const filePath = usageKey.slice(0, separatorIndex);
|
|
7830
|
-
const exportName = usageKey.slice(separatorIndex + 2);
|
|
7831
|
-
const basePath = platformStrippedBasePath(filePath, REACT_NATIVE_PLATFORM_SUFFIXES);
|
|
7832
|
-
baseKeys.add(`${basePath}::${exportName}`);
|
|
7833
|
-
}
|
|
7834
|
-
return baseKeys;
|
|
7835
|
-
};
|
|
7836
7808
|
const buildUsageMap = (graph) => {
|
|
7837
7809
|
const usedExportKeys = /* @__PURE__ */ new Set();
|
|
7838
7810
|
const sourceToTargetMap = buildSourceToTargetsMap(graph);
|
|
@@ -8909,7 +8881,19 @@ const detectCycles = (graph) => {
|
|
|
8909
8881
|
|
|
8910
8882
|
//#endregion
|
|
8911
8883
|
//#region src/report/redundancy.ts
|
|
8912
|
-
const isPlatformSpecificModulePath = (modulePath) =>
|
|
8884
|
+
const isPlatformSpecificModulePath = (modulePath) => {
|
|
8885
|
+
const extensionIndex = modulePath.lastIndexOf(".");
|
|
8886
|
+
if (extensionIndex === -1) return false;
|
|
8887
|
+
const withoutExtension = modulePath.slice(0, extensionIndex);
|
|
8888
|
+
return PLATFORM_SUFFIXES.some((suffix) => withoutExtension.endsWith(suffix));
|
|
8889
|
+
};
|
|
8890
|
+
const platformStrippedBasePath = (modulePath) => {
|
|
8891
|
+
const extensionIndex = modulePath.lastIndexOf(".");
|
|
8892
|
+
if (extensionIndex === -1) return modulePath;
|
|
8893
|
+
const withoutExtension = modulePath.slice(0, extensionIndex);
|
|
8894
|
+
for (const suffix of PLATFORM_SUFFIXES) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
|
|
8895
|
+
return modulePath;
|
|
8896
|
+
};
|
|
8913
8897
|
const buildPlatformSiblingGroupSizes = (graph) => {
|
|
8914
8898
|
const baseToCount = /* @__PURE__ */ new Map();
|
|
8915
8899
|
for (const module of graph.modules) {
|
|
@@ -12659,21 +12643,23 @@ const generateReport = (graph, config) => {
|
|
|
12659
12643
|
const simplifiableExpressions = config.reportRedundancy ? safeReportDetector("detectSimplifiableExpressions", () => detectSimplifiableExpressions(graph), [], errorSink) : [];
|
|
12660
12644
|
const duplicateConstants = config.reportRedundancy ? safeReportDetector("detectDuplicateConstants", () => detectDuplicateConstants(graph), [], errorSink) : [];
|
|
12661
12645
|
const crossFileDuplicateExports = config.reportRedundancy ? safeReportDetector("detectCrossFileDuplicateExports", () => detectCrossFileDuplicateExports(graph), [], errorSink) : [];
|
|
12662
|
-
const
|
|
12646
|
+
const emptyDuplicateBlockResult = {
|
|
12663
12647
|
duplicateBlocks: [],
|
|
12664
12648
|
duplicateBlockClusters: [],
|
|
12665
12649
|
shadowedDirectoryPairs: []
|
|
12666
|
-
}
|
|
12667
|
-
const
|
|
12668
|
-
const
|
|
12669
|
-
const
|
|
12670
|
-
const
|
|
12671
|
-
const
|
|
12650
|
+
};
|
|
12651
|
+
const duplicateBlockResult = config.reportCodeQuality ? safeReportDetector("detectDuplicateBlocks", () => detectDuplicateBlocks(graph, config.duplicateBlocks, config.rootDir), emptyDuplicateBlockResult, errorSink) : emptyDuplicateBlockResult;
|
|
12652
|
+
const reExportCycles = config.reportCodeQuality ? safeReportDetector("detectReExportCycles", () => detectReExportCycles(graph), [], errorSink) : [];
|
|
12653
|
+
const featureFlags = config.reportCodeQuality ? safeReportDetector("detectFeatureFlags", () => detectFeatureFlags(graph, config.featureFlags), [], errorSink) : [];
|
|
12654
|
+
const complexFunctions = config.reportCodeQuality ? safeReportDetector("detectComplexHotspots", () => detectComplexHotspots(graph, config.complexity), [], errorSink) : [];
|
|
12655
|
+
const privateTypeLeaks = config.reportCodeQuality ? safeReportDetector("detectPrivateTypeLeaks", () => detectPrivateTypeLeaks(graph), [], errorSink) : [];
|
|
12656
|
+
const emptyTypeScriptSmellsResult = {
|
|
12672
12657
|
unnecessaryAssertions: [],
|
|
12673
12658
|
lazyImportsAtTopLevel: [],
|
|
12674
12659
|
commonjsInEsm: [],
|
|
12675
12660
|
typeScriptEscapeHatches: []
|
|
12676
|
-
}
|
|
12661
|
+
};
|
|
12662
|
+
const typeScriptSmellsResult = config.reportCodeQuality ? safeReportDetector("detectTypeScriptSmells", () => detectTypeScriptSmells(graph), emptyTypeScriptSmellsResult, errorSink) : emptyTypeScriptSmellsResult;
|
|
12677
12663
|
let semanticResult;
|
|
12678
12664
|
try {
|
|
12679
12665
|
semanticResult = runSemanticAnalysis(graph, config);
|
|
@@ -12935,6 +12921,7 @@ const defineConfig = (options) => ({
|
|
|
12935
12921
|
reportTypes: options.reportTypes ?? false,
|
|
12936
12922
|
includeEntryExports: options.includeEntryExports ?? false,
|
|
12937
12923
|
reportRedundancy: options.reportRedundancy ?? true,
|
|
12924
|
+
reportCodeQuality: options.reportCodeQuality ?? true,
|
|
12938
12925
|
semantic: fillSemanticConfig(options.semantic),
|
|
12939
12926
|
duplicateBlocks: fillDuplicateBlocksConfig(options.duplicateBlocks),
|
|
12940
12927
|
featureFlags: fillFeatureFlagsConfig(options.featureFlags),
|
|
@@ -13243,7 +13230,6 @@ const analyze = async (config) => {
|
|
|
13243
13230
|
}));
|
|
13244
13231
|
return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
|
|
13245
13232
|
}
|
|
13246
|
-
moduleGraph.hasReactNative = hasReactNative;
|
|
13247
13233
|
try {
|
|
13248
13234
|
resolveReExportChains(moduleGraph);
|
|
13249
13235
|
} catch (reExportError) {
|
package/dist/index.d.cts
CHANGED
|
@@ -421,6 +421,16 @@ interface DeslopConfig {
|
|
|
421
421
|
reportTypes: boolean;
|
|
422
422
|
includeEntryExports: boolean;
|
|
423
423
|
reportRedundancy: boolean;
|
|
424
|
+
/**
|
|
425
|
+
* Run the non-dead-code "code quality" detectors — duplicate-block (copy-paste)
|
|
426
|
+
* detection, complexity hotspots, feature flags, TypeScript smells,
|
|
427
|
+
* private-type leaks, and re-export cycles. On by default. These are by far
|
|
428
|
+
* the most expensive detectors (duplicate-block detection alone can dominate
|
|
429
|
+
* a large-repo scan), and they're independent of the dead-code graph findings
|
|
430
|
+
* (unused files/exports/dependencies, circular dependencies) — so a consumer
|
|
431
|
+
* that only wants dead-code can set this `false` to skip the bulk of the work.
|
|
432
|
+
*/
|
|
433
|
+
reportCodeQuality: boolean;
|
|
424
434
|
semantic: SemanticConfig | undefined;
|
|
425
435
|
duplicateBlocks: DuplicateBlocksConfig | undefined;
|
|
426
436
|
featureFlags: FeatureFlagsConfig | undefined;
|
package/dist/index.d.mts
CHANGED
|
@@ -421,6 +421,16 @@ interface DeslopConfig {
|
|
|
421
421
|
reportTypes: boolean;
|
|
422
422
|
includeEntryExports: boolean;
|
|
423
423
|
reportRedundancy: boolean;
|
|
424
|
+
/**
|
|
425
|
+
* Run the non-dead-code "code quality" detectors — duplicate-block (copy-paste)
|
|
426
|
+
* detection, complexity hotspots, feature flags, TypeScript smells,
|
|
427
|
+
* private-type leaks, and re-export cycles. On by default. These are by far
|
|
428
|
+
* the most expensive detectors (duplicate-block detection alone can dominate
|
|
429
|
+
* a large-repo scan), and they're independent of the dead-code graph findings
|
|
430
|
+
* (unused files/exports/dependencies, circular dependencies) — so a consumer
|
|
431
|
+
* that only wants dead-code can set this `false` to skip the bulk of the work.
|
|
432
|
+
*/
|
|
433
|
+
reportCodeQuality: boolean;
|
|
424
434
|
semantic: SemanticConfig | undefined;
|
|
425
435
|
duplicateBlocks: DuplicateBlocksConfig | undefined;
|
|
426
436
|
featureFlags: FeatureFlagsConfig | undefined;
|
package/dist/index.mjs
CHANGED
|
@@ -64,8 +64,8 @@ const DEFAULT_EXCLUSIONS = [
|
|
|
64
64
|
"**/*.min.mjs",
|
|
65
65
|
"**/mockServiceWorker.js"
|
|
66
66
|
];
|
|
67
|
-
const SCRIPT_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|tsc|npx|bun|esr|esno|jiti|babel-node|zx)\s+(
|
|
68
|
-
const SCRIPT_EXTENSIONLESS_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|bun|esr|esno|jiti|babel-node|zx)\s+(
|
|
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
69
|
const SCRIPT_CONFIG_FILE_PATTERN = /--config\s+([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))/;
|
|
70
70
|
const SCRIPT_ENTRY_PATTERNS = [];
|
|
71
71
|
const DEFAULT_ENTRY_GLOBS = [
|
|
@@ -236,13 +236,6 @@ const PLATFORM_SUFFIXES = [
|
|
|
236
236
|
".server",
|
|
237
237
|
".client"
|
|
238
238
|
];
|
|
239
|
-
const REACT_NATIVE_PLATFORM_SUFFIXES = [
|
|
240
|
-
".web",
|
|
241
|
-
".react-native",
|
|
242
|
-
".native",
|
|
243
|
-
".ios",
|
|
244
|
-
".android"
|
|
245
|
-
];
|
|
246
239
|
const REACT_NATIVE_PLATFORM_EXTENSIONS = [
|
|
247
240
|
".web.ts",
|
|
248
241
|
".web.tsx",
|
|
@@ -1557,7 +1550,7 @@ const extractMdxImportsExports = (sourceText) => {
|
|
|
1557
1550
|
return statements.join("\n");
|
|
1558
1551
|
};
|
|
1559
1552
|
const ASTRO_FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
1560
|
-
const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script
|
|
1553
|
+
const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1561
1554
|
const ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
|
|
1562
1555
|
const extractAstroSources = (sourceText) => {
|
|
1563
1556
|
const sections = [];
|
|
@@ -1576,7 +1569,7 @@ const extractAstroSources = (sourceText) => {
|
|
|
1576
1569
|
}
|
|
1577
1570
|
return sections.join("\n");
|
|
1578
1571
|
};
|
|
1579
|
-
const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script
|
|
1572
|
+
const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1580
1573
|
const extractVueScriptContent = (sourceText) => {
|
|
1581
1574
|
const scriptBlocks = [];
|
|
1582
1575
|
let scriptMatch;
|
|
@@ -1584,7 +1577,7 @@ const extractVueScriptContent = (sourceText) => {
|
|
|
1584
1577
|
while ((scriptMatch = VUE_SCRIPT_PATTERN.exec(sourceText)) !== null) if (scriptMatch[1]) scriptBlocks.push(scriptMatch[1]);
|
|
1585
1578
|
return scriptBlocks.join("\n");
|
|
1586
1579
|
};
|
|
1587
|
-
const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script
|
|
1580
|
+
const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1588
1581
|
const extractSvelteScriptContent = (sourceText) => {
|
|
1589
1582
|
const scriptBlocks = [];
|
|
1590
1583
|
let scriptMatch;
|
|
@@ -3821,7 +3814,7 @@ const matchCompiledMapping = (specifier, mappings) => {
|
|
|
3821
3814
|
matchedWildcard = specifier.slice(mapping.prefix.length, specifier.length - mapping.suffix.length);
|
|
3822
3815
|
} else if (specifier !== mapping.prefix) continue;
|
|
3823
3816
|
for (const target of mapping.targets) {
|
|
3824
|
-
const resolved = resolveAliasTarget(target.
|
|
3817
|
+
const resolved = resolveAliasTarget(target.replaceAll("*", matchedWildcard));
|
|
3825
3818
|
if (resolved) return resolved;
|
|
3826
3819
|
}
|
|
3827
3820
|
}
|
|
@@ -3871,11 +3864,11 @@ const BABEL_CONFIG_GLOBS = [
|
|
|
3871
3864
|
];
|
|
3872
3865
|
const JEST_CONFIG_GLOBS = ["jest.config.{js,ts,mjs,cjs,json}", "**/jest.config.{js,ts,mjs,cjs,json}"];
|
|
3873
3866
|
const ALIAS_BLOCK_PATTERN = /alias\s*:\s*\{([\s\S]*?)\}/g;
|
|
3874
|
-
const ALIAS_ENTRY_PATTERN = /["']?([@\w$./-]+)["']?\s*:\s*(?:path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["']\s
|
|
3867
|
+
const ALIAS_ENTRY_PATTERN = /["']?([@\w$./-]+)["']?\s*:\s*(?:path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["'][\s,]*)+)\)|fileURLToPath\(\s*new URL\(\s*["']([^"']+)["']\s*,\s*import\.meta\.url\s*\)\s*\)|["']([^"']+)["'])/g;
|
|
3875
3868
|
const JEST_MODULE_NAME_MAPPER_BLOCK_PATTERN = /moduleNameMapper\s*:\s*\{([\s\S]*?)\}/g;
|
|
3876
3869
|
const JEST_MODULE_NAME_MAPPER_ENTRY_PATTERN = /["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
|
|
3877
3870
|
const WEBPACK_MODULES_BLOCK_PATTERN = /modules\s*:\s*\[([\s\S]*?)\]/g;
|
|
3878
|
-
const WEBPACK_PATH_CALL_PATTERN = /path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["']\s
|
|
3871
|
+
const WEBPACK_PATH_CALL_PATTERN = /path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["'][\s,]*)+)\)/g;
|
|
3879
3872
|
const STRING_LITERAL_PATTERN$1 = /["']([^"']+)["']/g;
|
|
3880
3873
|
const TSCONFIG_FILENAMES = [
|
|
3881
3874
|
"tsconfig.json",
|
|
@@ -5480,7 +5473,7 @@ const extractBundlerConfigEntryPoints = (directory) => {
|
|
|
5480
5473
|
};
|
|
5481
5474
|
const WEBPACK_ENTRY_BLOCK_PATTERN = /entry\s*:\s*(?:\{[^}]*\}|\[[^\]]*\]|['"][^'"]+['"]|path\.(?:join|resolve)\([^)]*\))/gs;
|
|
5482
5475
|
const WEBPACK_ENTRY_FILE_PATTERN = /['"]([^'"]+)['"]/g;
|
|
5483
|
-
const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"]\s
|
|
5476
|
+
const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"][\s,]*)+)\)/g;
|
|
5484
5477
|
const REQUIRE_RESOLVE_PATTERN = /require\.resolve\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
5485
5478
|
const extractWebpackEntryPoints = (directory) => {
|
|
5486
5479
|
const entries = [];
|
|
@@ -7199,10 +7192,13 @@ const discoverToolingEntryPoints = (rootDir, workspacePackages) => {
|
|
|
7199
7192
|
|
|
7200
7193
|
//#endregion
|
|
7201
7194
|
//#region src/utils/resolve-available-concurrency.ts
|
|
7195
|
+
const clampParseConcurrency = (value) => Math.max(1, Math.min(Math.floor(value), 16));
|
|
7202
7196
|
const resolveAvailableConcurrency = () => {
|
|
7197
|
+
const requestedConcurrency = Number(process.env["DESLOP_PARSE_CONCURRENCY"]);
|
|
7198
|
+
if (Number.isFinite(requestedConcurrency) && requestedConcurrency >= 1) return clampParseConcurrency(requestedConcurrency);
|
|
7203
7199
|
const available = os.availableParallelism();
|
|
7204
7200
|
if (!Number.isFinite(available) || available < 1) return 1;
|
|
7205
|
-
return
|
|
7201
|
+
return clampParseConcurrency(available);
|
|
7206
7202
|
};
|
|
7207
7203
|
|
|
7208
7204
|
//#endregion
|
|
@@ -7746,21 +7742,10 @@ const hasReachableDirectImporter = (targetModuleIndex, graph) => {
|
|
|
7746
7742
|
return false;
|
|
7747
7743
|
};
|
|
7748
7744
|
|
|
7749
|
-
//#endregion
|
|
7750
|
-
//#region src/utils/platform-stripped-base-path.ts
|
|
7751
|
-
const platformStrippedBasePath = (modulePath, suffixes = PLATFORM_SUFFIXES) => {
|
|
7752
|
-
const extensionIndex = modulePath.lastIndexOf(".");
|
|
7753
|
-
if (extensionIndex === -1) return modulePath;
|
|
7754
|
-
const withoutExtension = modulePath.slice(0, extensionIndex);
|
|
7755
|
-
for (const suffix of suffixes) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
|
|
7756
|
-
return modulePath;
|
|
7757
|
-
};
|
|
7758
|
-
|
|
7759
7745
|
//#endregion
|
|
7760
7746
|
//#region src/report/exports.ts
|
|
7761
7747
|
const detectDeadExports = (graph, config) => {
|
|
7762
7748
|
const usageMap = buildUsageMap(graph);
|
|
7763
|
-
const platformSiblingUsageKeys = graph.hasReactNative ? buildPlatformSiblingUsageKeys(usageMap) : void 0;
|
|
7764
7749
|
const unusedExports = [];
|
|
7765
7750
|
for (const module of graph.modules) {
|
|
7766
7751
|
if (!module.isReachable) continue;
|
|
@@ -7775,7 +7760,6 @@ const detectDeadExports = (graph, config) => {
|
|
|
7775
7760
|
if (!config.reportTypes && exportInfo.isTypeOnly) continue;
|
|
7776
7761
|
const usageKey = `${module.fileId.path}::${exportInfo.name}`;
|
|
7777
7762
|
if (usageMap.has(usageKey)) continue;
|
|
7778
|
-
if (platformSiblingUsageKeys?.has(`${platformStrippedBasePath(module.fileId.path, REACT_NATIVE_PLATFORM_SUFFIXES)}::${exportInfo.name}`)) continue;
|
|
7779
7763
|
if (module.localIdentifierReferences.includes(exportInfo.name)) continue;
|
|
7780
7764
|
if (!exportInfo.isDefault && defaultExportLinkedNames.has(exportInfo.name)) continue;
|
|
7781
7765
|
unusedExports.push({
|
|
@@ -7789,18 +7773,6 @@ const detectDeadExports = (graph, config) => {
|
|
|
7789
7773
|
}
|
|
7790
7774
|
return unusedExports;
|
|
7791
7775
|
};
|
|
7792
|
-
const buildPlatformSiblingUsageKeys = (usageMap) => {
|
|
7793
|
-
const baseKeys = /* @__PURE__ */ new Set();
|
|
7794
|
-
for (const usageKey of usageMap) {
|
|
7795
|
-
const separatorIndex = usageKey.lastIndexOf("::");
|
|
7796
|
-
if (separatorIndex === -1) continue;
|
|
7797
|
-
const filePath = usageKey.slice(0, separatorIndex);
|
|
7798
|
-
const exportName = usageKey.slice(separatorIndex + 2);
|
|
7799
|
-
const basePath = platformStrippedBasePath(filePath, REACT_NATIVE_PLATFORM_SUFFIXES);
|
|
7800
|
-
baseKeys.add(`${basePath}::${exportName}`);
|
|
7801
|
-
}
|
|
7802
|
-
return baseKeys;
|
|
7803
|
-
};
|
|
7804
7776
|
const buildUsageMap = (graph) => {
|
|
7805
7777
|
const usedExportKeys = /* @__PURE__ */ new Set();
|
|
7806
7778
|
const sourceToTargetMap = buildSourceToTargetsMap(graph);
|
|
@@ -8877,7 +8849,19 @@ const detectCycles = (graph) => {
|
|
|
8877
8849
|
|
|
8878
8850
|
//#endregion
|
|
8879
8851
|
//#region src/report/redundancy.ts
|
|
8880
|
-
const isPlatformSpecificModulePath = (modulePath) =>
|
|
8852
|
+
const isPlatformSpecificModulePath = (modulePath) => {
|
|
8853
|
+
const extensionIndex = modulePath.lastIndexOf(".");
|
|
8854
|
+
if (extensionIndex === -1) return false;
|
|
8855
|
+
const withoutExtension = modulePath.slice(0, extensionIndex);
|
|
8856
|
+
return PLATFORM_SUFFIXES.some((suffix) => withoutExtension.endsWith(suffix));
|
|
8857
|
+
};
|
|
8858
|
+
const platformStrippedBasePath = (modulePath) => {
|
|
8859
|
+
const extensionIndex = modulePath.lastIndexOf(".");
|
|
8860
|
+
if (extensionIndex === -1) return modulePath;
|
|
8861
|
+
const withoutExtension = modulePath.slice(0, extensionIndex);
|
|
8862
|
+
for (const suffix of PLATFORM_SUFFIXES) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
|
|
8863
|
+
return modulePath;
|
|
8864
|
+
};
|
|
8881
8865
|
const buildPlatformSiblingGroupSizes = (graph) => {
|
|
8882
8866
|
const baseToCount = /* @__PURE__ */ new Map();
|
|
8883
8867
|
for (const module of graph.modules) {
|
|
@@ -12627,21 +12611,23 @@ const generateReport = (graph, config) => {
|
|
|
12627
12611
|
const simplifiableExpressions = config.reportRedundancy ? safeReportDetector("detectSimplifiableExpressions", () => detectSimplifiableExpressions(graph), [], errorSink) : [];
|
|
12628
12612
|
const duplicateConstants = config.reportRedundancy ? safeReportDetector("detectDuplicateConstants", () => detectDuplicateConstants(graph), [], errorSink) : [];
|
|
12629
12613
|
const crossFileDuplicateExports = config.reportRedundancy ? safeReportDetector("detectCrossFileDuplicateExports", () => detectCrossFileDuplicateExports(graph), [], errorSink) : [];
|
|
12630
|
-
const
|
|
12614
|
+
const emptyDuplicateBlockResult = {
|
|
12631
12615
|
duplicateBlocks: [],
|
|
12632
12616
|
duplicateBlockClusters: [],
|
|
12633
12617
|
shadowedDirectoryPairs: []
|
|
12634
|
-
}
|
|
12635
|
-
const
|
|
12636
|
-
const
|
|
12637
|
-
const
|
|
12638
|
-
const
|
|
12639
|
-
const
|
|
12618
|
+
};
|
|
12619
|
+
const duplicateBlockResult = config.reportCodeQuality ? safeReportDetector("detectDuplicateBlocks", () => detectDuplicateBlocks(graph, config.duplicateBlocks, config.rootDir), emptyDuplicateBlockResult, errorSink) : emptyDuplicateBlockResult;
|
|
12620
|
+
const reExportCycles = config.reportCodeQuality ? safeReportDetector("detectReExportCycles", () => detectReExportCycles(graph), [], errorSink) : [];
|
|
12621
|
+
const featureFlags = config.reportCodeQuality ? safeReportDetector("detectFeatureFlags", () => detectFeatureFlags(graph, config.featureFlags), [], errorSink) : [];
|
|
12622
|
+
const complexFunctions = config.reportCodeQuality ? safeReportDetector("detectComplexHotspots", () => detectComplexHotspots(graph, config.complexity), [], errorSink) : [];
|
|
12623
|
+
const privateTypeLeaks = config.reportCodeQuality ? safeReportDetector("detectPrivateTypeLeaks", () => detectPrivateTypeLeaks(graph), [], errorSink) : [];
|
|
12624
|
+
const emptyTypeScriptSmellsResult = {
|
|
12640
12625
|
unnecessaryAssertions: [],
|
|
12641
12626
|
lazyImportsAtTopLevel: [],
|
|
12642
12627
|
commonjsInEsm: [],
|
|
12643
12628
|
typeScriptEscapeHatches: []
|
|
12644
|
-
}
|
|
12629
|
+
};
|
|
12630
|
+
const typeScriptSmellsResult = config.reportCodeQuality ? safeReportDetector("detectTypeScriptSmells", () => detectTypeScriptSmells(graph), emptyTypeScriptSmellsResult, errorSink) : emptyTypeScriptSmellsResult;
|
|
12645
12631
|
let semanticResult;
|
|
12646
12632
|
try {
|
|
12647
12633
|
semanticResult = runSemanticAnalysis(graph, config);
|
|
@@ -12903,6 +12889,7 @@ const defineConfig = (options) => ({
|
|
|
12903
12889
|
reportTypes: options.reportTypes ?? false,
|
|
12904
12890
|
includeEntryExports: options.includeEntryExports ?? false,
|
|
12905
12891
|
reportRedundancy: options.reportRedundancy ?? true,
|
|
12892
|
+
reportCodeQuality: options.reportCodeQuality ?? true,
|
|
12906
12893
|
semantic: fillSemanticConfig(options.semantic),
|
|
12907
12894
|
duplicateBlocks: fillDuplicateBlocksConfig(options.duplicateBlocks),
|
|
12908
12895
|
featureFlags: fillFeatureFlagsConfig(options.featureFlags),
|
|
@@ -13211,7 +13198,6 @@ const analyze = async (config) => {
|
|
|
13211
13198
|
}));
|
|
13212
13199
|
return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
|
|
13213
13200
|
}
|
|
13214
|
-
moduleGraph.hasReactNative = hasReactNative;
|
|
13215
13201
|
try {
|
|
13216
13202
|
resolveReExportChains(moduleGraph);
|
|
13217
13203
|
} catch (reExportError) {
|
package/dist/parse-worker.mjs
CHANGED
|
@@ -1234,7 +1234,7 @@ const extractMdxImportsExports = (sourceText) => {
|
|
|
1234
1234
|
return statements.join("\n");
|
|
1235
1235
|
};
|
|
1236
1236
|
const ASTRO_FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
1237
|
-
const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script
|
|
1237
|
+
const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1238
1238
|
const ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
|
|
1239
1239
|
const extractAstroSources = (sourceText) => {
|
|
1240
1240
|
const sections = [];
|
|
@@ -1253,7 +1253,7 @@ const extractAstroSources = (sourceText) => {
|
|
|
1253
1253
|
}
|
|
1254
1254
|
return sections.join("\n");
|
|
1255
1255
|
};
|
|
1256
|
-
const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script
|
|
1256
|
+
const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1257
1257
|
const extractVueScriptContent = (sourceText) => {
|
|
1258
1258
|
const scriptBlocks = [];
|
|
1259
1259
|
let scriptMatch;
|
|
@@ -1261,7 +1261,7 @@ const extractVueScriptContent = (sourceText) => {
|
|
|
1261
1261
|
while ((scriptMatch = VUE_SCRIPT_PATTERN.exec(sourceText)) !== null) if (scriptMatch[1]) scriptBlocks.push(scriptMatch[1]);
|
|
1262
1262
|
return scriptBlocks.join("\n");
|
|
1263
1263
|
};
|
|
1264
|
-
const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script
|
|
1264
|
+
const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script\b[^>]*>/gi;
|
|
1265
1265
|
const extractSvelteScriptContent = (sourceText) => {
|
|
1266
1266
|
const scriptBlocks = [];
|
|
1267
1267
|
let scriptMatch;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deslop-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Deslop JavaScript code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dead-code",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"typescript",
|
|
13
13
|
"unused"
|
|
14
14
|
],
|
|
15
|
-
"homepage": "https://github.com/millionco/
|
|
15
|
+
"homepage": "https://github.com/millionco/react-doctor#readme",
|
|
16
16
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/millionco/
|
|
17
|
+
"url": "https://github.com/millionco/react-doctor/issues"
|
|
18
18
|
},
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"author": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "https://github.com/millionco/
|
|
26
|
+
"url": "https://github.com/millionco/react-doctor.git",
|
|
27
27
|
"directory": "packages/deslop-js"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|