Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
110 changes: 110 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,110 @@
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.`,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
}
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