@mrclrchtr/supi-lsp 11.0.1 → 11.2.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/README.md +43 -6
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +41 -1
- package/node_modules/ignore/README.md +36 -10
- package/node_modules/ignore/index.d.ts +3 -2
- package/node_modules/ignore/index.js +57 -53
- package/node_modules/ignore/legacy.js +50 -46
- package/node_modules/ignore/package.json +1 -1
- package/node_modules/semver/LICENSE +15 -0
- package/node_modules/semver/README.md +680 -0
- package/node_modules/semver/bin/semver.js +195 -0
- package/node_modules/semver/classes/comparator.js +143 -0
- package/node_modules/semver/classes/index.js +7 -0
- package/node_modules/semver/classes/range.js +577 -0
- package/node_modules/semver/classes/semver.js +350 -0
- package/node_modules/semver/functions/clean.js +8 -0
- package/node_modules/semver/functions/cmp.js +54 -0
- package/node_modules/semver/functions/coerce.js +62 -0
- package/node_modules/semver/functions/compare-build.js +9 -0
- package/node_modules/semver/functions/compare-loose.js +5 -0
- package/node_modules/semver/functions/compare.js +7 -0
- package/node_modules/semver/functions/diff.js +60 -0
- package/node_modules/semver/functions/eq.js +5 -0
- package/node_modules/semver/functions/gt.js +5 -0
- package/node_modules/semver/functions/gte.js +5 -0
- package/node_modules/semver/functions/inc.js +21 -0
- package/node_modules/semver/functions/lt.js +5 -0
- package/node_modules/semver/functions/lte.js +5 -0
- package/node_modules/semver/functions/major.js +5 -0
- package/node_modules/semver/functions/minor.js +5 -0
- package/node_modules/semver/functions/neq.js +5 -0
- package/node_modules/semver/functions/parse.js +18 -0
- package/node_modules/semver/functions/patch.js +5 -0
- package/node_modules/semver/functions/prerelease.js +8 -0
- package/node_modules/semver/functions/rcompare.js +5 -0
- package/node_modules/semver/functions/rsort.js +5 -0
- package/node_modules/semver/functions/satisfies.js +12 -0
- package/node_modules/semver/functions/sort.js +5 -0
- package/node_modules/semver/functions/truncate.js +48 -0
- package/node_modules/semver/functions/valid.js +8 -0
- package/node_modules/semver/index.js +93 -0
- package/node_modules/semver/internal/constants.js +37 -0
- package/node_modules/semver/internal/debug.js +11 -0
- package/node_modules/semver/internal/identifiers.js +29 -0
- package/node_modules/semver/internal/lrucache.js +42 -0
- package/node_modules/semver/internal/parse-options.js +17 -0
- package/node_modules/semver/internal/re.js +223 -0
- package/node_modules/semver/package.json +78 -0
- package/node_modules/semver/preload.js +4 -0
- package/node_modules/semver/range.bnf +17 -0
- package/node_modules/semver/ranges/gtr.js +6 -0
- package/node_modules/semver/ranges/intersects.js +9 -0
- package/node_modules/semver/ranges/ltr.js +6 -0
- package/node_modules/semver/ranges/max-satisfying.js +27 -0
- package/node_modules/semver/ranges/min-satisfying.js +26 -0
- package/node_modules/semver/ranges/min-version.js +63 -0
- package/node_modules/semver/ranges/outside.js +82 -0
- package/node_modules/semver/ranges/simplify.js +49 -0
- package/node_modules/semver/ranges/subset.js +249 -0
- package/node_modules/semver/ranges/to-comparators.js +10 -0
- package/node_modules/semver/ranges/valid.js +13 -0
- package/node_modules/vscode-jsonrpc/README.md +5 -0
- package/node_modules/vscode-jsonrpc/lib/browser/ril.js +8 -0
- package/node_modules/vscode-jsonrpc/lib/common/connection.js +7 -2
- package/node_modules/vscode-jsonrpc/lib/common/ral.d.ts +8 -0
- package/node_modules/vscode-jsonrpc/lib/node/main.d.ts +14 -0
- package/node_modules/vscode-jsonrpc/lib/node/main.js +6 -4
- package/node_modules/vscode-jsonrpc/lib/node/ril.js +5 -1
- package/node_modules/vscode-jsonrpc/package.json +4 -2
- package/node_modules/vscode-languageserver-protocol/package.json +4 -3
- package/node_modules/vscode-languageserver-types/lib/esm/main.js +2 -2
- package/node_modules/vscode-languageserver-types/lib/umd/main.js +2 -2
- package/node_modules/vscode-languageserver-types/package.json +2 -1
- package/package.json +8 -6
- package/src/client/client.ts +44 -5
- package/src/config/config.ts +53 -2
- package/src/config/operation-support.ts +19 -4
- package/src/config/server-config.ts +22 -0
- package/src/config/typescript-launch.ts +276 -0
- package/src/manager/manager-project-info.ts +22 -10
- package/src/manager/manager.ts +53 -20
- package/src/provider/refactor-planning.ts +4 -3
- package/src/session/runtime-controller.ts +2 -2
- package/src/session/scanner.ts +27 -15
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npm install @mrclrchtr/supi-lsp
|
|
|
20
20
|
|
|
21
21
|
## Language-server support
|
|
22
22
|
|
|
23
|
-
SuPi provides 13 built-in server definitions. All are enabled unless `lsp.servers.<key>.enabled` is `false`. Install the language servers yourself; SuPi does not download or install them. Built-in commands must be on `PATH
|
|
23
|
+
SuPi provides 13 built-in server definitions. All are enabled unless `lsp.servers.<key>.enabled` is `false`. Install the language servers yourself; SuPi does not download or install them. Built-in commands must be on `PATH`, except project-local native TypeScript; a configured absolute command path is also supported. Code Intelligence starts the runtime only for trusted projects.
|
|
24
24
|
|
|
25
25
|
### Built-in route matrix
|
|
26
26
|
|
|
@@ -28,7 +28,7 @@ This matrix describes the default routing configuration, not a live test or a gu
|
|
|
28
28
|
|
|
29
29
|
| Language / config key | Default command and arguments | File extensions | Root markers for automatic discovery |
|
|
30
30
|
|---|---|---|---|
|
|
31
|
-
| TypeScript / JavaScript — `typescript` | `typescript-language-server --stdio` | `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.mjs`, `.cjs` | `tsconfig.json`, `jsconfig.json`, `package.json` |
|
|
31
|
+
| TypeScript / JavaScript — `typescript` | Project-local native `tsc --lsp --stdio`, otherwise `typescript-language-server --stdio` | `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.mjs`, `.cjs` | `tsconfig.json`, `jsconfig.json`, `package.json` |
|
|
32
32
|
| Python — `python` | `pyright-langserver --stdio` | `.py`, `.pyi` | `pyproject.toml`, `setup.py`, `setup.cfg`, `requirements.txt`, `pyrightconfig.json` |
|
|
33
33
|
| Rust — `rust` | `rust-analyzer` | `.rs` | `Cargo.toml` |
|
|
34
34
|
| Go — `go` | `gopls serve` | `.go`, `.mod` | `go.mod`, `go.sum` |
|
|
@@ -59,7 +59,7 @@ A configured route is not a ready server. Features depend on the active server,
|
|
|
59
59
|
| Hover, definitions, references, implementations, document/workspace symbols | Supported through the server's corresponding LSP methods; availability and coverage differ by server. |
|
|
60
60
|
| Diagnostics | Native pull, the eligible TypeScript request adapter, or ambient push. Push observation alone cannot confirm that a file is clean. See the diagnostic policy below. |
|
|
61
61
|
| `rename_symbol` | Requires server rename support and validated text edits; uses `prepareRename` when advertised. |
|
|
62
|
-
| `update_imports` / `delete_dead_code` | Requires matching `source.organizeImports` / `source.removeUnused` code actions that produce validated text edits. |
|
|
62
|
+
| `update_imports` / `delete_dead_code` | Requires matching `source.organizeImports` / (`source.removeUnused` or `source.removeUnusedImports`) code actions that produce validated text edits. Imports-only advertisement reports limited support, not arbitrary unused-declaration removal. |
|
|
63
63
|
| `extract_function` / `extract_variable` | Unavailable for all languages: standard LSP code actions do not provide a safe exact extraction-name contract. |
|
|
64
64
|
|
|
65
65
|
The [operation-support model](src/config/operation-support.ts) separates server advertisement from SuPi planning limits. A server can advertise an action that SuPi cannot safely turn into a plan.
|
|
@@ -72,6 +72,40 @@ Pyright remains the default Python server, but the audited server lacks implemen
|
|
|
72
72
|
|
|
73
73
|
JDTLS acceptance uses a compatible pinned JDK and isolated writable `-data` and `-configuration` directories. Keep test workspaces separate from personal JDTLS state. The [capability baseline](../../docs/conventions/code-language-baseline.md) defines the operation and evidence requirements.
|
|
74
74
|
|
|
75
|
+
### TypeScript compiler selection
|
|
76
|
+
|
|
77
|
+
The default TypeScript route prefers a project-local stable native TS7 server. Native launches use the selected package's declared `tsc` Node launcher with `--lsp --stdio`; neither TLS nor a global native executable is required. Legacy compiler launches still require `typescript-language-server` (TLS) on PATH. Nothing is installed automatically.
|
|
78
|
+
|
|
79
|
+
| Configuration | Launch policy |
|
|
80
|
+
|---|---|
|
|
81
|
+
| No explicit launch fields | Prefer stable native TS7 as `typescript`, then as `@typescript/native`. Otherwise use legacy `typescript`, or—only if absent—`@typescript/typescript6`. If all are absent, retain TLS's existing lookup. |
|
|
82
|
+
| `lsp.servers.typescript.typescriptPackage` | Resolve exactly this installed package name/alias. No arbitrary alias scan, package subpath, URL, or filesystem-path selector. |
|
|
83
|
+
| Explicit `command` **or** `args` | Manual launch, including inherited values equal to defaults. No automatic compiler selection or initialization-option injection. |
|
|
84
|
+
| Explicit `initializationOptions.tsserver.path` | Use the configured path without automatic compiler selection. |
|
|
85
|
+
| Package selector plus any explicit launch field above | Configuration conflict; the failure identifies fields and their configuration origins when available. Remove one side and reload. |
|
|
86
|
+
|
|
87
|
+
For example, select a compiler installed as `my-ts6` without changing the TLS command:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{ "lsp": { "servers": { "typescript": { "typescriptPackage": "my-ts6" } } } }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- Resolve packages using the route root's normal Node package search paths, including hoisted and symlinked packages. A package whose actual identity is `@typescript/typescript6`, including one installed as `typescript` or another alias, resolves `@typescript/old` from the wrapper's dependency graph. Pass the underlying implementation's actual `lib/tsserver.js` through `initializationOptions.tsserver.path`; preserve other initialization options.
|
|
94
|
+
- Native-first precedence deliberately replaces canonical-package-first selection: a native `@typescript/native` alias wins over legacy `typescript`. This alias must resolve to the stable native `typescript` package, not legacy TS or the old preview package. Other aliases require `typescriptPackage`.
|
|
95
|
+
- A consulted broken symlink, malformed manifest, missing compiler version, unsupported identity/major, or prerelease is an error, not absence. Stable TS7 releases are eligible without a patch allowlist; unknown native majors and preview/nightly packages require manual launch. Do not inspect lower-priority packages after a native winner. Only the winner's launch files are validated, so a legacy loser's missing `tsserver.js` does not block a healthy native alias.
|
|
96
|
+
- Never fall back after resolution or startup failure of the selected package. Manual launches receive no automatic initialization-option injection.
|
|
97
|
+
- Discovery, missing-server warnings, lazy starts, and recovery use the same policy. Every actual start/retry resolves fresh package files using the active settings snapshot. Installing or repairing a compiler can be followed by explicit health refresh; changing settings requires reload/restart. Healthy clients do not hot-swap.
|
|
98
|
+
- `code_health` and `/supi-ci-status` show the `native` or `tsserver` backend and resolved compiler version when known, separately from wrapper version. Manual/legacy-fallback launches have no inferred compiler identity. Missing versions remain unknown. Selection failures identify the package/root and a corrective action.
|
|
99
|
+
- Topmost-root collapsing remains unchanged. Mixed nested backends, Windows live qualification, dependency watchers, performance benchmarking, and full TLS/native feature parity are outside this change. Existing legacy TLS support is not deliberately removed.
|
|
100
|
+
|
|
101
|
+
Focused qualification pins native TypeScript **7.0.2**, TLS **6.0.2**, ordinary TypeScript **6.0.3**, and `@typescript/typescript6` **6.0.2** backed by `@typescript/old` → TypeScript **6.0.3**. The wrapper's dependency range does not guarantee that compiler version; the lockfile and [prerequisite pins](../../scripts/language-baseline/pins.json) fix these combinations. SuPi retains TS6 for its compiler-API dependency and legacy-server tests. Workspace typechecking and declaration builds use the native TS7 launcher directly to avoid ambiguity between the installed `tsc` binaries.
|
|
102
|
+
|
|
103
|
+
The required [Linux/macOS CI matrix](../../.github/workflows/ci.yml) keeps [selection/layout tests](__tests__/unit/typescript-launch.test.ts), [real native lifecycle tests](__tests__/integration/workspace-runtime.integration.typescript-native.test.ts), and [wrapper lifecycle tests](__tests__/integration/workspace-runtime.integration.typescript-launch.test.ts) separate from the [public TS/JS semantic matrix](../supi-code-intelligence/__tests__/acceptance/language-baseline-harness.ts). Required missing/unavailable behavior fails rather than skips. The matrix covers hover, definitions, references, meaningful implementations, document/workspace symbols, confirmed error → clean diagnostics, refresh, cross-file invalidation, Unicode-safe rename plan/apply and stale-plan rejection, and precise organize-imports. The TypeScript public matrix also checks unused-import removal, unchanged source during planning, stale-plan rejection, and preservation of unrelated text; native support is imports-only. Native TSX/JSX and module suffixes receive routing/synchronization checks rather than duplicate matrices. Existing stale-result and edit-safety suites remain in place.
|
|
104
|
+
|
|
105
|
+
[Bounded config comparisons](__tests__/integration/typescript-config-coverage.test.ts) compare TS6 and native TS7 `--showConfig` root-file coverage for TS/JS defaults, include/exclude, `allowJs`/`checkJs`, inherited patterns (including late-created files), and separate referenced projects. These cases found no false exclusion; filtering is unchanged. **Config coverage state is not TypeScript program membership**: imports may bring excluded files into a program, and references do not merge root-file lists.
|
|
106
|
+
|
|
107
|
+
Native `delete_dead_code` consumes `source.removeUnusedImports` through the same safe plan/apply workflow ([#554](https://github.com/mrclrchtr/supi/issues/554)). This mapping removes unused imports only: operation support reports `server: advertised`, `supi: limited`, with an imports-only reason. It does not qualify arbitrary unused-declaration removal. Extraction remains unavailable ([#467](https://github.com/mrclrchtr/supi/issues/467)); no TLS/native feature-parity claim is made. Only the pinned combinations are tested, not every stable TS7 release. Focused qualification passed on macOS arm64 (Node 24.22.1) and Linux arm64 (Debian container, Node 24.22.0), including the native/TS6 public TS/JS matrix. The unused-import follow-up has local macOS arm64 verification only; its Linux verification remains pending. The pinned Linux/macOS CI jobs remain pending until they run; this is not a qualification claim for the complete multi-language baseline.
|
|
108
|
+
|
|
75
109
|
### Java startup and project JDKs
|
|
76
110
|
|
|
77
111
|
The JDK that starts JDTLS can differ from the JDK that builds your project. Set a compatible server runtime through the Java route without changing the shell or project compiler:
|
|
@@ -153,7 +187,7 @@ Caller cancellation or deadline stops only that caller's wait. Explicit diagnost
|
|
|
153
187
|
|
|
154
188
|
If another request first opens a document during an active semantic or diagnostic request, the client can synchronize and repeat the request once. The retry retains the caller's original signal and deadline. Content, workspace, close, and lifecycle changes still reject stale results. A diagnostic retry collects new request evidence; it does not promote an old cache. This retry is not route recovery. A second enrollment during the retry still rejects the result. Semantic failure reasons state whether the retry failed or was exhausted. The bounded `request.retry` debug event records the retry reason and outcome with the public call's opaque operation ID, when supplied. A completed RPC alone does not establish accepted semantic evidence.
|
|
155
189
|
|
|
156
|
-
A read-only semantic request can also retry once after an LSP `ContentModified` error (`-32801`). This retry shares the same one-retry budget as enrollment; it does not add another try. The client keeps the original request parameters and checks changes from the first failed input revision. It runs the full input sync before retry and checks current inputs before it accepts a result. An enrollment is still the only input change that can permit a retry. A content, workspace, close, lifecycle, or retirement change blocks it. Client replacement, caller cancellation, and a deadline also block retry. The post-request check rejects a result if inputs change while that attempt runs. This error-code retry applies only to read-only semantic requests. Diagnostics, rename, code actions, and refactors keep the default policy and do not retry `ContentModified`. See the [LSP 3.17 message-ordering rule](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#messageOrdering), the [retry guard](src/client/client-request-enrollment.ts), and the [client query path](src/client/client.ts).
|
|
190
|
+
A read-only semantic request can also retry once after an LSP `ContentModified` error (`-32801`). This retry shares the same one-retry budget as enrollment; it does not add another try. Before this protocol retry, the client waits 100 ms so an immediate retry does not race the same server analysis. The wait is cancellable, respects the original absolute deadline, and does not delay enrollment-only retries. The client keeps the original request parameters and checks changes from the first failed input revision. It runs the full input sync before retry and checks current inputs before it accepts a result. An enrollment is still the only input change that can permit a retry. A content, workspace, close, lifecycle, or retirement change blocks it. Client replacement, caller cancellation, and a deadline also block retry. The post-request check rejects a result if inputs change while that attempt runs. This error-code retry applies only to read-only semantic requests. Diagnostics, rename, code actions, and refactors keep the default policy and do not retry `ContentModified`. See the [LSP 3.17 message-ordering rule](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#messageOrdering), the [retry guard](src/client/client-request-enrollment.ts), and the [client query path](src/client/client.ts).
|
|
157
191
|
|
|
158
192
|
Diagnostic collection does not close and reopen a document to obtain confirmation. It does not send a no-op `didChange` for that purpose. Refresh retains unchanged documents and their current server state, and resynchronizes changed or invalidated documents. A client restart may reopen tracked documents to restore client state; that is recovery, not diagnostic confirmation.
|
|
159
193
|
|
|
@@ -253,8 +287,11 @@ Disable one language with `lsp.servers.<language>.enabled: false`. Use `/supi-ci
|
|
|
253
287
|
- negotiated operation support in project-server status
|
|
254
288
|
|
|
255
289
|
Operation-specific planners request `source.organizeImports` for import updates and
|
|
256
|
-
`source.removeUnused` for dead-code removal.
|
|
257
|
-
|
|
290
|
+
both `source.removeUnused` and `source.removeUnusedImports` for dead-code removal.
|
|
291
|
+
These distinct kinds are alternatives, not parent/child kinds; either exact kind
|
|
292
|
+
or its dotted descendants can match the returned `CodeAction.kind`, never its title.
|
|
293
|
+
An imports-only advertisement reports a SuPi limitation rather than general
|
|
294
|
+
unused-declaration support. Edit-only actions are validated before planning. Command actions,
|
|
258
295
|
resource operations, snippets, annotations, malformed actions, and stale responses
|
|
259
296
|
stay unavailable. Distinct matching plans return an ambiguous result instead of
|
|
260
297
|
selecting the first action. Rename uses `prepareRename` when the server advertises
|
|
@@ -14,6 +14,8 @@ export interface DebugRegistryConfig {
|
|
|
14
14
|
enabled: boolean;
|
|
15
15
|
/** What the agent-callable debug tool may return. */
|
|
16
16
|
agentAccess: DebugAgentAccess;
|
|
17
|
+
/** Whether Agent Run retrospective capture is explicitly enabled. */
|
|
18
|
+
agentRunRetrospectivesEnabled: boolean;
|
|
17
19
|
/** Maximum number of session-local events to keep in memory. */
|
|
18
20
|
maxEvents: number;
|
|
19
21
|
}
|
|
@@ -21,6 +23,7 @@ export interface DebugRegistryConfig {
|
|
|
21
23
|
export const DEBUG_REGISTRY_DEFAULTS: DebugRegistryConfig = {
|
|
22
24
|
enabled: false,
|
|
23
25
|
agentAccess: "sanitized",
|
|
26
|
+
agentRunRetrospectivesEnabled: false,
|
|
24
27
|
maxEvents: 100,
|
|
25
28
|
};
|
|
26
29
|
|
|
@@ -75,6 +78,9 @@ export interface DebugEventQueryResult {
|
|
|
75
78
|
/** Receives a sanitized event whenever the registry records one. */
|
|
76
79
|
export type DebugEventListener = (event: DebugEventView) => void;
|
|
77
80
|
|
|
81
|
+
/** Receives sanitized events for persistence; separate from ordinary event subscriptions. */
|
|
82
|
+
export type DebugPersistenceSink = (event: DebugEventView) => void;
|
|
83
|
+
|
|
78
84
|
export interface DebugSummary {
|
|
79
85
|
total: number;
|
|
80
86
|
byLevel: Partial<Record<DebugLevel, number>>;
|
|
@@ -85,6 +91,7 @@ interface DebugRegistryState {
|
|
|
85
91
|
config: DebugRegistryConfig;
|
|
86
92
|
events: DebugEvent[];
|
|
87
93
|
listeners: Set<DebugEventListener>;
|
|
94
|
+
persistenceSinks: Set<DebugPersistenceSink>;
|
|
88
95
|
nextId: number;
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -111,13 +118,15 @@ function getState(): DebugRegistryState {
|
|
|
111
118
|
config: cloneConfig(DEBUG_REGISTRY_DEFAULTS),
|
|
112
119
|
events: [],
|
|
113
120
|
listeners: new Set(),
|
|
121
|
+
persistenceSinks: new Set(),
|
|
114
122
|
nextId: 1,
|
|
115
123
|
};
|
|
116
124
|
(globalThis as Record<symbol, unknown>)[REGISTRY_KEY] = state;
|
|
117
125
|
}
|
|
118
126
|
// Keep the shared registry compatible with extension reloads that reuse an
|
|
119
|
-
// instance created before
|
|
127
|
+
// instance created before listener sets existed.
|
|
120
128
|
if (!state.listeners) state.listeners = new Set();
|
|
129
|
+
if (!state.persistenceSinks) state.persistenceSinks = new Set();
|
|
121
130
|
return state;
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -200,6 +209,19 @@ export function isDebugRegistryEnabled(): boolean {
|
|
|
200
209
|
return getState().config.enabled;
|
|
201
210
|
}
|
|
202
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Return whether a retrospective can be captured: Debug and its explicit opt-in
|
|
214
|
+
* must be enabled, and at least one persistence sink must be registered.
|
|
215
|
+
*/
|
|
216
|
+
export function canRecordAgentRunRetrospective(): boolean {
|
|
217
|
+
const state = getState();
|
|
218
|
+
return (
|
|
219
|
+
state.config.enabled &&
|
|
220
|
+
state.config.agentRunRetrospectivesEnabled &&
|
|
221
|
+
state.persistenceSinks.size > 0
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
203
225
|
/** Best-effort redaction helper for data exposed through sanitized debug views. */
|
|
204
226
|
export function redactDebugData<T>(value: T): T {
|
|
205
227
|
return redactValue(value, 8) as T;
|
|
@@ -226,6 +248,16 @@ export function subscribeDebugEvents(listener: DebugEventListener): () => void {
|
|
|
226
248
|
return () => state.listeners.delete(listener);
|
|
227
249
|
}
|
|
228
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Register a sanitized event persistence sink. Sinks are tracked separately from
|
|
253
|
+
* ordinary listeners so eligibility checks only succeed when logging is active.
|
|
254
|
+
*/
|
|
255
|
+
export function subscribeDebugPersistence(sink: DebugPersistenceSink): () => void {
|
|
256
|
+
const state = getState();
|
|
257
|
+
state.persistenceSinks.add(sink);
|
|
258
|
+
return () => state.persistenceSinks.delete(sink);
|
|
259
|
+
}
|
|
260
|
+
|
|
229
261
|
/** Record a session-local debug event if debugging is enabled. */
|
|
230
262
|
export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
|
|
231
263
|
const state = getState();
|
|
@@ -251,6 +283,13 @@ export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
|
|
|
251
283
|
// Debug-event consumers must not alter producer behavior.
|
|
252
284
|
}
|
|
253
285
|
}
|
|
286
|
+
for (const sink of state.persistenceSinks) {
|
|
287
|
+
try {
|
|
288
|
+
sink(toSanitizedView(event));
|
|
289
|
+
} catch {
|
|
290
|
+
// Persistence failures must not alter producer behavior.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
254
293
|
return {
|
|
255
294
|
...event,
|
|
256
295
|
data: event.data === undefined ? undefined : redactDebugData(event.data),
|
|
@@ -309,5 +348,6 @@ export function resetDebugRegistry(): void {
|
|
|
309
348
|
state.config = cloneConfig(DEBUG_REGISTRY_DEFAULTS);
|
|
310
349
|
state.events = [];
|
|
311
350
|
state.listeners.clear();
|
|
351
|
+
state.persistenceSinks.clear();
|
|
312
352
|
state.nextId = 1;
|
|
313
353
|
}
|
|
@@ -282,6 +282,12 @@ Returns `function(path)` the filter function.
|
|
|
282
282
|
|
|
283
283
|
> New in 5.0.0
|
|
284
284
|
|
|
285
|
+
**This method is NOT meant for ordinary users or everyday use.** If all you need is to tell whether a `pathname` is ignored, this is not the method you are looking for.
|
|
286
|
+
|
|
287
|
+
`.test()` is meant for packages that build upon `ignore`. Besides whether a `pathname` is ignored, it tells whether a negative pattern unignored it, which such a package can use to control its own caching, such as what to cache and when a cached result can be reused.
|
|
288
|
+
|
|
289
|
+
If you only need to know whether a `pathname` should be ignored, use `.ignores()` instead. It returns that answer as a boolean, and always agrees with `.test(pathname).ignored`.
|
|
290
|
+
|
|
285
291
|
Returns `TestResult`
|
|
286
292
|
|
|
287
293
|
```ts
|
|
@@ -291,7 +297,8 @@ interface TestResult {
|
|
|
291
297
|
// true if a negative pattern matched the `pathname` itself, and no parent
|
|
292
298
|
// directory of it is ignored -- see "When both are false" below
|
|
293
299
|
unignored: boolean
|
|
294
|
-
// The `IgnoreRule` which ignores the pathname
|
|
300
|
+
// The `IgnoreRule` which ignores the pathname: the last rule to match, the
|
|
301
|
+
// one `git check-ignore -v` reports
|
|
295
302
|
rule?: IgnoreRule
|
|
296
303
|
}
|
|
297
304
|
|
|
@@ -339,13 +346,15 @@ To tell the two situations apart, test the parent directories of the `pathname`,
|
|
|
339
346
|
['foobar/'].some(dir => ig2.test(dir).unignored) // true
|
|
340
347
|
```
|
|
341
348
|
|
|
342
|
-
`unignored` is meant for packages that implement their own caching mechanism upon `node-ignore`. To simply tell whether a `pathname` is ignored, use `.ignores()` or check `TestResult::ignored`.
|
|
343
|
-
|
|
344
349
|
## .checkIgnore(target: string): TestResult
|
|
345
350
|
|
|
346
351
|
> new in 7.0.0
|
|
347
352
|
|
|
348
|
-
Debugs gitignore / exclude files
|
|
353
|
+
Debugs gitignore / exclude files the way `git check-ignore -v` does. Usually this method is used for other packages to implement the function of `git check-ignore -v` upon `node-ignore`. Apart from the [known differences](#known-differences-from-git):
|
|
354
|
+
|
|
355
|
+
- `ignored` is what `git check-ignore` answers for `target`.
|
|
356
|
+
- When `ignored` is `true`, `rule` is the rule `git check-ignore -v` reports. Its `pattern` is the pattern as it was added, so trailing spaces that git trims are still there.
|
|
357
|
+
- When the last pattern to match is a negative one, `git check-ignore -v` reports it, while `.checkIgnore()` returns `unignored: true` and no `rule`. For a directory passed with a trailing slash, `unignored` is also `true` when a negative pattern re-includes the directory itself, which `git check-ignore -v` does not report.
|
|
349
358
|
|
|
350
359
|
- **target** `string` the target to test.
|
|
351
360
|
|
|
@@ -471,13 +480,30 @@ not in `git`. For ASCII paths and patterns there is no difference.
|
|
|
471
480
|
code runs against a real repository, pass the value of the repository's
|
|
472
481
|
`core.ignorecase` explicitly.
|
|
473
482
|
|
|
474
|
-
###
|
|
483
|
+
### A run of asterisks
|
|
484
|
+
|
|
485
|
+
`git` reads a run of two or more asterisks as a globstar whenever the run
|
|
486
|
+
fills a path segment: it starts the pattern or follows a `/`, and it ends
|
|
487
|
+
the pattern or comes before a `/`. `ignore` does the same, so `***/foo`,
|
|
488
|
+
`a/***` and `a/***/b` behave exactly as `**/foo`, `a/**` and `a/**/b`.
|
|
489
|
+
|
|
490
|
+
`git` takes two more cases as a globstar, and `ignore` does not:
|
|
491
|
+
|
|
492
|
+
- **A run right after a literal prefix**, as in `a**/b` or `a***/b`. Before
|
|
493
|
+
matching, `git` strips the part of a pattern that holds no wildcard, and
|
|
494
|
+
the run then starts what is left. gitignore(5) lists the positions where
|
|
495
|
+
`**` is special and calls every other run "regular asterisks", which is
|
|
496
|
+
what `ignore` does here.
|
|
497
|
+
- **A run next to an escaped slash**, as in `**\/b` or `x\/**/b`.
|
|
498
|
+
|
|
499
|
+
```js
|
|
500
|
+
ignore().add('a**/b').ignores('axy/z/b') // false; git: true
|
|
501
|
+
ignore().add('**\\/b').ignores('x/y/b') // false; git: true
|
|
502
|
+
ignore().add('x\\/**/b').ignores('x/y/z/b') // false; git: true
|
|
503
|
+
```
|
|
475
504
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
pattern before matching, which makes patterns like `***/foo`, `a/***` and
|
|
479
|
-
`a**/b` behave as `**` globstars there. `ignore` follows the documented
|
|
480
|
-
behavior. If you need the globstar, write `**`.
|
|
505
|
+
To get a globstar in either case, give the run a segment of its own, such as
|
|
506
|
+
`a*/**/b`.
|
|
481
507
|
|
|
482
508
|
### `checkIgnore()` and a directory passed with a trailing slash
|
|
483
509
|
|
|
@@ -50,8 +50,9 @@ declare namespace ignore {
|
|
|
50
50
|
*/
|
|
51
51
|
unignored: boolean
|
|
52
52
|
/**
|
|
53
|
-
* The rule that ignores the pathname
|
|
54
|
-
*
|
|
53
|
+
* The rule that ignores the pathname, or the parent directory it is
|
|
54
|
+
* ignored with: the last rule to match, which is the rule
|
|
55
|
+
* `git check-ignore -v` reports. Present exactly when `ignored` is true.
|
|
55
56
|
*/
|
|
56
57
|
rule?: IgnoreRule
|
|
57
58
|
}
|
|
@@ -15,6 +15,8 @@ const ESCAPE = '\\'
|
|
|
15
15
|
const REGEX_LITERAL_SPECIAL = /[.*+?()[\]{}^$|\\/]/
|
|
16
16
|
const BOM = '\uFEFF'
|
|
17
17
|
const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/
|
|
18
|
+
// A run of two or more asterisks is a globstar (#175)
|
|
19
|
+
const REGEX_ANCHORED_GLOBSTAR = /^\/\*{2,}$/
|
|
18
20
|
const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/
|
|
19
21
|
const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/
|
|
20
22
|
const REGEX_SPLITALL_CRLF = /\r?\n/g
|
|
@@ -423,6 +425,21 @@ const REPLACERS = [
|
|
|
423
425
|
ESCAPE + SPACE
|
|
424
426
|
],
|
|
425
427
|
|
|
428
|
+
// git's wildmatch() reads a run of two or more asterisks as one globstar
|
|
429
|
+
// whenever the run fills a path segment: it starts the pattern or follows
|
|
430
|
+
// a `/`, and it ends the pattern or comes before a `/`. A run of two is
|
|
431
|
+
// already `**`; a longer one is folded into `**` here, so the steps
|
|
432
|
+
// below only ever have to know `**`.
|
|
433
|
+
// `***/foo` -> `**/foo`, `a/***/b` -> `a/**/b`, `a/***` -> `a/**`
|
|
434
|
+
// git also takes a run beside a literal prefix (`a***/b`) or beside an
|
|
435
|
+
// escaped slash (`***\/b`) as a globstar, and this does not; the README
|
|
436
|
+
// lists both, under "A run of asterisks".
|
|
437
|
+
[
|
|
438
|
+
/(^|\/)\*{3,}(?=\/|$)/g,
|
|
439
|
+
(_, start) => `${start}**`,
|
|
440
|
+
'*'
|
|
441
|
+
],
|
|
442
|
+
|
|
426
443
|
// Escape metacharacters
|
|
427
444
|
// which is written down by users but means special for regular expressions.
|
|
428
445
|
|
|
@@ -853,6 +870,15 @@ const pinWildcards = source => {
|
|
|
853
870
|
|
|
854
871
|
// @param {pattern}
|
|
855
872
|
const makeRegexPrefix = pattern => {
|
|
873
|
+
// `/**` matches every path, as `**` does. The translation below only finds
|
|
874
|
+
// a globstar beside a slash it keeps, and the leading one is turned into
|
|
875
|
+
// an anchor, so `/**` would come out as `/*` and match one level only.
|
|
876
|
+
// Here rather than in `createRule`, so that `checkSourceOf`, which
|
|
877
|
+
// compiles `/**/` again without its trailing slash, gets it too.
|
|
878
|
+
if (REGEX_ANCHORED_GLOBSTAR.test(pattern)) {
|
|
879
|
+
pattern = '**'
|
|
880
|
+
}
|
|
881
|
+
|
|
856
882
|
const {source, sources} = extractBrackets(pattern)
|
|
857
883
|
|
|
858
884
|
const replaced = REPLACERS.reduce(
|
|
@@ -1271,17 +1297,10 @@ class RuleManager {
|
|
|
1271
1297
|
|
|
1272
1298
|
// Test one single path without recursively checking parent directories
|
|
1273
1299
|
//
|
|
1274
|
-
// -
|
|
1275
|
-
// setting `checkUnignored` to `false` could reduce additional
|
|
1276
|
-
// path matching.
|
|
1277
|
-
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
1278
|
-
|
|
1279
|
-
// @returns {TestResult} true if a file is ignored
|
|
1280
|
-
test (path, checkUnignored, mode) {
|
|
1281
|
-
let ignored = false
|
|
1282
|
-
let unignored = false
|
|
1283
|
-
let matchedRule
|
|
1300
|
+
// - mode `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
1284
1301
|
|
|
1302
|
+
// @returns {TestResult}
|
|
1303
|
+
test (path, mode) {
|
|
1285
1304
|
// Most of a .gitignore is patterns with no slash in them, and running
|
|
1286
1305
|
// those against the whole path makes the regular expression engine walk
|
|
1287
1306
|
// every directory name on the way to the only segment that could match.
|
|
@@ -1296,52 +1315,40 @@ class RuleManager {
|
|
|
1296
1315
|
? basenameOf(path)
|
|
1297
1316
|
: path
|
|
1298
1317
|
|
|
1318
|
+
// The last rule to match decides, as in git, and it is the rule
|
|
1319
|
+
// `git check-ignore -v` reports, so the scan runs from the end and stops
|
|
1320
|
+
// at the first match.
|
|
1299
1321
|
// A plain loop rather than `forEach`, so that `path`, `basename` and
|
|
1300
1322
|
// `shortcut` are locals. As a callback they became closure variables,
|
|
1301
1323
|
// and reaching for one of those once per rule cost 10% of a scan over a
|
|
1302
1324
|
// large rule set -- more than the shortcut they were there to serve.
|
|
1303
|
-
for (let index =
|
|
1325
|
+
for (let index = length - 1; index >= 0; index --) {
|
|
1304
1326
|
const rule = rules[index]
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
// | ignored : unignored
|
|
1308
|
-
// -------- | ---------------------------------------
|
|
1309
|
-
// negative | 0:0 | 0:1 | 1:0 | 1:1
|
|
1310
|
-
// -------- | ------- | ------- | ------- | --------
|
|
1311
|
-
// 0 | TEST | TEST | SKIP | X
|
|
1312
|
-
// 1 | TESTIF | SKIP | TEST | X
|
|
1313
|
-
|
|
1314
|
-
// - SKIP: always skip
|
|
1315
|
-
// - TEST: always test
|
|
1316
|
-
// - TESTIF: only test if checkUnignored
|
|
1317
|
-
// - X: that never happen
|
|
1318
|
-
const skip = unignored === negative && ignored !== unignored
|
|
1319
|
-
|| negative && !ignored && !unignored && !checkUnignored
|
|
1320
|
-
|
|
1321
|
-
if (!skip && rule[mode].test(
|
|
1327
|
+
|
|
1328
|
+
if (rule[mode].test(
|
|
1322
1329
|
shortcut && rule._basenameOnly
|
|
1323
1330
|
? basename
|
|
1324
1331
|
: path
|
|
1325
1332
|
)) {
|
|
1326
|
-
|
|
1327
|
-
unignored = negative
|
|
1333
|
+
const {negative} = rule
|
|
1328
1334
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
:
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1335
|
+
const ret = {
|
|
1336
|
+
ignored: !negative,
|
|
1337
|
+
unignored: negative
|
|
1338
|
+
}
|
|
1334
1339
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
}
|
|
1340
|
+
if (!negative) {
|
|
1341
|
+
ret.rule = rule
|
|
1342
|
+
}
|
|
1339
1343
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1344
|
+
return ret
|
|
1345
|
+
}
|
|
1342
1346
|
}
|
|
1343
1347
|
|
|
1344
|
-
return
|
|
1348
|
+
return {
|
|
1349
|
+
ignored: false,
|
|
1350
|
+
unignored: false
|
|
1351
|
+
}
|
|
1345
1352
|
}
|
|
1346
1353
|
}
|
|
1347
1354
|
|
|
@@ -1457,7 +1464,7 @@ class Ignore {
|
|
|
1457
1464
|
}
|
|
1458
1465
|
|
|
1459
1466
|
// @returns {TestResult}
|
|
1460
|
-
_test (originalPath, cache
|
|
1467
|
+
_test (originalPath, cache) {
|
|
1461
1468
|
const path = originalPath
|
|
1462
1469
|
// Supports nullable path
|
|
1463
1470
|
&& checkPath.convert(originalPath)
|
|
@@ -1470,7 +1477,7 @@ class Ignore {
|
|
|
1470
1477
|
: RETURN_FALSE
|
|
1471
1478
|
)
|
|
1472
1479
|
|
|
1473
|
-
return this._t(path, cache
|
|
1480
|
+
return this._t(path, cache)
|
|
1474
1481
|
}
|
|
1475
1482
|
|
|
1476
1483
|
checkIgnore (path) {
|
|
@@ -1483,7 +1490,7 @@ class Ignore {
|
|
|
1483
1490
|
// Like `git check-ignore`: a directory excluded by itself or by an
|
|
1484
1491
|
// ancestor settles it, and only then is the literal 'abc/' matched
|
|
1485
1492
|
// against the rules (see `checkSourceOf`).
|
|
1486
|
-
const dir = this._t(path, this._testCache
|
|
1493
|
+
const dir = this._t(path, this._testCache)
|
|
1487
1494
|
|
|
1488
1495
|
if (dir.ignored) {
|
|
1489
1496
|
return dir
|
|
@@ -1501,10 +1508,7 @@ class Ignore {
|
|
|
1501
1508
|
path,
|
|
1502
1509
|
|
|
1503
1510
|
// The cache for the result of a certain checking
|
|
1504
|
-
cache
|
|
1505
|
-
|
|
1506
|
-
// Whether should check if the path is unignored
|
|
1507
|
-
checkUnignored
|
|
1511
|
+
cache
|
|
1508
1512
|
) {
|
|
1509
1513
|
if (path in cache) {
|
|
1510
1514
|
return cache[path]
|
|
@@ -1514,18 +1518,18 @@ class Ignore {
|
|
|
1514
1518
|
|
|
1515
1519
|
// If the path contains a parent directory, check the parent first
|
|
1516
1520
|
const parent = parentPath
|
|
1517
|
-
? this._t(parentPath, cache
|
|
1521
|
+
? this._t(parentPath, cache)
|
|
1518
1522
|
: UNDEFINED
|
|
1519
1523
|
|
|
1520
1524
|
return cache[path] = parent && parent.ignored
|
|
1521
1525
|
// > It is not possible to re-include a file if a parent directory of
|
|
1522
1526
|
// > that file is excluded.
|
|
1523
1527
|
? parent
|
|
1524
|
-
: this._rules.test(path,
|
|
1528
|
+
: this._rules.test(path, MODE_IGNORE)
|
|
1525
1529
|
}
|
|
1526
1530
|
|
|
1527
1531
|
ignores (path) {
|
|
1528
|
-
return this._test(path, this._ignoreCache
|
|
1532
|
+
return this._test(path, this._ignoreCache).ignored
|
|
1529
1533
|
}
|
|
1530
1534
|
|
|
1531
1535
|
createFilter () {
|
|
@@ -1538,7 +1542,7 @@ class Ignore {
|
|
|
1538
1542
|
|
|
1539
1543
|
// @returns {TestResult}
|
|
1540
1544
|
test (path) {
|
|
1541
|
-
return this._test(path, this._testCache
|
|
1545
|
+
return this._test(path, this._testCache)
|
|
1542
1546
|
}
|
|
1543
1547
|
}
|
|
1544
1548
|
|