dbdiff-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/bin/cli.js +83 -0
- package/bin/install-local.js +57 -0
- package/electron/generate-icon.mjs +54 -0
- package/electron/icon.icns +0 -0
- package/electron/icon.png +0 -0
- package/electron/icon.svg +21 -0
- package/electron/main.js +169 -0
- package/electron/patch-dev-plist.js +31 -0
- package/electron/preload.cjs +18 -0
- package/electron/wait-for-vite.js +43 -0
- package/index.html +13 -0
- package/package.json +91 -0
- package/public/favicon.svg +15 -0
- package/public/vite.svg +1 -0
- package/server/export.ts +57 -0
- package/server/index.ts +392 -0
- package/src/App.css +1 -0
- package/src/App.tsx +543 -0
- package/src/assets/react.svg +1 -0
- package/src/components/CommandPalette.tsx +243 -0
- package/src/components/ConnectedView.tsx +78 -0
- package/src/components/ConnectionPicker.tsx +381 -0
- package/src/components/ConsoleView.tsx +360 -0
- package/src/components/CsvExportModal.tsx +144 -0
- package/src/components/DataGrid/DataGrid.tsx +262 -0
- package/src/components/DataGrid/DataGridCell.tsx +73 -0
- package/src/components/DataGrid/DataGridHeader.tsx +89 -0
- package/src/components/DataGrid/index.ts +20 -0
- package/src/components/DataGrid/types.ts +63 -0
- package/src/components/DataGrid/useColumnResize.ts +153 -0
- package/src/components/DataGrid/useDataGridSelection.ts +340 -0
- package/src/components/DataGrid/utils.ts +184 -0
- package/src/components/DatabaseMenu.tsx +93 -0
- package/src/components/DatabaseSwitcher.tsx +208 -0
- package/src/components/DiffView.tsx +215 -0
- package/src/components/EditConnectionModal.tsx +417 -0
- package/src/components/ErrorBoundary.tsx +69 -0
- package/src/components/GlobalShortcuts.tsx +201 -0
- package/src/components/InnerTabBar.tsx +129 -0
- package/src/components/JsonTreeViewer.tsx +387 -0
- package/src/components/MemberAccessEditor.tsx +443 -0
- package/src/components/MembersModal.tsx +446 -0
- package/src/components/NewConnectionModal.tsx +274 -0
- package/src/components/Resizer.tsx +66 -0
- package/src/components/ScanSuccessModal.tsx +113 -0
- package/src/components/ShortcutSettingsModal.tsx +318 -0
- package/src/components/Sidebar.tsx +532 -0
- package/src/components/TabBar.tsx +188 -0
- package/src/components/TableView.tsx +2147 -0
- package/src/components/ThemeToggle.tsx +44 -0
- package/src/components/index.ts +17 -0
- package/src/constants.ts +12 -0
- package/src/electron.d.ts +12 -0
- package/src/index.css +44 -0
- package/src/main.tsx +13 -0
- package/src/stores/hooks.ts +1146 -0
- package/src/stores/index.ts +12 -0
- package/src/stores/store.ts +1514 -0
- package/src/stores/useCloudSync.ts +274 -0
- package/src/stores/useSyncDatabase.ts +422 -0
- package/src/types.ts +277 -0
- package/src/utils/csv.ts +27 -0
- package/src/vite-env.d.ts +2 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/tsconfig.server.json +14 -0
- package/vite.config.ts +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default defineConfig([
|
|
20
|
+
globalIgnores(['dist']),
|
|
21
|
+
{
|
|
22
|
+
files: ['**/*.{ts,tsx}'],
|
|
23
|
+
extends: [
|
|
24
|
+
// Other configs...
|
|
25
|
+
|
|
26
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
27
|
+
tseslint.configs.recommendedTypeChecked,
|
|
28
|
+
// Alternatively, use this for stricter rules
|
|
29
|
+
tseslint.configs.strictTypeChecked,
|
|
30
|
+
// Optionally, add this for stylistic rules
|
|
31
|
+
tseslint.configs.stylisticTypeChecked,
|
|
32
|
+
|
|
33
|
+
// Other configs...
|
|
34
|
+
],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
38
|
+
tsconfigRootDir: import.meta.dirname,
|
|
39
|
+
},
|
|
40
|
+
// other options...
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// eslint.config.js
|
|
50
|
+
import reactX from 'eslint-plugin-react-x'
|
|
51
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
52
|
+
|
|
53
|
+
export default defineConfig([
|
|
54
|
+
globalIgnores(['dist']),
|
|
55
|
+
{
|
|
56
|
+
files: ['**/*.{ts,tsx}'],
|
|
57
|
+
extends: [
|
|
58
|
+
// Other configs...
|
|
59
|
+
// Enable lint rules for React
|
|
60
|
+
reactX.configs['recommended-typescript'],
|
|
61
|
+
// Enable lint rules for React DOM
|
|
62
|
+
reactDom.configs.recommended,
|
|
63
|
+
],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parserOptions: {
|
|
66
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
67
|
+
tsconfigRootDir: import.meta.dirname,
|
|
68
|
+
},
|
|
69
|
+
// other options...
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const packageDir = path.join(__dirname, "..");
|
|
10
|
+
const command = process.argv[2];
|
|
11
|
+
|
|
12
|
+
if (command !== "install-from-source") {
|
|
13
|
+
console.log("Usage: npx dbdiff-app install-from-source");
|
|
14
|
+
console.log(" Builds dbdiff from source and installs to /Applications/");
|
|
15
|
+
process.exit(command ? 1 : 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (process.platform !== "darwin") {
|
|
19
|
+
console.error("install-from-source is currently macOS only.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pkg = JSON.parse(
|
|
24
|
+
fs.readFileSync(path.join(packageDir, "package.json"), "utf-8"),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
console.log(`\nInstalling dbdiff v${pkg.version} from source...\n`);
|
|
28
|
+
|
|
29
|
+
// Step 1: Install all dependencies (including devDependencies needed for building)
|
|
30
|
+
console.log("Installing dependencies...");
|
|
31
|
+
execSync("npm install --include=dev", { cwd: packageDir, stdio: "inherit" });
|
|
32
|
+
|
|
33
|
+
// Step 2: Build the Electron app
|
|
34
|
+
console.log("\nBuilding...");
|
|
35
|
+
execSync("npm run build:electron", { cwd: packageDir, stdio: "inherit" });
|
|
36
|
+
|
|
37
|
+
// Step 3: Find the built .app bundle
|
|
38
|
+
const releaseDir = path.join(packageDir, "release");
|
|
39
|
+
const entries = fs.readdirSync(releaseDir).filter((e) => e.startsWith("mac"));
|
|
40
|
+
let appPath = null;
|
|
41
|
+
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const candidate = path.join(releaseDir, entry, "dbdiff.app");
|
|
44
|
+
if (fs.existsSync(candidate)) {
|
|
45
|
+
appPath = candidate;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!appPath) {
|
|
51
|
+
console.error("Build failed — could not find dbdiff.app in release/");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Step 4: Install to /Applications
|
|
56
|
+
const dest = "/Applications/dbdiff.app";
|
|
57
|
+
|
|
58
|
+
let wasRunning = false;
|
|
59
|
+
try {
|
|
60
|
+
execSync("pkill -f '/Applications/dbdiff.app'", { stdio: "pipe" });
|
|
61
|
+
wasRunning = true;
|
|
62
|
+
console.log("\nQuit running dbdiff instance.");
|
|
63
|
+
} catch {
|
|
64
|
+
// Not running — that's fine
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (fs.existsSync(dest)) {
|
|
68
|
+
console.log("Removing existing /Applications/dbdiff.app...");
|
|
69
|
+
execSync(`rm -rf "${dest}"`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
`Copying ${path.basename(path.dirname(appPath))}/dbdiff.app → /Applications/`,
|
|
74
|
+
);
|
|
75
|
+
execSync(`cp -R "${appPath}" "${dest}"`);
|
|
76
|
+
|
|
77
|
+
console.log("\nInstalled dbdiff to /Applications/dbdiff.app");
|
|
78
|
+
console.log("Launch it from Spotlight or your Applications folder.\n");
|
|
79
|
+
|
|
80
|
+
if (wasRunning) {
|
|
81
|
+
console.log("Reopening dbdiff...");
|
|
82
|
+
execSync("open /Applications/dbdiff.app");
|
|
83
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const releaseDir = path.join(__dirname, "..", "release");
|
|
10
|
+
|
|
11
|
+
// Find the .app bundle — electron-builder outputs to release/mac-<arch>/
|
|
12
|
+
const entries = fs.readdirSync(releaseDir).filter((e) => e.startsWith("mac"));
|
|
13
|
+
let appPath = null;
|
|
14
|
+
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const candidate = path.join(releaseDir, entry, "dbdiff.app");
|
|
17
|
+
if (fs.existsSync(candidate)) {
|
|
18
|
+
appPath = candidate;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!appPath) {
|
|
24
|
+
console.error(
|
|
25
|
+
"Could not find dbdiff.app in release/. Run `npm run dist:dir` first.",
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dest = "/Applications/dbdiff.app";
|
|
31
|
+
|
|
32
|
+
// Kill dbdiff if it's currently running
|
|
33
|
+
let wasRunning = false;
|
|
34
|
+
try {
|
|
35
|
+
execSync("pkill -f '/Applications/dbdiff.app'", { stdio: "pipe" });
|
|
36
|
+
wasRunning = true;
|
|
37
|
+
console.log("Quit running dbdiff instance.");
|
|
38
|
+
} catch {
|
|
39
|
+
// pkill exits non-zero when no processes match — that's fine
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (fs.existsSync(dest)) {
|
|
43
|
+
console.log("Removing existing /Applications/dbdiff.app...");
|
|
44
|
+
execSync(`rm -rf "${dest}"`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(
|
|
48
|
+
`Copying ${path.basename(path.dirname(appPath))}/dbdiff.app → /Applications/`,
|
|
49
|
+
);
|
|
50
|
+
execSync(`cp -R "${appPath}" "${dest}"`);
|
|
51
|
+
|
|
52
|
+
console.log("Installed!");
|
|
53
|
+
|
|
54
|
+
if (wasRunning) {
|
|
55
|
+
console.log("Reopening dbdiff...");
|
|
56
|
+
execSync("open /Applications/dbdiff.app");
|
|
57
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
import { mkdirSync, readFileSync, rmSync } from "fs";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const svgPath = join(__dirname, "icon.svg");
|
|
9
|
+
const iconsetDir = join(__dirname, "icon.iconset");
|
|
10
|
+
const icnsPath = join(__dirname, "icon.icns");
|
|
11
|
+
|
|
12
|
+
const svg = readFileSync(svgPath);
|
|
13
|
+
|
|
14
|
+
// macOS iconset sizes
|
|
15
|
+
const sizes = [
|
|
16
|
+
{ name: "icon_16x16.png", size: 16 },
|
|
17
|
+
{ name: "icon_16x16@2x.png", size: 32 },
|
|
18
|
+
{ name: "icon_32x32.png", size: 32 },
|
|
19
|
+
{ name: "icon_32x32@2x.png", size: 64 },
|
|
20
|
+
{ name: "icon_128x128.png", size: 128 },
|
|
21
|
+
{ name: "icon_128x128@2x.png", size: 256 },
|
|
22
|
+
{ name: "icon_256x256.png", size: 256 },
|
|
23
|
+
{ name: "icon_256x256@2x.png", size: 512 },
|
|
24
|
+
{ name: "icon_512x512.png", size: 512 },
|
|
25
|
+
{ name: "icon_512x512@2x.png", size: 1024 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Clean and create iconset directory
|
|
29
|
+
rmSync(iconsetDir, { recursive: true, force: true });
|
|
30
|
+
mkdirSync(iconsetDir, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Generate all sizes
|
|
33
|
+
await Promise.all(
|
|
34
|
+
sizes.map(({ name, size }) =>
|
|
35
|
+
sharp(svg, { density: Math.round((72 * size) / 1024) * 10 })
|
|
36
|
+
.resize(size, size)
|
|
37
|
+
.png()
|
|
38
|
+
.toFile(join(iconsetDir, name)),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Convert to .icns using macOS iconutil
|
|
43
|
+
execSync(`iconutil -c icns "${iconsetDir}" -o "${icnsPath}"`);
|
|
44
|
+
|
|
45
|
+
// Also generate a 1024px PNG for Electron's icon option (used on Linux/Windows)
|
|
46
|
+
await sharp(svg, { density: 720 })
|
|
47
|
+
.resize(1024, 1024)
|
|
48
|
+
.png()
|
|
49
|
+
.toFile(join(__dirname, "icon.png"));
|
|
50
|
+
|
|
51
|
+
// Clean up iconset directory
|
|
52
|
+
rmSync(iconsetDir, { recursive: true, force: true });
|
|
53
|
+
|
|
54
|
+
console.log("Generated icon.icns and icon.png");
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
|
2
|
+
<!-- macOS icon grid: 824x824 content area centered in 1024x1024 (100px padding) -->
|
|
3
|
+
<!-- Background rect — macOS applies squircle mask automatically -->
|
|
4
|
+
<rect x="100" y="100" width="824" height="824" rx="185" ry="185" fill="#1e1b4b"/>
|
|
5
|
+
|
|
6
|
+
<!-- Database cylinders centered within the 824x824 content area -->
|
|
7
|
+
<g transform="translate(512, 512) scale(18) translate(-16, -16)">
|
|
8
|
+
<ellipse cx="11" cy="8" rx="8" ry="3" fill="#6366f1" opacity="0.7"/>
|
|
9
|
+
<rect x="3" y="8" width="16" height="14" fill="#6366f1" opacity="0.7"/>
|
|
10
|
+
<ellipse cx="11" cy="22" rx="8" ry="3" fill="#6366f1" opacity="0.7"/>
|
|
11
|
+
|
|
12
|
+
<!-- Database cylinder 2 (right, in front) -->
|
|
13
|
+
<ellipse cx="21" cy="10" rx="8" ry="3" fill="#a5b4fc"/>
|
|
14
|
+
<rect x="13" y="10" width="16" height="14" fill="#a5b4fc"/>
|
|
15
|
+
<ellipse cx="21" cy="24" rx="8" ry="3" fill="#a5b4fc"/>
|
|
16
|
+
|
|
17
|
+
<!-- Inner lines for database look -->
|
|
18
|
+
<ellipse cx="21" cy="14" rx="8" ry="3" fill="none" stroke="#6366f1" stroke-width="0.5" opacity="0.5"/>
|
|
19
|
+
<ellipse cx="21" cy="18" rx="8" ry="3" fill="none" stroke="#6366f1" stroke-width="0.5" opacity="0.5"/>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
package/electron/main.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { app, BrowserWindow, Menu, ipcMain } from "electron";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const isMac = process.platform === "darwin";
|
|
7
|
+
|
|
8
|
+
let win;
|
|
9
|
+
const devServerURL = process.env.VITE_DEV_SERVER_URL;
|
|
10
|
+
const serverPort = process.env.PORT || "4088";
|
|
11
|
+
|
|
12
|
+
// Database menu items — kept as references so we can enable/disable them
|
|
13
|
+
let exportSchemaItem;
|
|
14
|
+
let exportSchemaAndDataItem;
|
|
15
|
+
|
|
16
|
+
function sendMenuAction(action) {
|
|
17
|
+
win?.webContents.send("menu-action", action);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildMenu() {
|
|
21
|
+
exportSchemaItem = {
|
|
22
|
+
label: "Export Schema",
|
|
23
|
+
enabled: false,
|
|
24
|
+
click: () => sendMenuAction("export-schema"),
|
|
25
|
+
};
|
|
26
|
+
exportSchemaAndDataItem = {
|
|
27
|
+
label: "Export Schema + Data",
|
|
28
|
+
enabled: false,
|
|
29
|
+
click: () => sendMenuAction("export-schema-and-data"),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const template = [
|
|
33
|
+
// App menu (macOS only) — includes dbdiff-specific items
|
|
34
|
+
...(isMac
|
|
35
|
+
? [
|
|
36
|
+
{
|
|
37
|
+
label: app.name,
|
|
38
|
+
submenu: [
|
|
39
|
+
{ role: "about" },
|
|
40
|
+
{ type: "separator" },
|
|
41
|
+
{
|
|
42
|
+
label: "Shortcut Settings...",
|
|
43
|
+
click: () => sendMenuAction("shortcut-settings"),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: "Scan Localhost",
|
|
47
|
+
click: () => sendMenuAction("scan-localhost"),
|
|
48
|
+
},
|
|
49
|
+
{ type: "separator" },
|
|
50
|
+
{
|
|
51
|
+
label: "Reset UI State",
|
|
52
|
+
click: () => sendMenuAction("reset-ui-state"),
|
|
53
|
+
},
|
|
54
|
+
{ type: "separator" },
|
|
55
|
+
{ role: "hide" },
|
|
56
|
+
{ role: "hideOthers" },
|
|
57
|
+
{ role: "unhide" },
|
|
58
|
+
{ type: "separator" },
|
|
59
|
+
{ role: "quit" },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
64
|
+
// Database menu (macOS only — on other platforms these stay in the web UI)
|
|
65
|
+
...(isMac
|
|
66
|
+
? [
|
|
67
|
+
{
|
|
68
|
+
label: "Database",
|
|
69
|
+
submenu: [exportSchemaItem, exportSchemaAndDataItem],
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
: []),
|
|
73
|
+
{
|
|
74
|
+
label: "Edit",
|
|
75
|
+
submenu: [
|
|
76
|
+
{ role: "undo" },
|
|
77
|
+
{ role: "redo" },
|
|
78
|
+
{ type: "separator" },
|
|
79
|
+
{ role: "cut" },
|
|
80
|
+
{ role: "copy" },
|
|
81
|
+
{ role: "paste" },
|
|
82
|
+
{ role: "selectAll" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
label: "View",
|
|
87
|
+
submenu: [
|
|
88
|
+
{ role: "reload" },
|
|
89
|
+
{ role: "forceReload" },
|
|
90
|
+
{ role: "toggleDevTools" },
|
|
91
|
+
{ type: "separator" },
|
|
92
|
+
{ role: "resetZoom" },
|
|
93
|
+
{ role: "zoomIn" },
|
|
94
|
+
{ role: "zoomOut" },
|
|
95
|
+
{ role: "togglefullscreen" },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function createWindow() {
|
|
104
|
+
// In dev mode, the Express server is already running externally via tsx watch.
|
|
105
|
+
// In production, we start it ourselves from the built output.
|
|
106
|
+
if (!devServerURL) {
|
|
107
|
+
const { serverReady } = await import("../dist-server/server/index.js");
|
|
108
|
+
await serverReady;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
win = new BrowserWindow({
|
|
112
|
+
width: 1400,
|
|
113
|
+
height: 900,
|
|
114
|
+
icon: path.join(__dirname, "icon.png"),
|
|
115
|
+
titleBarStyle: isMac ? "hiddenInset" : "default",
|
|
116
|
+
webPreferences: {
|
|
117
|
+
nodeIntegration: false,
|
|
118
|
+
contextIsolation: true,
|
|
119
|
+
preload: path.join(__dirname, "preload.cjs"),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
buildMenu();
|
|
124
|
+
|
|
125
|
+
// Renderer tells us when a database is active so we can enable/disable menu items
|
|
126
|
+
ipcMain.on("set-database-menu-enabled", (_event, enabled) => {
|
|
127
|
+
if (exportSchemaItem) exportSchemaItem.enabled = enabled;
|
|
128
|
+
if (exportSchemaAndDataItem) exportSchemaAndDataItem.enabled = enabled;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// In dev mode, load from Vite dev server (HMR). Otherwise use the Express server.
|
|
132
|
+
// Handle child windows opened via window.open() (e.g. cloud auth popup).
|
|
133
|
+
// When the auth flow redirects back to localhost with key+state params,
|
|
134
|
+
// forward them to the main window and close the popup.
|
|
135
|
+
win.webContents.on("did-create-window", (childWindow) => {
|
|
136
|
+
childWindow.webContents.on("did-navigate", (_event, url) => {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = new URL(url);
|
|
139
|
+
if (
|
|
140
|
+
(parsed.hostname === "localhost" ||
|
|
141
|
+
parsed.hostname === "127.0.0.1") &&
|
|
142
|
+
parsed.searchParams.has("key") &&
|
|
143
|
+
parsed.searchParams.has("state")
|
|
144
|
+
) {
|
|
145
|
+
win.webContents.send("cloud-auth-callback", {
|
|
146
|
+
key: parsed.searchParams.get("key"),
|
|
147
|
+
state: parsed.searchParams.get("state"),
|
|
148
|
+
});
|
|
149
|
+
childWindow.close();
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// ignore malformed URLs
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
win.loadURL(devServerURL || `http://localhost:${serverPort}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
app.whenReady().then(() => {
|
|
161
|
+
if (isMac) {
|
|
162
|
+
app.dock.setIcon(path.join(__dirname, "icon.png"));
|
|
163
|
+
}
|
|
164
|
+
createWindow();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.on("window-all-closed", () => {
|
|
168
|
+
app.quit();
|
|
169
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patches the Electron.app Info.plist so the macOS menu bar shows "dbdiff"
|
|
3
|
+
* instead of "Electron" during development.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const electronPath = dirname(require.resolve("electron"));
|
|
11
|
+
const plistPath = join(
|
|
12
|
+
electronPath,
|
|
13
|
+
"dist",
|
|
14
|
+
"Electron.app",
|
|
15
|
+
"Contents",
|
|
16
|
+
"Info.plist",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const appName = "dbdiff";
|
|
20
|
+
let plist = readFileSync(plistPath, "utf8");
|
|
21
|
+
|
|
22
|
+
plist = plist.replace(
|
|
23
|
+
/(<key>CFBundleDisplayName<\/key>\s*<string>)[^<]*(<\/string>)/,
|
|
24
|
+
`$1${appName}$2`,
|
|
25
|
+
);
|
|
26
|
+
plist = plist.replace(
|
|
27
|
+
/(<key>CFBundleName<\/key>\s*<string>)[^<]*(<\/string>)/,
|
|
28
|
+
`$1${appName}$2`,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
writeFileSync(plistPath, plist);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require("electron");
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld("electronAPI", {
|
|
4
|
+
onMenuAction: (callback) => {
|
|
5
|
+
const handler = (_event, action) => callback(action);
|
|
6
|
+
ipcRenderer.on("menu-action", handler);
|
|
7
|
+
return () => ipcRenderer.removeListener("menu-action", handler);
|
|
8
|
+
},
|
|
9
|
+
setDatabaseMenuEnabled: (enabled) => {
|
|
10
|
+
ipcRenderer.send("set-database-menu-enabled", enabled);
|
|
11
|
+
},
|
|
12
|
+
onCloudAuthCallback: (callback) => {
|
|
13
|
+
const handler = (_event, data) => callback(data);
|
|
14
|
+
ipcRenderer.on("cloud-auth-callback", handler);
|
|
15
|
+
return () => ipcRenderer.removeListener("cloud-auth-callback", handler);
|
|
16
|
+
},
|
|
17
|
+
platform: process.platform,
|
|
18
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Waits for the Vite dev server to be ready, then launches Electron.
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const VITE_URL = "http://localhost:4089";
|
|
8
|
+
|
|
9
|
+
async function waitForServer(url, timeoutMs = 30000) {
|
|
10
|
+
const start = Date.now();
|
|
11
|
+
while (Date.now() - start < timeoutMs) {
|
|
12
|
+
try {
|
|
13
|
+
await fetch(url);
|
|
14
|
+
return;
|
|
15
|
+
} catch {
|
|
16
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Timed out waiting for ${url}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
console.log("Waiting for Vite dev server...");
|
|
24
|
+
await waitForServer(VITE_URL);
|
|
25
|
+
console.log("Vite ready — launching Electron");
|
|
26
|
+
|
|
27
|
+
const electronBin = path.resolve(
|
|
28
|
+
__dirname,
|
|
29
|
+
"..",
|
|
30
|
+
"node_modules",
|
|
31
|
+
".bin",
|
|
32
|
+
"electron",
|
|
33
|
+
);
|
|
34
|
+
const child = spawn(electronBin, ["."], {
|
|
35
|
+
cwd: path.resolve(__dirname, ".."),
|
|
36
|
+
stdio: "inherit",
|
|
37
|
+
env: { ...process.env, VITE_DEV_SERVER_URL: VITE_URL },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
child.on("close", (code) => process.exit(code));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main();
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>dbdiff</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dbdiff-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A database client for PostgreSQL",
|
|
5
|
+
"author": "Louis Arge",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "electron/main.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"dbdiff-app": "./bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"server",
|
|
15
|
+
"electron",
|
|
16
|
+
"public",
|
|
17
|
+
"index.html",
|
|
18
|
+
"vite.config.ts",
|
|
19
|
+
"tsconfig.json",
|
|
20
|
+
"tsconfig.app.json",
|
|
21
|
+
"tsconfig.server.json",
|
|
22
|
+
"tsconfig.node.json"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "concurrently -k -n vite,server -c blue,green \"vite\" \"tsx watch --clear-screen=false server/index.ts\"",
|
|
26
|
+
"dev:electron": "node electron/patch-dev-plist.js && concurrently -k -n vite,server,electron -c blue,green,magenta \"vite\" \"tsx watch --clear-screen=false server/index.ts\" \"node electron/wait-for-vite.js\"",
|
|
27
|
+
"build:vite": "tsc -b && vite build",
|
|
28
|
+
"build:server": "tsc -p tsconfig.server.json",
|
|
29
|
+
"build": "npm run build:vite && npm run build:server",
|
|
30
|
+
"build:electron": "npm run build && electron-builder --mac --dir",
|
|
31
|
+
"start": "node electron/patch-dev-plist.js && electron .",
|
|
32
|
+
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\" \"server/**/*.ts\"",
|
|
35
|
+
"install-local-production": "npm run build:electron && node bin/install-local.js"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@codemirror/lang-sql": "^6.10.0",
|
|
39
|
+
"@tanstack/react-virtual": "^3.13.18",
|
|
40
|
+
"@uiw/react-codemirror": "^4.25.4",
|
|
41
|
+
"express": "^4.21.0",
|
|
42
|
+
"fuse.js": "^7.1.0",
|
|
43
|
+
"lucide-react": "^0.563.0",
|
|
44
|
+
"pg": "^8.13.0",
|
|
45
|
+
"react": "^19.2.0",
|
|
46
|
+
"react-dom": "^19.2.0",
|
|
47
|
+
"zustand": "^5.0.11"
|
|
48
|
+
},
|
|
49
|
+
"build": {
|
|
50
|
+
"appId": "app.dbdiff.client",
|
|
51
|
+
"productName": "dbdiff",
|
|
52
|
+
"asar": false,
|
|
53
|
+
"files": [
|
|
54
|
+
"electron/",
|
|
55
|
+
"dist/",
|
|
56
|
+
"dist-server/",
|
|
57
|
+
"package.json"
|
|
58
|
+
],
|
|
59
|
+
"mac": {
|
|
60
|
+
"icon": "electron/icon.icns",
|
|
61
|
+
"identity": null,
|
|
62
|
+
"target": "dir"
|
|
63
|
+
},
|
|
64
|
+
"directories": {
|
|
65
|
+
"output": "release"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@eslint/js": "^9.39.1",
|
|
70
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
71
|
+
"@types/express": "^4.17.21",
|
|
72
|
+
"@types/node": "^24.10.1",
|
|
73
|
+
"@types/pg": "^8.11.10",
|
|
74
|
+
"@types/react": "^19.2.5",
|
|
75
|
+
"@types/react-dom": "^19.2.3",
|
|
76
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
77
|
+
"concurrently": "^9.1.0",
|
|
78
|
+
"electron": "^35.1.2",
|
|
79
|
+
"electron-builder": "^26.0.12",
|
|
80
|
+
"eslint": "^9.39.1",
|
|
81
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
82
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
83
|
+
"globals": "^16.5.0",
|
|
84
|
+
"prettier": "^3.8.1",
|
|
85
|
+
"tailwindcss": "^4.1.18",
|
|
86
|
+
"tsx": "^4.19.0",
|
|
87
|
+
"typescript": "~5.9.3",
|
|
88
|
+
"typescript-eslint": "^8.46.4",
|
|
89
|
+
"vite": "^7.2.4"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<!-- Database cylinder 1 (left, slightly behind) -->
|
|
3
|
+
<ellipse cx="11" cy="8" rx="8" ry="3" fill="#6366f1" opacity="0.7"/>
|
|
4
|
+
<rect x="3" y="8" width="16" height="14" fill="#6366f1" opacity="0.7"/>
|
|
5
|
+
<ellipse cx="11" cy="22" rx="8" ry="3" fill="#6366f1" opacity="0.7"/>
|
|
6
|
+
|
|
7
|
+
<!-- Database cylinder 2 (right, in front) -->
|
|
8
|
+
<ellipse cx="21" cy="10" rx="8" ry="3" fill="#a5b4fc"/>
|
|
9
|
+
<rect x="13" y="10" width="16" height="14" fill="#a5b4fc"/>
|
|
10
|
+
<ellipse cx="21" cy="24" rx="8" ry="3" fill="#a5b4fc"/>
|
|
11
|
+
|
|
12
|
+
<!-- Inner lines for database look -->
|
|
13
|
+
<ellipse cx="21" cy="14" rx="8" ry="3" fill="none" stroke="#6366f1" stroke-width="0.5" opacity="0.5"/>
|
|
14
|
+
<ellipse cx="21" cy="18" rx="8" ry="3" fill="none" stroke="#6366f1" stroke-width="0.5" opacity="0.5"/>
|
|
15
|
+
</svg>
|