Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"hono": "4.7.10",
"minimatch": "6.2.0",
"openapi-fetch": "0.14.0",
"package-manager-detector": "1.8.0",
"postgres": "3.4.7",
"prisma-6-language-server": "npm:@prisma/language-server@6.19.0-hotfix.1",
"vscode-languageclient": "7.0.0",
Expand Down
108 changes: 108 additions & 0 deletions packages/vscode/src/plugins/prisma-language-server/installPrismaCli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { randomUUID } from 'node:crypto'
import { detect } from 'package-manager-detector/detect'
import { resolveCommand } from 'package-manager-detector/commands'
import { commands, tasks, Task, ShellExecution, TaskRevealKind, window, workspace, type WorkspaceFolder } from 'vscode'

const supportedAgents = ['npm', 'pnpm', 'pnpm@6', 'yarn', 'yarn@berry', 'bun'] as const

type SupportedAgent = (typeof supportedAgents)[number]

function isSupportedAgent(agent: string): agent is SupportedAgent {
return supportedAgents.some((supported) => supported === agent)
}

async function runInstallTask(folder: WorkspaceFolder, command: string, args: string[]): Promise<number | undefined> {
const installId = randomUUID()
const task = new Task(
{ type: 'prisma-cli-install', installId },
folder,
'Install Prisma ORM 8 CLI',
'Prisma',
new ShellExecution(command, args, { cwd: folder.uri.fsPath }),
[],
)
task.presentationOptions = { reveal: TaskRevealKind.Always, focus: true }

let complete!: (exitCode: number | undefined) => void
const completion = new Promise<number | undefined>((resolve) => {
complete = resolve
})
// Subscribe before launching: a short-lived process may exit before executeTask resolves.
const processEnd = tasks.onDidEndTaskProcess((event) => {
if (event.execution.task.definition.installId === installId) complete(event.exitCode)
})
const taskEnd = tasks.onDidEndTask((event) => {
// Covers cancellation or a task that never started a process. A process-end event, when
// present, precedes this event and has already settled completion with the exit code.
if (event.execution.task.definition.installId === installId) complete(undefined)
})
try {
await tasks.executeTask(task)
return await completion
} finally {
processEnd.dispose()
taskEnd.dispose()
}
}

export async function installPrismaCli(folder: WorkspaceFolder, isDisposed: () => boolean): Promise<void> {
const canInstall = () => !isDisposed() && workspace.isTrusted && folder.uri.scheme === 'file'
if (!canInstall()) return

try {
const detected = await detect({
cwd: folder.uri.fsPath,
stopDir: folder.uri.fsPath,
strategies: ['packageManager-field', 'install-metadata', 'lockfile'],
})
if (!canInstall()) return

let agent = detected?.agent
if (!agent || !isSupportedAgent(agent)) {
const selected = await window.showQuickPick(
[
{ label: 'npm', agent: 'npm' as const },
{ label: 'pnpm', agent: 'pnpm' as const },
{ label: 'Yarn Classic', agent: 'yarn' as const },
{ label: 'Yarn (2 or later)', agent: 'yarn@berry' as const },
{ label: 'Bun', agent: 'bun' as const },
],
{ placeHolder: `Choose a package manager to install prisma@latest in "${folder.name}"` },
)
if (!selected) return
agent = selected.agent
}

const args = ['-D', 'prisma@latest']
// Installation targets the open root, including monorepo roots protected by these managers.
if (agent === 'pnpm' || agent === 'pnpm@6' || agent === 'yarn') {
args.push('--ignore-workspace-root-check')
}
const command = resolveCommand(agent, 'add', args)
if (!command) throw new Error(`No add command available for ${agent}`)

if (!canInstall()) return
const exitCode = await runInstallTask(folder, command.command, command.args)
if (exitCode !== 0) {
throw new Error(`Prisma CLI install task did not succeed (exit code: ${exitCode ?? 'unknown'})`)
}
} catch (error) {
console.error('Automatic Prisma ORM 8 CLI installation failed', error)
if (canInstall()) {
void window.showErrorMessage(
`Automatic Prisma ORM 8 CLI installation failed in workspace "${folder.name}". Install "prisma@latest" manually.`,
)
}
return
}

if (!canInstall()) return
try {
await commands.executeCommand('prisma.restartLanguageServer')
} catch (error) {
console.error('Prisma ORM 8 language server restart failed', error)
if (canInstall()) {
void window.showErrorMessage(`Prisma ORM 8 CLI was installed, but the language server could not restart.`)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import path from 'node:path'
import { stat } from 'node:fs/promises'
import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process'
import { workspace, type Disposable, type TextDocument, type WorkspaceFolder } from 'vscode'
import { window, workspace, type Disposable, type MessageItem, type TextDocument, type WorkspaceFolder } from 'vscode'
import { CloseAction, ErrorAction, type LanguageClientOptions } from 'vscode-languageclient'
import { LanguageClient, type ChildProcessInfo, type ServerOptions } from 'vscode-languageclient/node'

import { installPrismaCli } from './installPrismaCli'

const prismaCliRelativePath = ['node_modules', 'prisma', 'dist', 'prisma.js'] as const

export type SpawnPrismaNextProcess = (
Expand Down Expand Up @@ -33,6 +35,7 @@ export class PrismaNextClients {

private readonly clients = new Map<string, Promise<LanguageClient | undefined>>()
private readonly failedAt = new Map<string, number>()
private readonly missingCliWarnings = new Set<string>()
private disposed = false

constructor(private readonly registerDisposable: (disposable: Disposable) => void) {}
Expand Down Expand Up @@ -72,6 +75,7 @@ export class PrismaNextClients {
const pending = [...this.clients.values()]
this.clients.clear()
this.failedAt.clear()
this.missingCliWarnings.clear()
await Promise.allSettled(pending.map(async (client) => (await client)?.stop()))
}

Expand All @@ -84,11 +88,22 @@ export class PrismaNextClients {
const entrypoint = getPrismaNextEntrypoint(workspaceFolder)
let client: LanguageClient | undefined
try {
if (!(await isFile(entrypoint)) || this.disposed) return undefined
const cliExists = await isFile(entrypoint)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (this.disposed) return undefined

const key = workspaceFolder.uri.toString()
if (!cliExists) {
if (!this.missingCliWarnings.has(key)) {
this.missingCliWarnings.add(key)
void this.showMissingCliWarning(workspaceFolder)
}
return undefined
}
this.missingCliWarnings.delete(key)

client = new LanguageClient(
`prisma-next:${workspaceFolder.uri.toString()}`,
`Prisma Next Language Server (${workspaceFolder.name})`,
`Prisma ORM 8 Language Server (${workspaceFolder.name})`,
createPrismaNextServerOptions(workspaceFolder, entrypoint, {
handleProcessError: (error) => this.handleError(workspaceFolder, error),
}),
Expand All @@ -114,6 +129,19 @@ export class PrismaNextClients {
}
}

private async showMissingCliWarning(workspaceFolder: WorkspaceFolder): Promise<void> {
const installAction: MessageItem = { title: 'Install prisma@latest' }
const selected = await window.showWarningMessage(
`The Prisma ORM 8 CLI is required for autocomplete, formatting, and error checking in workspace "${workspaceFolder.name}".`,
{ modal: true },
installAction,
{ title: 'Continue without language features', isCloseAffordance: true },
)
if (selected === installAction) {
await installPrismaCli(workspaceFolder, () => this.disposed)
}
}

private handleError(workspaceFolder: WorkspaceFolder, error: unknown): void {
console.error(`Prisma Next Language Server failed for ${workspaceFolder.uri.toString()}`, error)
}
Expand Down
Loading
Loading