@cplieger/deadset-ts 4.0.0 → 5.0.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.
Files changed (117) hide show
  1. package/README.md +68 -50
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +230 -37
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +11 -4
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +6 -3
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +4 -2
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +395 -71
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +11 -7
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +6 -3
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +4 -2
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
package/README.md CHANGED
@@ -2,13 +2,22 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@cplieger/deadset-ts)](https://www.npmjs.com/package/@cplieger/deadset-ts) [![JSR](https://jsr.io/badges/@cplieger/deadset-ts)](https://jsr.io/@cplieger/deadset-ts)
4
4
 
5
- > Deterministic dead-code analysis for TypeScript, class and type members included.
5
+ deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
6
6
 
7
- deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It builds the program from your `tsconfig.json` files through the TypeScript 7 compiler API, resolves every reference in one pass, and reports the declarations nothing reaches: unused exports, type-only exports without a consumer, unused class members (private, protected, static and accessors), unused interface, type-alias, enum and namespace members, exports that can be narrowed to a smaller visibility, whole groups of declarations that only keep each other alive, and inside a function the unused parameters, results nobody reads, unreachable statements, dead stores and unreachable cases. Every finding carries an issue code, a confidence and a stable symbol reference from the [deadset Contract](https://github.com/cplieger/deadset-spec), so the same report shape works for a Go analyzer and for a merged multi-language report. The analysis is deterministic: the same tree gives the same report, with no cache and no text search.
7
+ It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261003.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
8
8
 
9
- deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
9
+ ## Why use it
10
10
 
11
- **Status: pre-release.** This version implements contract 4.0.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, where a discovered project whose configuration names no input, carries an option the compiler refuses or cannot be read is dropped, named in the report's `configurations_not_built` with its error and on standard error, and the rest analyzed, while a project `analysis.configurations` or the scope names ends the run with exit code 3, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. `analyze` writes the report the Contract's report schema declares, with text, JSON, GitHub annotation, SARIF 2.1.0 and template renderings beside it on request. A scope document passed with `--scope` names the consumers loaded beside the target, each a directory whose compiler configurations open in the same session; a reference from a consumer keeps a target declaration live, the report names every consumer it loaded, and a consumer that is absent or does not load ends the run with exit code 3. A library target's published API is `certain` once every declared consumer loads and `possible` with no consumer information, and in that second case `DS1001` defaults to `allow` and the narrowing kinds report only in files no manifest export reaches. `explain` answers why one declaration is reported, retained, held by an unanswered question, live or dead, from the same analysis the report is built from. `describe` prints the analyzer's description with its conformance record over the published corpus and the declared gaps `conformance.json` lists. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process. Every question the analysis asks the compiler is guarded: a question the compiler server fails on is answered as unknown, every declaration that answer could have kept live is kept live and reported by nothing, as is every declaration only those keep live or whose own references went unanswered, and standard error counts the unanswered questions by project. The one runtime dependency is the `typescript` package at exactly 7.1.0-dev.20261003.1, installed under the `@typescript/native` name.
11
+ deadset-ts is built for a CI gate on dead code. Each finding names code to delete, an `export` to drop or a setting to fix.
12
+
13
+ - It reports unused exports, files and dependencies, and unused members of classes, interfaces, type aliases, enums and namespaces, private and static included.
14
+ - Inside a function, it reports unused parameters and results, unreachable statements and cases, and dead stores.
15
+ - It reports an `export` only its own file uses, and declarations that only reference each other.
16
+ - It keeps what decorators, reflective lookups, `JSON.stringify` and the frameworks, containers, serializers and templates you name in `deadset.json` use.
17
+ - The same tree gives the same report, and a baseline keyed on code and symbol survives moved lines.
18
+ - It writes text, JSON, GitHub annotations, SARIF 2.1.0 or a custom template.
19
+
20
+ Consider [Knip](https://knip.dev) if you want issues fixed with `--fix`, plugins for over 100 tools, or MDX files checked.
12
21
 
13
22
  ## Install
14
23
 
@@ -20,74 +29,83 @@ npx jsr add -D @cplieger/deadset-ts
20
29
  npx @cplieger/deadset-ts version
21
30
  ```
22
31
 
23
- The npm package carries the command and the library as JavaScript that the compiler emits at publish, so `npx deadset-ts` and `import("@cplieger/deadset-ts")` both run on plain Node.js 24 or later, with no loader and no flag. It carries the TypeScript source as well, which is what a TypeScript consumer's own compiler resolves. That consumer sets `allowImportingTsExtensions`, because the source names each import with its `.ts` extension. The JSR package ships source only, and Deno compiles it: a Deno program imports and type-checks the library, but the compiler's synchronous session does not open under Deno, so running an analysis needs Node.
24
-
25
32
  ## Usage
26
33
 
34
+ Create a `deadset.json` at the project root that says whether the project is an `application` or a `library`, then run `analyze`. A run reads every `tsconfig` project under that folder, project references included, and reports a declaration only when it is dead in all of them.
35
+
27
36
  ```sh
28
- npx deadset-ts version
37
+ echo '{ "target": { "kind": "application" } }' > deadset.json
38
+ npx @cplieger/deadset-ts analyze --report=deadset-report.json
29
39
  ```
30
40
 
31
- `version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class or an `analysis.template_dirs` entry is not a directory below the target root. `analyze --report=PATH` writes the JSON report to `PATH` through a temporary file renamed into place, each `--format` rendering at `PATH` with its suffix appended (`text` as `.txt`, `json` as `.json`, `github` as `.annotations` in GitHub workflow-command form, `sarif` as `.sarif`, and `template` as `.tmpl`, rendering the file `--template=FILE` names, written in a subset of Go's `text/template` action grammar over the report's JSON member names), and with `--baseline-write=FILE` a baseline of every finding; it writes nothing to standard output and exits with the Contract's exit-code table: 0 clean, 1 for a finding at or above `reporters.fail_on` or a stale suppression, 2 for a usage error, including a template that cannot be read or does not parse, 3 for a failed load or a rendering that cannot be written, and 4 for a pending finding, which outranks 1. `--exit-code=off` exits 0 whatever the verdict and names the verdict on standard error. The report names the target relative to the directory the run is invoked from, so a target outside that directory fails with 3. `describe` writes its JSON document to standard output and exits 0. `explain --why=SYM`, or the same request spelled `--why-live=SYM` or `--why-not=SYM`, prints why that one declaration is reported, retained, held by an unanswered question, live or dead, with the shortest reference path from a root or a loaded consumer for a live one, and exits 0; a request that names no one declaration exits 2 and lists up to 20 declarations it partially matches. Any other invocation and no arguments print the usage line and exit 2.
32
-
33
- | Verb | Job |
34
- | ---------------- | ---------------------------------------------------------------------------------- |
35
- | `analyze` | Analyze a project and write the report, its renderings and a baseline |
36
- | `explain` | Explain one declaration: the relation, the roots and the references that decide it |
37
- | `print-config` | Print the resolved configuration and where each setting came from |
38
- | `print-projects` | Print the identifier of every project a run analyzes |
39
- | `print-roots` | Print every declaration the analysis keeps live without a reference, and why |
40
- | `print-retained` | Print every declaration an exemption held back: each class, its site and detail |
41
- | `describe` | Describe the analyzer: versions, accepted report schemas, languages, conformance |
42
- | `version` | Print the analyzer and Contract versions |
43
-
44
- ## Requirements
45
-
46
- - Node.js 24 or later. The npm command is JavaScript the compiler emits at publish, so it runs with no loader and no flag; a clone of this repository runs the same command from its TypeScript sources through Node's own type stripping, which is what needs the version.
47
- - TypeScript 7. The analyzer is written against the `typescript` package at exactly version 7.1.0-dev.20261003.1 and its `unstable/*` API. It does not run on TypeScript 6, whose compiler API TypeScript 7 removed, and it does not fall back to it.
48
- - Linux. Other platforms are untested.
41
+ `analyze` writes the JSON report to `deadset-report.json` and a text rendering beside it as `deadset-report.json.txt`. Take this `src/greet.ts`, where the rest of the project calls only `greet` and `Counter.increment`:
42
+
43
+ ```ts
44
+ export function greet(name: string): string {
45
+ return `hello ${name}`;
46
+ }
47
+ export function farewell(name: string): string {
48
+ return `bye ${name}`;
49
+ }
50
+ export class Counter {
51
+ private count = 0;
52
+ increment(): void {
53
+ this.count += 1;
54
+ }
55
+ reset(): void {
56
+ this.count = 0;
57
+ }
58
+ }
59
+ ```
49
60
 
50
- ## API
61
+ The text rendering lists three findings:
51
62
 
52
- The package exports the command line, for callers that run it in-process, and the vocabularies and helpers its output is spelled with. The command line reads the platform through a `Host`: the filesystem, the directory relative paths resolve against, and the version of the package the analyzer was installed from. `bin/node-host.ts` is the Node one the command line binds; a caller embedding the analyzer, or running it on another platform, supplies its own and supplies its own version with it.
63
+ ```text
64
+ src/greet.ts:4:17: function farewell: exported function has no reference in the target and none from any loaded consumer [certain] (DS1001)
65
+ src/greet.ts:8:11: class-member Counter.count: member Counter.count is written at 2 positions and never read [certain] (DS1301)
66
+ src/greet.ts:12:3: method Counter.reset: method has no reference in the target [certain] (DS1003)
67
+ summary: 3 findings (0 allow, 0 warn, 3 deny), 6 deletable lines, 0 suppressions in effect, 0 reasons recorded, 0 stale suppressions, 0 pending, 0 omitted
68
+ ```
53
69
 
54
- The command line:
70
+ The run exits 1 because the report holds a finding at `deny` severity, and 0 when it holds none. Three common next steps:
55
71
 
56
- - `run(args, out, err, host, openClient?)`: runs the command line over `args` (the arguments after the program name), writing to the two `Writer` streams, and returns the exit code. It never exits the process.
57
- - `SETTING_OPTIONS`: the configuration setting each command-line option supplies, by option name.
58
- - `Writer`: `{ write(text: string): void }`. `process.stdout` and `process.stderr` satisfy it.
72
+ - Add `--format=sarif` or `--format=github` for a SARIF file or GitHub workflow annotations. Naming any format turns off the default text rendering, so add `--format=text` as well to keep it.
73
+ - Run `npx @cplieger/deadset-ts explain --why=farewell` to see why one declaration is reported, retained, held by an unanswered question, live or dead.
74
+ - Add `--baseline-write=deadset-baseline.json` to record today's findings. Later runs fail on a new finding and on a baseline row whose finding has gone. To keep one declaration, put `// deadset:ignore DS1001 -- <reason>` on the line above it.
59
75
 
60
- The platform:
76
+ [Commands and exit codes](docs/commands.md) covers every verb and option, and [Configuration](docs/configuration.md) covers every setting.
61
77
 
62
- - `Host`: the filesystem a run reads, the directory it reads relative paths against, and the analyzer's own version.
63
- - `DirectoryEntry`, `PathKind`: one entry of a directory read, and what one path names.
78
+ ## API
64
79
 
65
- Positions:
80
+ The package exports the command line as a function, and the helpers that spell the positions and symbol references in a report.
66
81
 
67
- - `renderPosition(file, root, offset)`: one compiler offset as a path, a line and a column, the column counting UTF-16 code units.
68
- - `positionKey(position)`, `byPosition(a, b)`: the string one position is keyed by, and the order positions are reported in.
69
- - `PositionError`: the refusal for a position that cannot be rendered against the target root.
70
- - `Position`: one rendered position.
82
+ - `run` runs the command line and returns its exit code, without exiting the process. `Writer` is its output stream, and `SETTING_OPTIONS` maps each option to the setting it sets.
83
+ - `Host`, `DirectoryEntry` and `PathKind` describe the filesystem, working directory and version a run reads.
84
+ - `renderPosition`, `positionKey`, `byPosition`, `PositionError` and `Position` render and order positions.
85
+ - `renderRef`, `nameComponent`, `computedComponent`, `isRef`, `REF_EXPRESSIONS`, `Module`, `Component`, `Fragment` and `DependencySection` spell stable symbol references.
86
+ - `DECLINED_CONVENTIONS` and `DeclinedConvention` list the entry-point conventions of other tools the analysis does not read.
87
+ - `CONTRACT_VERSION` is the version of the [deadset Contract](https://github.com/cplieger/deadset-spec) this analyzer implements.
71
88
 
72
- Stable symbol references:
89
+ A TypeScript caller that imports the source sets `allowImportingTsExtensions`, because each import in it ends in `.ts`. [Using deadset-ts as a library](docs/library.md) covers the packages and in-process runs, and [JSR](https://jsr.io/@cplieger/deadset-ts/doc) has the full reference.
73
90
 
74
- - `renderRef(module, fragment)`: one symbol's stable reference, in the canonical spelling the contract's grammar fixes.
75
- - `nameComponent(name)`, `computedComponent(text)`: the canonical spelling of one component of that reference, from a declared name or from a computed key.
76
- - `isRef(value)`: whether one string is a reference of this language's grammar.
77
- - `REF_EXPRESSIONS`: one regular expression per form the grammar defines.
78
- - `Module`, `Component`, `Fragment`, `DependencySection`: the module half of a reference, one component of its fragment, what the fragment names, and the manifest section a dependency was declared in.
91
+ ## Related projects
79
92
 
80
- Entry points:
93
+ deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes every fixture of the Contract's conformance corpus except one reflective-lookup fixture, the gap [`conformance.json`](conformance.json) declares.
81
94
 
82
- - `DECLINED_CONVENTIONS`, `DeclinedConvention`: the entry-point conventions of other tools the analysis does not read, one row per tool or convention with the reason.
95
+ - [deadset-go](https://github.com/cplieger/deadset-go) is the same analysis for Go modules.
96
+ - [deadset](https://github.com/cplieger/deadset) runs both analyzers as one command and merges their reports, resolving the edges between Go and TypeScript code.
83
97
 
84
- Versions:
98
+ ## Documentation
85
99
 
86
- - `CONTRACT_VERSION`: the contract version this analyzer implements.
100
+ - [Commands and exit codes](docs/commands.md) lists every verb, option, rendering and exit code.
101
+ - [Configuration](docs/configuration.md) lists every setting, the suppression files and the scope document.
102
+ - [How deadset-ts works](docs/how-it-works.md) explains the projects it loads, the roots it starts from and what it holds back.
103
+ - [Using deadset-ts as a library](docs/library.md) covers the npm and JSR packages and running the analyzer in-process.
104
+ - [Batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size of the reference pass.
87
105
 
88
106
  ## Contributing
89
107
 
90
- See [CONTRIBUTING.md](https://github.com/cplieger/.github/blob/main/CONTRIBUTING.md).
108
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
91
109
 
92
110
  ## Disclaimer
93
111
 
@@ -0,0 +1,5 @@
1
+ import { mapperConnection } from "./mapper-connection.js";
2
+ const connection = mapperConnection((bytes) => process.stdout.write(bytes));
3
+ process.stdin.on("data", (chunk) => {
4
+ connection.receive(chunk);
5
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The content mapper the compiler runs for component files: JSON-RPC 2.0 messages, each
3
+ * framed by a `Content-Length` header, over the process's standard streams. It answers
4
+ * `transform` with {@link componentModule} and keeps no state between requests.
5
+ * The protocol is the compiler's own:
6
+ * https://github.com/microsoft/TypeScript/blob/f9f8d01292562242b6e7c7142e46ed7e470926f9/tsc/internal/contentmapper/hostimpl.go
7
+ */
8
+ import { componentModule } from "../src/component-files.js";
9
+ /** The JSON-RPC code of a method this mapper does not serve. */
10
+ const METHOD_NOT_FOUND = -32601;
11
+ /** The JSON-RPC code of a request whose parameters are not the protocol's. */
12
+ const INVALID_PARAMS = -32602;
13
+ const HEADER_END = new Uint8Array([0x0d, 0x0a, 0x0d, 0x0a]);
14
+ /** The offset of `needle` in `bytes` at or after `from`, or -1. */
15
+ function indexOf(bytes, needle, from) {
16
+ outer: for (let at = from; at + needle.length <= bytes.length; at += 1) {
17
+ for (let index = 0; index < needle.length; index += 1) {
18
+ if (bytes[at + index] !== needle[index]) {
19
+ continue outer;
20
+ }
21
+ }
22
+ return at;
23
+ }
24
+ return -1;
25
+ }
26
+ /** The answer to one message, or undefined for a notification, which takes none. */
27
+ function answer(message) {
28
+ if (message.id === undefined) {
29
+ return undefined;
30
+ }
31
+ const reply = (result) => ({ jsonrpc: "2.0", id: message.id, result });
32
+ const fail = (code, text) => ({
33
+ jsonrpc: "2.0",
34
+ id: message.id,
35
+ error: { code, message: text },
36
+ });
37
+ switch (message.method) {
38
+ case "initialize":
39
+ return reply({ positionEncoding: "utf-16", diagnosticSource: "deadset-ts" });
40
+ case "openProject":
41
+ return reply({});
42
+ case "closeProject":
43
+ return reply(null);
44
+ case "transform": {
45
+ const content = typeof message.params === "object" && message.params !== null
46
+ ? message.params.content
47
+ : undefined;
48
+ if (typeof content !== "string") {
49
+ return fail(INVALID_PARAMS, "transform: params.content is not a string");
50
+ }
51
+ const module = componentModule(content);
52
+ return reply({ text: module.text, extension: module.extension, mappings: module.mappings });
53
+ }
54
+ default:
55
+ return fail(METHOD_NOT_FOUND, `no method ${JSON.stringify(message.method)}`);
56
+ }
57
+ }
58
+ /**
59
+ * A connection over one byte stream. A message whose body is not JSON is answered with
60
+ * nothing, because it carries no id to answer to.
61
+ */
62
+ export function mapperConnection(write) {
63
+ const encoder = new TextEncoder();
64
+ const decoder = new TextDecoder();
65
+ let pending = new Uint8Array(0);
66
+ return {
67
+ receive(chunk) {
68
+ const joined = new Uint8Array(pending.length + chunk.length);
69
+ joined.set(pending);
70
+ joined.set(chunk, pending.length);
71
+ pending = joined;
72
+ for (;;) {
73
+ const headerEnd = indexOf(pending, HEADER_END, 0);
74
+ if (headerEnd < 0) {
75
+ return;
76
+ }
77
+ const header = decoder.decode(pending.subarray(0, headerEnd));
78
+ const length = Number(/content-length:\s*(\d+)/iu.exec(header)?.[1] ?? "0");
79
+ const bodyStart = headerEnd + HEADER_END.length;
80
+ if (pending.length < bodyStart + length) {
81
+ return;
82
+ }
83
+ const body = decoder.decode(pending.subarray(bodyStart, bodyStart + length));
84
+ pending = pending.slice(bodyStart + length);
85
+ let message;
86
+ try {
87
+ message = JSON.parse(body);
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ const reply = answer(message);
93
+ if (reply !== undefined) {
94
+ const bytes = encoder.encode(JSON.stringify(reply));
95
+ write(encoder.encode(`Content-Length: ${String(bytes.length)}\r\n\r\n`));
96
+ write(bytes);
97
+ }
98
+ }
99
+ },
100
+ };
101
+ }
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { closeSync, fchmodSync, fsyncSync, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
3
- import { basename, dirname, join } from "node:path";
2
+ import { closeSync, fchmodSync, fsyncSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, extname, join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  /** The name the manifest of this package declares. */
6
7
  const PACKAGE_NAME = "@cplieger/deadset-ts";
@@ -75,6 +76,24 @@ function writeAtomically(path, pieces) {
75
76
  throw error;
76
77
  }
77
78
  }
79
+ /** A new directory under the platform's temporary directory. */
80
+ function temporaryDirectory() {
81
+ const path = mkdtempSync(join(tmpdir(), "deadset-ts-mapper-"));
82
+ return {
83
+ path,
84
+ remove: () => {
85
+ rmSync(path, { recursive: true, force: true });
86
+ },
87
+ };
88
+ }
89
+ /**
90
+ * The mapper beside this file, in the same form: the emitted tree runs the emitted
91
+ * mapper and a checkout runs the source through the runtime's own type stripping.
92
+ */
93
+ function componentMapperCommand() {
94
+ const here = fileURLToPath(import.meta.url);
95
+ return [process.execPath, join(dirname(here), `component-mapper${extname(here)}`)];
96
+ }
78
97
  /**
79
98
  * The filesystem of the platform the command runs on, and the version of the
80
99
  * package it was installed from.
@@ -98,10 +117,13 @@ export function nodeHost() {
98
117
  }
99
118
  return stat.isDirectory() ? "directory" : "file";
100
119
  },
120
+ realPath: (path) => realpathSync(path),
101
121
  analyzerVersion: () => {
102
122
  found ??= readVersion();
103
123
  return found;
104
124
  },
105
125
  writeDocument: writeAtomically,
126
+ temporaryDirectory,
127
+ componentMapperCommand,
106
128
  };
107
129
  }