Commit a049b56
authored
TML-2930: add prisma-next lsp subcommand + PSL diagnostics language server (#852)
## Linked issue
Refs
[TML-2930](https://linear.app/prisma-company/issue/TML-2930/lsp-scaffold-prisma-next-lsp-subcommand-psl-diagnostics)
— foundation work for the [Language Tools Support Prisma Next
PSL](https://linear.app/prisma-company/project/language-tools-support-prisma-next-psl-3422a7e44b9c)
project. The spec + plan live under `projects/lsp-scaffold/` for review
and are removed at project close-out.
## At a glance
The editor launches the project-local CLI as a language server over
stdio:
```
node_modules/.bin/prisma-next lsp --stdio
```
It completes the `initialize` handshake, advertising incremental
document sync and diagnostics:
```jsonc
// → initialize response (real handshake, verified)
{ "capabilities": { "textDocumentSync": 2 } }
```
and then publishes PSL parse diagnostics for the schema files declared
in `prisma-next.config.ts`, at the exact source ranges the parser
reports — the same diagnostics `format` / `contract emit` would surface,
now live at the cursor instead of only at build time.
## The decision
This PR ships the first piece of Prisma Next LSP support. It carries
three things:
1. **A new `prisma-next lsp` subcommand and a new
`@prisma-next/language-server` package.** The subcommand is a thin entry
point (`createLspCommand`) that delegates to the package, which owns the
LSP connection, lifecycle, document sync, and diagnostics. `lsp` is a
CLI subcommand — version-matched to the project's own `@prisma-next` by
construction, like `deno lsp` — not a separate binary.
2. **A new `@prisma-next/config-loader` package** that owns loading
`prisma-next.config.ts`. This is where config loading now lives for both
the CLI and the language server, replacing the loader that previously
lived inside `@prisma-next/cli`.
3. **Diagnostics sourced from the CST `parse`**, not the legacy
`parsePslDocument` (which is slated for removal independently).
One server runs per project. Schema documents are identified from config
(a document is a schema input when its path is one of the resolved
`contract.source.inputs`). The server watches `prisma-next.config.ts`
and re-resolves on change, so the input set stays live without a
restart.
## How config loading is organised now
Before this PR, loading `prisma-next.config.ts` lived inside
`@prisma-next/cli` (`config-loader.ts` + `config-path-validation.ts`),
so a language-server package could not reuse it without importing from
the CLI — which the dependency rules forbid. This PR moves that loading
into its own package and removes the CLI's private copy:
1. **`@prisma-next/config-loader` exposes a single
`loadConfig(configPath?)`.** It runs `c12` to execute the config file,
validates it, resolves `contract.source.inputs` / `contract.output` to
absolute paths, and applies the emitter output/input collision check (it
imports `getEmittedArtifactPaths` from `@prisma-next/emitter` itself).
On failure it throws the CLI's structured `@prisma-next/errors/control`
errors — the same errors, with the same codes, the CLI produced before.
See
[`config-loader/src/load.ts`](packages/1-framework/3-tooling/config-loader/src/load.ts).
2. **The CLI's `config-loader.ts` wrapper is deleted.** Its ~17 source
consumers (`contract-emit`, `db-sign`, `db-verify`,
`inspect-live-schema`, `migrate`, the `migration-*` commands, …) now
import `loadConfig` from `@prisma-next/config-loader` directly — same
function name, same behaviour, same error codes.
`cli/src/config-path-validation.ts` is deleted; its path-resolution
logic moved alongside the loader.
3. **`@prisma-next/config` goes back to types + validation +
path-finalising helpers** (`finalize-config.ts`, `errors.ts`,
`config-types.ts`, `config-validation.ts`); it no longer loads config
and no longer depends on `c12`.
## How the language server works
1. **`initialize`** — resolves the project root from the client's
`rootUri`/`rootPath`, loads `prisma-next.config.ts` via
`@prisma-next/config-loader`'s `loadConfig`, and records which absolute
paths are schema inputs. A missing or invalid config degrades
gracefully: the server still initializes with an empty input set and
logs a warning. Config is resolved once here.
2. **Reading config errors** — `loadConfig` throws structured
`CliStructuredError`s, so the server's
[`config-resolution.ts`](packages/1-framework/3-tooling/language-server/src/config-resolution.ts)
branches on the stable error `code` (`4001` = config file not found,
`4009` = config validation) to decide whether to degrade; any other code
is re-thrown as a genuine failure. The CLI error codes are unchanged by
this PR.
3. **Document sync** — incremental (`TextDocumentSyncKind.Incremental`);
the `TextDocuments` manager applies the client's deltas and the server
re-parses the full current buffer on each change.
4. **Diagnostics** — on `didOpen` / `didChange` of a document whose URI
is a configured input, the server runs `@prisma-next/psl-parser`'s
`parse()` and publishes the mapped diagnostics. A clean parse publishes
an empty array (clearing markers); a non-input document publishes
nothing.
5. **Live config** — on a `workspace/didChangeWatchedFiles` event for
the config path, the server re-resolves the input set and re-publishes
across open documents, so adding/removing an input takes effect without
a restart. Works whether or not the config file is open in the editor.
The `ParseDiagnostic → LSP diagnostic` mapping in
[`diagnostic-mapping.ts`](packages/1-framework/3-tooling/language-server/src/diagnostic-mapping.ts)
is a pure transform with no `vscode-languageserver` import: the parser
already emits zero-based, LSP-shaped ranges, so ranges pass through
unchanged and the connection layer adapts severity at the boundary.
## Behavior changes & evidence
- **New `prisma-next lsp` command** launches a PSL language server over
stdio. Impl:
[`cli/src/commands/lsp.ts`](packages/1-framework/3-tooling/cli/src/commands/lsp.ts),
[`language-server/src/start-server.ts`](packages/1-framework/3-tooling/language-server/src/start-server.ts).
Evidence:
[`cli/test/commands/lsp.test.ts`](packages/1-framework/3-tooling/cli/test/commands/lsp.test.ts)
and the live handshake (see Testing performed).
- **PSL diagnostics for open schema documents**, config-gated and
sourced from the parser. Impl:
[`language-server/src/server.ts`](packages/1-framework/3-tooling/language-server/src/server.ts),
[`language-server/src/document-diagnostics.ts`](packages/1-framework/3-tooling/language-server/src/document-diagnostics.ts),
[`language-server/src/schema-inputs.ts`](packages/1-framework/3-tooling/language-server/src/schema-inputs.ts).
Evidence:
[`test/server.test.ts`](packages/1-framework/3-tooling/language-server/test/server.test.ts),
[`test/document-diagnostics.test.ts`](packages/1-framework/3-tooling/language-server/test/document-diagnostics.test.ts).
- **Live re-resolution on config change**, without restart. Impl:
[`language-server/src/server.ts`](packages/1-framework/3-tooling/language-server/src/server.ts),
[`language-server/src/config-resolution.ts`](packages/1-framework/3-tooling/language-server/src/config-resolution.ts).
Evidence: the config-watching cases in
[`test/server.test.ts`](packages/1-framework/3-tooling/language-server/test/server.test.ts),
[`test/config-resolution.test.ts`](packages/1-framework/3-tooling/language-server/test/config-resolution.test.ts).
- **Config loading moved to `@prisma-next/config-loader`**; CLI
consumers re-pointed. Impl:
[`config-loader/src/load.ts`](packages/1-framework/3-tooling/config-loader/src/load.ts),
CLI wrapper + `config-path-validation.ts` deleted. Evidence:
[`config-loader/test/load.test.ts`](packages/1-framework/3-tooling/config-loader/test/load.test.ts),
[`config-loader/test/to-structured-config-error.test.ts`](packages/1-framework/3-tooling/config-loader/test/to-structured-config-error.test.ts),
and the unchanged CLI/integration suites.
## Notes for the reviewer
- **`@prisma-next/config-loader` is the one substantive new package
besides the language server.** It sits at the tooling layer (peer of the
CLI and emitter), which is why it may import `@prisma-next/emitter`,
`@prisma-next/errors`, and `@prisma-next/config` directly. `loadConfig`
keeps the CLI's existing error codes (`PN-CLI-4001`/`4009`/…), so
nothing user-facing changes for CLI consumers.
- **The language server reads those error codes** (`4001`/`4009`) to
decide graceful degradation rather than catching typed error classes.
The codes are stable, documented constants (asserted in
`@prisma-next/errors` tests), so this coupling is to a public contract,
not an internal detail.
- **`@prisma-next/config` lost its `c12` dependency** and its
config-loading exports; it is back to types + validation +
path-finalising. The path-finalising still takes the emitter collision
check as a hook so `@prisma-next/config` keeps no edge to the emitter.
- **`psl-parser` barrel gains a `parse` export.** The CST `parse` wasn't
exported from any public barrel (only `parsePslDocument` was);
[`psl-parser/src/exports/parser.ts`](packages/1-framework/2-authoring/psl-parser/src/exports/parser.ts)
now exports it plus its `ParseDiagnostic`/`Range` types.
`parsePslDocument` is untouched.
- **Pre-existing CLI test flakes, NOT from this PR.**
`cli/test/version.test.ts`, `removed-verb-redirects.test.ts`, and
`migration-list-json-golden` fail on a hard-coded 500ms CLI-spawn
timeout (a cold spawn measures ~1.2s on a dev machine); they fail
identically on clean `main` with this branch's changes stashed. There is
also a committed bare-`as` lint item in
`cli/src/commands/init/hygiene-package-scripts.ts` that predates and is
untouched here. If CI's runner is slow, those spawn tests may need a
longer timeout — flagging so the red isn't mis-attributed.
- **Project artefacts on disk.** `projects/lsp-scaffold/{spec,plan}.md`
are included for review and removed at project close-out (the ADR
recording the "version-matched CLI subcommand, one server per project"
decision lands in `docs/` then).
## Compatibility / migration / risk
- **No public API or behaviour change for existing CLI commands.**
Config loading moved packages and the CLI wrapper was deleted, but
`loadConfig`'s signature, behaviour, and thrown error codes are
preserved. The additive public surface is the new `lsp` command, the
`@prisma-next/config-loader` package, and the new
`@prisma-next/psl-parser/parser` `parse` export.
## Testing performed
Run on final HEAD:
- `pnpm install --frozen-lockfile` — clean (lockfile and manifests
agree).
- `pnpm lint:deps` — clean (no dependency violations; the new package's
imports are legal).
- `pnpm --filter @prisma-next/config-loader test` — passes.
- `pnpm --filter @prisma-next/language-server test` — passes (incl.
config-watching + graceful-degradation cases).
- `pnpm --filter @prisma-next/config test` — passes.
- `pnpm --filter @prisma-next/psl-parser test` — passes.
- `pnpm --filter @prisma-next/integration-tests test cli.db-verify` —
passes (CLI structured-error behaviour preserved end-to-end).
- `pnpm --filter @prisma-next/cli typecheck && build` — clean; `pnpm
--filter prisma-next lint` (facade dependency sync) — clean.
- Manual: scripted LSP `initialize` handshake against the built CLI
(`prisma-next lsp --stdio`) returns `textDocumentSync: 2`.
- Ruled pre-existing (reproduced on clean `main`): the CLI 500ms
spawn-timeout flakes noted above.
## Skill update
n/a — internal foundation work. No user-facing skill surface changes
here; the `prisma-next lsp` command is documented in the package README
and the project spec. A user-facing skill / docs update for the LSP will
follow with the editor-extension work.
## Checklist
- [x] All commits are signed off (`git commit -s`).
- [x] I read CONTRIBUTING.md and the change is scoped to one logical
concern.
- [x] Tests are updated.
- [x] The PR title is in `TML-NNNN: <sentence-case title>` form.
- [x] The **Skill update** section is filled in.
## Alternatives considered
- **Separate `prisma-next-lsp` binary** instead of a subcommand —
rejected: a subcommand is version-matched to the project's own
`@prisma-next` by construction and avoids a second package to keep
pinned together. (`deno lsp` is the precedent.)
- **Server inline in `@prisma-next/cli`** instead of its own package —
rejected: a dedicated package keeps `vscode-languageserver`'s footprint
off the core CLI surface and gives later LSP features a clean home.
- **Keeping config loading in `@prisma-next/cli` (or moving it into
`@prisma-next/config`)** — rejected: the CLI can't be imported by the
language server (dependency rules), and `@prisma-next/config` sits below
the tooling layer, so it can't reach the emitter or the CLI error
factories the loader needs. A dedicated tooling-layer
`@prisma-next/config-loader` package is the home that lets both the CLI
and the language server share one `loadConfig`.
- **Two load functions (structured errors for the CLI, typed errors for
the language server)** — rejected: one `loadConfig` throwing structured
errors, with the language server reading the stable error codes, keeps a
single public entry point and avoids duplicating the loader's surface.
- **Diagnose every configured input eagerly / watch schema files** — out
of scope: diagnostics are parse-only, so a closed input's content has no
bearing on any open document; only the config file is watched. Eager
whole-schema diagnosis is a nameable later capability.
- **Source diagnostics from `parsePslDocument`** — rejected: it's legacy
and slated for removal; the CST `parse` is what `format` already
consumes, so the language server and the formatter agree on what a
diagnostic is.
---------
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>1 parent c278ee2 commit a049b56
109 files changed
Lines changed: 2757 additions & 633 deletions
File tree
- packages/1-framework
- 1-core/config
- test
- 2-authoring/psl-parser
- src/exports
- 3-tooling
- cli
- src
- commands
- control-api/operations
- utils
- test
- commands
- control-api
- config-loader
- src
- exports
- test
- language-server
- src
- exports
- test
- prisma-next
- vite-plugin-contract-emit
- src
- test
- test/integration
- test
- authoring
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
117 | 123 | | |
118 | 124 | | |
119 | 125 | | |
120 | 126 | | |
121 | 127 | | |
122 | 128 | | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
123 | 135 | | |
124 | 136 | | |
125 | 137 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
58 | | - | |
| 57 | + | |
| 58 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
48 | 63 | | |
49 | 64 | | |
50 | 65 | | |
| |||
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
1 | 3 | | |
| 4 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
1470 | 1470 | | |
1471 | 1471 | | |
1472 | 1472 | | |
1473 | | - | |
| 1473 | + | |
1474 | 1474 | | |
1475 | 1475 | | |
1476 | 1476 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| 30 | + | |
29 | 31 | | |
30 | 32 | | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
35 | | - | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
157 | 158 | | |
158 | 159 | | |
159 | 160 | | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | 161 | | |
165 | 162 | | |
166 | 163 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
312 | 313 | | |
313 | 314 | | |
314 | 315 | | |
| 316 | + | |
315 | 317 | | |
316 | 318 | | |
317 | 319 | | |
| |||
321 | 323 | | |
322 | 324 | | |
323 | 325 | | |
| 326 | + | |
324 | 327 | | |
325 | 328 | | |
326 | 329 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
1 | 2 | | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
7 | | - | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
0 commit comments