diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a45ddc..6583ec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## [1.0.2] - 2026-07-03 + +### Fixed +- Load bundled `diff2html` webview assets from `node_modules` when the copied `media` assets are unavailable during local development and testing + ## [1.0.0] - 2026-01-30 ### Added diff --git a/src/patchEditorProvider.ts b/src/patchEditorProvider.ts index ee07025..04f34fd 100644 --- a/src/patchEditorProvider.ts +++ b/src/patchEditorProvider.ts @@ -1,5 +1,38 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; import * as vscode from 'vscode'; +const DIFF2HTML_ASSET_FILES = [ + path.join('css', 'diff2html.min.css'), + path.join('js', 'diff2html.min.js') +]; + +/** + * Check whether all required diff2html asset files exist under the given asset root. + */ +function hasDiff2HtmlAssets(assetRoot: string): boolean { + return DIFF2HTML_ASSET_FILES.every(file => fs.existsSync(path.join(assetRoot, file))); +} + +/** + * Resolve the diff2html asset directory for this extension installation. + * Prefers packaged media assets and falls back to the local dependency bundle + * when running from a source checkout where copied media assets are absent. + */ +export function resolveDiff2HtmlAssetDirectory(extensionPath: string): string { + const mediaAssetRoot = path.join(extensionPath, 'media', 'diff2html'); + if (hasDiff2HtmlAssets(mediaAssetRoot)) { + return mediaAssetRoot; + } + + const nodeModulesAssetRoot = path.join(extensionPath, 'node_modules', 'diff2html', 'bundles'); + if (hasDiff2HtmlAssets(nodeModulesAssetRoot)) { + return nodeModulesAssetRoot; + } + + return mediaAssetRoot; +} + /** * Custom editor provider for patch/diff files */ @@ -59,19 +92,26 @@ export class PatchEditorProvider implements vscode.CustomTextEditorProvider { this.activeWebviewPanel = webviewPanel; // Setup webview options + const diff2htmlAssetDirectory = resolveDiff2HtmlAssetDirectory(this.context.extensionUri.fsPath); + if (!hasDiff2HtmlAssets(diff2htmlAssetDirectory)) { + this.logError(`Diff2Html assets were not found in ${diff2htmlAssetDirectory}`); + } + + const diff2htmlAssetRoot = vscode.Uri.file(diff2htmlAssetDirectory); + webviewPanel.webview.options = { enableScripts: true, localResourceRoots: [ - vscode.Uri.joinPath(this.context.extensionUri, 'media') + diff2htmlAssetRoot ] }; - // Get URIs for diff2html resources from media folder + // Get URIs for diff2html resources from the packaged media folder or the local dependency fallback const diff2htmlCssUri = webviewPanel.webview.asWebviewUri( - vscode.Uri.joinPath(this.context.extensionUri, 'media', 'diff2html', 'css', 'diff2html.min.css') + vscode.Uri.joinPath(diff2htmlAssetRoot, 'css', 'diff2html.min.css') ); const diff2htmlJsUri = webviewPanel.webview.asWebviewUri( - vscode.Uri.joinPath(this.context.extensionUri, 'media', 'diff2html', 'js', 'diff2html.min.js') + vscode.Uri.joinPath(diff2htmlAssetRoot, 'js', 'diff2html.min.js') ); // Set initial HTML content diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index d5b27ca..46a3b41 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,5 +1,9 @@ import * as assert from 'assert'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; import * as vscode from 'vscode'; +import { resolveDiff2HtmlAssetDirectory } from '../patchEditorProvider'; suite('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); @@ -22,4 +26,50 @@ suite('Extension Test Suite', () => { assert.ok(commands.includes('tlcsdm.patchReader.toggleView')); }); }); + + test('Should prefer packaged diff2html assets when available', () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'patch-reader-media-')); + try { + const mediaCss = path.join(tempRoot, 'media', 'diff2html', 'css', 'diff2html.min.css'); + const mediaJs = path.join(tempRoot, 'media', 'diff2html', 'js', 'diff2html.min.js'); + const nodeModulesCss = path.join(tempRoot, 'node_modules', 'diff2html', 'bundles', 'css', 'diff2html.min.css'); + const nodeModulesJs = path.join(tempRoot, 'node_modules', 'diff2html', 'bundles', 'js', 'diff2html.min.js'); + + fs.mkdirSync(path.dirname(mediaCss), { recursive: true }); + fs.mkdirSync(path.dirname(mediaJs), { recursive: true }); + fs.mkdirSync(path.dirname(nodeModulesCss), { recursive: true }); + fs.mkdirSync(path.dirname(nodeModulesJs), { recursive: true }); + fs.writeFileSync(mediaCss, ''); + fs.writeFileSync(mediaJs, ''); + fs.writeFileSync(nodeModulesCss, ''); + fs.writeFileSync(nodeModulesJs, ''); + + assert.strictEqual( + resolveDiff2HtmlAssetDirectory(tempRoot), + path.join(tempRoot, 'media', 'diff2html') + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + test('Should fall back to node_modules diff2html assets when media assets are missing', () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'patch-reader-node-modules-')); + try { + const nodeModulesCss = path.join(tempRoot, 'node_modules', 'diff2html', 'bundles', 'css', 'diff2html.min.css'); + const nodeModulesJs = path.join(tempRoot, 'node_modules', 'diff2html', 'bundles', 'js', 'diff2html.min.js'); + + fs.mkdirSync(path.dirname(nodeModulesCss), { recursive: true }); + fs.mkdirSync(path.dirname(nodeModulesJs), { recursive: true }); + fs.writeFileSync(nodeModulesCss, ''); + fs.writeFileSync(nodeModulesJs, ''); + + assert.strictEqual( + resolveDiff2HtmlAssetDirectory(tempRoot), + path.join(tempRoot, 'node_modules', 'diff2html', 'bundles') + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); });