Skip to content

Commit f5a6664

Browse files
authored
Normalize JSON for diffs (#237)
* actually use GIT_CONFIG.webdiff.maxLinesForSyntax * update ruff * update build commands * mention pipx install * loosen dependencies * add &normalize=1 * wire up should_normalize * working! * add some JSON diffs * tolerate JSONC/JSON5 * prettier
1 parent 74db0a9 commit f5a6664

File tree

18 files changed

+1436
-799
lines changed

18 files changed

+1436
-799
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ Features include:
2828

2929
pip install webdiff
3030

31+
or using [pipx]:
32+
33+
pipx install webdiff
34+
3135
or, if you prefer [Homebrew]:
3236

3337
brew install danvk/webdiff/webdiff
@@ -108,8 +112,7 @@ Options are:
108112
poetry install
109113
cd ts
110114
yarn
111-
# see https://github.com/webpack/webpack/issues/14532
112-
NODE_OPTIONS=--openssl-legacy-provider webpack
115+
yarn build
113116

114117
Then from the root directory:
115118

@@ -189,3 +192,4 @@ There's one complication involving symlinks. `git difftool -d` may fill one of t
189192
[git config]: https://git-scm.com/docs/git-config
190193
[themes]: http://example.com
191194
[poetry]: https://python-poetry.org/docs/repositories/#publishable-repositories
195+
[pipx]: https://pipx.pypa.io/stable/

poetry.lock

Lines changed: 1034 additions & 764 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ include = ["webdiff/static/js/file_diff.js"]
2828
python = "^3.10"
2929
binaryornot = "*"
3030
pillow = "*"
31-
PyGithub = "2.3.0"
32-
unidiff = "==0.7.4"
31+
PyGithub = "^2.3.0"
32+
unidiff = "^0.7.4"
3333
aiohttp = "^3.9.5"
3434

3535
[tool.poetry.group.dev.dependencies]
3636
pytest = "^7.1.3"
37-
ruff = "^0.4.6"
37+
ruff = "0.7"
3838

3939
[tool.poetry.scripts]
4040
webdiff = "webdiff.app:run"

testdata/jsondiffs/left/continents.geo.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "webdiff",
3+
"version": "1.3.0",
4+
"description": "client-side code for webdiff",
5+
"main": "index.js",
6+
"author": "Dan Vanderkam ([email protected])",
7+
"license": "MIT",
8+
"scripts": {
9+
"build": "NODE_OPTIONS=--openssl-legacy-provider webpack",
10+
"format": "yarn prettier --write '**/*.ts{,x}'",
11+
"format:check": "yarn prettier --check '**/*.ts{,x}'",
12+
"watch": "yarn run build --watch",
13+
"test": "jest",
14+
"knip": "knip",
15+
"lint": "yarn run eslint ."
16+
},
17+
"devDependencies": {
18+
"@eslint/js": "^9.4.0",
19+
"@types/diff": "^5.2.1",
20+
"@types/eslint__js": "^8.42.3",
21+
"@types/jest": "^29.5.12",
22+
"@types/lodash": "^4.14.161",
23+
"@types/node": "^20.14.2",
24+
"@types/react": "^16.9.49",
25+
"@types/react-dom": "^16.9.8",
26+
"@types/react-router": "^5.1.8",
27+
"@types/react-router-dom": "^5.1.5",
28+
"eslint": "^9.4.0",
29+
"eslint-plugin-react-hooks": "^5.1.0-rc-cc1ec60d0d-20240607",
30+
"jest": "^29.7.0",
31+
"jest-environment-jsdom": "^29.7.0",
32+
"knip": "^5.17.4",
33+
"prettier": "^2.1.2",
34+
"ts-jest": "^29.1.4",
35+
"ts-loader": "^8.0.4",
36+
"typescript": "^5.5.3",
37+
"typescript-eslint": "^7.12.0",
38+
"webpack": "^4.44.2",
39+
"webpack-cli": "^3.3.12"
40+
},
41+
"dependencies": {
42+
"diff": "^5.2.0",
43+
"highlight.js": "^11.9.0",
44+
"lodash": "^4.17.20",
45+
"react": "^16.13.1",
46+
"react-dom": "^16.13.1",
47+
"react-router": "^6.25.0",
48+
"react-router-dom": "^6.25.0"
49+
},
50+
"packageManager": "[email protected]+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
51+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"compilerOptions": {
3+
/* Visit https://aka.ms/tsconfig.json to read more about this file */
4+
5+
/* Basic Options */
6+
// "incremental": true, /* Enable incremental compilation */
7+
"target": "es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8+
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9+
"lib": ["es2021", "dom"] /* Specify library files to be included in the compilation. */,
10+
// "allowJs": true, /* Allow javascript files to be compiled. */
11+
// "checkJs": true, /* Report errors in .js files. */
12+
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
13+
// "declaration": true, /* Generates corresponding '.d.ts' file. */
14+
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15+
"sourceMap": true /* Generates corresponding '.map' file. */,
16+
// "outFile": "./", /* Concatenate and emit output to single file. */
17+
// "outDir": "./", /* Redirect output structure to the directory. */
18+
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19+
// "composite": true, /* Enable project compilation */
20+
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21+
// "removeComments": true, /* Do not emit comments to output. */
22+
// "noEmit": true, /* Do not emit outputs. */
23+
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
24+
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25+
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26+
27+
/* Strict Type-Checking Options */
28+
"strict": true /* Enable all strict type-checking options. */,
29+
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
30+
// "strictNullChecks": true, /* Enable strict null checks. */
31+
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
32+
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33+
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34+
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35+
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36+
37+
/* Additional Checks */
38+
"noUnusedLocals": true /* Report errors on unused locals. */,
39+
// "noUnusedParameters": true, /* Report errors on unused parameters. */
40+
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41+
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42+
43+
/* Module Resolution Options */
44+
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
45+
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46+
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47+
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48+
// "typeRoots": [], /* List of folders to include type definitions from. */
49+
// "types": [], /* Type declaration files to be included in compilation. */
50+
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
51+
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
52+
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53+
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54+
55+
/* Source Map Options */
56+
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57+
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58+
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59+
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60+
61+
/* Experimental Options */
62+
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63+
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64+
65+
/* Advanced Options */
66+
"skipLibCheck": true /* Skip type checking of declaration files. */,
67+
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
68+
}
69+
}

testdata/jsondiffs/right/continents.geo.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "webdiff",
3+
"description": "client-side code for webdiff", "main": "index.js", "author": "Dan Vanderkam ([email protected])", "license": "MIT", "version": "1.3.0",
4+
"scripts": {
5+
"test": "jest",
6+
"format": "yarn prettier --write '**/*.ts{,x}'",
7+
"format:check": "yarn prettier --check '**/*.ts{,x}'",
8+
"build": "NODE_OPTIONS=--openssl-legacy-provider webpack",
9+
"watch": "yarn run build --watch",
10+
"lint": "yarn run eslint .",
11+
"knip": "knip"
12+
},
13+
"dependencies": {
14+
"diff": "^5.2.0",
15+
"highlight.js": "^11.9.0",
16+
"lodash": "^4.17.20",
17+
"react": "^16.13.2",
18+
"react-dom": "^16.13.1",
19+
"react-router": "^6.25.0",
20+
"react-router-dom": "^6.25.0"
21+
},
22+
"devDependencies": {
23+
"@eslint/js": "^9.4.0",
24+
"@types/diff": "^5.2.1",
25+
"@types/eslint__js": "^8.42.3",
26+
"@types/jest": "^29.5.12",
27+
"@types/lodash": "^4.14.161",
28+
"@types/node": "^20.14.2",
29+
"@types/react": "^16.9.49",
30+
"@types/react-dom": "^16.9.8",
31+
"@types/react-router": "^5.1.8",
32+
"@types/react-router-dom": "^5.1.5",
33+
"eslint": "^9.4.0",
34+
"eslint-plugin-react-hooks": "^5.1.0-rc-cc1ec60d0d-20240607",
35+
"jest": "^29.7.0",
36+
"jest-environment-jsdom": "^29.7.0",
37+
"knip": "^5.17.4",
38+
"prettier": "^2.1.2",
39+
"ts-jest": "^29.1.4",
40+
"ts-loader": "^8.0.4",
41+
"typescript": "^5.5.3",
42+
"typescript-eslint": "^7.12.0",
43+
"webpack": "^4.44.2",
44+
"webpack-cli": "^3.3.12"
45+
},
46+
"packageManager": "[email protected]+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
47+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"compilerOptions": {
3+
/* Note that this file isn't really JSON! */
4+
5+
/* Basic Options */
6+
// "incremental": true, /* Enable incremental compilation */
7+
"target": "es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8+
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9+
/* Specify library files to be included in the compilation. */
10+
"lib": [
11+
"es2021",
12+
"dom"
13+
] ,
14+
// "allowJs": true, /* Allow javascript files to be compiled. */
15+
// "checkJs": true, /* Report errors in .js files. */
16+
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
17+
// "declaration": true, /* Generates corresponding '.d.ts' file. */
18+
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
19+
"sourceMap": true /* Generates corresponding '.map' file. */,
20+
// "outFile": "./", /* Concatenate and emit output to single file. */
21+
// "outDir": "./", /* Redirect output structure to the directory. */
22+
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
23+
// "composite": true, /* Enable project compilation */
24+
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
25+
// "removeComments": true, /* Do not emit comments to output. */
26+
// "noEmit": true, /* Do not emit outputs. */
27+
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
28+
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
29+
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
30+
31+
/* Strict Type-Checking Options */
32+
"strict": true /* Enable all strict type-checking options. */,
33+
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
34+
// "strictNullChecks": true, /* Enable strict null checks. */
35+
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
36+
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
37+
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
38+
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
39+
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
40+
41+
/* Additional Checks */
42+
"noUnusedLocals": true /* Report errors on unused locals. */,
43+
// "noUnusedParameters": true, /* Report errors on unused parameters. */
44+
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
45+
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
46+
47+
/* Module Resolution Options */
48+
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
49+
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
50+
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
51+
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
52+
// "typeRoots": [], /* List of folders to include type definitions from. */
53+
// "types": [], /* Type declaration files to be included in compilation. */
54+
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
55+
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
56+
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
57+
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
58+
59+
/* Source Map Options */
60+
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
61+
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
62+
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
63+
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
64+
65+
/* Experimental Options */
66+
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
67+
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
68+
69+
/* Advanced Options */
70+
"skipLibCheck": true /* Skip type checking of declaration files. */,
71+
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
72+
}
73+
}

ts/CodeDiffContainer.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,22 +67,30 @@ export function NoChanges(props: {filePair: FilePair}) {
6767
return null;
6868
}
6969

70-
// Either side can be empty (i.e. an add or a delete), in which case
71-
// getOrNull resolves to null
72-
async function getOrNull(side: string, path: string) {
70+
// Either side can be empty (i.e. an add or a delete), in which case getOrNull resolves to null.
71+
async function getOrNull(side: string, path: string, normalizeJSON: boolean) {
7372
if (!path) return null;
7473
const data = new URLSearchParams();
7574
data.set('path', path);
75+
if (normalizeJSON) {
76+
data.set('normalize_json', '1');
77+
}
7678
const response = await fetch(`/${side}/get_contents`, {
7779
method: 'post',
7880
body: data,
7981
});
8082
return response.text();
8183
}
8284

85+
export interface CodeDiffContainerProps {
86+
filePair: FilePair;
87+
diffOptions: Partial<DiffOptions>;
88+
normalizeJSON: boolean;
89+
}
90+
8391
// A side-by-side diff of source code.
84-
export function CodeDiffContainer(props: {filePair: FilePair; diffOptions: Partial<DiffOptions>}) {
85-
const {filePair, diffOptions} = props;
92+
export function CodeDiffContainer(props: CodeDiffContainerProps) {
93+
const {filePair, diffOptions, normalizeJSON} = props;
8694
const [contents, setContents] = React.useState<
8795
{before: string | null; after: string | null; diffOps: DiffRange[]} | undefined
8896
>();
@@ -95,7 +103,10 @@ export function CodeDiffContainer(props: {filePair: FilePair; diffOptions: Parti
95103
Accept: 'application/json',
96104
'Content-Type': 'application/json',
97105
},
98-
body: JSON.stringify({options: encodeDiffOptions(diffOptions)}),
106+
body: JSON.stringify({
107+
options: encodeDiffOptions(diffOptions),
108+
normalize_json: normalizeJSON,
109+
}),
99110
});
100111
return response.json() as Promise<DiffRange[]>;
101112
};
@@ -105,16 +116,16 @@ export function CodeDiffContainer(props: {filePair: FilePair; diffOptions: Parti
105116
// TODO: split these into three useEffects to avoid over-fetching when diff options change.
106117
(async () => {
107118
const [before, after, diffOps] = await Promise.all([
108-
getOrNull('a', a),
109-
getOrNull('b', b),
119+
getOrNull('a', a, normalizeJSON),
120+
getOrNull('b', b, normalizeJSON),
110121
getDiff(),
111122
]);
112123
setContents({before, after, diffOps});
113124
})().catch((e: unknown) => {
114125
alert('Unable to get diff!');
115126
console.error(e);
116127
});
117-
}, [filePair, diffOptions]);
128+
}, [filePair, diffOptions, normalizeJSON]);
118129

119130
return (
120131
<div>

0 commit comments

Comments
 (0)