Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
48 changes: 44 additions & 4 deletions src/patchEditorProvider.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions src/test/extension.test.ts
Original file line number Diff line number Diff line change
@@ -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.');
Expand All @@ -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 });
}
});
});
Loading