import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
import fs from 'fs';
import os from 'os';
import path from 'path';
import clipboardy from 'clipboardy';
import open from 'open';
import { parse, html, Diff2HtmlConfig } from 'diff2html';
import { put } from './http-utils.js';
import * as log from './logger.js';
import { Configuration, InputType, DiffyType } from './types.js';
import * as utils from './utils.js';
import { ColorSchemeType } from 'diff2html/lib/types.js';
const defaultArgs = ['-M', '-C', 'HEAD'];
const lightGitHubTheme = ``;
const darkGitHubTheme = ``;
const autoGitHubTheme = `
`;
const lightBaseStyle = ``;
const darkBaseStyle = ``;
const autoBaseStyle = ``;
function generateGitDiffArgs(gitArgsArr: string[], ignore: string[]): string[] {
const gitDiffArgs: string[] = ['diff'];
if (!gitArgsArr.includes('--no-color')) gitDiffArgs.push('--no-color');
if (gitArgsArr.length === 0) Array.prototype.push.apply(gitDiffArgs, defaultArgs);
Array.prototype.push.apply(gitDiffArgs, gitArgsArr);
if (ignore.length > 0) {
if (!gitArgsArr.includes('--')) gitDiffArgs.push('--');
Array.prototype.push.apply(
gitDiffArgs,
ignore.map(path => `:(exclude)${path}`),
);
}
return gitDiffArgs;
}
function runGitDiff(gitArgsArr: string[], ignore: string[]): string {
const gitDiffArgs = generateGitDiffArgs(gitArgsArr, ignore);
return utils.execute('git', gitDiffArgs);
}
function prepareHTML(diffHTMLContent: string, config: Configuration, colorScheme?: ColorSchemeType): string {
const template = utils.readFile(config.htmlWrapperTemplate);
const diff2htmlPath = path.join(path.dirname(require.resolve('diff2html')), '..');
const cssFilePath = path.resolve(diff2htmlPath, 'bundles', 'css', 'diff2html.min.css');
const cssContent = utils.readFile(cssFilePath);
const jsUiFilePath = path.resolve(diff2htmlPath, 'bundles', 'js', 'diff2html-ui-slim.min.js');
const jsUiContent = utils.readFile(jsUiFilePath);
const pageTitle = config.pageTitle;
const pageHeader = config.pageHeader;
const gitHubTheme =
colorScheme === 'light' ? lightGitHubTheme : colorScheme === 'dark' ? darkGitHubTheme : autoGitHubTheme;
const baseStyle = colorScheme === 'light' ? lightBaseStyle : colorScheme === 'dark' ? darkBaseStyle : autoBaseStyle;
/* HACK:
* Replace needs to receive a function as the second argument to perform an exact replacement.
* This will avoid the replacements from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
*/
return [
{ searchValue: '', replaceValue: pageTitle },
{
searchValue: '',
replaceValue: `${baseStyle}\n${gitHubTheme}\n`,
},
{ searchValue: '', replaceValue: `` },
{
searchValue: '//diff2html-fileListToggle',
replaceValue: `diff2htmlUi.fileListToggle(${config.showFilesOpen});`,
},
{
searchValue: '//diff2html-fileContentToggle',
replaceValue: config.fileContentToggle ? `diff2htmlUi.fileContentToggle();` : '',
},
{
searchValue: '//diff2html-synchronisedScroll',
replaceValue: config.synchronisedScroll ? `diff2htmlUi.synchronisedScroll();` : '',
},
{
searchValue: '//diff2html-highlightCode',
replaceValue: config.highlightCode ? `diff2htmlUi.highlightCode();` : '',
},
{ searchValue: '', replaceValue: pageHeader },
{ searchValue: '', replaceValue: diffHTMLContent },
].reduce(
(previousValue, replacement) =>
utils.replaceExactly(previousValue, replacement.searchValue, replacement.replaceValue),
template,
);
}
/**
* Get unified diff input from type
* @param inputType - a string `file`, `stdin`, or `command`
* @param inputArgs - a string array
* @param ignore - a string array
*/
export async function getInput(inputType: InputType, inputArgs: string[], ignore: string[]): Promise {
switch (inputType) {
case 'file':
return utils.readFile(inputArgs[0]);
case 'stdin':
return utils.readStdin();
case 'command':
return runGitDiff(inputArgs, ignore);
}
}
export function getOutput(options: Diff2HtmlConfig, config: Configuration, input: string): string {
if (config.htmlWrapperTemplate && !fs.existsSync(config.htmlWrapperTemplate)) {
process.exitCode = 4;
throw new Error(`Template ('${config.htmlWrapperTemplate}') not found!`);
}
const diffJson = parse(input, options);
switch (config.formatType) {
case 'html': {
const htmlContent = html(diffJson, { ...options });
return prepareHTML(htmlContent, config, options.colorScheme);
}
case 'json': {
return JSON.stringify(diffJson);
}
}
}
export function preview(content: string, format: string): void {
const filename = `diff.${format}`;
const filePath: string = path.resolve(os.tmpdir(), filename);
utils.writeFile(filePath, content);
open(filePath, { wait: false });
}
type CreateDiffResponse = { id: string };
type ApiError = { error: string };
function isCreateDiffResponse(obj: unknown): obj is CreateDiffResponse {
return (obj as CreateDiffResponse).id !== undefined;
}
function isApiError(obj: unknown): obj is ApiError {
return (obj as ApiError).error !== undefined;
}
export async function postToDiffy(diff: string, diffyOutput: DiffyType): Promise {
const response = await put('https://diffy.org/api/diff/', { diff: diff });
if (!isCreateDiffResponse(response)) {
if (isApiError(response)) {
throw new Error(response.error);
} else {
throw new Error(
`Could not find 'id' of created diff in the response json.\nBody:\n\n${JSON.stringify(response, null, 2)}`,
);
}
}
const url = `https://diffy.org/diff/${response.id}`;
log.print('Link powered by https://diffy.org');
log.print(url);
if (diffyOutput === 'browser') {
open(url);
} else if (diffyOutput === 'pbcopy') {
clipboardy.writeSync(url);
}
return url;
}