Use typescript

main
Gerard Braad 2023-06-17 23:45:41 +08:00
parent ff1323b4a6
commit f473967016
12 changed files with 2599 additions and 1615 deletions

242
@types/cockpitjs/index.d.ts vendored Normal file
View File

@ -0,0 +1,242 @@
declare namespace Cockpit {
/**********************************
* Some helpful primitive typedefs
*********************************/
type integer = number; //A typedef for an integer. Doesn't actually prevent compilation, but provides an IDE hint
/**********************************
* Cockpit D-Bus
* http://cockpit-project.org/guide/latest/cockpit-dbus.html
*********************************/
type BYTE = number;
type BOOLEAN = boolean;
type INT16 = number;
type UINT16 = number;
type INT32 = number;
type UINT32 = number;
type INT64 = number;
type UINT64 = number;
type DOUBLE = number;
type STRING = string;
type OBJECT_PATH = string;
type SIGNATURE = string;
type ARRAY_BYTE = string[];
type ARRAY_DICT_ENTRY_STRING = object;
type ARRAY_DICT_ENTRY_OTHER = object;
type ARRAY_OTHER = any[];
interface VARIANT {
"t": STRING,
"v": any
}
//TODO - Not sure on specifics for handle
type HANDLE = object;
interface DBusOptions {
"bus" : string
"host" : string
"superuser" : string
"track" : string
}
interface DBusProxy {
client : string
path : string
iface : string
valid : boolean
data : object
}
//Todo unfinished
interface DBusClient {
}
/**********************************
* Cockpit File Access
* http://cockpit-project.org/guide/latest/cockpit-file.html
**********************************/
interface ParsingFunction {
(data: string) : string
}
interface StringifyingFunction {
(data: string) : string
}
interface SyntaxObject {
parse: ParsingFunction
stringify: StringifyingFunction
}
interface FileAccessOptions {
syntax?: SyntaxObject,
binary?: boolean,
max_read_size?: integer,
superuser?: string,
host?: string
}
interface FileReadDoneCallback {
(content: string, tag: string) : void
}
interface FileReadFailCallback {
(error: string) : void
}
interface FileReadPromise {
done (callback : FileReadDoneCallback) : FileReadPromise
fail (callback : FileReadFailCallback) : FileReadPromise
}
interface FileReplaceDoneCallback {
(newTag: string) : void
}
interface FileReplaceFailCallback {
(error: string) : void
}
interface FileReplacePromise {
done (callback : FileReplaceDoneCallback) : FileReplacePromise
fail (callback : FileReplaceFailCallback) : FileReplacePromise
}
interface FileModifyDoneCallback {
(newContent : string, newTag: string) : void
}
interface FileModifyFailCallback {
(error: string) : void
}
interface FileModifyPromise {
done (callback : FileModifyDoneCallback) : FileModifyPromise
fail (callback : FileModifyFailCallback) : FileModifyPromise
}
interface FileWatchCallback {
content : string,
tag : string,
error? : any //TODO - what is the error content?
}
interface File {
read () : FileReadPromise
replace (content : string, expected_tag?: string) : FileReplacePromise
modify (callback : any, initial_content?: string, initial_tag?: string) : FileModifyPromise
watch (callback : FileWatchCallback) : void
close () : void
}
/**********************************
* Cockpit Processes
* http://cockpit-project.org/guide/latest/cockpit-spawn.html
**********************************/
interface ProcessFailureException {
message?: string
problem?: string
exit_status?: integer
exit_signal?: string
}
enum ProcessProblemCodes {
"access-denied", //"The user is not permitted to perform the action in question."
"authentication-failed", //"User authentication failed."
"internal-error", //"An unexpected internal error without further info. This should not happen during the normal course of operations."
"no-cockpit", //"The system does not have a compatible version of Cockpit installed or installed properly."
"no-session", //"Cockpit is not logged in."
"not-found", //"Something specifically requested was not found, such as a file, executable etc."
"terminated", //"Something was terminated forcibly, such as a connection, process session, etc."
"timeout", //"Something timed out."
"unknown-hostkey", //"The remote host had an unexpected or unknown key."
"no-forwarding" //"Could not forward authentication credentials to the remote host."
}
interface ProcessPromiseDoneCallback {
(data: string, message?: string) : void
}
interface ProcessPromiseFailCallback {
(exception: ProcessFailureException, data?: string) : void
}
interface ProcessPromiseStreamCallback {
(data: string) : void
}
interface ProcessPromise {
done( callback: ProcessPromiseDoneCallback ) : ProcessPromise,
fail( callback: ProcessPromiseFailCallback ) : ProcessPromise,
stream( callback: ProcessPromiseStreamCallback ) : ProcessPromise,
input( data: string, stream?: boolean ) : ProcessPromise,
close( problem?: ProcessProblemCodes ) : ProcessPromise,
}
/**********************************
* Cockpit User Session
* http://cockpit-project.org/guide/latest/cockpit-login.html
**********************************/
interface UserSessionPermission {
allowed : boolean
onChanged : any //TODO need to see how to do events in TS
close() : void
}
interface UserSessionObject {
onchanged : any
}
interface UserSessionDetails {
"id" : string //This is unix user id.
"name" : string //This is the unix user name like "root".
"full_name" : string //This is a readable name for the user.
"groups" : string //This is an array of group names to which the user belongs.
"home" : string //This is user's home directory.
"shell" : string //This is unix user shell.
}
interface UserSessionPromiseDoneCallback {
(user: UserSessionDetails) : void
}
interface UserSessionPromiseFailCallback { //Todo - is this defined?
}
interface UserSessionPromise {
}
/**********************************
* Cockpit Object
* Generally brought into your app in the root HTML file via a <script>
**********************************/
interface CockpitObject {
//File system
file(path : string, options? : FileAccessOptions) : File
//Processes
spawn(
arguments: Array<string>,
parameters?: object
): ProcessPromise;
//User Session
logout(reload? : boolean) : void
user() : UserSessionPromise
//user : UserSessionObject // TODO ts doesn't like this
}
}
declare var cockpit : Cockpit.CockpitObject;

View File

@ -79,8 +79,8 @@ $(SPEC): packaging/$(SPEC).in $(NODE_MODULES_TEST)
provides=$$(npm ls --omit dev --package-lock-only --depth=Infinity | grep -Eo '[^[:space:]]+@[^[:space:]]+' | sort -u | sed 's/^/Provides: bundled(npm(/; s/\(.*\)@/\1)) = /'); \
awk -v p="$$provides" '{gsub(/%{VERSION}/, "$(VERSION)"); gsub(/%{NPM_PROVIDES}/, p)}1' $< > $@
$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js
NODE_ENV=$(NODE_ENV) ./build.js
$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json
NODE_ENV=$(NODE_ENV) npm run build
watch: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
NODE_ENV=$(NODE_ENV) npm run watch

136
build.js
View File

@ -1,136 +0,0 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import copy from 'esbuild-plugin-copy';
import { cleanPlugin } from './pkg/lib/esbuild-cleanup-plugin.js';
import { cockpitCompressPlugin } from './pkg/lib/esbuild-compress-plugin.js';
import { cockpitPoEsbuildPlugin } from './pkg/lib/cockpit-po-plugin.js';
import { cockpitRsyncEsbuildPlugin } from './pkg/lib/cockpit-rsync-plugin.js';
import { esbuildStylesPlugins } from './pkg/lib/esbuild-common.js';
import { eslintPlugin } from './pkg/lib/esbuild-eslint-plugin.js';
import { stylelintPlugin } from './pkg/lib/esbuild-stylelint-plugin.js';
const useWasm = os.arch() !== 'x64';
const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default;
const production = process.env.NODE_ENV === 'production';
const watchMode = process.env.ESBUILD_WATCH === "true";
// linters dominate the build time, so disable them for production builds by default, but enable in watch mode
const lint = process.env.LINT ? (process.env.LINT !== 0) : (watchMode || !production);
// List of directories to use when using import statements
const nodePaths = ['pkg/lib'];
const outdir = 'dist';
// Obtain package name from package.json
const packageJson = JSON.parse(fs.readFileSync('package.json'));
function notifyEndPlugin() {
return {
name: 'notify-end',
setup(build) {
let startTime;
build.onStart(() => {
startTime = new Date();
});
build.onEnd(() => {
const endTime = new Date();
const timeStamp = endTime.toTimeString().split(' ')[0];
console.log(`${timeStamp}: Build finished in ${endTime - startTime} ms`);
});
}
};
}
const cwd = process.cwd();
// similar to fs.watch(), but recursively watches all subdirectories
function watch_dirs(dir, on_change) {
const callback = (ev, dir, fname) => {
// only listen for "change" events, as renames are noisy
// ignore hidden files
const isHidden = /^\./.test(fname);
if (ev !== "change" || isHidden) {
return;
}
on_change(path.join(dir, fname));
};
fs.watch(dir, {}, (ev, path) => callback(ev, dir, path));
// watch all subdirectories in dir
const d = fs.opendirSync(dir);
let dirent;
while ((dirent = d.readSync()) !== null) {
if (dirent.isDirectory())
watch_dirs(path.join(dir, dirent.name), on_change);
}
d.closeSync();
}
const context = await esbuild.context({
...!production ? { sourcemap: "linked" } : {},
bundle: true,
entryPoints: ['./src/index.js'],
external: ['*.woff', '*.woff2', '*.jpg', '*.svg', '../../assets*'], // Allow external font files which live in ../../static/fonts
legalComments: 'external', // Move all legal comments to a .LEGAL.txt file
loader: { ".js": "jsx" },
minify: production,
nodePaths,
outdir,
target: ['es2020'],
plugins: [
cleanPlugin(),
...lint
? [
stylelintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(css?|scss?)$') }),
eslintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(jsx?|js?)$') })
]
: [],
// Esbuild will only copy assets that are explicitly imported and used
// in the code. This is a problem for index.html and manifest.json which are not imported
copy({
assets: [
{ from: ['./src/manifest.json'], to: ['./manifest.json'] },
{ from: ['./src/index.html'], to: ['./index.html'] },
]
}),
...esbuildStylesPlugins,
cockpitPoEsbuildPlugin(),
...production ? [cockpitCompressPlugin()] : [],
cockpitRsyncEsbuildPlugin({ dest: packageJson.name }),
notifyEndPlugin(),
]
});
try {
await context.rebuild();
} catch (e) {
if (!watchMode)
process.exit(1);
// ignore errors in watch mode
}
if (watchMode) {
const on_change = async path => {
console.log("change detected:", path);
await context.cancel();
try {
await context.rebuild();
} catch (e) {} // ignore in watch mode
};
watch_dirs('src', on_change);
// wait forever until Control-C
await new Promise(() => {});
}
context.dispose();

View File

@ -1,48 +1,31 @@
{
"name": "tailscale",
"description": "Tailscale application for Cockpit",
"type": "module",
"main": "index.js",
"main": "dist/index.js",
"repository": "git@github.com:spotsnel/tailscale-cockpit.git",
"author": "",
"license": "LGPL-2.1",
"scripts": {
"watch": "ESBUILD_WATCH='true' ./build.js",
"build": "./build.js",
"eslint": "eslint --ext .js --ext .jsx src/",
"eslint:fix": "eslint --fix --ext .js --ext .jsx src/",
"stylelint": "stylelint src/*{.css,scss}",
"stylelint:fix": "stylelint --fix src/*{.css,scss}"
"build": "webpack --mode production",
"watch": ""
},
"devDependencies": {
"@types/react": "^18.2.12",
"@types/react-dom": "^18.2.5",
"argparse": "^2.0.1",
"babel-loader": "^9.1.2",
"chrome-remote-interface": "^0.32.1",
"esbuild": "^0.17.15",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-replace": "^1.3.0",
"esbuild-sass-plugin": "^2.8.0",
"esbuild-wasm": "^0.17.16",
"eslint": "^8.13.0",
"eslint-config-standard": "^17.0.0-1",
"eslint-config-standard-jsx": "^11.0.0-1",
"eslint-config-standard-react": "^13.0.0",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"eslint-plugin-standard": "^5.0.0",
"htmlparser": "^1.7.7",
"jed": "^1.1.1",
"po2json": "^1.0.0-alpha",
"qunit": "^2.9.3",
"sass": "^1.61.0",
"sizzle": "^2.3.3",
"stylelint": "^14.9.1",
"stylelint-config-standard": "^25.0.0",
"stylelint-config-standard-scss": "^5.0.0",
"stylelint-formatter-pretty": "^3.2.0"
"ts-loader": "^9.4.3",
"typescript": "^4.8.4",
"webpack": "^5.87.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"dependencies": {
"@patternfly/patternfly": "5.0.0-alpha.64",

View File

@ -1,31 +0,0 @@
import cockpit from 'cockpit';
import React from 'react';
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
const _ = cockpit.gettext;
export class Application extends React.Component {
constructor() {
super();
this.state = { hostname: _("Unknown") };
cockpit.file('/etc/hostname').watch(content => {
this.setState({ hostname: content.trim() });
});
}
render() {
return (
<Card>
<CardTitle>Tailscale</CardTitle>
<CardBody>
<Alert
variant="info"
title={ cockpit.format(_("Running on $0"), this.state.hostname) }
/>
</CardBody>
</Card>
);
}
}

32
src/app.tsx Normal file
View File

@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
//import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
//import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
type ApplicationProps = {
}
type ApplicationState = {
response: string
}
export class Application extends React.Component<ApplicationProps, ApplicationState> {
state: ApplicationState = {
response: ""
}
constructor(props: ApplicationProps) {
super(props);
cockpit.spawn(['tailscale', 'status']).done(content => {
this.setState(state => ({response: content.trim()}));
});
}
render() {
return (
<pre>
{ this.state.response }
</pre>
);
}
}

View File

@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="index.css">
<script type="text/javascript" src="index.js"></script>
<script type="text/javascript" src="./base1/cockpit.js"></script>
<script type="text/javascript" src="bundle.js"></script>
<script type="text/javascript" src="po.js"></script>
</head>

View File

@ -1,11 +0,0 @@
import "cockpit-dark-theme";
import "patternfly/patternfly-5-cockpit.scss";
import React from 'react';
import ReactDOM from 'react-dom';
import { Application } from './app.jsx';
import './app.scss';
document.addEventListener("DOMContentLoaded", function () {
ReactDOM.render(React.createElement(Application, {}), document.getElementById('app'));
});

16
src/index.tsx Normal file
View File

@ -0,0 +1,16 @@
//import "cockpit-dark-theme";
//import "patternfly/patternfly-5-cockpit.scss";
import React from 'react';
import ReactDOM from 'react-dom';
import { Application } from './app';
//import './app.scss';
document.addEventListener("DOMContentLoaded", function () {
ReactDOM.render(
<Application />,
document.getElementById("app"),
)
});

104
tsconfig.json Normal file
View File

@ -0,0 +1,104 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
"typeRoots": ["@types"], /* Specify multiple folders that act like `./node_modules/@types`. */
//"types": ["src/types"], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": false, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"jsx": "react"
},
"include": ["src"],
}

27
webpack.config.js Normal file
View File

@ -0,0 +1,27 @@
const path = require('path');
module.exports = {
devtool: "source-map",
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
static: path.join(__dirname, "dist"),
compress: true,
port: 4000,
},
};

3568
yarn.lock

File diff suppressed because it is too large Load Diff