diff --git a/esbuild.js b/esbuild.js index 7303585..5f164dd 100644 --- a/esbuild.js +++ b/esbuild.js @@ -115,7 +115,7 @@ const webviewConfig = { entryPoints: webviewEntryPoints(), bundle: true, minify: production, - sourcemap: !production, + sourcemap: production ? false : 'inline', platform: 'browser', // Pin the syntax level: webview assets run in the Electron renderer, not Node, so they must not inherit // esbuild's `esnext` default. diff --git a/eslint.config.js b/eslint.config.js index 12cc562..964ec2b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -40,4 +40,15 @@ module.exports = [ ...js.configs.recommended.rules, }, }, + { + // Webview client scripts run in the Electron renderer, not the extension host + files: ['src/webview/client/**/*.ts'], + languageOptions: { + globals: { + ...globals.browser, + // Injected by VS Code into the webview; not a browser global. + acquireVsCodeApi: 'readonly', + }, + }, + }, ]; diff --git a/src/extension.ts b/src/extension.ts index b940ffa..7c011fe 100755 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { ComponentDocumentLinkProvider } from './providers/documentLinkProvider' import { ComponentBrowserProvider } from './providers/componentBrowserProvider'; import { detectIncludeComponent, Component } from './providers/componentDetector'; import { getComponentCacheManager, ComponentCacheManager } from './services/cache/componentCacheManager'; +import { assetRoots } from './webview/webviewHtml'; import { Logger } from './utils/logger'; import { ValidationProvider } from './providers/validationProvider'; import type { CachedComponent } from './types/cache'; @@ -318,7 +319,7 @@ export function activate(context: vscode.ExtensionContext) { { enableScripts: true, retainContextWhenHidden: true, - localResourceRoots: [] + localResourceRoots: assetRoots(context.extensionUri) } ); diff --git a/src/providers/componentBrowserProvider.ts b/src/providers/componentBrowserProvider.ts index e767e1b..4c2e782 100644 --- a/src/providers/componentBrowserProvider.ts +++ b/src/providers/componentBrowserProvider.ts @@ -16,7 +16,7 @@ import { generateComponentText } from './componentBrowserGenerate'; import { findComponentLineRange, parseExistingComponentText } from './componentBrowserEdit'; import { transformCachedComponentsToGroups } from './componentBrowserTransform'; import { buildVersionLabels, compileTagTemplate, stripTagPrefix } from '../services/component/tagScoping'; -import { assetUri, createNonce, cspMetaTag } from '../webview/webviewHtml'; +import { assetRoots, assetUri, createNonce, cspMetaTag } from '../webview/webviewHtml'; /** * Component shape carried through the detach-hover webview's "Open in Detailed View" round trip. @@ -92,9 +92,7 @@ export class ComponentBrowserProvider { { enableScripts: true, retainContextWhenHidden: true, - localResourceRoots: [ - vscode.Uri.joinPath(this.context.extensionUri, 'out', 'webview') - ] + localResourceRoots: assetRoots(this.context.extensionUri) } ); @@ -492,7 +490,8 @@ export class ComponentBrowserProvider { `Component: ${component.name}`, vscode.ViewColumn.Beside, { - enableScripts: true + enableScripts: true, + localResourceRoots: assetRoots(this.context.extensionUri) } ); @@ -664,7 +663,7 @@ export class ComponentBrowserProvider { - ${cspMetaTag(webview, nonce)} + ${cspMetaTag(webview.cspSource, nonce)} GitLab CI/CD Components diff --git a/src/webview/csp.ts b/src/webview/csp.ts new file mode 100644 index 0000000..72e0dd6 --- /dev/null +++ b/src/webview/csp.ts @@ -0,0 +1,52 @@ +/** + * Content-Security-Policy plumbing for webview documents. + * + * `vscode`-free and pure so the unit suite can drive it directly: `cspMetaTag` takes the webview's `cspSource` as a + * string rather than the webview itself, leaving `src/webview/webviewHtml.ts` to hold the parts that need the API. + */ + +const NONCE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; +const NONCE_LENGTH = 32; + +/** + * Generates a random nonce for the CSP `script-src 'nonce-...'` directive. + * + * The alphabet is 64 characters so that indexing by a random byte (`byte % 64`) draws uniformly — a 62-character + * alphabet would over-represent its first two characters. `crypto.getRandomValues` is available in the extension + * host runtime and is preferred over `Math.random` for a value that gates script execution. + * + * @returns 32 characters drawn from `[A-Za-z0-9-_]`, unique per call. + */ +export function createNonce(): string { + const bytes = new Uint8Array(NONCE_LENGTH); + crypto.getRandomValues(bytes); + let nonce = ''; + for (const byte of bytes) { + nonce += NONCE_CHARS[byte % NONCE_CHARS.length]; + } + return nonce; +} + +/** + * Builds the Content-Security-Policy meta tag for a webview document. + * + * Styles and scripts must come from files under the webview's own origin — no inline `