diff --git a/.github/workflows/release-hana-cli.yml b/.github/workflows/release-hana-cli.yml index 1729033e..f71b5368 100644 --- a/.github/workflows/release-hana-cli.yml +++ b/.github/workflows/release-hana-cli.yml @@ -168,6 +168,18 @@ jobs: - name: Install dependencies run: npm ci + - name: Build VS Code extension .vsix + run: | + # Install sub-project deps (root npm ci does not cover these) and + # build a fresh, self-contained .vsix that ships inside the npm + # package for `hana-cli vscode install`. Installing app/vue deps here + # also satisfies the root prepack step (build:vue) run by npm publish. + rm -f vscode-extension/*.vsix + npm ci --prefix app/vue + npm ci --prefix vscode-extension + npm run build:vscode + cd vscode-extension && npx vsce package --no-dependencies + - name: Verify package version matches tag run: | PKG_VERSION=$(node -e "import fs from 'fs'; console.log(JSON.parse(fs.readFileSync('package.json','utf8')).version)") diff --git a/docs/03-features/vscode-extension.md b/docs/03-features/vscode-extension.md index c453326e..f10653bd 100644 --- a/docs/03-features/vscode-extension.md +++ b/docs/03-features/vscode-extension.md @@ -107,11 +107,13 @@ The extension uses a **Hybrid Direct Webview + Embedded Server** architecture: ### From the CLI +Once `hana-cli` is installed (globally via `npm install -g hana-cli`, or in a project), a prebuilt `.vsix` ships inside the npm package, so `install` works offline with no build step: + ```bash # Check if the extension is installed hana-cli vscode status -# Install from a local .vsix package +# Install from the bundled .vsix package hana-cli vscode install # Install for VS Code Insiders @@ -121,6 +123,8 @@ hana-cli vscode install --insiders hana-cli vscode uninstall ``` +The bundled `.vsix` is self-contained and OS-portable — it includes the web UI assets and uses Node's built-in `node:sqlite` driver, so there is no platform-specific native binary and no per-OS rebuild. + ### From VS Code 1. Open the Extensions panel (Ctrl+Shift+X) @@ -129,16 +133,24 @@ hana-cli vscode uninstall ### From Source (Development) +::: warning Install the parent project first +The extension's bundle step reaches into the **root** hana-cli project's `node_modules`, `routes/`, and `utils/` (they are inlined into the extension). You must install the root project's dependencies **before** building the extension, or esbuild fails with dozens of `Could not resolve "express" / "exceljs" / "@sap/cds" …` errors. +::: + ```bash -# Build the extension -cd vscode-extension -npm install -npm run bundle +# 1. From the repo ROOT — install parent deps the bundle inlines +npm ci + +# 2. Build the Vue web UI for the webview + bundle the extension. +# This copies the UI assets into vscode-extension/webview-dist so the +# packaged .vsix is self-contained. +npm run build:vscode -# Package as .vsix -npx vsce package +# 3. Package as .vsix +cd vscode-extension +npx vsce package --no-dependencies -# Install the generated .vsix +# 4. Install the generated .vsix code --install-extension hana-cli-0.1.0.vsix ``` diff --git a/utils/database/index.js b/utils/database/index.js index 4903f9ee..852b51cd 100644 --- a/utils/database/index.js +++ b/utils/database/index.js @@ -73,6 +73,15 @@ export default class dbClientClass { throw new Error(base.bundle.getText("error.cdsProjectMissing")) } if (optionsCDS.kind === 'sqlite') { //SQLite CDS + // Prefer Node's built-in node:sqlite driver over the native + // better-sqlite3 binding. This avoids a platform-specific + // native .node binary (portable VS Code .vsix, no per-OS + // rebuild) and matches the CAP 10 default. Node >=22 (our + // engines baseline) always ships node:sqlite. An explicit + // driver in the project/credentials config still wins. + if (!optionsCDS.driver && !optionsCDS.credentials?.driver) { + optionsCDS.driver = 'node' + } // Load actual SQLite credentials and merge them into optionsCDS if needed const conn = await import("../connections.js") const credentials = await conn.getConnOptions(prompts) diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore index fd956fb2..b1cc2c1d 100644 --- a/vscode-extension/.gitignore +++ b/vscode-extension/.gitignore @@ -1,4 +1,5 @@ out/ dist/ +webview-dist/ _i18n/ *.vsix diff --git a/vscode-extension/.npmignore b/vscode-extension/.npmignore new file mode 100644 index 00000000..ed38801a --- /dev/null +++ b/vscode-extension/.npmignore @@ -0,0 +1,22 @@ +# npm packaging rules for the vscode-extension folder. +# +# npm uses this file INSTEAD of .gitignore for this directory when deciding +# what to include in the published hana-cli npm tarball. The prebuilt *.vsix +# is git-ignored (build artifact) but MUST ship in the npm package so +# `hana-cli vscode install` can find and install it offline. +# +# Ship: *.vsix and package.json. Exclude everything else in this folder — +# consumers of the npm package never build the extension from source. +src/ +out/ +test/ +dist/ +webview-dist/ +node_modules/ +_i18n/ +.vscode/ +esbuild.config.mjs +tsconfig.json +tsconfig.test.json +package-lock.json +.gitignore diff --git a/vscode-extension/esbuild.config.mjs b/vscode-extension/esbuild.config.mjs index dad2e5e4..0d87351a 100644 --- a/vscode-extension/esbuild.config.mjs +++ b/vscode-extension/esbuild.config.mjs @@ -7,6 +7,29 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const production = process.argv.includes('--production') const projectRoot = path.resolve(__dirname, '..') +// Copy the built Vue webview assets into the extension so a packaged .vsix is +// self-contained. At runtime htmlProvider prefers vscode-extension/webview-dist +// and only falls back to ../app/vue/dist-vscode when running from source (F5). +// Without this copy, an installed .vsix would resolve to VS Code's extensions +// dir where the sibling app/ tree does not exist → blank webviews. +function copyWebviewAssets() { + const src = path.resolve(projectRoot, 'app', 'vue', 'dist-vscode') + const dest = path.resolve(__dirname, 'webview-dist') + if (!fs.existsSync(path.join(src, 'assets', 'index.js'))) { + console.warn( + `[esbuild] Vue webview assets not found at ${src}.\n` + + ` Run "npm run build:vscode" from the project root first, ` + + `or the packaged .vsix will have no UI.` + ) + return + } + fs.rmSync(dest, { recursive: true, force: true }) + fs.cpSync(src, dest, { recursive: true }) + console.log(`[esbuild] Copied Vue webview assets → ${dest}`) +} + +copyWebviewAssets() + // The route modules in src/server/routes.ts import from '../../routes/*.js' // which TypeScript preserves as-is (@ts-ignore). From the compiled location // (out/server/), these paths don't resolve correctly. This plugin redirects @@ -142,7 +165,18 @@ await esbuild.build({ 'vscode', // Optional/dynamic dependencies from the parent project's node_modules // that are not needed at runtime in the VS Code extension context + // + // sqlite3 is optionally require()d by @sap/cds (lib/srv/middlewares/trace.js) + // but never installed here; keep it external so esbuild leaves the guarded + // require in place instead of failing the build. 'sqlite3', + // better-sqlite3 is a native (.node) module. We force @cap-js/sqlite to use + // Node's built-in node:sqlite driver (see utils/database/index.js), so the + // better-sqlite3 require path is never executed at runtime. Marking it + // external keeps its platform-specific binary out of the bundle, making the + // .vsix OS-portable (no per-OS rebuild). node:sqlite is a Node builtin and + // is external automatically. + 'better-sqlite3', '@cap-js/cds-test', '@sap-cloud-sdk/connectivity', '@sap-cloud-sdk/http-client', diff --git a/vscode-extension/src/webview/htmlProvider.ts b/vscode-extension/src/webview/htmlProvider.ts index 3d46fbf6..6593801d 100644 --- a/vscode-extension/src/webview/htmlProvider.ts +++ b/vscode-extension/src/webview/htmlProvider.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode' import * as crypto from 'crypto' +import * as fs from 'fs' interface WebviewContentOptions { route?: string @@ -7,6 +8,25 @@ interface WebviewContentOptions { chromeless?: boolean } +/** + * Resolve the directory containing the built Vue webview assets. + * + * A packaged .vsix ships the assets inside the extension itself at + * `webview-dist/` (copied there by the bundle step). When running from source + * via F5 (--extensionDevelopmentPath), that folder may not exist yet, so we + * fall back to the sibling `../app/vue/dist-vscode` build output in the repo. + * + * @param extensionUri - The extension's root URI + * @returns URI of the directory holding `assets/index.js` and `assets/index.css` + */ +function resolveWebviewDist(extensionUri: vscode.Uri): vscode.Uri { + const bundled = vscode.Uri.joinPath(extensionUri, 'webview-dist') + if (fs.existsSync(vscode.Uri.joinPath(bundled, 'assets', 'index.js').fsPath)) { + return bundled + } + return vscode.Uri.joinPath(extensionUri, '..', 'app', 'vue', 'dist-vscode') +} + export function getWebviewContent( webview: vscode.Webview, extensionUri: vscode.Uri, @@ -14,7 +34,7 @@ export function getWebviewContent( ): string { const nonce = crypto.randomBytes(16).toString('hex') - const distPath = vscode.Uri.joinPath(extensionUri, '..', 'app', 'vue', 'dist-vscode') + const distPath = resolveWebviewDist(extensionUri) const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(distPath, 'assets', 'index.js')) const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(distPath, 'assets', 'index.css')) @@ -72,7 +92,7 @@ export function getWebviewContent( } export function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions { - const distPath = vscode.Uri.joinPath(extensionUri, '..', 'app', 'vue', 'dist-vscode') + const distPath = resolveWebviewDist(extensionUri) return { enableScripts: true, localResourceRoots: [distPath]