diff --git a/build/esbuild/build.ts b/build/esbuild/build.ts index 4a46d57b0..576120536 100644 --- a/build/esbuild/build.ts +++ b/build/esbuild/build.ts @@ -483,7 +483,8 @@ async function buildAll() { copyZeroMQ(), copyZeroMQOld(), copyNodeGypBuild(), - buildVSCodeJsonRPC() + buildVSCodeJsonRPC(), + buildSqlLanguageServer() ); } @@ -540,6 +541,84 @@ async function copyNodeGypBuild() { await fs.copy(source, target, { recursive: true }); } +async function buildSqlLanguageServer() { + // Bundle the sql-language-server with all its dependencies into a single file + const entryPoint = path.join( + extensionFolder, + 'node_modules', + '@deepnote', + 'sql-language-server', + 'dist', + 'bin', + 'vscodeExtensionServer.js' + ); + const outfile = path.join(extensionFolder, 'dist', 'sqlLanguageServer.cjs'); + + await esbuild.build({ + entryPoints: [entryPoint], + bundle: true, + platform: 'node', + target: 'node18', + outfile, + format: 'cjs', + external: [ + // These are optional database drivers - exclude to reduce bundle size + // They will be loaded dynamically if available + 'sqlite3', + 'mysql2', + 'pg', + 'pg-native', + '@google-cloud/bigquery', + // SSH tunneling dependencies with native modules - must be copied separately + 'ssh2', + 'cpu-features', + 'node-ssh-forward' + ], + minify: false, + sourcemap: false + }); + + // Copy ALL node_modules that the sql-language-server needs + // This includes the full transitive dependency tree for: + // - node-ssh-forward (SSH tunneling) + // - mysql2, pg, sqlite3 (database drivers) + // Instead of manually tracking dependencies, we copy all required packages + const sqlLspNodeModules = path.join(extensionFolder, 'dist', 'sql-lsp-modules'); + await fs.ensureDir(sqlLspNodeModules); + + // Create a minimal package.json and install dependencies + const packageJson = { + name: 'sql-lsp-deps', + version: '1.0.0', + dependencies: { + 'node-ssh-forward': '^0.6.3', + mysql2: '^3.9.8', + pg: '^8.9.0', + sqlite3: '^5.0.3', + '@google-cloud/bigquery': '^8.1.1' + } + }; + + const packageJsonPath = path.join(sqlLspNodeModules, 'package.json'); + await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); + + // Run npm install in the sql-lsp-modules directory + const { execSync } = require('child_process'); + + try { + execSync('npm install --omit=dev --ignore-scripts', { + cwd: sqlLspNodeModules, + stdio: 'inherit' + }); + } catch (error) { + console.error('Failed to install sql-lsp dependencies:', error); + throw error; + } + + // Keep package.json for debugging/audit purposes, remove only lock file + await fs.remove(path.join(sqlLspNodeModules, 'package-lock.json')); +} + async function buildVSCodeJsonRPC() { const source = path.join(extensionFolder, 'node_modules', 'vscode-jsonrpc'); const target = path.join(extensionFolder, 'dist', 'node_modules', 'vscode-jsonrpc', 'index.js'); diff --git a/src/kernels/deepnote/deepnoteLspClientManager.node.ts b/src/kernels/deepnote/deepnoteLspClientManager.node.ts index fe52e5a76..227e29bda 100644 --- a/src/kernels/deepnote/deepnoteLspClientManager.node.ts +++ b/src/kernels/deepnote/deepnoteLspClientManager.node.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import * as vscode from 'vscode'; import { CancellationError } from 'vscode'; import { inject, injectable } from 'inversify'; @@ -381,12 +382,20 @@ export class DeepnoteLspClientManager logger.info(`Starting SQL LSP with ${connections.length} database connection(s)`); // Use IPC transport - must match the server's hardcoded 'node-ipc' method + // Set NODE_PATH to include the sql-lsp-modules directory for runtime dependencies + const sqlLspModulesPath = this.getSqlLspModulesPath(); + const nodePathEnv = sqlLspModulesPath ? { NODE_PATH: sqlLspModulesPath } : {}; + const serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, + run: { + module: serverModule, + transport: TransportKind.ipc, + options: { env: { ...process.env, ...nodePathEnv } } + }, debug: { module: serverModule, transport: TransportKind.ipc, - options: { execArgv: ['--nolazy', '--inspect=6009'] } + options: { execArgv: ['--nolazy', '--inspect=6009'], env: { ...process.env, ...nodePathEnv } } } }; @@ -532,7 +541,7 @@ export class DeepnoteLspClientManager * @returns Path to the vscodeExtensionServer.js module for IPC transport */ private getSqlLanguageServerModule(): string { - // Try require.resolve first - this handles different package layouts + // Try require.resolve first - this handles different package layouts (works in dev mode) try { const serverModule = require.resolve('@deepnote/sql-language-server/dist/bin/vscodeExtensionServer.js'); @@ -543,7 +552,8 @@ export class DeepnoteLspClientManager logger.trace('require.resolve failed, falling back to path construction:', error); } - // Fallback: use extension path construction + // Fallback: use extension path construction (works in packaged extension) + // The sql-language-server is bundled into dist/sqlLanguageServer.cjs during build let extensionPath = vscode.extensions.getExtension('Deepnote.vscode-deepnote')?.extensionPath; if (!extensionPath) { @@ -552,20 +562,33 @@ export class DeepnoteLspClientManager logger.trace('Using __dirname to find extension path:', extensionPath); } - const serverModule = path.join( - extensionPath, - 'node_modules', - '@deepnote', - 'sql-language-server', - 'dist', - 'bin', - 'vscodeExtensionServer.js' - ); + const serverModule = path.join(extensionPath, 'dist', 'sqlLanguageServer.cjs'); logger.trace('SQL LSP server module (fallback):', serverModule); return serverModule; } + /** + * Get the path to the sql-lsp-modules directory containing runtime dependencies + * @returns Path to the node_modules directory for SQL LSP, or undefined if not found + */ + private getSqlLspModulesPath(): string | undefined { + let extensionPath = vscode.extensions.getExtension('Deepnote.vscode-deepnote')?.extensionPath; + + if (!extensionPath) { + extensionPath = path.join(__dirname, '..', '..', '..'); + } + + const modulesPath = path.join(extensionPath, 'dist', 'sql-lsp-modules', 'node_modules'); + + // Return undefined if the directory doesn't exist + if (!fs.existsSync(modulesPath)) { + return undefined; + } + + return modulesPath; + } + /** * Get SQL connections configuration from integration storage for the current project. * Only returns integrations that are configured for the specific project.