- commit
- 321fa30
- parent
- 2c1728f
- author
- Eric Bower
- date
- 2022-07-01 03:35:22 +0000 UTC
Revert "Remove plugin page (#86)" This reverts commit 1b6016774358fd2d2febd1bbc08f564354f68c0a.
7 files changed,
+308,
-10
+3,
-2
1@@ -14,14 +14,15 @@
2 "scrape": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/scrape.ts",
3 "patch": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/patch.ts",
4 "process": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/process.ts",
5+ "html": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/html.ts",
6 "resource": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/resource.ts",
7 "resource:clean": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/resource-clean.ts",
8 "upload:clean": "gsutil -m rm -r gs://neovimcraft.com/*",
9 "upload": "gsutil -m -h 'Cache-Control:private, max-age=0, no-transform' rsync -r ./build gs://neovimcraft.com",
10 "upload:db": "gsutil -m -h 'Cache-Control:private, max-age=0, no-transform' cp ./src/lib/db.json gs://neovimcraft.com/db.json",
11- "build:all": "yarn scrape && yarn patch && yarn process && yarn build:clean && yarn build",
12+ "build:all": "yarn scrape && yarn patch && yarn process && yarn html && yarn build:clean && yarn build",
13 "deploy": "yarn build:clean && yarn build && yarn upload:clean && yarn upload && yarn upload:db",
14- "deploy:all": "yarn scrape && yarn patch && yarn process && yarn deploy"
15+ "deploy:all": "yarn scrape && yarn patch && yarn process && yarn html && yarn deploy"
16 },
17 "devDependencies": {
18 "@sveltejs/adapter-static": "^1.0.0-next.13",
+54,
-0
1@@ -0,0 +1,54 @@
2+import fs from 'fs';
3+import util from 'util';
4+import marked from 'marked';
5+import prettier from 'prettier';
6+
7+import type { Plugin } from '../src/lib/types';
8+
9+const writeFile = util.promisify(fs.writeFile);
10+const readFile = util.promisify(fs.readFile);
11+
12+clean().catch(console.error);
13+
14+async function clean() {
15+ const file = await readFile('./src/lib/db.json', 'utf-8');
16+ const db = JSON.parse(file.toString());
17+ const markdownFile = await readFile('./src/lib/markdown.json', 'utf-8');
18+ const markdownDb = JSON.parse(markdownFile.toString());
19+
20+ const plugins = Object.values(db.plugins);
21+ const nextDb = {};
22+ plugins.forEach((plugin: Plugin) => {
23+ console.log(`processing ${plugin.id}`);
24+ marked.use({
25+ walkTokens: (token) => {
26+ const domain = 'https://github.com';
27+ const pre = `${domain}/${plugin.username}/${plugin.repo}/blob/${plugin.branch}`;
28+
29+ if (token.type === 'link' || token.type === 'image') {
30+ if (token.href && !token.href.startsWith('http') && !token.href.startsWith('#')) {
31+ token.href = `${pre}/${token.href.replace('./', ``)}`;
32+ }
33+ } else if (token.type === 'html') {
34+ token.text = '';
35+ // token.text = token.text.replace(/\.\//g, `${pre}/`);
36+ }
37+ },
38+ });
39+
40+ const markdown = markdownDb.markdown[plugin.id];
41+ if (!markdown) return;
42+ const html = marked(markdown);
43+ nextDb[plugin.id] = html;
44+ });
45+
46+ try {
47+ const json = prettier.format(JSON.stringify({ html: nextDb }), {
48+ parser: 'json',
49+ printWidth: 100,
50+ });
51+ await writeFile('./src/lib/html.json', json);
52+ } catch (err) {
53+ console.error(err);
54+ }
55+}
+50,
-7
1@@ -43,8 +43,11 @@ async function processMissingResources() {
2 console.log(`Missing ${missing.length} resources`);
3
4 const results = await processResources(missing);
5+ const markdownFile = await readFile('./src/lib/markdown.json');
6+ const markdownJson = JSON.parse(markdownFile.toString());
7 const plugins = { ...db.plugins, ...results.plugins };
8- return plugins;
9+ const markdown = { ...markdownJson.markdown, ...results.markdown };
10+ return { plugins, markdown };
11 }
12
13 async function delay(ms: number): Promise<void> {
14@@ -78,9 +81,9 @@ async function githubApi(endpoint: string): Promise<Resp<{ [key: string]: any }>
15 ok: false,
16 data: {
17 status: res.status,
18- error: new Error(`JSON parsing error [${url}]`),
19- },
20- };
21+ error: new Error(`JSON parsing error [${url}]`)
22+ }
23+ }
24 }
25
26 if (res.ok) {
27@@ -99,6 +102,25 @@ async function githubApi(endpoint: string): Promise<Resp<{ [key: string]: any }>
28 };
29 }
30
31+async function fetchReadme({ username, repo }: Props): Promise<Resp<string>> {
32+ const result = await githubApi(`/repos/${username}/${repo}/readme`);
33+ if (!result.ok) {
34+ return {
35+ ok: false,
36+ data: result.data as any,
37+ };
38+ }
39+
40+ const url = result.data.download_url;
41+ console.log(`Fetching ${url}`);
42+ const readme = await fetch(url);
43+ const data = await readme.text();
44+ return {
45+ ok: true,
46+ data,
47+ };
48+}
49+
50 async function fetchRepo({ username, repo }: Props): Promise<Resp<{ [key: string]: any }>> {
51 const result = await githubApi(`/repos/${username}/${repo}`);
52 return result;
53@@ -137,10 +159,18 @@ async function fetchGithubData(props: Props): Promise<Resp<any>> {
54 console.log(`${branch.data.status}: ${branch.data.error.message}`);
55 }
56
57+ const readme = await fetchReadme({
58+ username: props.username,
59+ repo: props.repo,
60+ });
61+ if (readme.ok === false) {
62+ console.log(`${readme.data.status}: ${readme.data.error.message}`);
63+ }
64+
65 return {
66 ok: true,
67 data: {
68- readme: '',
69+ readme: readme.ok ? readme.data : '',
70 repo: repo.data,
71 branch: branch.data,
72 },
73@@ -149,6 +179,7 @@ async function fetchGithubData(props: Props): Promise<Resp<any>> {
74
75 async function processResources(resources: Resource[]) {
76 const plugins: { [key: string]: Plugin } = {};
77+ const markdown: { [key: string]: string } = {};
78
79 console.log(`Fetching ${resources.length} resources`);
80
81@@ -161,6 +192,7 @@ async function processResources(resources: Resource[]) {
82 const resp = result.data;
83 const id = `${d.username}/${d.repo}`;
84
85+ markdown[id] = resp.readme;
86 plugins[id] = createPlugin({
87 id,
88 username: d.username,
89@@ -184,13 +216,24 @@ async function processResources(resources: Resource[]) {
90 }
91 }
92
93- return plugins;
94+ return { plugins, markdown };
95 }
96
97-async function saveData(plugins: { [key: string]: Plugin }) {
98+async function saveData({
99+ plugins,
100+ markdown,
101+}: {
102+ plugins: { [key: string]: Plugin };
103+ markdown: { [key: string]: string };
104+}) {
105 const pluginJson = prettier.format(JSON.stringify({ plugins }), {
106 parser: 'json',
107 printWidth: 100,
108 });
109+ const markdownJson = prettier.format(JSON.stringify({ markdown }), {
110+ parser: 'json',
111+ printWidth: 100,
112+ });
113 await writeFile('./src/lib/db.json', pluginJson);
114+ await writeFile('./src/lib/markdown.json', markdownJson);
115 }
+97,
-0
1@@ -0,0 +1,97 @@
2+<script lang="ts">
3+ import qs from 'query-string';
4+ import { goto } from '$app/navigation';
5+
6+ import type { Plugin, Tag } from './types';
7+ import TagItem from './tag.svelte';
8+ import Icon from './icon.svelte';
9+ import Tooltip from '$lib/tooltip.svelte';
10+ import { format, relativeTimeFromDates } from '$lib/date';
11+
12+ export let plugin: Plugin;
13+ export let tags: Tag[];
14+ export let html: string = '<div>readme not found</div>';
15+
16+ function onSearch(curSearch: string) {
17+ const query = qs.parseUrl(window.location.search);
18+ const s = encodeURIComponent(curSearch);
19+ query.query.search = s;
20+ goto(`/${qs.stringifyUrl(query)}`);
21+ }
22+</script>
23+
24+<div class="meta">
25+ <div class="tags_view">
26+ {#each tags as tag}
27+ <TagItem {tag} {onSearch} showCount={false} />
28+ {/each}
29+ </div>
30+ <div class="metrics">
31+ <Tooltip tip="stars" bottom>
32+ <div class="metric"><Icon icon="star" /> <span>{plugin.stars}</span></div>
33+ </Tooltip>
34+ <Tooltip tip="open issues" bottom>
35+ <div class="metric"><Icon icon="alert-circle" /> <span>{plugin.openIssues}</span></div>
36+ </Tooltip>
37+ <Tooltip tip="subscribers" bottom>
38+ <div class="metric"><Icon icon="users" /> <span>{plugin.subscribers}</span></div>
39+ </Tooltip>
40+ <Tooltip tip="forks" bottom>
41+ <div class="metric"><Icon icon="git-branch" /> <span>{plugin.forks}</span></div>
42+ </Tooltip>
43+ </div>
44+ <div class="timestamps">
45+ <div>
46+ <h5 class="ts-header">CREATED</h5>
47+ <h2>{format(new Date(plugin.createdAt))}</h2>
48+ </div>
49+ <div>
50+ <h5 class="ts-header">UPDATED</h5>
51+ <h2>{relativeTimeFromDates(new Date(plugin.updatedAt))}</h2>
52+ </div>
53+ </div>
54+ <hr />
55+</div>
56+{@html html}
57+
58+<style>
59+ :global(img) {
60+ max-width: 100%;
61+ height: auto;
62+ }
63+
64+ .timestamps {
65+ display: flex;
66+ justify-content: space-between;
67+ background-color: var(--primary-color);
68+ padding: 15px;
69+ margin: 15px 0;
70+ }
71+
72+ .ts-header {
73+ margin-bottom: 10px;
74+ }
75+
76+ .meta {
77+ margin-bottom: 20px;
78+ }
79+
80+ .metrics {
81+ display: flex;
82+ justify-content: space-between;
83+ }
84+
85+ .metric {
86+ display: flex;
87+ align-items: center;
88+ cursor: pointer;
89+ }
90+
91+ .tags_view {
92+ margin-bottom: 10px;
93+ }
94+
95+ .install {
96+ margin-top: 10px;
97+ }
98+</style>
+4,
-1
1@@ -12,9 +12,12 @@
2 <div class="container">
3 <div class="header">
4 <h2 class="item_header">
5- <a href="{plugin.link}">{plugin.repo}</a>
6+ <a href="/plugin/{plugin.username}/{plugin.repo}">{plugin.repo}</a>
7 </h2>
8 <div class="metrics">
9+ <Tooltip tip="github repo" bottom>
10+ <a href={plugin.link}><Icon icon="github" /></a>
11+ </Tooltip>
12 <Tooltip tip="stars" bottom>
13 <div class="metric-item"><Icon icon="star" /> <span>{plugin.stars}</span></div>
14 </Tooltip>
1@@ -0,0 +1,15 @@
2+import type { Plugin } from '$lib/types';
3+import * as db from '$lib/db.json';
4+import * as pluginHtml from '$lib/html.json';
5+import { derivePluginData } from '$lib/plugin-data';
6+
7+export async function get({ params }) {
8+ const { username, repo } = params;
9+ const id = `${username}/${repo}`;
10+ const plugin = db.plugins[id] as Plugin;
11+ const { tagDb } = derivePluginData(db.plugins);
12+ const html = pluginHtml.html[id];
13+ const tags = plugin.tags.map((t) => tagDb[t]).filter(Boolean);
14+
15+ return { body: { plugin, html, tags } };
16+}
1@@ -0,0 +1,85 @@
2+<script context="module" lang="ts">
3+ export const prerender = true;
4+
5+ import type { LoadInput } from '@sveltejs/kit';
6+
7+ export async function load({ page, fetch }: LoadInput) {
8+ const { username, repo } = page.params;
9+ const url = `/plugin/${username}/${repo}.json`;
10+ const res = await fetch(url);
11+
12+ if (res.ok) {
13+ return {
14+ props: await res.json(),
15+ };
16+ }
17+
18+ return {
19+ status: res.status,
20+ error: new Error(`Could not load ${url}`),
21+ };
22+ }
23+</script>
24+
25+<script lang="ts">
26+ import type { Plugin, Tag } from '$lib/types';
27+ import PluginView from '$lib/plugin-view.svelte';
28+ import Icon from '$lib/icon.svelte';
29+ import Nav from '$lib/nav.svelte';
30+
31+ export let plugin: Plugin;
32+ export let tags: Tag[];
33+ export let html: string;
34+</script>
35+
36+<svelte:head>
37+ <title>
38+ {plugin.id}: {plugin.description}
39+ </title>
40+ <meta property="og:title" content={plugin.id} />
41+ <meta name="twitter:title" content={plugin.id} />
42+ <meta itemprop="name" content={plugin.id} />
43+
44+ <meta name="description" content="{plugin.id}: {plugin.description}" />
45+ <meta itemprop="description" content="{plugin.id}: {plugin.description}" />
46+ <meta property="og:description" content="{plugin.id}: {plugin.description}" />
47+ <meta name="twitter:description" content="{plugin.id}: {plugin.description}" />
48+</svelte:head>
49+
50+<Nav />
51+
52+<div class="container">
53+ <div class="view">
54+ <div class="header">
55+ <h1>{plugin.id}</h1>
56+ {#if plugin.homepage}<a href={plugin.homepage}>website</a>{/if}
57+ <a href={plugin.link}><Icon icon="github" /> <span>github</span></a>
58+ </div>
59+ <PluginView {plugin} {tags} {html} />
60+ </div>
61+</div>
62+
63+<style>
64+ .container {
65+ display: flex;
66+ justify-content: center;
67+ width: 100%;
68+ }
69+
70+ .view {
71+ max-width: 800px;
72+ width: 95%;
73+ padding: 0 10px 30px 10px;
74+ }
75+
76+ .header {
77+ display: flex;
78+ align-items: center;
79+ }
80+
81+ .header > a {
82+ margin-left: 15px;
83+ display: flex;
84+ align-items: center;
85+ }
86+</style>