megit-app 0.9.0 → 0.11.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/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <title>megit</title>
8
- <script type="module" crossorigin src="/assets/index-BlIDqqEW.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-ByiPZ5H3.css">
8
+ <script type="module" crossorigin src="/assets/index-JAmzg-th.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BXzSxW-2.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  import { createApp, serveStatic } from './http.js';
2
2
  import { execFile } from 'node:child_process';
3
- import { existsSync, readdirSync } from 'node:fs';
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
4
4
  import { readFile, realpath, writeFile } from 'node:fs/promises';
5
5
  import { homedir } from 'node:os';
6
6
  import { dirname, join, resolve, sep } from 'node:path';
@@ -941,6 +941,66 @@ app.get('/api/blob', repoGuard, async (req, res) => {
941
941
  res.status(404).end();
942
942
  }
943
943
  });
944
+ // The repo's own favicon, used as its icon in the picker. Checked against the
945
+ // framework conventions people actually ship; no hit is a 404 and the client
946
+ // falls back to a letter chip. Not repoGuard'd: a recent repo need not be open,
947
+ // but it must still be one the user has added at some point.
948
+ // Most projects don't ship a file literally called favicon.*: they declare one
949
+ // in their entry html (megit's own is /logo.svg). Reading that link first is
950
+ // what makes the icon right rather than merely present.
951
+ function declaredIcon(repo) {
952
+ for (const html of ['index.html', 'public/index.html', 'app/index.html', 'src/index.html']) {
953
+ const file = join(repo, html);
954
+ if (!existsSync(file))
955
+ continue;
956
+ const href = /<link[^>]+rel=["'][^"']*icon[^"']*["'][^>]*>/i.exec(readFileSync(file, 'utf8'))?.[0]
957
+ ?.match(/href=["']([^"']+)["']/i)?.[1];
958
+ // data: and http: hrefs have nothing on disk to serve
959
+ if (!href || /^[a-z]+:/i.test(href))
960
+ continue;
961
+ const rel = href.split(/[?#]/)[0].replace(/^\/+/, '');
962
+ return [rel, join('public', rel), join('static', rel), join(dirname(html), rel)];
963
+ }
964
+ return [];
965
+ }
966
+ const FAVICONS = [
967
+ 'public/favicon.svg', 'public/favicon.ico', 'public/favicon.png', 'public/favicon-32x32.png',
968
+ 'app/favicon.ico', 'src/favicon.svg', 'static/favicon.svg', 'static/favicon.ico', 'static/favicon.png',
969
+ 'assets/favicon.ico', 'favicon.svg', 'favicon.ico', 'favicon.png',
970
+ ];
971
+ app.get('/api/favicon', async (req, res) => {
972
+ const repo = String(req.query.repo ?? '');
973
+ const cfg = loadConfig();
974
+ if (!cfg.repos.includes(repo) && !cfg.recent.includes(repo)) {
975
+ res.status(400).json({ error: 'unknown repo' });
976
+ return;
977
+ }
978
+ const hit = [...declaredIcon(repo), ...FAVICONS].map(f => join(repo, f)).find(f => existsSync(f) && MIME[f.split('.').pop().toLowerCase()]);
979
+ if (!hit) {
980
+ res.status(404).end();
981
+ return;
982
+ }
983
+ try {
984
+ // same symlink escape as /api/blob: a clone can carry public/favicon.ico ->
985
+ // /etc/passwd, and serving it would leak the bytes under an image type
986
+ const abs = await realpath(hit);
987
+ const root = await realpath(repo);
988
+ if (!abs.startsWith(root + sep)) {
989
+ res.status(400).json({ error: 'path outside repo' });
990
+ return;
991
+ }
992
+ // repo content is untrusted (an SVG can carry script)
993
+ res.set('Content-Security-Policy', "sandbox; default-src 'none'").set('X-Content-Type-Options', 'nosniff');
994
+ // A project's icon changes about never, and every picker open would otherwise
995
+ // re-walk the filesystem for each recent repo. Short enough that swapping a
996
+ // favicon shows up without a restart.
997
+ res.set('Cache-Control', 'private, max-age=300');
998
+ res.type(MIME[hit.split('.').pop().toLowerCase()]).send(await readFile(abs));
999
+ }
1000
+ catch {
1001
+ res.status(404).end();
1002
+ }
1003
+ });
944
1004
  const dist = join(import.meta.dirname, '..', 'dist');
945
1005
  if (existsSync(dist))
946
1006
  app.fallback(serveStatic(dist));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "megit-app",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Git repository viewer in the browser: commit graph with branch lanes, diffs, stashes and WIP",
5
5
  "license": "MIT",
6
6
  "author": "Hoang Vuong Vu",
@@ -38,7 +38,7 @@
38
38
  "test": "vitest run",
39
39
  "prepublishOnly": "pnpm test && pnpm build && pnpm build:server"
40
40
  },
41
- "packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
41
+ "packageManager": "pnpm@11.25.0+sha512.5cde925b4f075f725eb71fbae18a42ffe784524789f19b61c731cb8721ec28aaee160e01a8d5af4fedb2a42cdbf300efe23db356b0d4a17b4d63e11f8ab7c956",
42
42
  "dependencies": {
43
43
  "ws": "^8.21.3"
44
44
  },
@@ -46,11 +46,11 @@
46
46
  "node-pty": "^1.1.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@types/node": "^26.4.0",
49
+ "@types/node": "^26.4.1",
50
50
  "@types/react": "^19.2.18",
51
- "@types/react-dom": "^19.2.5",
51
+ "@types/react-dom": "^19.2.7",
52
52
  "@types/ws": "^8.18.1",
53
- "@vitejs/plugin-react": "^6.1.0",
53
+ "@vitejs/plugin-react": "^6.1.1",
54
54
  "@xterm/addon-fit": "^0.11.0",
55
55
  "@xterm/xterm": "^6.0.0",
56
56
  "concurrently": "^10.0.5",
@@ -60,6 +60,6 @@
60
60
  "react-dom": "^19.2.8",
61
61
  "typescript": "^7.0.2",
62
62
  "vite": "^8.2.2",
63
- "vitest": "^4.1.11"
63
+ "vitest": "^5.0.0"
64
64
  }
65
65
  }