Skip to content

Commit d826b91

Browse files
committed
Updates composer diffs to use precompiled templates
1 parent 2f9b82c commit d826b91

File tree

8 files changed

+89
-65
lines changed

8 files changed

+89
-65
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ images/icons/template/mapping.json
1111
images/icons/template/sass-map.hbs
1212
images/icons/template/styles.hbs
1313
src/emojis.generated.ts
14+
src/webviews/apps/plus/composer/components/diff/diff-templates.compiled.ts
1415
src/webviews/apps/shared/components/icons/codicons-map.ts
1516
src/webviews/apps/shared/components/icons/glicons-map.ts
1617
src/webviews/apps/shared/styles/icons/codicons-map.scss

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24910,7 +24910,7 @@
2491024910
"build:quick": "webpack --mode development --env skipLint",
2491124911
"build:extension": "webpack --mode development --config-name extension:node",
2491224912
"build:extension:browser": "webpack --mode development --config-name extension:webworker",
24913-
"build:webviews": "webpack --mode development --config-name webviews:common --config-name webviews",
24913+
"build:webviews": "node ./scripts/compile-composer-templates.mjs && webpack --mode development --config-name webviews:common --config-name webviews",
2491424914
"build:icons": "pnpm run icons:svgo && pnpm fantasticon && pnpm run icons:apply && pnpm run icons:export",
2491524915
"build:tests": "node ./scripts/esbuild.tests.mjs",
2491624916
"// Extracts the contributions from package.json into contributions.json": "//",
@@ -25045,6 +25045,7 @@
2504525045
"fork-ts-checker-webpack-plugin": "9.1.0",
2504625046
"glob": "11.0.3",
2504725047
"globals": "16.3.0",
25048+
"hogan.js": "3.0.2",
2504825049
"html-loader": "5.1.0",
2504925050
"html-webpack-plugin": "5.6.3",
2505025051
"image-minimizer-webpack-plugin": "4.1.3",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Precompile Composer custom diff2html Hogan templates to avoid runtime eval
2+
// Usage: node scripts/compile-composer-templates.mjs
3+
4+
import fs from 'node:fs';
5+
import path from 'node:path';
6+
import { createRequire } from 'node:module';
7+
const require = createRequire(import.meta.url);
8+
let Hogan;
9+
try {
10+
// Prefer root-level hogan.js if hoisted
11+
Hogan = await import('hogan.js');
12+
} catch {
13+
// Fallback: resolve from diff2html's nested dependency to support pnpm non-hoisted layout
14+
const diff2htmlPkg = require.resolve('diff2html/package.json');
15+
const hoganPath = require.resolve('hogan.js', {
16+
paths: [require('node:path').join(require('node:path').dirname(diff2htmlPkg), 'node_modules')],
17+
});
18+
const { pathToFileURL } = await import('node:url');
19+
Hogan = await import(pathToFileURL(hoganPath).href);
20+
}
21+
Hogan = (Hogan && Hogan.default) || Hogan;
22+
23+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\//, '')), '..');
24+
const srcPath = path.join(repoRoot, 'src/webviews/apps/plus/composer/components/diff/diff-templates.ts');
25+
const outPath = path.join(repoRoot, 'src/webviews/apps/plus/composer/components/diff/diff-templates.compiled.ts');
26+
27+
const source = fs.readFileSync(srcPath, 'utf8');
28+
29+
// Extract template strings from the TS source
30+
function extractTemplate(name) {
31+
const re = new RegExp(`export const ${name} = \`([\\s\\S]*?)\`;`);
32+
const m = source.match(re);
33+
if (!m) throw new Error(`Template ${name} not found in ${srcPath}`);
34+
return m[1];
35+
}
36+
37+
const blockHeader = extractTemplate('blockHeaderTemplate');
38+
const lineByLineFile = extractTemplate('lineByLineFileTemplate');
39+
const sideBySideFile = extractTemplate('sideBySideFileTemplate');
40+
const genericFilePath = extractTemplate('genericFilePathTemplate');
41+
42+
function precompile(name, tpl) {
43+
// Generate a code object compatible with new Hogan.Template(<code object>)
44+
const code = Hogan.compile(tpl, { asString: true });
45+
return ` "${name}": new Hogan.Template(${code})`;
46+
}
47+
48+
const header = `/* eslint-disable */\n// @ts-nocheck\n// Generated by scripts/compile-composer-templates.mjs — DO NOT EDIT\nimport type { CompiledTemplates } from 'diff2html/lib-esm/hoganjs-utils';\nimport * as Hogan from 'hogan.js';\n`;
49+
50+
const body = `export const compiledComposerTemplates: CompiledTemplates = {\n${precompile(
51+
'generic-block-header',
52+
blockHeader,
53+
)},\n${precompile('line-by-line-file-diff', lineByLineFile)},\n${precompile('side-by-side-file-diff', sideBySideFile)},\n${precompile('generic-file-path', genericFilePath)}\n};\n`;
54+
55+
fs.writeFileSync(outPath, header + body, 'utf8');
56+
console.log(`Wrote ${outPath}`);

src/webviews/apps/plus/composer/components/diff/diff-file.ts

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
1-
import { html as html2, parse as parseDiff } from 'diff2html';
2-
import type { RawTemplates } from 'diff2html/lib-esm/hoganjs-utils';
1+
import { parse as parseDiff } from 'diff2html';
32
import type { DiffFile } from 'diff2html/lib-esm/types';
43
import { ColorSchemeType } from 'diff2html/lib-esm/types';
54
import type { Diff2HtmlUIConfig } from 'diff2html/lib-esm/ui/js/diff2html-ui.js';
65
import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui.js';
7-
import { css, html, LitElement, nothing } from 'lit';
6+
import { css, html, LitElement } from 'lit';
87
import { customElement, property, query, state } from 'lit/decorators.js';
9-
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
108
import type { ComposerHunk } from '../../../../../plus/composer/protocol';
119
import { focusableBaseStyles } from '../../../../shared/components/styles/lit/a11y.css';
1210
import { boxSizingBase, scrollableBase } from '../../../../shared/components/styles/lit/base.css';
13-
import {
14-
blockHeaderTemplate,
15-
genericFilePathTemplate,
16-
lineByLineFileTemplate,
17-
sideBySideFileTemplate,
18-
} from './diff-templates';
11+
import { compiledComposerTemplates } from './diff-templates.compiled';
1912
import { diff2htmlStyles, diffStyles, hljsStyles } from './diff.css';
2013
import '../../../../shared/components/code-icon';
2114

@@ -57,15 +50,6 @@ export class GlDiffFile extends LitElement {
5750
@state()
5851
private parsedDiff?: DiffFile[];
5952

60-
get rawTemplates(): RawTemplates {
61-
return {
62-
'block-header': blockHeaderTemplate,
63-
'line-by-line-file-diff': lineByLineFileTemplate,
64-
'side-by-side-file-diff': sideBySideFileTemplate,
65-
'generic-file-path': genericFilePathTemplate,
66-
};
67-
}
68-
6953
private diff2htmlUi?: Diff2HtmlUI;
7054

7155
override firstUpdated() {
@@ -86,9 +70,6 @@ export class GlDiffFile extends LitElement {
8670
override render() {
8771
return html`<div id="diff"></div>`;
8872
}
89-
// override render() {
90-
// return this.renderDiff2();
91-
// }
9273

9374
private renderDiff() {
9475
if (!this.parsedDiff) {
@@ -100,29 +81,17 @@ export class GlDiffFile extends LitElement {
10081
outputFormat: this.sideBySide ? 'side-by-side' : 'line-by-line',
10182
drawFileList: false,
10283
highlight: false,
103-
rawTemplates: this.rawTemplates,
84+
// NOTE: Avoiding passing rawTemplates to Diff2HtmlUI to prevent Diff2Html from
85+
// compiling templates at runtime via Hogan.compile (which uses eval), which violates
86+
// the webview CSP (no 'unsafe-eval'). If we need to customize templates in the future,
87+
// switch to providing precompiled templates in the bundle instead of raw strings.
88+
compiledTemplates: compiledComposerTemplates,
10489
};
10590
this.diff2htmlUi = new Diff2HtmlUI(this.targetElement, this.parsedDiff, config);
10691
this.diff2htmlUi.draw();
10792
// this.diff2htmlUi.highlightCode();
10893
}
10994

110-
private renderDiff2() {
111-
if (!this.diffText) {
112-
return nothing;
113-
}
114-
115-
const html = html2(this.diffText, {
116-
drawFileList: false,
117-
// matching: 'lines',
118-
outputFormat: 'line-by-line',
119-
// colorScheme: this.isDarkMode ? ColorSchemeType.DARK : ColorSchemeType.LIGHT,
120-
colorScheme: ColorSchemeType.AUTO,
121-
});
122-
123-
return unsafeHTML(html);
124-
}
125-
12695
private processDiff() {
12796
// create diff text, then call parseDiff
12897
if (!this.filename || !this.hunks || this.hunks.length === 0) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* eslint-disable */
2+
// @ts-nocheck
3+
// Generated by scripts/compile-composer-templates.mjs — DO NOT EDIT
4+
import type { CompiledTemplates } from 'diff2html/lib-esm/hoganjs-utils';
5+
import * as Hogan from 'hogan.js';
6+
export const compiledComposerTemplates: CompiledTemplates = {
7+
"generic-block-header": new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<tr>");t.b("\n" + i);t.b(" <td class=\"");t.b(t.v(t.f("lineClass",c,p,0)));t.b(" ");t.b(t.v(t.d("CSSLineClass.INFO",c,p,0)));t.b("\"></td>");t.b("\n" + i);t.b(" <td class=\"");t.b(t.v(t.d("CSSLineClass.INFO",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" <div class=\"");t.b(t.v(t.f("contentClass",c,p,0)));t.b("\">");if(t.s(t.f("blockHeader",c,p,1),c,p,0,156,173,"{{ }}")){t.rs(c,p,function(c,p,t){t.b(t.t(t.f("blockHeader",c,p,0)));});c.pop();}if(!t.s(t.f("blockHeader",c,p,1),c,p,1,0,0,"")){t.b("&nbsp;");};t.b("</div>");t.b("\n" + i);t.b(" </td>");t.b("\n" + i);t.b("</tr>");return t.fl(); },partials: {}, subs: { }}),
8+
"line-by-line-file-diff": new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<details open id=\"");t.b(t.v(t.f("fileHtmlId",c,p,0)));t.b("\" class=\"d2h-file-wrapper\" data-lang=\"");t.b(t.v(t.d("file.language",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" <summary class=\"d2h-file-header\">");t.b("\n" + i);t.b(" <code-icon class=\"file-icon--open\" icon=\"chevron-down\"></code-icon>");t.b("\n" + i);t.b(" <code-icon class=\"file-icon--closed\" icon=\"chevron-right\"></code-icon>");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("filePath",c,p,0)));t.b("\n" + i);t.b(" </summary>");t.b("\n" + i);t.b(" <div class=\"d2h-file-diff scrollable\">");t.b("\n" + i);t.b(" <div class=\"d2h-code-wrapper\">");t.b("\n" + i);t.b(" <table class=\"d2h-diff-table\">");t.b("\n" + i);t.b(" <tbody class=\"d2h-diff-tbody\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("diffs",c,p,0)));t.b("\n" + i);t.b(" </tbody>");t.b("\n" + i);t.b(" </table>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b("</details>");return t.fl(); },partials: {}, subs: { }}),
9+
"side-by-side-file-diff": new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<details id=\"");t.b(t.v(t.f("fileHtmlId",c,p,0)));t.b("\" class=\"d2h-file-wrapper\" data-lang=\"");t.b(t.v(t.d("file.language",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" <summary class=\"d2h-file-header\">");t.b("\n" + i);t.b(" <code-icon class=\"file-icon--open\" icon=\"chevron-down\"></code-icon>");t.b("\n" + i);t.b(" <code-icon class=\"file-icon--closed\" icon=\"chevron-right\"></code-icon>");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("filePath",c,p,0)));t.b("\n" + i);t.b(" </summary>");t.b("\n" + i);t.b(" <div class=\"d2h-files-diff\">");t.b("\n" + i);t.b(" <div class=\"d2h-file-side-diff\">");t.b("\n" + i);t.b(" <div class=\"d2h-code-wrapper\">");t.b("\n" + i);t.b(" <table class=\"d2h-diff-table\">");t.b("\n" + i);t.b(" <tbody class=\"d2h-diff-tbody\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.d("diffs.left",c,p,0)));t.b("\n" + i);t.b(" </tbody>");t.b("\n" + i);t.b(" </table>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" <div class=\"d2h-file-side-diff\">");t.b("\n" + i);t.b(" <div class=\"d2h-code-wrapper\">");t.b("\n" + i);t.b(" <table class=\"d2h-diff-table\">");t.b("\n" + i);t.b(" <tbody class=\"d2h-diff-tbody\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.d("diffs.right",c,p,0)));t.b("\n" + i);t.b(" </tbody>");t.b("\n" + i);t.b(" </table>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b("</details>");return t.fl(); },partials: {}, subs: { }}),
10+
"generic-file-path": new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<span class=\"d2h-file-name-wrapper\">");t.b("\n" + i);t.b(" <span class=\"d2h-file-name\">");t.b(t.v(t.f("fileDiffName",c,p,0)));t.b("</span>");t.b("\n" + i);t.b(t.rp("<fileTag0",c,p," "));t.b("</span>");t.b("\n" + i);t.b("<label class=\"d2h-file-collapse\" hidden>");t.b("\n" + i);t.b(" <input class=\"d2h-file-collapse-input\" type=\"checkbox\" name=\"viewed\" value=\"viewed\">");t.b("\n" + i);t.b(" Viewed");t.b("\n" + i);t.b("</label>");return t.fl(); },partials: {"<fileTag0":{name:"fileTag", partials: {}, subs: { }}}, subs: { }})
11+
};

src/webviews/apps/plus/composer/components/diff/diff.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { html as html2 } from 'diff2html';
21
import { ColorSchemeType } from 'diff2html/lib-esm/types';
32
import type { Diff2HtmlUIConfig } from 'diff2html/lib-esm/ui/js/diff2html-ui.js';
43
import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui.js';
5-
import { css, html, LitElement, nothing } from 'lit';
4+
import { css, html, LitElement } from 'lit';
65
import { customElement, property, query } from 'lit/decorators.js';
7-
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
86
import { boxSizingBase } from '../../../../shared/components/styles/lit/base.css';
7+
import { compiledComposerTemplates } from './diff-templates.compiled';
98
import { diff2htmlStyles, diffStyles, hljsStyles } from './diff.css';
109

1110
@customElement('gl-diff-hunk')
@@ -79,31 +78,15 @@ export class GlDiffHunk extends LitElement {
7978
outputFormat: 'line-by-line',
8079
drawFileList: false,
8180
highlight: false,
82-
rawTemplates: this.rawTemplates,
81+
// NOTE: Avoiding passing rawTemplates to Diff2HtmlUI to prevent Diff2Html from
82+
// compiling templates at runtime via Hogan.compile (which uses eval), which violates
83+
// the webview CSP (no 'unsafe-eval'). If we need to customize templates in the future,
84+
// switch to providing precompiled templates in the bundle instead of raw strings.
85+
compiledTemplates: compiledComposerTemplates,
8386
};
8487
const diff = `${diffHeader}\n${hunkHeader}\n${hunkContent}`;
8588
this.diff2htmlUi = new Diff2HtmlUI(this.targetElement, diff, config);
8689
this.diff2htmlUi.draw();
8790
// this.diff2htmlUi.highlightCode();
8891
}
89-
90-
private renderDiff2() {
91-
const diffHeader = this.diffHeader.trim();
92-
const hunkHeader = this.hunkHeader.trim();
93-
const hunkContent = this.hunkContent.trim();
94-
if (!diffHeader || !hunkHeader || !hunkContent) {
95-
return nothing;
96-
}
97-
98-
const diff = `${diffHeader}\n${hunkHeader}\n${hunkContent}`;
99-
const html = html2(diff, {
100-
drawFileList: false,
101-
// matching: 'lines',
102-
outputFormat: 'line-by-line',
103-
// colorScheme: this.isDarkMode ? ColorSchemeType.DARK : ColorSchemeType.LIGHT,
104-
colorScheme: ColorSchemeType.AUTO,
105-
});
106-
107-
return unsafeHTML(html);
108-
}
10992
}

webpack.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ function getCspHtmlPlugin(mode, env) {
675675
'script-src':
676676
mode !== 'production'
677677
? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
678-
: ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"],
678+
: ['#{cspSource}', "'nonce-#{cspNonce}'"],
679679
'style-src':
680680
mode === 'production'
681681
? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-hashes'"]

0 commit comments

Comments
 (0)