From da8dbf814f448aafb8489258d74dd31a81bf8b62 Mon Sep 17 00:00:00 2001 From: Thomas Jung <12159356+jung-thomas@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:53:20 +0200 Subject: [PATCH 1/7] docs: design spec for design-time to runtime name resolution in artifact inspectors --- ...26-07-15-runtime-name-resolution-design.md | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-15-runtime-name-resolution-design.md diff --git a/docs/superpowers/specs/2026-07-15-runtime-name-resolution-design.md b/docs/superpowers/specs/2026-07-15-runtime-name-resolution-design.md new file mode 100644 index 0000000..5a3188e --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-runtime-name-resolution-design.md @@ -0,0 +1,213 @@ +# Design: Design-time → Runtime Name Resolution for Artifact Inspectors + +**Date:** 2026-07-15 +**Component:** VSCode extension (`vscode-extension/`) +**Status:** Approved + +## Problem + +When a user opens an HDI design-time file in a hana-cli custom editor (e.g. +`cds.outbox.Messages.hdbtable`), the artifact inspector injects the file's +**design-time** name into the inspect input field. HANA does not know that +name, so the lookup fails with "Invalid Input Table." + +The design-time name and the runtime object name differ: + +| Design-time (filename) | Runtime (deployed object) | +| ------------------------------ | ------------------------- | +| `cds.outbox.Messages.hdbtable` | `cds_outbox_Messages` | +| `star.wars.X.hdbview` | `star_wars_X` | + +Today, [`artifactInspector.ts`](../../../vscode-extension/src/editors/artifactInspector.ts) +(lines 78–80) strips only the file extension: + +```ts +const filename = path.basename(document.uri.fsPath) +const dotIndex = filename.lastIndexOf('.') +const name = dotIndex > 0 ? filename.substring(0, dotIndex) : filename +``` + +For `cds.outbox.Messages.hdbtable` this yields `cds.outbox.Messages`, which is +then injected verbatim via a route query param +(`/inspect-table?table=cds.outbox.Messages`). + +### Why the lookup fails + +1. **Dot → underscore transformation.** CAP/HDI replaces `.` with `_` in the + deployed object name. The file contains the real name in its DDL, e.g. + `COLUMN TABLE cds_outbox_Messages (...)`. +2. **Case sensitivity.** The backend matches names **exactly and + case-sensitively** — see + [`dbInspect.js`](../../../utils/dbInspect.js) `getTable`, which queries + `SYS.TABLES` with `AND TABLE_NAME = ?` (not `LIKE`, not uppercased). HDI + deploys objects with quoted, mixed-case identifiers + (`cds_outbox_Messages`), so any casing change breaks the match. + +## Goal + +For every connected artifact inspector editor, inject the correct **runtime** +object name — preserving exact case — so the inspect lookup succeeds without +the user editing the field manually. + +## Scope + +**In scope** — the artifact kinds registered in `ARTIFACT_CONFIGS` +([`artifactInspector.ts`](../../../vscode-extension/src/editors/artifactInspector.ts) +lines 14–22): + +| Kind | File pattern(s) | Route | +| --------- | ---------------------------------- | ------------------ | +| table | `.hdbtable`, `.hdbmigrationtable` | `/inspect-table` | +| view | `.hdbview` | `/inspect-view` | +| procedure | `.hdbprocedure` | `/call-procedure` | +| function | `.hdbfunction` | `/inspect-function`| +| synonym | `.hdbsynonym` | `/inspect-table` | +| role | `.hdbrole` | `/inspect-table` | +| sequence | `.hdbsequence` | `/inspect-table` | + +**Out of scope** + +- `calcViewEditor.ts` — the `.hdbcalculationview` editor is a separate provider + with its own XML handling; not touched by this change. +- Server (`routes/`), CLI, and Vue view changes — the resolution is entirely + extension-side. + +## Approach + +A new extension-side resolver module reads the design-time file and extracts +the runtime name from its DDL/content. This is authoritative: it uses the exact +name the deployer wrote, preserving case and any namespace prefix. If parsing +fails, it falls back to a filename-based naming rule. + +**Decisions confirmed during brainstorming:** + +- **Translation source:** parse file content (not a naming rule alone, not a + live DB lookup). +- **Fallback:** naming-rule fallback (strip extension, dots→underscores, apply + `.hdinamespace` prefix) when parsing fails — always inject a best-effort name. +- **Namespace:** handled by the parse path automatically (DDL is already fully + qualified); `.hdinamespace` is only read for the fallback rule. +- **Location:** extension-side resolver module (TypeScript), unit-testable in + the extension test suite; no server round-trip. + +## Component: `resolveRuntimeName` + +New file: `vscode-extension/src/editors/runtimeName.ts` + +```ts +/** + * Resolve the runtime (deployed) object name for an HDI design-time file. + * Reads the file content and extracts the name from its DDL/JSON; falls back + * to a filename-based naming rule if parsing fails. Never throws. + * + * @param fsPath absolute path to the design-time file + * @param kind artifact kind from ArtifactConfig ('table' | 'view' | ...) + * @returns the runtime object name, case preserved + */ +export function resolveRuntimeName(fsPath: string, kind: string): string +``` + +### Per-kind parsers + +Matched by the `kind` value already present on each `ArtifactConfig`. All +parsers operate on the file's text content (read synchronously; these files are +small). + +| Kind | Content type | Extraction rule | +| --------- | ------------ | ------------------------------------------------------ | +| table | DDL | first match of `(?:COLUMN\|ROW)?\s*TABLE\s+` | +| view | DDL | `VIEW\s+\s+AS` | +| function | DDL | `FUNCTION\s+` | +| procedure | DDL | `PROCEDURE\s+` | +| sequence | DDL or JSON | detect: JSON → top-level name key; else `SEQUENCE\s+` | +| synonym | JSON | top-level object key (the synonym's own name) | +| role | JSON | top-level object key (the role's own name) | + +Where `` is either a bare identifier (`cds_outbox_Messages`) or a +double-quoted identifier (`"cds_outbox_Messages"`), optionally schema-qualified +(`schema.name`, `"schema"."name"`). + +### Identifier normalization + +- Strip surrounding double-quotes but **preserve inner case**. +- For **DDL** names that are schema-qualified with a dot (`schema.name`, + `"schema"."name"`), take the **last dot segment** (the object name); the + schema is supplied separately by the inspector (defaults to + `**CURRENT_SCHEMA**`). +- For **JSON** keys (synonym/role/sequence), use the key verbatim after + quote-stripping. A `::` namespace separator is part of the runtime object + name and is **kept** (not split), unlike a dot schema qualifier. +- Do **not** upper/lowercase the result — inject verbatim so it matches the + quoted mixed-case identifier HDI deployed. + +### Fallback rule + +Triggered when: file read fails, content does not match the parser, the kind is +unknown, or the extracted name is empty. + +1. Strip the file extension from the basename. +2. Replace `.` with `_`. +3. If an ancestor `.hdinamespace` file exists with a non-empty `name` field, + prepend `::`. + +This mirrors HDI's own naming transformation and guarantees the field is always +populated with a reasonable guess (never worse than today's behavior). + +## Integration + +In [`artifactInspector.ts`](../../../vscode-extension/src/editors/artifactInspector.ts), +`resolveCustomEditor` replaces the extension-strip logic (lines 77–80) with: + +```ts +const name = resolveRuntimeName(document.uri.fsPath, this._config.kind) +``` + +The remainder of the flow — building `routeWithName`, the webview content, the +message handlers — is unchanged. The resolved name flows through the existing +`queryKey` query param mechanism to the target Vue view. + +## Error handling + +- `resolveRuntimeName` never throws. All failure modes (I/O error, malformed + content, unsupported kind) degrade to the fallback rule. +- No new user-facing error surface; the inspector continues to render, and a + wrong guess is correctable by the user in the field as today. + +## Testing + +Unit tests in `vscode-extension/test/suite/` (e.g. `runtimeName.test.ts`), +using real DDL/JSON fixtures mirroring the samples observed in the +`cloud-cap-hana-swapi` project: + +**Parse-path cases (one per kind):** + +- table: `COLUMN TABLE cds_outbox_Messages (...)` → `cds_outbox_Messages` +- view: `VIEW star_wars_CloneWarsChronologicalOrder AS SELECT ...` → + `star_wars_CloneWarsChronologicalOrder` +- function / procedure / sequence: analogous DDL snippets +- synonym / role: JSON with a single top-level name key + +**Normalization cases:** + +- quoted identifier: `TABLE "cds_outbox_Messages"` → unquoted, case preserved +- schema-qualified: `"MYSCHEMA"."cds_outbox_Messages"` → last segment +- mixed case preserved (no upper/lowercasing) + +**Fallback cases:** + +- empty file → naming rule +- unknown kind → naming rule +- unparseable content → naming rule +- `.hdinamespace` with non-empty `name` → `::` prefix applied +- `.hdinamespace` empty / absent → no prefix + +## Files changed + +| File | Change | +| ------------------------------------------------- | ------------------------------- | +| `vscode-extension/src/editors/runtimeName.ts` | new — resolver + parsers | +| `vscode-extension/src/editors/artifactInspector.ts` | use `resolveRuntimeName` | +| `vscode-extension/test/suite/runtimeName.test.ts` | new — unit tests | + +Per project memory: bump the extension version before repackaging the `.vsix`, +and package via `npm run package` (not `vsce package --no-dependencies`). From 99c09cae212268a86c6feec630c3135e4db167a1 Mon Sep 17 00:00:00 2001 From: Thomas Jung <12159356+jung-thomas@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:57:18 +0200 Subject: [PATCH 2/7] docs: implementation plan for runtime name resolution --- .../2026-07-15-runtime-name-resolution.md | 508 ++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-15-runtime-name-resolution.md diff --git a/docs/superpowers/plans/2026-07-15-runtime-name-resolution.md b/docs/superpowers/plans/2026-07-15-runtime-name-resolution.md new file mode 100644 index 0000000..76cb9bf --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-runtime-name-resolution.md @@ -0,0 +1,508 @@ +# Runtime Name Resolution Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Resolve the correct runtime (deployed) HANA object name from an HDI design-time file so the VSCode extension's artifact inspectors pre-fill a name HANA can actually find. + +**Architecture:** A new pure, synchronous, extension-side resolver (`resolveRuntimeName`) reads the design-time file and extracts the runtime name from its DDL/JSON content (case preserved). On any failure it falls back to a filename naming rule (strip extension → dots→underscores → optional `.hdinamespace` prefix). `artifactInspector.ts` calls it in place of the current extension-strip logic. + +**Tech Stack:** TypeScript (NodeNext ESM), VSCode extension API, Mocha + node `assert`, `@vscode/test-electron`. + +## Global Constraints + +- Language: TypeScript, `module`/`moduleResolution` = **NodeNext** — all relative imports MUST use the `.js` extension (e.g. `import { resolveRuntimeName } from './runtimeName.js'`). +- Target: ES2022. Source lives in `vscode-extension/src/`, compiles to `out/`. +- Tests: Mocha BDD (`suite`/`test`), node `assert`, files named `*.test.ts` under `vscode-extension/test/suite/`. Test harness globs `**/*.test.js` in `out/test/`. +- The resolver MUST be pure and synchronous and MUST NOT throw — every failure path returns the fallback name. +- Names are injected **verbatim** — never upper/lowercase the result (HANA matches case-sensitively). +- Run tests from `vscode-extension/` with: `npm test` (runs `pretest` compile then the electron harness). +- Per project memory: bump the extension version before repackaging the `.vsix`, and package via `npm run package` (never `vsce package --no-dependencies`). Packaging is out of scope for this plan. + +--- + +## File Structure + +- **Create** `vscode-extension/src/editors/runtimeName.ts` — the resolver: public `resolveRuntimeName(fsPath, kind)`, per-kind parsers, identifier normalization, and the filename fallback rule. One responsibility: turn a design-time file into a runtime object name. +- **Modify** `vscode-extension/src/editors/artifactInspector.ts` — replace the extension-strip logic (lines 77–80) with a call to `resolveRuntimeName`. +- **Create** `vscode-extension/test/suite/runtimeName.test.ts` — unit tests for the resolver (parse paths, normalization, fallback). + +--- + +## Task 1: Resolver module with parse + fallback + +**Files:** +- Create: `vscode-extension/src/editors/runtimeName.ts` +- Test: `vscode-extension/test/suite/runtimeName.test.ts` + +**Interfaces:** +- Consumes: node `fs`, `path` only. +- Produces: `export function resolveRuntimeName(fsPath: string, kind: string): string` — returns the runtime object name (case preserved), never throws. `kind` is one of `'table' | 'view' | 'procedure' | 'function' | 'synonym' | 'role' | 'sequence'`; any other value uses the fallback rule. + +- [ ] **Step 1: Write the failing tests** + +Create `vscode-extension/test/suite/runtimeName.test.ts`: + +```ts +import * as assert from 'assert' +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' +import { resolveRuntimeName } from '../../src/editors/runtimeName.js' + +suite('resolveRuntimeName', () => { + let tempDir: string + + suiteSetup(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hana-cli-rtn-')) + }) + + suiteTeardown(() => { + if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true }) + }) + + // Helper: write a fixture file and resolve it + const resolve = (filename: string, content: string, kind: string): string => { + const p = path.join(tempDir, filename) + fs.writeFileSync(p, content) + return resolveRuntimeName(p, kind) + } + + // --- Parse path, one per kind --- + + test('table: COLUMN TABLE', () => { + const name = resolve( + 'cds.outbox.Messages.hdbtable', + 'COLUMN TABLE cds_outbox_Messages (\n ID NVARCHAR(36) NOT NULL,\n PRIMARY KEY(ID)\n)', + 'table' + ) + assert.strictEqual(name, 'cds_outbox_Messages') + }) + + test('table: ROW TABLE', () => { + const name = resolve( + 'my.Row.hdbtable', + 'ROW TABLE my_Row (ID INTEGER)', + 'table' + ) + assert.strictEqual(name, 'my_Row') + }) + + test('view: VIEW ... AS SELECT', () => { + const name = resolve( + 'star.wars.Order.hdbview', + 'VIEW star_wars_CloneWarsChronologicalOrder AS SELECT\n Episode_0.ID\nFROM x', + 'view' + ) + assert.strictEqual(name, 'star_wars_CloneWarsChronologicalOrder') + }) + + test('procedure: PROCEDURE', () => { + const name = resolve( + 'my.Proc.hdbprocedure', + 'PROCEDURE my_Proc (IN a INTEGER) AS BEGIN END', + 'procedure' + ) + assert.strictEqual(name, 'my_Proc') + }) + + test('function: FUNCTION', () => { + const name = resolve( + 'my.Func.hdbfunction', + 'FUNCTION my_Func (IN a INTEGER) RETURNS INTEGER AS BEGIN END', + 'function' + ) + assert.strictEqual(name, 'my_Func') + }) + + test('sequence: DDL SEQUENCE', () => { + const name = resolve( + 'my.Seq.hdbsequence', + 'SEQUENCE my_Seq START WITH 1 INCREMENT BY 1', + 'sequence' + ) + assert.strictEqual(name, 'my_Seq') + }) + + test('sequence: JSON form', () => { + const name = resolve( + 'my.Seq.hdbsequence', + '{ "name": "my_Seq", "start_with": 1 }', + 'sequence' + ) + assert.strictEqual(name, 'my_Seq') + }) + + test('synonym: JSON top-level key', () => { + const name = resolve( + 'my.Syn.hdbsynonym', + '{ "my_Syn": { "target": { "object": "FOO", "schema": "BAR" } } }', + 'synonym' + ) + assert.strictEqual(name, 'my_Syn') + }) + + test('role: JSON top-level key', () => { + const name = resolve( + 'my.Role.hdbrole', + '{ "role": { "name": "my_Role" } }', + 'role' + ) + // top-level key is "role" -> its .name field is the object name + assert.strictEqual(name, 'my_Role') + }) + + // --- Normalization --- + + test('quoted identifier: quotes stripped, case preserved', () => { + const name = resolve( + 'q.hdbtable', + 'COLUMN TABLE "cds_outbox_Messages" (ID INTEGER)', + 'table' + ) + assert.strictEqual(name, 'cds_outbox_Messages') + }) + + test('schema-qualified DDL: last dot segment', () => { + const name = resolve( + 'q.hdbtable', + 'COLUMN TABLE "MYSCHEMA"."cds_outbox_Messages" (ID INTEGER)', + 'table' + ) + assert.strictEqual(name, 'cds_outbox_Messages') + }) + + test('mixed case preserved', () => { + const name = resolve( + 'q.hdbtable', + 'COLUMN TABLE MixedCase_Table (ID INTEGER)', + 'table' + ) + assert.strictEqual(name, 'MixedCase_Table') + }) + + // --- Fallback rule --- + + test('fallback: empty file uses filename naming rule', () => { + const name = resolve('cds.outbox.Messages.hdbtable', '', 'table') + assert.strictEqual(name, 'cds_outbox_Messages') + }) + + test('fallback: unknown kind uses filename naming rule', () => { + const name = resolve('a.b.C.hdbxyz', 'whatever', 'mystery') + assert.strictEqual(name, 'a_b_C') + }) + + test('fallback: unparseable content uses filename naming rule', () => { + const name = resolve('a.b.C.hdbtable', 'not a table definition', 'table') + assert.strictEqual(name, 'a_b_C') + }) + + test('fallback: missing file uses filename naming rule (no throw)', () => { + const p = path.join(tempDir, 'does.not.Exist.hdbtable') + const name = resolveRuntimeName(p, 'table') + assert.strictEqual(name, 'does_not_Exist') + }) + + test('fallback: .hdinamespace name is prepended', () => { + const nsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hana-cli-ns-')) + fs.writeFileSync(path.join(nsDir, '.hdinamespace'), '{ "name": "com.acme", "subfolder": "ignore" }') + const p = path.join(nsDir, 'a.b.hdbtable') + fs.writeFileSync(p, '') // empty -> fallback + const name = resolveRuntimeName(p, 'table') + fs.rmSync(nsDir, { recursive: true, force: true }) + assert.strictEqual(name, 'com.acme::a_b') + }) + + test('fallback: empty .hdinamespace name adds no prefix', () => { + const nsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hana-cli-ns2-')) + fs.writeFileSync(path.join(nsDir, '.hdinamespace'), '{ "name": "", "subfolder": "ignore" }') + const p = path.join(nsDir, 'a.b.hdbtable') + fs.writeFileSync(p, '') + const name = resolveRuntimeName(p, 'table') + fs.rmSync(nsDir, { recursive: true, force: true }) + assert.strictEqual(name, 'a_b') + }) +}) +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd vscode-extension && npm test` +Expected: FAIL — compile/runtime error `Cannot find module '../../src/editors/runtimeName.js'` (module does not exist yet). + +- [ ] **Step 3: Write the resolver implementation** + +Create `vscode-extension/src/editors/runtimeName.ts`: + +```ts +import * as fs from 'fs' +import * as path from 'path' + +/** + * Resolve the runtime (deployed) HANA object name for an HDI design-time file. + * + * Reads the file and extracts the name from its DDL/JSON content (case + * preserved). Falls back to a filename naming rule if the content cannot be + * parsed, the kind is unknown, or the file cannot be read. Never throws. + * + * @param fsPath absolute path to the design-time file + * @param kind artifact kind: 'table' | 'view' | 'procedure' | 'function' | + * 'synonym' | 'role' | 'sequence' + * @returns the runtime object name, verbatim (no case change) + */ +export function resolveRuntimeName(fsPath: string, kind: string): string { + let content: string | undefined + try { + content = fs.readFileSync(fsPath, 'utf8') + } catch { + return fallbackName(fsPath) + } + + const parsed = parseByKind(content, kind) + if (parsed && parsed.trim().length > 0) { + return parsed + } + return fallbackName(fsPath) +} + +/** Dispatch to the parser for the given artifact kind. */ +function parseByKind(content: string, kind: string): string | undefined { + switch (kind) { + case 'table': + return matchDdl(content, /\b(?:COLUMN\s+|ROW\s+)?TABLE\s+([^\s(]+)/i) + case 'view': + return matchDdl(content, /\bVIEW\s+([^\s(]+)\s+AS\b/i) + case 'procedure': + return matchDdl(content, /\bPROCEDURE\s+([^\s(]+)/i) + case 'function': + return matchDdl(content, /\bFUNCTION\s+([^\s(]+)/i) + case 'sequence': + return parseSequence(content) + case 'synonym': + case 'role': + return parseJsonObjectName(content) + default: + return undefined + } +} + +/** Extract and normalize an identifier captured by a DDL regex. */ +function matchDdl(content: string, re: RegExp): string | undefined { + const m = content.match(re) + if (!m) return undefined + return normalizeIdentifier(m[1]) +} + +/** Sequences may be JSON ({ "name": ... }) or DDL (SEQUENCE ). */ +function parseSequence(content: string): string | undefined { + const trimmed = content.trim() + if (trimmed.startsWith('{')) { + try { + const obj = JSON.parse(trimmed) + if (typeof obj.name === 'string') return normalizeIdentifier(obj.name) + // Fall through to top-level key if no explicit name + return parseJsonObjectName(trimmed) + } catch { + return undefined + } + } + return matchDdl(content, /\bSEQUENCE\s+([^\s(]+)/i) +} + +/** + * For synonym/role JSON files: the object's own name is the single top-level + * key. If that key's value carries an explicit `.name`, prefer it. + */ +function parseJsonObjectName(content: string): string | undefined { + try { + const obj = JSON.parse(content) + const keys = Object.keys(obj) + if (keys.length === 0) return undefined + const topKey = keys[0] + const val = obj[topKey] + if (val && typeof val === 'object' && typeof val.name === 'string') { + return normalizeIdentifier(val.name) + } + return normalizeIdentifier(topKey) + } catch { + return undefined + } +} + +/** + * Normalize a raw identifier token: strip surrounding double-quotes, take the + * last dot-qualified segment (object name, not schema), preserve inner case + * and any `::` namespace separator. + */ +function normalizeIdentifier(raw: string): string { + let id = raw.trim().replace(/;$/, '') + // Split on unquoted dots to drop a schema qualifier; keep the last segment. + // Handle both name and "schema"."name" forms. + const segments = id.split('.') + let last = segments[segments.length - 1] + // Strip surrounding double quotes. + last = last.replace(/^"(.*)"$/, '$1') + return last +} + +/** + * Filename naming rule: strip the extension, replace '.' with '_', and prepend + * the nearest ancestor .hdinamespace `name` (as `::`) when non-empty. + */ +function fallbackName(fsPath: string): string { + const base = path.basename(fsPath) + const dot = base.lastIndexOf('.') + const stem = dot > 0 ? base.substring(0, dot) : base + const runtime = stem.replace(/\./g, '_') + const ns = readNamespace(path.dirname(fsPath)) + return ns ? `${ns}::${runtime}` : runtime +} + +/** Walk up from `dir` looking for a .hdinamespace with a non-empty name. */ +function readNamespace(dir: string): string | undefined { + let current = dir + // Bound the walk to avoid infinite loops at filesystem root. + for (let i = 0; i < 50; i++) { + const candidate = path.join(current, '.hdinamespace') + try { + const raw = fs.readFileSync(candidate, 'utf8') + const obj = JSON.parse(raw) + if (typeof obj.name === 'string' && obj.name.length > 0) { + return obj.name + } + return undefined // found the file; empty name means no prefix + } catch { + // not here; keep walking up + } + const parent = path.dirname(current) + if (parent === current) break + current = parent + } + return undefined +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd vscode-extension && npm test` +Expected: PASS — all `resolveRuntimeName` tests green. + +- [ ] **Step 5: Commit** + +```bash +git add vscode-extension/src/editors/runtimeName.ts vscode-extension/test/suite/runtimeName.test.ts +git commit -m "feat(vscode): add resolveRuntimeName for design-time to runtime object names" +``` + +--- + +## Task 2: Wire resolver into the artifact inspector + +**Files:** +- Modify: `vscode-extension/src/editors/artifactInspector.ts:77-80` + +**Interfaces:** +- Consumes: `resolveRuntimeName(fsPath: string, kind: string): string` from Task 1. Each `ArtifactConfig` already carries a `kind` field (`this._config.kind`). +- Produces: no new exports; the injected `name` in `resolveCustomEditor` now holds the runtime name. + +- [ ] **Step 1: Add the import** + +At the top of `vscode-extension/src/editors/artifactInspector.ts`, alongside the existing imports, add: + +```ts +import { resolveRuntimeName } from './runtimeName.js' +``` + +- [ ] **Step 2: Replace the extension-strip logic** + +In `resolveCustomEditor`, replace these lines (currently ~77–80): + +```ts + // Parse artifact name from the filename (strip extension) + const filename = path.basename(document.uri.fsPath) + const dotIndex = filename.lastIndexOf('.') + const name = dotIndex > 0 ? filename.substring(0, dotIndex) : filename +``` + +with: + +```ts + // Resolve the runtime (deployed) object name from the design-time file. + // The filename is the design-time name (e.g. cds.outbox.Messages.hdbtable); + // HANA knows the runtime name (e.g. cds_outbox_Messages), matched + // case-sensitively. resolveRuntimeName reads the file content to get it. + const name = resolveRuntimeName(document.uri.fsPath, this._config.kind) +``` + +- [ ] **Step 3: Verify the `path` import is still used** + +Run: `cd vscode-extension && npx tsc -p tsconfig.test.json --noEmit` +Expected: PASS — no "unused import" or "cannot find name" errors. (`path` remains imported; if the compiler flags it as unused because no other usage exists, remove the `import * as path` line. Check first with: `grep -n "path\." src/editors/artifactInspector.ts` — if only the removed block used it, drop the import.) + +- [ ] **Step 4: Run the full extension test suite** + +Run: `cd vscode-extension && npm test` +Expected: PASS — existing tests plus Task 1 tests all green; no regressions in `calcViewEditor.test.ts` / `extension.test.ts`. + +- [ ] **Step 5: Commit** + +```bash +git add vscode-extension/src/editors/artifactInspector.ts +git commit -m "fix(vscode): inject runtime object name into artifact inspectors" +``` + +--- + +## Task 3: End-to-end verification against a real design-time file + +**Files:** none (verification only). + +- [ ] **Step 1: Confirm the resolver returns the runtime name for the reported case** + +Run from `vscode-extension/`: + +```bash +node --input-type=module -e " +import { resolveRuntimeName } from './out/editors/runtimeName.js'; +const p = 'D:/projects/cloud-cap-hana-swapi/cap/gen/db/src/gen/cds.outbox.Messages.hdbtable'; +console.log(resolveRuntimeName(p, 'table')); +" +``` + +Expected output: `cds_outbox_Messages` + +(If `out/editors/runtimeName.js` does not exist, run `npm run compile` first. On non-Windows or if that path is absent, substitute any local `*.hdbtable` file whose DDL contains a `TABLE` statement and confirm the printed name matches the DDL, not the filename.) + +- [ ] **Step 2: Confirm the view case** + +```bash +node --input-type=module -e " +import { resolveRuntimeName } from './out/editors/runtimeName.js'; +const p = 'D:/projects/cloud-cap-hana-swapi/cap/gen/db/src/gen/star.wars.CloneWarsChronologicalOrder.hdbview'; +console.log(resolveRuntimeName(p, 'view')); +" +``` + +Expected output: `star_wars_CloneWarsChronologicalOrder` + +- [ ] **Step 3: Record verification result** + +No commit. Report both outputs. If either mismatches, return to Task 1 and adjust the relevant parser regex. + +--- + +## Self-Review + +**Spec coverage:** +- Parse file content per kind → Task 1 `parseByKind` + tests (table/view/procedure/function/synonym/role/sequence). ✓ +- Case preserved / injected verbatim → `normalizeIdentifier` does not change case; test "mixed case preserved". ✓ +- Naming-rule fallback (dots→underscores, `.hdinamespace` prefix) → `fallbackName` + `readNamespace`; fallback tests. ✓ +- Namespace handled by parse path; `.hdinamespace` only for fallback → parsers return fully-qualified DDL name; `readNamespace` only called in `fallbackName`. ✓ +- Never throws → try/catch around read, JSON parse, and namespace walk; test "missing file … no throw". ✓ +- Integration point at `artifactInspector.ts:77-80` → Task 2. ✓ +- Scope excludes calc view editor → not modified. ✓ +- Schema-qualified DDL takes last segment; JSON `::` kept → `normalizeIdentifier` splits on `.` only; test "schema-qualified DDL". ✓ + +**Placeholder scan:** No TBD/TODO; all code steps contain full code; commands have expected output. ✓ + +**Type consistency:** `resolveRuntimeName(fsPath: string, kind: string): string` used identically in Task 1 (definition), Task 2 (call), Task 3 (verification). ✓ From 6273395f3ed62c34ea3a16e9c98f4bbc18987000 Mon Sep 17 00:00:00 2001 From: Thomas Jung <12159356+jung-thomas@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:00:59 +0200 Subject: [PATCH 3/7] feat(vscode): add resolveRuntimeName for design-time to runtime object names --- vscode-extension/src/editors/runtimeName.ts | 147 ++++++++++++++ .../test/suite/runtimeName.test.ts | 180 ++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 vscode-extension/src/editors/runtimeName.ts create mode 100644 vscode-extension/test/suite/runtimeName.test.ts diff --git a/vscode-extension/src/editors/runtimeName.ts b/vscode-extension/src/editors/runtimeName.ts new file mode 100644 index 0000000..4788a64 --- /dev/null +++ b/vscode-extension/src/editors/runtimeName.ts @@ -0,0 +1,147 @@ +import * as fs from 'fs' +import * as path from 'path' + +/** + * Resolve the runtime (deployed) HANA object name for an HDI design-time file. + * + * Reads the file and extracts the name from its DDL/JSON content (case + * preserved). Falls back to a filename naming rule if the content cannot be + * parsed, the kind is unknown, or the file cannot be read. Never throws. + * + * @param fsPath absolute path to the design-time file + * @param kind artifact kind: 'table' | 'view' | 'procedure' | 'function' | + * 'synonym' | 'role' | 'sequence' + * @returns the runtime object name, verbatim (no case change) + */ +export function resolveRuntimeName(fsPath: string, kind: string): string { + let content: string | undefined + try { + content = fs.readFileSync(fsPath, 'utf8') + } catch { + return fallbackName(fsPath) + } + + const parsed = parseByKind(content, kind) + if (parsed && parsed.trim().length > 0) { + return parsed + } + return fallbackName(fsPath) +} + +/** Dispatch to the parser for the given artifact kind. */ +function parseByKind(content: string, kind: string): string | undefined { + switch (kind) { + case 'table': + // Anchor to a line start so the word "table" in prose can't false-match. + // Real HDI DDL begins the statement at a line (optionally after comments). + return matchDdl(content, /^\s*(?:COLUMN\s+|ROW\s+)?TABLE\s+([^\s(]+)/im) + case 'view': + return matchDdl(content, /^\s*VIEW\s+([^\s(]+)\s+AS\b/im) + case 'procedure': + return matchDdl(content, /^\s*PROCEDURE\s+([^\s(]+)/im) + case 'function': + return matchDdl(content, /^\s*FUNCTION\s+([^\s(]+)/im) + case 'sequence': + return parseSequence(content) + case 'synonym': + case 'role': + return parseJsonObjectName(content) + default: + return undefined + } +} + +/** Extract and normalize an identifier captured by a DDL regex. */ +function matchDdl(content: string, re: RegExp): string | undefined { + const m = content.match(re) + if (!m) return undefined + return normalizeIdentifier(m[1]) +} + +/** Sequences may be JSON ({ "name": ... }) or DDL (SEQUENCE ). */ +function parseSequence(content: string): string | undefined { + const trimmed = content.trim() + if (trimmed.startsWith('{')) { + try { + const obj = JSON.parse(trimmed) + if (typeof obj.name === 'string') return normalizeIdentifier(obj.name) + // Fall through to top-level key if no explicit name + return parseJsonObjectName(trimmed) + } catch { + return undefined + } + } + return matchDdl(content, /^\s*SEQUENCE\s+([^\s(]+)/im) +} + +/** + * For synonym/role JSON files: the object's own name is the single top-level + * key. If that key's value carries an explicit `.name`, prefer it. + */ +function parseJsonObjectName(content: string): string | undefined { + try { + const obj = JSON.parse(content) + const keys = Object.keys(obj) + if (keys.length === 0) return undefined + const topKey = keys[0] + const val = obj[topKey] + if (val && typeof val === 'object' && typeof val.name === 'string') { + return normalizeIdentifier(val.name) + } + return normalizeIdentifier(topKey) + } catch { + return undefined + } +} + +/** + * Normalize a raw identifier token: strip surrounding double-quotes, take the + * last dot-qualified segment (object name, not schema), preserve inner case + * and any `::` namespace separator. + */ +function normalizeIdentifier(raw: string): string { + const id = raw.trim().replace(/;$/, '') + // Split on dots to drop a schema qualifier; keep the last segment. + // Handles both `name` and `"schema"."name"` forms. + const segments = id.split('.') + let last = segments[segments.length - 1] + // Strip surrounding double quotes. + last = last.replace(/^"(.*)"$/, '$1') + return last +} + +/** + * Filename naming rule: strip the extension, replace '.' with '_', and prepend + * the nearest ancestor .hdinamespace `name` (as `::`) when non-empty. + */ +function fallbackName(fsPath: string): string { + const base = path.basename(fsPath) + const dot = base.lastIndexOf('.') + const stem = dot > 0 ? base.substring(0, dot) : base + const runtime = stem.replace(/\./g, '_') + const ns = readNamespace(path.dirname(fsPath)) + return ns ? `${ns}::${runtime}` : runtime +} + +/** Walk up from `dir` looking for a .hdinamespace with a non-empty name. */ +function readNamespace(dir: string): string | undefined { + let current = dir + // Bound the walk to avoid infinite loops at filesystem root. + for (let i = 0; i < 50; i++) { + const candidate = path.join(current, '.hdinamespace') + try { + const raw = fs.readFileSync(candidate, 'utf8') + const obj = JSON.parse(raw) + if (typeof obj.name === 'string' && obj.name.length > 0) { + return obj.name + } + return undefined // found the file; empty name means no prefix + } catch { + // not here; keep walking up + } + const parent = path.dirname(current) + if (parent === current) break + current = parent + } + return undefined +} diff --git a/vscode-extension/test/suite/runtimeName.test.ts b/vscode-extension/test/suite/runtimeName.test.ts new file mode 100644 index 0000000..eb5f7d0 --- /dev/null +++ b/vscode-extension/test/suite/runtimeName.test.ts @@ -0,0 +1,180 @@ +import * as assert from 'assert' +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' +import { resolveRuntimeName } from '../../src/editors/runtimeName.js' + +suite('resolveRuntimeName', () => { + let tempDir: string + + suiteSetup(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hana-cli-rtn-')) + }) + + suiteTeardown(() => { + if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true }) + }) + + // Helper: write a fixture file and resolve it + const resolve = (filename: string, content: string, kind: string): string => { + const p = path.join(tempDir, filename) + fs.writeFileSync(p, content) + return resolveRuntimeName(p, kind) + } + + // --- Parse path, one per kind --- + + test('table: COLUMN TABLE', () => { + const name = resolve( + 'cds.outbox.Messages.hdbtable', + 'COLUMN TABLE cds_outbox_Messages (\n ID NVARCHAR(36) NOT NULL,\n PRIMARY KEY(ID)\n)', + 'table' + ) + assert.strictEqual(name, 'cds_outbox_Messages') + }) + + test('table: ROW TABLE', () => { + const name = resolve( + 'my.Row.hdbtable', + 'ROW TABLE my_Row (ID INTEGER)', + 'table' + ) + assert.strictEqual(name, 'my_Row') + }) + + test('view: VIEW ... AS SELECT', () => { + const name = resolve( + 'star.wars.Order.hdbview', + 'VIEW star_wars_CloneWarsChronologicalOrder AS SELECT\n Episode_0.ID\nFROM x', + 'view' + ) + assert.strictEqual(name, 'star_wars_CloneWarsChronologicalOrder') + }) + + test('procedure: PROCEDURE', () => { + const name = resolve( + 'my.Proc.hdbprocedure', + 'PROCEDURE my_Proc (IN a INTEGER) AS BEGIN END', + 'procedure' + ) + assert.strictEqual(name, 'my_Proc') + }) + + test('function: FUNCTION', () => { + const name = resolve( + 'my.Func.hdbfunction', + 'FUNCTION my_Func (IN a INTEGER) RETURNS INTEGER AS BEGIN END', + 'function' + ) + assert.strictEqual(name, 'my_Func') + }) + + test('sequence: DDL SEQUENCE', () => { + const name = resolve( + 'my.Seq.hdbsequence', + 'SEQUENCE my_Seq START WITH 1 INCREMENT BY 1', + 'sequence' + ) + assert.strictEqual(name, 'my_Seq') + }) + + test('sequence: JSON form', () => { + const name = resolve( + 'my.Seq.hdbsequence', + '{ "name": "my_Seq", "start_with": 1 }', + 'sequence' + ) + assert.strictEqual(name, 'my_Seq') + }) + + test('synonym: JSON top-level key', () => { + const name = resolve( + 'my.Syn.hdbsynonym', + '{ "my_Syn": { "target": { "object": "FOO", "schema": "BAR" } } }', + 'synonym' + ) + assert.strictEqual(name, 'my_Syn') + }) + + test('role: JSON top-level key', () => { + const name = resolve( + 'my.Role.hdbrole', + '{ "role": { "name": "my_Role" } }', + 'role' + ) + // top-level key value carries an explicit .name -> prefer it + assert.strictEqual(name, 'my_Role') + }) + + // --- Normalization --- + + test('quoted identifier: quotes stripped, case preserved', () => { + const name = resolve( + 'q.hdbtable', + 'COLUMN TABLE "cds_outbox_Messages" (ID INTEGER)', + 'table' + ) + assert.strictEqual(name, 'cds_outbox_Messages') + }) + + test('schema-qualified DDL: last dot segment', () => { + const name = resolve( + 'q.hdbtable', + 'COLUMN TABLE "MYSCHEMA"."cds_outbox_Messages" (ID INTEGER)', + 'table' + ) + assert.strictEqual(name, 'cds_outbox_Messages') + }) + + test('mixed case preserved', () => { + const name = resolve( + 'q.hdbtable', + 'COLUMN TABLE MixedCase_Table (ID INTEGER)', + 'table' + ) + assert.strictEqual(name, 'MixedCase_Table') + }) + + // --- Fallback rule --- + + test('fallback: empty file uses filename naming rule', () => { + const name = resolve('cds.outbox.Messages.hdbtable', '', 'table') + assert.strictEqual(name, 'cds_outbox_Messages') + }) + + test('fallback: unknown kind uses filename naming rule', () => { + const name = resolve('a.b.C.hdbxyz', 'whatever', 'mystery') + assert.strictEqual(name, 'a_b_C') + }) + + test('fallback: unparseable content uses filename naming rule', () => { + const name = resolve('a.b.C.hdbtable', 'not a table definition', 'table') + assert.strictEqual(name, 'a_b_C') + }) + + test('fallback: missing file uses filename naming rule (no throw)', () => { + const p = path.join(tempDir, 'does.not.Exist.hdbtable') + const name = resolveRuntimeName(p, 'table') + assert.strictEqual(name, 'does_not_Exist') + }) + + test('fallback: .hdinamespace name is prepended', () => { + const nsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hana-cli-ns-')) + fs.writeFileSync(path.join(nsDir, '.hdinamespace'), '{ "name": "com.acme", "subfolder": "ignore" }') + const p = path.join(nsDir, 'a.b.hdbtable') + fs.writeFileSync(p, '') // empty -> fallback + const name = resolveRuntimeName(p, 'table') + fs.rmSync(nsDir, { recursive: true, force: true }) + assert.strictEqual(name, 'com.acme::a_b') + }) + + test('fallback: empty .hdinamespace name adds no prefix', () => { + const nsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hana-cli-ns2-')) + fs.writeFileSync(path.join(nsDir, '.hdinamespace'), '{ "name": "", "subfolder": "ignore" }') + const p = path.join(nsDir, 'a.b.hdbtable') + fs.writeFileSync(p, '') + const name = resolveRuntimeName(p, 'table') + fs.rmSync(nsDir, { recursive: true, force: true }) + assert.strictEqual(name, 'a_b') + }) +}) From a63df77c5ca14f297401feb48ae133e93fcd94ba Mon Sep 17 00:00:00 2001 From: Thomas Jung <12159356+jung-thomas@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:02:17 +0200 Subject: [PATCH 4/7] fix(vscode): inject runtime object name into artifact inspectors --- vscode-extension/src/editors/artifactInspector.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/vscode-extension/src/editors/artifactInspector.ts b/vscode-extension/src/editors/artifactInspector.ts index 3147e0e..7f4177d 100644 --- a/vscode-extension/src/editors/artifactInspector.ts +++ b/vscode-extension/src/editors/artifactInspector.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' -import * as path from 'path' import { getWebviewContent, getWebviewOptions } from '../webview/htmlProvider.js' import { ensureServer, trackWebviewOpen, trackWebviewClose } from '../extension.js' +import { resolveRuntimeName } from './runtimeName.js' interface ArtifactConfig { viewType: string @@ -74,10 +74,11 @@ class ArtifactInspectorProvider implements vscode.CustomReadonlyEditorProvider { enableScripts: true, } - // Parse artifact name from the filename (strip extension) - const filename = path.basename(document.uri.fsPath) - const dotIndex = filename.lastIndexOf('.') - const name = dotIndex > 0 ? filename.substring(0, dotIndex) : filename + // Resolve the runtime (deployed) object name from the design-time file. + // The filename is the design-time name (e.g. cds.outbox.Messages.hdbtable); + // HANA knows the runtime name (e.g. cds_outbox_Messages), matched + // case-sensitively. resolveRuntimeName reads the file content to get it. + const name = resolveRuntimeName(document.uri.fsPath, this._config.kind) const port = await ensureServer(this._context) From 0b3cb4b5e4c65797256f75c45f1c8a60991198f7 Mon Sep 17 00:00:00 2001 From: Thomas Jung <12159356+jung-thomas@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:56:05 +0200 Subject: [PATCH 5/7] chore(vscode): bump extension to 0.1.9 for runtime name resolution fix --- vscode-extension/package-lock.json | 4 ++-- vscode-extension/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 03b485d..1dc5256 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "hana-cli", - "version": "0.1.3", + "version": "0.1.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hana-cli", - "version": "0.1.3", + "version": "0.1.9", "dependencies": { "@sap/cds": "10.0.3", "@sap/cds-dk": "10.0.4", diff --git a/vscode-extension/package.json b/vscode-extension/package.json index d8c0011..3f3297f 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "hana-cli", "displayName": "hana-cli Tools for SAP HANA", "description": "Visual editors and database tools for SAP HANA powered by hana-cli", - "version": "0.1.8", + "version": "0.1.9", "publisher": "SAP-samples", "icon": "icon.png", "license": "Apache-2.0", From 68e541519587829aacea1cfd7b7f8c32bc1e453c Mon Sep 17 00:00:00 2001 From: Thomas Jung <12159356+jung-thomas@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:09:41 +0200 Subject: [PATCH 6/7] fix(vscode): sanitize resolved runtime names to prevent webview script injection - resolveRuntimeName now validates parsed names against a safe HANA identifier grammar ([A-Za-z0-9_.:]); unsafe DDL/namespace values fall through to the sanitized filename rule instead of passing verbatim. - htmlProvider embeds the webview route via JSON.stringify so the value is escaped at the sink regardless of caller. - Adds 4 security tests (quote/angle-bracket DDL, quoted filename, malicious .hdinamespace prefix). Addresses automated security review (xss-injection). --- vscode-extension/src/editors/runtimeName.ts | 30 +++++++++++-- vscode-extension/src/webview/htmlProvider.ts | 9 ++-- .../test/suite/runtimeName.test.ts | 45 +++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/vscode-extension/src/editors/runtimeName.ts b/vscode-extension/src/editors/runtimeName.ts index 4788a64..b0da759 100644 --- a/vscode-extension/src/editors/runtimeName.ts +++ b/vscode-extension/src/editors/runtimeName.ts @@ -1,6 +1,20 @@ import * as fs from 'fs' import * as path from 'path' +/** + * HANA runtime object names consist of letters, digits, underscores, dots + * (schema qualifier — already stripped by normalization) and the `::` + * namespace separator. Anything else (quotes, angle brackets, semicolons, + * whitespace) is rejected so a crafted design-time file cannot inject content + * into the webview route/inline script that consumes the resolved name. + */ +const SAFE_IDENTIFIER = /^[A-Za-z0-9_.:]+$/ + +/** Remove any character outside the safe identifier grammar. */ +function stripUnsafe(value: string): string { + return value.replace(/[^A-Za-z0-9_.:]/g, '') +} + /** * Resolve the runtime (deployed) HANA object name for an HDI design-time file. * @@ -22,7 +36,9 @@ export function resolveRuntimeName(fsPath: string, kind: string): string { } const parsed = parseByKind(content, kind) - if (parsed && parsed.trim().length > 0) { + // Only trust a parsed name if it is a safe HANA identifier. A malicious file + // (e.g. `TABLE x';alert(1)//`) would otherwise flow into the webview route. + if (parsed && SAFE_IDENTIFIER.test(parsed)) { return parsed } return fallbackName(fsPath) @@ -118,12 +134,18 @@ function fallbackName(fsPath: string): string { const base = path.basename(fsPath) const dot = base.lastIndexOf('.') const stem = dot > 0 ? base.substring(0, dot) : base - const runtime = stem.replace(/\./g, '_') + // Replace '.' with '_' per HDI naming, then strip anything outside the safe + // grammar so the fallback can never emit an injectable string. + const runtime = stripUnsafe(stem.replace(/\./g, '_')) const ns = readNamespace(path.dirname(fsPath)) return ns ? `${ns}::${runtime}` : runtime } -/** Walk up from `dir` looking for a .hdinamespace with a non-empty name. */ +/** + * Walk up from `dir` looking for a .hdinamespace with a non-empty name. + * The returned prefix is validated against the safe identifier grammar; an + * unsafe namespace value is ignored (returns undefined) rather than trusted. + */ function readNamespace(dir: string): string | undefined { let current = dir // Bound the walk to avoid infinite loops at filesystem root. @@ -133,7 +155,7 @@ function readNamespace(dir: string): string | undefined { const raw = fs.readFileSync(candidate, 'utf8') const obj = JSON.parse(raw) if (typeof obj.name === 'string' && obj.name.length > 0) { - return obj.name + return SAFE_IDENTIFIER.test(obj.name) ? obj.name : undefined } return undefined // found the file; empty name means no prefix } catch { diff --git a/vscode-extension/src/webview/htmlProvider.ts b/vscode-extension/src/webview/htmlProvider.ts index 6593801..c49a31d 100644 --- a/vscode-extension/src/webview/htmlProvider.ts +++ b/vscode-extension/src/webview/htmlProvider.ts @@ -55,6 +55,9 @@ export function getWebviewContent( ` : '' const route = options.route || '/' + // Serialize the route as a JS string literal so any characters in the value + // are safely escaped rather than interpolated raw into the inline script. + const routeLiteral = JSON.stringify(route) return ` @@ -74,10 +77,10 @@ export function getWebviewContent(