website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
60e8429
— pyrossh
2026-07-04T15:14:01+05:30
style: fix formatting issues from merge
- .alchemy/prd/repos-bucket-prd.json +1 -1
- .alchemy/prd/rewrite-rules-prd.json +2 -5
- .alchemy/prd/website-bucket-prd.json +3 -7
- .alchemy/prd/website-bucket-prd/pyrossh.dev.json +1 -1
- .alchemy/prd/zone-prd.json +2 -5
- .vscode/extensions.json +1 -1
- README.md +3 -3
- ec.config.mjs +24 -24
- playwright.config.ts +9 -9
- pnpm-workspace.yaml +1 -1
- scripts/apply-cloudflare-rules.ts +5 -16
- src/actions/index.ts +2 -11
- src/components/HeaderLink.astro +1 -1
- src/consts.ts +31 -25
- src/content/gopibot-to-the-rescue.md +76 -76
- src/content/react-powertools-swr.md +82 -80
- src/pages/robots.txt.ts +3 -3
- src/utils/commit.ts +25 -13
- src/utils/content.ts +4 -12
- src/utils/files.ts +14 -14
- src/utils/gitBug.ts +6 -19
- tests/home.spec.ts +4 -4
- tests/rss.spec.ts +3 -3
.alchemy/prd/repos-bucket-prd.json
CHANGED
|
@@ -33,4 +33,4 @@
|
|
|
33
33
|
"force": false
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
}
|
|
36
|
+
}
|
.alchemy/prd/rewrite-rules-prd.json
CHANGED
|
@@ -88,10 +88,7 @@
|
|
|
88
88
|
"status": "pending",
|
|
89
89
|
"paused": false,
|
|
90
90
|
"accountId": "dfdb8ca4e419990852eb214c5b50bd5e",
|
|
91
|
-
"nameservers": [
|
|
92
|
-
|
|
91
|
+
"nameservers": ["glen.ns.cloudflare.com", "riya.ns.cloudflare.com"],
|
|
93
|
-
"riya.ns.cloudflare.com"
|
|
94
|
-
],
|
|
95
92
|
"originalNameservers": [
|
|
96
93
|
"ns-1020.awsdns-63.net",
|
|
97
94
|
"ns-1447.awsdns-52.org",
|
|
@@ -160,4 +157,4 @@
|
|
|
160
157
|
}
|
|
161
158
|
]
|
|
162
159
|
}
|
|
163
|
-
}
|
|
160
|
+
}
|
.alchemy/prd/website-bucket-prd.json
CHANGED
|
@@ -26,17 +26,13 @@
|
|
|
26
26
|
"remote": false,
|
|
27
27
|
"isDeployed": true
|
|
28
28
|
},
|
|
29
|
-
"domains": [
|
|
30
|
-
|
|
29
|
+
"domains": ["pyrossh.dev"]
|
|
31
|
-
]
|
|
32
30
|
},
|
|
33
31
|
"props": {
|
|
34
32
|
"name": "pyrossh-website-prd",
|
|
35
|
-
"domains": [
|
|
36
|
-
|
|
33
|
+
"domains": ["pyrossh.dev"],
|
|
37
|
-
],
|
|
38
34
|
"dev": {
|
|
39
35
|
"force": false
|
|
40
36
|
}
|
|
41
37
|
}
|
|
42
|
-
}
|
|
38
|
+
}
|
.alchemy/prd/website-bucket-prd/pyrossh.dev.json
CHANGED
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
"isDeployed": true
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|
.alchemy/prd/zone-prd.json
CHANGED
|
@@ -19,10 +19,7 @@
|
|
|
19
19
|
"status": "pending",
|
|
20
20
|
"paused": false,
|
|
21
21
|
"accountId": "dfdb8ca4e419990852eb214c5b50bd5e",
|
|
22
|
-
"nameservers": [
|
|
23
|
-
|
|
22
|
+
"nameservers": ["glen.ns.cloudflare.com", "riya.ns.cloudflare.com"],
|
|
24
|
-
"riya.ns.cloudflare.com"
|
|
25
|
-
],
|
|
26
23
|
"originalNameservers": [
|
|
27
24
|
"ns-1020.awsdns-63.net",
|
|
28
25
|
"ns-1447.awsdns-52.org",
|
|
@@ -62,4 +59,4 @@
|
|
|
62
59
|
"http3": "on"
|
|
63
60
|
}
|
|
64
61
|
}
|
|
65
|
-
}
|
|
62
|
+
}
|
.vscode/extensions.json
CHANGED
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
"myriad-dreamin.tinymist"
|
|
8
8
|
],
|
|
9
9
|
"unwantedRecommendations": []
|
|
10
|
-
}
|
|
10
|
+
}
|
README.md
CHANGED
|
@@ -44,9 +44,9 @@ Any static assets, like images, can be placed in the `public/` directory.
|
|
|
44
44
|
|
|
45
45
|
All commands are run from the root of the project, from a terminal:
|
|
46
46
|
|
|
47
|
-
| Command
|
|
47
|
+
| Command | Action |
|
|
48
|
-
| :--------------------
|
|
48
|
+
| :-------------------- | :----------------------------------------------- |
|
|
49
|
-
| `bun install`
|
|
49
|
+
| `bun install` | Installs dependencies |
|
|
50
50
|
| `bun dev` | Starts local dev server at `localhost:4321` |
|
|
51
51
|
| `bun build` | Build your production site to `./dist/` |
|
|
52
52
|
| `bun preview` | Preview your build locally, before deploying |
|
ec.config.mjs
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { defineEcConfig } from
|
|
1
|
+
import { defineEcConfig } from "astro-expressive-code";
|
|
2
2
|
|
|
3
3
|
export default defineEcConfig({
|
|
4
4
|
// themes: [],
|
|
5
5
|
shiki: {
|
|
6
6
|
langAlias: {
|
|
7
|
-
h:
|
|
7
|
+
h: "c",
|
|
8
|
-
ino:
|
|
8
|
+
ino: "c",
|
|
9
|
-
mm:
|
|
9
|
+
mm: "c",
|
|
10
|
-
plum:
|
|
10
|
+
plum: "txt",
|
|
11
|
-
mi:
|
|
11
|
+
mi: "txt",
|
|
12
|
-
scm:
|
|
12
|
+
scm: "txt",
|
|
13
|
-
podspec:
|
|
13
|
+
podspec: "txt",
|
|
14
|
-
lock:
|
|
14
|
+
lock: "txt",
|
|
15
|
-
xcconfig:
|
|
15
|
+
xcconfig: "txt",
|
|
16
|
-
plist:
|
|
16
|
+
plist: "xml",
|
|
17
|
-
xcworkspacedata:
|
|
17
|
+
xcworkspacedata: "xml",
|
|
18
|
-
xcsettings:
|
|
18
|
+
xcsettings: "xml",
|
|
19
|
-
storyboard:
|
|
19
|
+
storyboard: "txt",
|
|
20
|
-
pbxproj:
|
|
20
|
+
pbxproj: "txt",
|
|
21
|
-
mjs:
|
|
21
|
+
mjs: "javascript",
|
|
22
|
-
snap:
|
|
22
|
+
snap: "javascript",
|
|
23
|
-
gradle:
|
|
23
|
+
gradle: "groovy",
|
|
24
|
-
class:
|
|
24
|
+
class: "java",
|
|
25
|
-
mod:
|
|
25
|
+
mod: "go",
|
|
26
|
-
sum:
|
|
26
|
+
sum: "go",
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
styleOverrides: {
|
|
30
|
-
codeFontSize:
|
|
30
|
+
codeFontSize: "0.8rem",
|
|
31
|
-
uiFontSize:
|
|
31
|
+
uiFontSize: "0.8rem",
|
|
32
32
|
// frames: {
|
|
33
33
|
// shadowColor: '#124',
|
|
34
34
|
// },
|
|
35
35
|
},
|
|
36
|
-
})
|
|
36
|
+
});
|
playwright.config.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { defineConfig, devices } from
|
|
1
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
-
testDir:
|
|
4
|
+
testDir: "./tests",
|
|
5
5
|
fullyParallel: true,
|
|
6
6
|
forbidOnly: !!process.env.CI,
|
|
7
7
|
retries: process.env.CI ? 2 : 0,
|
|
8
8
|
workers: process.env.CI ? 1 : undefined,
|
|
9
|
-
reporter:
|
|
9
|
+
reporter: "html",
|
|
10
10
|
use: {
|
|
11
|
-
baseURL:
|
|
11
|
+
baseURL: "http://localhost:4321",
|
|
12
|
-
trace:
|
|
12
|
+
trace: "on-first-retry",
|
|
13
13
|
},
|
|
14
14
|
projects: [
|
|
15
15
|
{
|
|
16
|
-
name:
|
|
16
|
+
name: "chromium",
|
|
17
|
-
use: { ...devices[
|
|
17
|
+
use: { ...devices["Desktop Chrome"] },
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
webServer: {
|
|
21
|
-
command:
|
|
21
|
+
command: "bun run dev",
|
|
22
|
-
url:
|
|
22
|
+
url: "http://localhost:4321",
|
|
23
23
|
reuseExistingServer: !process.env.CI,
|
|
24
24
|
},
|
|
25
25
|
});
|
pnpm-workspace.yaml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
allowBuilds:
|
|
2
|
-
|
|
2
|
+
"@tensorflow/tfjs-node": set this to true or false
|
|
3
3
|
core-js: set this to true or false
|
|
4
4
|
es5-ext: set this to true or false
|
|
5
5
|
esbuild: set this to true or false
|
scripts/apply-cloudflare-rules.ts
CHANGED
|
@@ -34,10 +34,7 @@ type Rule = {
|
|
|
34
34
|
enabled?: boolean;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
const cloudflare = async <T>(
|
|
38
|
-
path: string,
|
|
39
|
-
|
|
37
|
+
const cloudflare = async <T>(path: string, init?: RequestInit & { allowNotFound?: boolean }) => {
|
|
40
|
-
) => {
|
|
41
38
|
const response = await fetch(`${apiBase}${path}`, {
|
|
42
39
|
method: init?.method,
|
|
43
40
|
body: init?.body,
|
|
@@ -54,9 +51,7 @@ const cloudflare = async <T>(
|
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
if (!response.ok || !payload.success) {
|
|
57
|
-
const message = payload.errors
|
|
58
|
-
|
|
54
|
+
const message = payload.errors?.map((error) => `${error.code}: ${error.message}`).join("; ");
|
|
59
|
-
.join("; ");
|
|
60
55
|
throw new Error(message || `Cloudflare API request failed: ${response.status}`);
|
|
61
56
|
}
|
|
62
57
|
|
|
@@ -68,9 +63,7 @@ const getZoneId = async () => {
|
|
|
68
63
|
return process.env.CLOUDFLARE_ZONE_ID;
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
const zones = await cloudflare<Zone[]>(
|
|
72
|
-
|
|
66
|
+
const zones = await cloudflare<Zone[]>(`/zones?name=${encodeURIComponent(zoneName)}`);
|
|
73
|
-
);
|
|
74
67
|
const zone = zones.find((item) => item.name === zoneName);
|
|
75
68
|
|
|
76
69
|
if (!zone) {
|
|
@@ -120,9 +113,7 @@ const makeRewriteRules = (host: string): Rule[] => [
|
|
|
120
113
|
];
|
|
121
114
|
|
|
122
115
|
const rewriteRules = makeRewriteRules(zoneName);
|
|
123
|
-
const rewriteRuleDescriptions = new Set(
|
|
124
|
-
|
|
116
|
+
const rewriteRuleDescriptions = new Set(rewriteRules.map((rule) => rule.description));
|
|
125
|
-
);
|
|
126
117
|
|
|
127
118
|
const makeRulesetBody = (rules: Rule[], existingRuleset?: Ruleset) => ({
|
|
128
119
|
name: rulesetName,
|
|
@@ -138,9 +129,7 @@ const existingRuleset = await cloudflare<Ruleset>(entrypointPath, {
|
|
|
138
129
|
});
|
|
139
130
|
const existingRules = existingRuleset?.rules ?? [];
|
|
140
131
|
const nextRules = [
|
|
141
|
-
...existingRules.filter(
|
|
142
|
-
|
|
132
|
+
...existingRules.filter((rule) => !rewriteRuleDescriptions.has(rule.description)),
|
|
143
|
-
),
|
|
144
133
|
...rewriteRules,
|
|
145
134
|
];
|
|
146
135
|
|
src/actions/index.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { defineAction } from "astro:actions";
|
|
2
2
|
import { z } from "astro/zod";
|
|
3
|
-
import {
|
|
4
|
-
addGitBugIssueComment,
|
|
5
|
-
createGitBugIssue,
|
|
6
|
-
setGitBugIssueState,
|
|
7
|
-
} from "@/utils/gitBug";
|
|
3
|
+
import { addGitBugIssueComment, createGitBugIssue, setGitBugIssueState } from "@/utils/gitBug";
|
|
8
4
|
|
|
9
5
|
const repoId = z.string().regex(/^[a-zA-Z0-9._-]+$/);
|
|
10
6
|
const issueId = z.string().regex(/^[a-f0-9]{16}$/);
|
|
@@ -55,12 +51,7 @@ export const server = {
|
|
|
55
51
|
author: z.string().trim().optional(),
|
|
56
52
|
}),
|
|
57
53
|
handler: async (input) => {
|
|
58
|
-
return await setGitBugIssueState(
|
|
54
|
+
return await setGitBugIssueState(input.repoId, input.issueId, input.state, input.author);
|
|
59
|
-
input.repoId,
|
|
60
|
-
input.issueId,
|
|
61
|
-
input.state,
|
|
62
|
-
input.author,
|
|
63
|
-
);
|
|
64
55
|
},
|
|
65
56
|
}),
|
|
66
57
|
};
|
src/components/HeaderLink.astro
CHANGED
|
@@ -5,7 +5,7 @@ type Props = HTMLAttributes<"a">;
|
|
|
5
5
|
|
|
6
6
|
const { href, class: className, ...props } = Astro.props;
|
|
7
7
|
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, "");
|
|
8
|
-
const subpath = pathname.match(/[^
|
|
8
|
+
const subpath = pathname.match(/[^/]+/g);
|
|
9
9
|
const isActive = href === pathname || href === "/" + (subpath?.[0] || "");
|
|
10
10
|
const isExternal = href.startsWith("http");
|
|
11
11
|
const target = isExternal ? "_blank" : "_parent";
|
src/consts.ts
CHANGED
|
@@ -8,80 +8,86 @@ import zenSvg from "@/assets/logos/zen.svg";
|
|
|
8
8
|
import brunoPng from "@/assets/logos/bruno.png";
|
|
9
9
|
// import hyectorPng from "@/assets/logos/hyvector.png";
|
|
10
10
|
|
|
11
|
-
export const SITE_TITLE =
|
|
11
|
+
export const SITE_TITLE = "pyrossh";
|
|
12
|
-
export const SITE_DESCRIPTION =
|
|
12
|
+
export const SITE_DESCRIPTION = "Welcome to my website!";
|
|
13
13
|
export const REPOS = [
|
|
14
14
|
{
|
|
15
15
|
title: "rust-embed",
|
|
16
|
+
description:
|
|
16
|
-
|
|
17
|
+
"rust macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev.",
|
|
17
|
-
tags: [
|
|
18
|
+
tags: ["rust", "proc-macro", "http"],
|
|
18
19
|
},
|
|
19
20
|
{
|
|
20
21
|
title: "website",
|
|
21
22
|
description: "木 Personal website of pyrossh. Built with astrojs, shiki, vite.",
|
|
22
|
-
tags: [
|
|
23
|
+
tags: ["astro", "js", "html", "css"],
|
|
23
24
|
},
|
|
24
25
|
{
|
|
25
26
|
title: "plum",
|
|
26
27
|
description: "A statically typed, imperative programming language inspired by rust, python",
|
|
27
|
-
tags: [
|
|
28
|
+
tags: ["treesitter", "compiler", "wasm"],
|
|
28
29
|
},
|
|
29
30
|
{
|
|
30
31
|
title: "edge-city",
|
|
32
|
+
description:
|
|
31
|
-
|
|
33
|
+
"edge-city is a next level meta-framework for react that runs only on edge runtimes",
|
|
32
|
-
tags: [
|
|
34
|
+
tags: ["react", "js", "ssr"],
|
|
33
35
|
},
|
|
34
36
|
{
|
|
35
37
|
title: "gromer",
|
|
38
|
+
description:
|
|
36
|
-
|
|
39
|
+
"gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.",
|
|
37
|
-
tags: [
|
|
40
|
+
tags: ["golang", "htmx", "ssr"],
|
|
38
41
|
},
|
|
39
42
|
{
|
|
40
43
|
title: "atoms-element",
|
|
44
|
+
description:
|
|
41
|
-
|
|
45
|
+
"A simple web component library for defining your custom elements. It works on both client and server.",
|
|
42
|
-
tags: [
|
|
46
|
+
tags: ["js"],
|
|
43
47
|
},
|
|
44
48
|
{
|
|
45
49
|
title: "atoms-state",
|
|
46
50
|
description: "Simple State management for react",
|
|
47
|
-
tags: [
|
|
51
|
+
tags: ["js", "react", "flux"],
|
|
48
52
|
},
|
|
49
53
|
{
|
|
50
54
|
title: "remote-monitor",
|
|
51
55
|
description: "Remote Monitoring and Control using GSM-SMS",
|
|
52
|
-
tags: [
|
|
56
|
+
tags: ["c++", "teensy", "arduino"],
|
|
53
57
|
},
|
|
54
58
|
{
|
|
55
59
|
title: "only-bible-app",
|
|
60
|
+
description:
|
|
56
|
-
|
|
61
|
+
"The only bible app you will ever need. No ads. No in-app purchases. No distractions.",
|
|
57
|
-
tags: [
|
|
62
|
+
tags: ["kotlin", "android", "ios"],
|
|
58
63
|
},
|
|
59
64
|
{
|
|
60
65
|
title: "gdx-studio",
|
|
66
|
+
description:
|
|
61
|
-
|
|
67
|
+
"An IDE for creating Games using libgdx and Java supported on all platforms Android, iOS, Desktop",
|
|
62
|
-
tags: [
|
|
68
|
+
tags: ["libgdx", "java", "desktop"],
|
|
63
69
|
},
|
|
64
70
|
{
|
|
65
71
|
title: "rp2350",
|
|
66
72
|
description: "code to drive rp2350",
|
|
67
|
-
tags: [
|
|
73
|
+
tags: ["zig", "raspberry-pi"],
|
|
68
74
|
},
|
|
69
75
|
{
|
|
70
76
|
title: "config",
|
|
71
77
|
description: "Common configuration",
|
|
72
|
-
tags: [
|
|
78
|
+
tags: ["brew", "nushell"],
|
|
73
79
|
},
|
|
74
80
|
{
|
|
75
81
|
title: "sabel-ide",
|
|
76
82
|
description: "sabel-ide",
|
|
77
|
-
tags: [
|
|
83
|
+
tags: ["python", "qt"],
|
|
78
84
|
},
|
|
79
85
|
{
|
|
80
86
|
title: "tide-jsx",
|
|
81
87
|
description: "Tide + JSX",
|
|
82
|
-
tags: [
|
|
88
|
+
tags: ["rust", "proc-macro", "jsx"],
|
|
83
89
|
},
|
|
84
|
-
]
|
|
90
|
+
];
|
|
85
91
|
export const TOOLS = [
|
|
86
92
|
{
|
|
87
93
|
name: "Stats",
|
|
@@ -127,11 +133,11 @@ export const TOOLS = [
|
|
|
127
133
|
name: "Secretive",
|
|
128
134
|
link: "https://github.com/maxgoedjen/secretive",
|
|
129
135
|
image: null,
|
|
130
|
-
}
|
|
136
|
+
},
|
|
131
137
|
// TODO: check if this is useful as well
|
|
132
138
|
// {
|
|
133
139
|
// name: "Hyvector",
|
|
134
140
|
// link: "https://www.hyvector.com/",
|
|
135
141
|
// image: import("@/assets/logos/hyvector.png"),
|
|
136
142
|
// },
|
|
137
|
-
]
|
|
143
|
+
];
|
src/content/gopibot-to-the-rescue.md
CHANGED
|
@@ -27,92 +27,92 @@ First I went and created the bot in my slack team settings. And then wrote a scr
|
|
|
27
27
|
Here is the simple script,
|
|
28
28
|
|
|
29
29
|
```js
|
|
30
|
-
const RtmClient = require(
|
|
30
|
+
const RtmClient = require("@slack/client").RtmClient;
|
|
31
|
-
const RTM_EVENTS = require(
|
|
31
|
+
const RTM_EVENTS = require("@slack/client").RTM_EVENTS;
|
|
32
|
-
const CLIENT_EVENTS = require(
|
|
32
|
+
const CLIENT_EVENTS = require("@slack/client").CLIENT_EVENTS;
|
|
33
33
|
const bot_token = process.env.SLACK_BOT_TOKEN;
|
|
34
34
|
|
|
35
35
|
const rtm = new RtmClient(bot_token);
|
|
36
36
|
const COMMANDS = {
|
|
37
|
-
|
|
37
|
+
web: "ssh -i qa.pem user@url docker pull image-name && docker rm -f container-id && docker run -d image-name",
|
|
38
38
|
};
|
|
39
39
|
let deploymentInProgress = false;
|
|
40
40
|
let counter = 0;
|
|
41
41
|
|
|
42
42
|
rtm.on(RTM_EVENTS.MESSAGE, (event) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
43
|
+
console.log("Got event", event);
|
|
44
|
+
if (
|
|
45
|
+
(event.subtype === "message_changed" || event.subtype === "message_deleted") &&
|
|
46
|
+
event.text &&
|
|
47
|
+
event.text.indexOf("$slackBotId") > -1
|
|
48
|
+
) {
|
|
49
|
+
return rtm.sendMessage(
|
|
50
|
+
"Please dont change the message and expect me to correct your past mistakes",
|
|
51
|
+
event.channel,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (event.subtype) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (event.type === "message" && event.text && event.text.indexOf("$slackBotId") > -1) {
|
|
58
|
+
if (deploymentInProgress === true) {
|
|
59
|
+
counter = counter + 1;
|
|
60
|
+
if (counter > 3) {
|
|
61
|
+
counter = 0;
|
|
62
|
+
return rtm.sendMessage(
|
|
63
|
+
"Stop bugging me noob or I'll tell to raise you bugs",
|
|
64
|
+
event.channel,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return rtm.sendMessage("I am already processing a deploy request please wait", event.channel);
|
|
68
|
+
}
|
|
69
|
+
var input = event.text.trim().replace("$slackBotId ", "");
|
|
70
|
+
console.log("Got input", input);
|
|
71
|
+
const arr = input.split(" ");
|
|
72
|
+
arr.forEach((word) => {
|
|
73
|
+
word = word.replace(/\s/g, "");
|
|
74
|
+
});
|
|
75
|
+
const currCommand = arr[0];
|
|
76
|
+
if (COMMANDS.indexOf(currCommand) > -1) {
|
|
77
|
+
rtm.sendMessage("Starting to deploy ${currCommand}", event.channel);
|
|
78
|
+
const ssh = spawn(COMMANDS[currCommand]);
|
|
79
|
+
deploymentInProgress = true;
|
|
80
|
+
ssh.stdout.on("data", (data) => {
|
|
81
|
+
rtm.sendMessage(data, event.channel);
|
|
82
|
+
});
|
|
83
|
+
ssh.stderr.on("data", (data) => {
|
|
84
|
+
rtm.sendMessage(data, event.channel);
|
|
85
|
+
});
|
|
86
|
+
ssh.on("close", (code) => {
|
|
87
|
+
deploymentInProgress = false;
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
console.log("Deployed Successfully", currCommand);
|
|
90
|
+
rtm.sendMessage("Deployed Successfully " + currCommand, event.channel);
|
|
91
|
+
} else {
|
|
92
|
+
console.log("child process exited with code ", code);
|
|
93
|
+
rtm.sendMessage("child process exited with code " + code);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
} else {
|
|
98
|
+
counter = counter + 1;
|
|
99
|
+
if (counter > 3) {
|
|
100
|
+
counter = 0;
|
|
101
|
+
return rtm.sendMessage(
|
|
102
|
+
"Stop bugging me noob or I'll tell <@U30TXGLS1|gopi> to raise you bugs",
|
|
103
|
+
event.channel,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return rtm.sendMessage(
|
|
107
|
+
`command '${event.text} ' not found.You need to specify one of these commands [${COMMANDS.map(
|
|
108
|
+
(v, k) => k,
|
|
109
|
+
).join(",")} ]`,
|
|
110
|
+
event.channel,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
114
|
});
|
|
115
|
-
console.log(
|
|
115
|
+
console.log("Starting deploybot");
|
|
116
116
|
rtm.start();
|
|
117
117
|
```
|
|
118
118
|
|
src/content/react-powertools-swr.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
2
|
+
title: "React Powertools: SWR"
|
|
3
3
|
description: A react library that makes it easier to fetch data
|
|
4
4
|
pubDate: 2024-08-16
|
|
5
5
|
tags:
|
|
@@ -40,49 +40,49 @@ const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, op
|
|
|
40
40
|
> Before SWR
|
|
41
41
|
|
|
42
42
|
```tsx
|
|
43
|
-
import { useState, useEffect } from
|
|
43
|
+
import { useState, useEffect } from "react";
|
|
44
44
|
|
|
45
45
|
const useUser = (id: string) => {
|
|
46
|
-
|
|
46
|
+
const [user, setUser] = useState(null);
|
|
47
|
-
|
|
47
|
+
const [loading, setLoading] = useState(false);
|
|
48
|
-
|
|
48
|
+
const [error, setError] = useState(null);
|
|
49
|
-
|
|
49
|
+
|
|
50
|
-
|
|
50
|
+
useEffect(() => {
|
|
51
|
-
|
|
51
|
+
setLoading(true);
|
|
52
|
-
|
|
52
|
+
setError(null);
|
|
53
|
-
|
|
53
|
+
fetch(`/users/${id}`)
|
|
54
|
-
|
|
54
|
+
.then((res) => res.json())
|
|
55
|
-
|
|
55
|
+
.then((data) => {
|
|
56
|
-
|
|
56
|
+
setUser(data);
|
|
57
|
-
|
|
57
|
+
setLoading(false);
|
|
58
|
-
|
|
58
|
+
})
|
|
59
|
-
|
|
59
|
+
.catch((err) => {
|
|
60
|
-
|
|
60
|
+
setError(err);
|
|
61
|
-
|
|
61
|
+
setLoading(false);
|
|
62
|
-
|
|
62
|
+
});
|
|
63
|
-
|
|
63
|
+
}, [id]);
|
|
64
|
-
|
|
64
|
+
return {
|
|
65
|
-
|
|
65
|
+
user,
|
|
66
|
-
|
|
66
|
+
loading,
|
|
67
|
-
|
|
67
|
+
error,
|
|
68
|
-
|
|
68
|
+
};
|
|
69
69
|
};
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
> After SWR
|
|
73
73
|
|
|
74
74
|
```tsx
|
|
75
|
-
import useSWR from
|
|
75
|
+
import useSWR from "swr";
|
|
76
76
|
|
|
77
77
|
const fetcher = (...args) => fetch(...args).then((res) => res.json());
|
|
78
78
|
|
|
79
79
|
const useUser = (id: string) => {
|
|
80
|
-
|
|
80
|
+
const { data, error, isLoading } = useSWR(`/users/${id}`, fetcher);
|
|
81
|
-
|
|
81
|
+
return {
|
|
82
|
-
|
|
82
|
+
user: data,
|
|
83
|
-
|
|
83
|
+
isLoading,
|
|
84
|
-
|
|
84
|
+
error: error,
|
|
85
|
-
|
|
85
|
+
};
|
|
86
86
|
};
|
|
87
87
|
```
|
|
88
88
|
|
|
@@ -91,16 +91,16 @@ const useUser = (id: string) => {
|
|
|
91
91
|
You can configure a global fetcher function so that you don't need pass the fetcher on every hook call using the `SWRConfig` Provider at the root app level. The library uses a global cache to store and share data across all components, you can also customise this behaviour with the `provider` option.
|
|
92
92
|
|
|
93
93
|
```tsx
|
|
94
|
-
import { SWRConfig } from
|
|
94
|
+
import { SWRConfig } from "swr";
|
|
95
95
|
|
|
96
96
|
const fetcher = (...args) => fetch(...args).then((res) => res.json());
|
|
97
97
|
|
|
98
98
|
function App() {
|
|
99
|
-
|
|
99
|
+
return (
|
|
100
|
-
|
|
100
|
+
<SWRConfig value={{ fetcher: fetcher, provider: () => new Map() }}>
|
|
101
|
-
|
|
101
|
+
<Page />
|
|
102
|
-
|
|
102
|
+
</SWRConfig>
|
|
103
|
-
|
|
103
|
+
);
|
|
104
104
|
}
|
|
105
105
|
```
|
|
106
106
|
|
|
@@ -123,47 +123,47 @@ There are 2 ways to trigger a revalidation request manually,
|
|
|
123
123
|
**1.** You can use the **mutate** function returned by the **useSWR** hook to trigger a revalidation of the data
|
|
124
124
|
|
|
125
125
|
```tsx
|
|
126
|
-
import useSWR from
|
|
126
|
+
import useSWR from "swr";
|
|
127
127
|
|
|
128
128
|
const Profile = () => {
|
|
129
|
-
|
|
129
|
+
const { data, error, isLoading, mutate } = useSWR(`/users/1`);
|
|
130
|
-
|
|
130
|
+
if (error) return <div>failed to load</div>;
|
|
131
|
-
|
|
131
|
+
if (isLoading) return <div className="text">loading...</div>;
|
|
132
|
-
|
|
132
|
+
return (
|
|
133
|
-
|
|
133
|
+
<div>
|
|
134
|
-
|
|
134
|
+
<div>{JSON.stringify(data, null, 2)}</div>
|
|
135
|
-
|
|
135
|
+
<button onClick={() => mutate()}>Update User</button>
|
|
136
|
-
|
|
136
|
+
</div>
|
|
137
|
-
|
|
137
|
+
);
|
|
138
138
|
};
|
|
139
139
|
```
|
|
140
140
|
|
|
141
141
|
This can be used to implement optimistic updates as well if you know what data needs to change and once the revalidation is complete the cache gets updated with new data from the server.
|
|
142
142
|
|
|
143
143
|
```tsx
|
|
144
|
-
mutate({ ...data, name:
|
|
144
|
+
mutate({ ...data, name: "John Doe" });
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
**2.** If you need to update the cache from another component which doesn't have access to the **useSWR** hook you can use the **mutate** function returned by the **useSWRConfig** to get access to the cache. Here you would need to provide the key to trigger a revalidation of the request.
|
|
148
148
|
|
|
149
149
|
```tsx
|
|
150
|
-
import { useSWRConfig } from
|
|
150
|
+
import { useSWRConfig } from "swr";
|
|
151
151
|
// or import { mutate } from "swr"
|
|
152
152
|
|
|
153
153
|
const UpdateButton = () => {
|
|
154
|
-
|
|
154
|
+
const { mutate } = useSWRConfig();
|
|
155
|
-
|
|
155
|
+
return (
|
|
156
|
-
|
|
156
|
+
<div>
|
|
157
|
-
|
|
157
|
+
<button onClick={() => mutate(`/users/1`)}>Update User</button>
|
|
158
|
-
|
|
158
|
+
</div>
|
|
159
|
-
|
|
159
|
+
);
|
|
160
160
|
};
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
Here as well you can do optimistic updates similarly,
|
|
164
164
|
|
|
165
165
|
```tsx
|
|
166
|
-
mutate(`/users/1`, { ...data, name:
|
|
166
|
+
mutate(`/users/1`, { ...data, name: "John Doe" });
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
### **useSWRMutation**
|
|
@@ -171,37 +171,39 @@ mutate(`/users/1`, { ...data, name: 'John Doe' });
|
|
|
171
171
|
This hook makes it easier to handle update requests and provides necessary state
|
|
172
172
|
|
|
173
173
|
```tsx
|
|
174
|
-
import useSWRMutation from
|
|
174
|
+
import useSWRMutation from "swr/mutation";
|
|
175
175
|
|
|
176
176
|
async function updateUser(url, data) {
|
|
177
|
-
|
|
177
|
+
await fetch(url, {
|
|
178
|
-
|
|
178
|
+
method: "POST",
|
|
179
|
-
|
|
179
|
+
body: JSON.stringify(data),
|
|
180
|
-
|
|
180
|
+
});
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
function Profile() {
|
|
184
|
-
|
|
184
|
+
const { data, error, isMutating, trigger } = useSWRMutation(
|
|
185
|
-
|
|
185
|
+
"/api/user/update",
|
|
186
|
-
|
|
186
|
+
updateUser,
|
|
187
|
-
|
|
187
|
+
options,
|
|
188
|
-
|
|
188
|
+
);
|
|
189
|
-
|
|
189
|
+
return (
|
|
190
|
-
|
|
190
|
+
<button onClick={() => trigger({ name: "John Doe" })}>
|
|
191
|
-
|
|
191
|
+
{isMutating ? "Updating..." : "Update User"}
|
|
192
|
-
|
|
192
|
+
</button>
|
|
193
|
-
|
|
193
|
+
);
|
|
194
194
|
}
|
|
195
195
|
```
|
|
196
196
|
|
|
197
197
|
**Parameters**
|
|
198
|
+
|
|
198
|
-
|
|
199
|
+
- `key`: a unique key string for the request
|
|
199
|
-
|
|
200
|
+
- `fetcher(key, { arg })`: an async function for remote mutation
|
|
200
|
-
|
|
201
|
+
- `options`: an optional object to configure revalidation and optimistic updates
|
|
201
202
|
|
|
202
203
|
**Returns**
|
|
204
|
+
|
|
203
|
-
|
|
205
|
+
- `data`: data for the given key returned from the update request
|
|
204
|
-
|
|
206
|
+
- `error`: error thrown by the request
|
|
205
|
-
|
|
207
|
+
- `trigger(arg, options)`: a function to trigger a remote mutation
|
|
206
|
-
|
|
208
|
+
- `reset`: a function to reset the state
|
|
207
|
-
|
|
209
|
+
- `isMutating`: if there's an ongoing update request
|
src/pages/robots.txt.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { APIRoute } from
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
2
|
|
|
3
3
|
const getRobotsTxt = (sitemapURL: URL) =>
|
|
4
4
|
`User-agent: *
|
|
@@ -8,6 +8,6 @@ Sitemap: ${sitemapURL.href}
|
|
|
8
8
|
`;
|
|
9
9
|
|
|
10
10
|
export const GET: APIRoute = ({ site }) => {
|
|
11
|
-
const sitemapURL = new URL(
|
|
11
|
+
const sitemapURL = new URL("sitemap-index.xml", site);
|
|
12
12
|
return new Response(getRobotsTxt(sitemapURL));
|
|
13
|
-
};
|
|
13
|
+
};
|
src/utils/commit.ts
CHANGED
|
@@ -15,22 +15,36 @@ export interface Commit {
|
|
|
15
15
|
tags: string[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export const collectCommits = (item: {
|
|
18
|
+
export const collectCommits = (item: {
|
|
19
|
+
hash: any;
|
|
20
|
+
message: any;
|
|
21
|
+
body: any;
|
|
22
|
+
author_name: any;
|
|
23
|
+
author_email: any;
|
|
24
|
+
date: any;
|
|
25
|
+
refs: string;
|
|
26
|
+
}) => ({
|
|
19
27
|
hash: item.hash,
|
|
20
28
|
message: item.message,
|
|
21
29
|
body: `${item.message}\n\n${item.body}`,
|
|
22
30
|
author_name: item.author_name,
|
|
23
31
|
author_email: item.author_email,
|
|
24
32
|
date: item.date,
|
|
33
|
+
branches: item.refs
|
|
34
|
+
.split(",")
|
|
25
|
-
|
|
35
|
+
.filter((ref: string) => !ref.includes("origin"))
|
|
36
|
+
.map((ref) => ref.trim()),
|
|
37
|
+
tags: item.refs
|
|
38
|
+
.split(",")
|
|
39
|
+
.filter((ref: string) => ref.includes("tag: "))
|
|
26
|
-
|
|
40
|
+
.map((ref) => ref.replace("tag: ", "")),
|
|
27
41
|
});
|
|
28
42
|
|
|
29
43
|
export const getCommits = async (repoPath: string): Promise<Commit[]> => {
|
|
30
44
|
const git = simpleGit(repoPath);
|
|
31
45
|
const commits = await git.log(["--branches", "--tags"]);
|
|
32
46
|
return commits.all.map(collectCommits);
|
|
33
|
-
}
|
|
47
|
+
};
|
|
34
48
|
|
|
35
49
|
export const getFiles = async (repoPath: string): Promise<FileNode[]> => {
|
|
36
50
|
const git = simpleGit(repoPath);
|
|
@@ -40,23 +54,21 @@ export const getFiles = async (repoPath: string): Promise<FileNode[]> => {
|
|
|
40
54
|
try {
|
|
41
55
|
const stat = await fs.stat(`${repoPath}/${p}`);
|
|
42
56
|
const ext = path.extname(p).replace(".", "");
|
|
43
|
-
const name = p.endsWith("index.html")
|
|
44
|
-
|
|
57
|
+
const name = p.endsWith("index.html") ? p.replace("index.html", "index_html") : p;
|
|
45
|
-
: p;
|
|
46
58
|
files.push({
|
|
47
59
|
name,
|
|
48
60
|
ext,
|
|
49
61
|
size: stat.size,
|
|
50
62
|
isDirectory: stat.isDirectory(),
|
|
51
63
|
absolutePath: `${repoPath}/${p}`,
|
|
52
|
-
})
|
|
64
|
+
});
|
|
53
65
|
} catch (e) {
|
|
54
66
|
// Ignore files not found
|
|
55
67
|
console.log(e);
|
|
56
68
|
}
|
|
57
69
|
}
|
|
58
70
|
return files;
|
|
59
|
-
}
|
|
71
|
+
};
|
|
60
72
|
|
|
61
73
|
export const getFileHistory = async (repoPath: string, filePath: string): Promise<Commit[]> => {
|
|
62
74
|
const git = simpleGit(repoPath);
|
|
@@ -64,18 +76,18 @@ export const getFileHistory = async (repoPath: string, filePath: string): Promis
|
|
|
64
76
|
const isLarge = stat.size > 1024 * 512;
|
|
65
77
|
const history = isLarge ? { all: [] } : await git.log([filePath.replace(`${repoPath}/`, "")]);
|
|
66
78
|
return history.all.map(collectCommits);
|
|
67
|
-
}
|
|
79
|
+
};
|
|
68
80
|
|
|
69
81
|
export const generateHTMLDiff = async (repoPath: string, hash: string): Promise<string> => {
|
|
70
82
|
const git = simpleGit(repoPath);
|
|
71
|
-
let diff =
|
|
83
|
+
let diff = "";
|
|
72
84
|
try {
|
|
73
85
|
diff = await git.diff([`${hash}^..${hash}`]);
|
|
74
86
|
} catch (e) {
|
|
75
87
|
diff = await git.diff([hash]);
|
|
76
88
|
}
|
|
77
89
|
if (diff.length > 1024 * 512) {
|
|
78
|
-
return
|
|
90
|
+
return "<p>Diff too large to display.</p>";
|
|
79
91
|
}
|
|
80
92
|
const diffJson = parse(diff);
|
|
81
93
|
const diffHtml = html(diffJson, {
|
|
@@ -83,4 +95,4 @@ export const generateHTMLDiff = async (repoPath: string, hash: string): Promise<
|
|
|
83
95
|
matching: "lines",
|
|
84
96
|
});
|
|
85
97
|
return diffHtml;
|
|
86
|
-
}
|
|
98
|
+
};
|
src/utils/content.ts
CHANGED
|
@@ -75,15 +75,11 @@ export const getRepos = (): RuntimeRepo[] =>
|
|
|
75
75
|
},
|
|
76
76
|
}));
|
|
77
77
|
|
|
78
|
-
export const getRepo = (repoId?: string) =>
|
|
79
|
-
|
|
78
|
+
export const getRepo = (repoId?: string) => getRepos().find((repo) => repo.id === repoId);
|
|
80
79
|
|
|
81
80
|
export const getRepoReadmeHtml = async (repo: RuntimeRepo) => {
|
|
82
81
|
try {
|
|
83
|
-
const readme = await fs.readFile(
|
|
84
|
-
|
|
82
|
+
const readme = await fs.readFile(path.join(repo.data.absolutePath, "README.md"), "utf-8");
|
|
85
|
-
"utf-8",
|
|
86
|
-
);
|
|
87
83
|
return await renderMarkdown(readme);
|
|
88
84
|
} catch {
|
|
89
85
|
return "";
|
|
@@ -105,12 +101,8 @@ export const getPosts = async (): Promise<RuntimePost[]> => {
|
|
|
105
101
|
title: String(parsed.data.title ?? ""),
|
|
106
102
|
description: String(parsed.data.description ?? ""),
|
|
107
103
|
pubDate: toDate(parsed.data.pubDate),
|
|
108
|
-
updatedDate: parsed.data.updatedDate
|
|
104
|
+
updatedDate: parsed.data.updatedDate ? toDate(parsed.data.updatedDate) : undefined,
|
|
109
|
-
? toDate(parsed.data.updatedDate)
|
|
110
|
-
: undefined,
|
|
111
|
-
heroImage: parsed.data.heroImage
|
|
105
|
+
heroImage: parsed.data.heroImage ? String(parsed.data.heroImage) : undefined,
|
|
112
|
-
? String(parsed.data.heroImage)
|
|
113
|
-
: undefined,
|
|
114
106
|
},
|
|
115
107
|
body: parsed.content,
|
|
116
108
|
html: await renderMarkdown(parsed.content),
|
src/utils/files.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const sortAll = (a: FileNode, b: FileNode): number => {
|
|
|
16
16
|
if (a.isDirectory && !b.isDirectory) return -1;
|
|
17
17
|
if (!a.isDirectory && b.isDirectory) return 1;
|
|
18
18
|
return a.name.localeCompare(b.name);
|
|
19
|
-
}
|
|
19
|
+
};
|
|
20
20
|
export const sortChildren = (nodes: FileNode[]) => {
|
|
21
21
|
for (const node of nodes) {
|
|
22
22
|
if (node.isDirectory && node.children) {
|
|
@@ -31,15 +31,15 @@ export const buildFileTree = (files: any[]): FileNode[] => {
|
|
|
31
31
|
const root: FileNode[] = [];
|
|
32
32
|
|
|
33
33
|
for (const file of files) {
|
|
34
|
-
const parts = file.name.split(
|
|
34
|
+
const parts = file.name.split("/");
|
|
35
35
|
let currentLevel = root;
|
|
36
36
|
|
|
37
37
|
for (let i = 0; i < parts.length; i++) {
|
|
38
38
|
const part = parts[i];
|
|
39
39
|
const isLastPart = i === parts.length - 1;
|
|
40
|
-
const currentPath = parts.slice(0, i + 1).join(
|
|
40
|
+
const currentPath = parts.slice(0, i + 1).join("/");
|
|
41
41
|
|
|
42
|
-
let existingNode = currentLevel.find(node => node.name === part);
|
|
42
|
+
let existingNode = currentLevel.find((node) => node.name === part);
|
|
43
43
|
|
|
44
44
|
if (!existingNode) {
|
|
45
45
|
const newNode: FileNode = {
|
|
@@ -66,7 +66,7 @@ export const buildFileTree = (files: any[]): FileNode[] => {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return root;
|
|
69
|
-
}
|
|
69
|
+
};
|
|
70
70
|
|
|
71
71
|
export const checkFileExists = async (filePath: string) => {
|
|
72
72
|
try {
|
|
@@ -75,7 +75,7 @@ export const checkFileExists = async (filePath: string) => {
|
|
|
75
75
|
} catch (error) {
|
|
76
76
|
return false; // File does not exist or cannot be accessed
|
|
77
77
|
}
|
|
78
|
-
}
|
|
78
|
+
};
|
|
79
79
|
|
|
80
80
|
export const resolveFolderIcon = (foldername: string) => {
|
|
81
81
|
for (const item of folderExtensions.supported) {
|
|
@@ -84,25 +84,25 @@ export const resolveFolderIcon = (foldername: string) => {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
return folderExtensions.default.folder?.icon;
|
|
87
|
-
}
|
|
87
|
+
};
|
|
88
88
|
|
|
89
89
|
export const resolveFileIcon = (filename: string, fileExt: string) => {
|
|
90
|
-
const baseName = filename.split(
|
|
90
|
+
const baseName = filename.split("/").pop() || filename;
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
// First check for exact filename matches
|
|
93
93
|
for (const item of fileExtensions.supported) {
|
|
94
94
|
if (item.filename && item.extensions?.includes(baseName)) {
|
|
95
95
|
return item.icon;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// Then check for extension matches (more specific than language extensions)
|
|
100
100
|
for (const item of fileExtensions.supported) {
|
|
101
101
|
if (item.extensions?.includes(fileExt)) {
|
|
102
102
|
return item.icon;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
// Check for language-specific extensions, prioritizing exact language ID matches
|
|
107
107
|
for (const item of fileExtensions.supported) {
|
|
108
108
|
for (const lang of item.languages ?? []) {
|
|
@@ -114,7 +114,7 @@ export const resolveFileIcon = (filename: string, fileExt: string) => {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
// Fallback to any language extension match
|
|
119
119
|
for (const item of fileExtensions.supported) {
|
|
120
120
|
for (const lang of item.languages ?? []) {
|
|
@@ -123,6 +123,6 @@ export const resolveFileIcon = (filename: string, fileExt: string) => {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
return fileExtensions.default.file?.icon;
|
|
128
|
-
}
|
|
128
|
+
};
|
src/utils/gitBug.ts
CHANGED
|
@@ -112,19 +112,15 @@ const assertNotSpam = async (input: {
|
|
|
112
112
|
|
|
113
113
|
const getR2Config = () => {
|
|
114
114
|
const bucket =
|
|
115
|
-
process.env.REPOS_R2_BUCKET ??
|
|
115
|
+
process.env.REPOS_R2_BUCKET ?? process.env.R2_BUCKET ?? process.env.AWS_BUCKET_NAME;
|
|
116
|
-
process.env.R2_BUCKET ??
|
|
117
|
-
process.env.AWS_BUCKET_NAME;
|
|
118
116
|
const endpoint =
|
|
119
117
|
process.env.R2_ENDPOINT ??
|
|
120
118
|
process.env.AWS_ENDPOINT_URL_S3 ??
|
|
121
119
|
(process.env.R2_ACCOUNT_ID
|
|
122
120
|
? `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`
|
|
123
121
|
: undefined);
|
|
124
|
-
const accessKeyId =
|
|
125
|
-
|
|
122
|
+
const accessKeyId = process.env.R2_ACCESS_KEY_ID ?? process.env.AWS_ACCESS_KEY_ID;
|
|
126
|
-
const secretAccessKey =
|
|
127
|
-
|
|
123
|
+
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY ?? process.env.AWS_SECRET_ACCESS_KEY;
|
|
128
124
|
|
|
129
125
|
if (!bucket || !endpoint || !accessKeyId || !secretAccessKey) {
|
|
130
126
|
throw new Error(
|
|
@@ -164,12 +160,7 @@ const getObjectJson = async <T>(key: string) => {
|
|
|
164
160
|
const body = await object.Body?.transformToString();
|
|
165
161
|
return body ? (JSON.parse(body) as T) : undefined;
|
|
166
162
|
} catch (error) {
|
|
167
|
-
if (
|
|
168
|
-
error &&
|
|
169
|
-
|
|
163
|
+
if (error && typeof error === "object" && "name" in error && error.name === "NoSuchKey") {
|
|
170
|
-
"name" in error &&
|
|
171
|
-
error.name === "NoSuchKey"
|
|
172
|
-
) {
|
|
173
164
|
return undefined;
|
|
174
165
|
}
|
|
175
166
|
|
|
@@ -212,12 +203,8 @@ export const getGitBugIssues = async (repoId: string): Promise<GitBugIssuesResul
|
|
|
212
203
|
}),
|
|
213
204
|
);
|
|
214
205
|
|
|
215
|
-
issues.push(
|
|
216
|
-
|
|
206
|
+
issues.push(...pageIssues.filter((issue): issue is GitBugIssue => Boolean(issue)));
|
|
217
|
-
);
|
|
218
|
-
continuationToken = page.IsTruncated
|
|
207
|
+
continuationToken = page.IsTruncated ? page.NextContinuationToken : undefined;
|
|
219
|
-
? page.NextContinuationToken
|
|
220
|
-
: undefined;
|
|
221
208
|
} while (continuationToken);
|
|
222
209
|
|
|
223
210
|
return {
|
tests/home.spec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { expect, test } from
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
2
|
|
|
3
|
-
test(
|
|
3
|
+
test("index page has expected h1", async ({ page }) => {
|
|
4
|
-
await page.goto(
|
|
4
|
+
await page.goto("/");
|
|
5
|
-
await expect(page.getByRole(
|
|
5
|
+
await expect(page.getByRole("heading", { name: "Hello!" })).toBeVisible();
|
|
6
6
|
});
|
tests/rss.spec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { expect, test } from
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
2
|
|
|
3
|
-
test(
|
|
3
|
+
test("/rss.xml is valid", async ({ page }) => {
|
|
4
|
-
const response = await page.goto(
|
|
4
|
+
const response = await page.goto("/rss.xml");
|
|
5
5
|
expect(response?.status()).toBe(200);
|
|
6
6
|
});
|