Skip to content

Commit d6a5152

Browse files
committed
SSR
1 parent 7e0b7eb commit d6a5152

File tree

7 files changed

+131
-146
lines changed

7 files changed

+131
-146
lines changed

build.mjs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import * as esbuild from 'esbuild';
22
import * as vuePlugin from 'esbuild-plugin-vue3';
3-
import * as fs from 'fs/promises';
4-
import * as path from 'path';
53
import htmlPlugin from '@chialab/esbuild-plugin-html';
4+
import { exec } from 'child_process';
5+
import fs from 'fs/promises';
66

77
const isDev = process.argv.includes('--dev');
88

9-
// Minify and copy api.json
10-
const interfaces = JSON.parse(await fs.readFile(path.resolve('api.json')));
11-
12-
await fs.writeFile(path.resolve('public', 'api.json'), JSON.stringify(interfaces));
13-
149
// Esbuild
1510
/** @type {esbuild.BuildOptions} */
1611
const esbuildOptions = {
17-
entryPoints: ['src/index.html'],
12+
entryPoints: ['src/index.html', 'src/ssr.ts'],
1813
minify: true,
1914
bundle: true,
2015
sourcemap: false,
@@ -56,4 +51,24 @@ if (isDev) {
5651

5752
await context.rebuild();
5853
await context.dispose();
54+
55+
console.log('Running SSR...');
56+
exec('node public/ssr.js', async (error, stdout, stderr) => {
57+
if (error) {
58+
console.error(`SSR Error: ${error}`);
59+
return;
60+
}
61+
62+
const ssrHtml = stdout.trim();
63+
64+
const indexPath = 'public/index.html';
65+
66+
let indexHtml = await fs.readFile(indexPath, 'utf8');
67+
indexHtml = indexHtml.replace('<div id="app"></div>', `<div id="app">${ssrHtml}</div>`);
68+
69+
await fs.writeFile(indexPath, indexHtml);
70+
await fs.unlink("public/ssr.js");
71+
72+
console.log('SSR HTML injected successfully');
73+
});
5974
}

src/App.ts

Lines changed: 97 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import type { SidebarGroupData, ApiServices, ApiInterface, ApiMethod, ApiMethodP
22

33
import { ref, defineComponent, markRaw } from 'vue'
44
import Fuse, { type FuseResultMatch, type FuseSortFunctionArg, type IFuseOptions } from 'fuse.js'
5-
import { getInterfaces } from './interfaces';
65
import ApiParameter from './ApiParameter.vue';
76

7+
import interfacesJson from '../api.json';
8+
89
interface FuseSearchType {
910
interface: string
1011
method: string
@@ -19,6 +20,48 @@ export default defineComponent({
1920
ApiParameter,
2021
},
2122
data() {
23+
// @ts-ignore
24+
const interfaces = interfacesJson as ApiServices;
25+
26+
const groupsMap = new Map<string, number>();
27+
const groupsData = new Map<number, SidebarGroupData>([
28+
// Order of apps here defines the order in the sidebar
29+
[0, { name: 'Steam', icon: 'steam.jpg', open: true, methods: {} }],
30+
[730, { name: 'Counter-Strike 2', icon: 'cs2.jpg', open: true, methods: {} }],
31+
[570, { name: 'Dota 2', icon: 'dota.jpg', open: true, methods: {} }],
32+
[1422450, { name: 'Deadlock', icon: 'deadlock.jpg', open: true, methods: {} }],
33+
[440, { name: 'Team Fortress 2', icon: 'tf.jpg', open: true, methods: {} }],
34+
[620, { name: 'Portal 2', icon: 'portal2.jpg', open: true, methods: {} }],
35+
[1046930, { name: 'Dota Underlords', icon: 'underlords.jpg', open: true, methods: {} }],
36+
[583950, { name: 'Artifact Classic', icon: 'artifact.jpg', open: true, methods: {} }],
37+
[1269260, { name: 'Artifact Foundry', icon: 'artifact.jpg', open: true, methods: {} }],
38+
39+
// Beta apps
40+
[247040, { name: 'Dota 2 Experimental', icon: 'dota.jpg', open: false, methods: {} }],
41+
[2305270, { name: 'Dota 2 Staging', icon: 'dota.jpg', open: false, methods: {} }],
42+
]);
43+
44+
for (const interfaceName in interfaces) {
45+
const interfaceAppid = interfaceName.match(/_(?<appid>[0-9]+)$/);
46+
47+
if (interfaceAppid) {
48+
const appid = parseInt(interfaceAppid.groups!.appid, 10);
49+
50+
groupsMap.set(interfaceName, appid);
51+
52+
let group = groupsData.get(appid);
53+
54+
if (!group) {
55+
groupsData.set(appid, {
56+
name: `App ${appid}`,
57+
icon: 'steam.jpg',
58+
open: false,
59+
methods: {}
60+
});
61+
}
62+
}
63+
}
64+
2265
return {
2366
userData: {
2467
webapi_key: '',
@@ -36,25 +79,10 @@ export default defineComponent({
3679
accessTokenVisible: false,
3780
currentFilter: '',
3881
currentInterface: '',
39-
interfaces: {} as ApiServices,
4082
fuzzy: new Object as Fuse<FuseSearchType>,
41-
groupsMap: new Map<string, number>(),
42-
groupsData: new Map<number, SidebarGroupData>([
43-
// Order of apps here defines the order in the sidebar
44-
[0, { name: 'Steam', icon: 'steam.jpg', open: true, methods: {} }],
45-
[730, { name: 'Counter-Strike 2', icon: 'cs2.jpg', open: true, methods: {} }],
46-
[570, { name: 'Dota 2', icon: 'dota.jpg', open: true, methods: {} }],
47-
[1422450, { name: 'Deadlock', icon: 'deadlock.jpg', open: true, methods: {} }],
48-
[440, { name: 'Team Fortress 2', icon: 'tf.jpg', open: true, methods: {} }],
49-
[620, { name: 'Portal 2', icon: 'portal2.jpg', open: true, methods: {} }],
50-
[1046930, { name: 'Dota Underlords', icon: 'underlords.jpg', open: true, methods: {} }],
51-
[583950, { name: 'Artifact Classic', icon: 'artifact.jpg', open: true, methods: {} }],
52-
[1269260, { name: 'Artifact Foundry', icon: 'artifact.jpg', open: true, methods: {} }],
53-
54-
// Beta apps
55-
[247040, { name: 'Dota 2 Experimental', icon: 'dota.jpg', open: false, methods: {} }],
56-
[2305270, { name: 'Dota 2 Staging', icon: 'dota.jpg', open: false, methods: {} }],
57-
]),
83+
interfaces,
84+
groupsMap,
85+
groupsData,
5886
}
5987
},
6088
setup() {
@@ -151,99 +179,74 @@ export default defineComponent({
151179
}
152180
},
153181
mounted(): void {
154-
getInterfaces().then((interfaces) => {
155-
const flattenedMethods: FuseSearchType[] = [];
182+
const flattenedMethods: FuseSearchType[] = [];
156183

157-
try {
158-
this.userData.format = localStorage.getItem('format') || 'json';
159-
this.userData.steamid = localStorage.getItem('steamid') || '';
160-
this.userData.webapi_key = localStorage.getItem('webapi_key') || '';
161-
this.userData.access_token = localStorage.getItem('access_token') || '';
184+
try {
185+
this.userData.format = localStorage.getItem('format') || 'json';
186+
this.userData.steamid = localStorage.getItem('steamid') || '';
187+
this.userData.webapi_key = localStorage.getItem('webapi_key') || '';
188+
this.userData.access_token = localStorage.getItem('access_token') || '';
162189

163-
const favoriteStrings = JSON.parse(localStorage.getItem('favorites') || '[]');
190+
const favoriteStrings = JSON.parse(localStorage.getItem('favorites') || '[]');
164191

165-
for (const favorite of favoriteStrings) {
166-
const [favoriteInterface, favoriteMethod] = favorite.split('/', 2);
192+
for (const favorite of favoriteStrings) {
193+
const [favoriteInterface, favoriteMethod] = favorite.split('/', 2);
167194

168-
if (Object.hasOwn(interfaces, favoriteInterface) &&
169-
Object.hasOwn(interfaces[favoriteInterface], favoriteMethod)) {
170-
interfaces[favoriteInterface][favoriteMethod].isFavorite = true;
195+
if (Object.hasOwn(this.interfaces, favoriteInterface) &&
196+
Object.hasOwn(this.interfaces[favoriteInterface], favoriteMethod)) {
197+
this.interfaces[favoriteInterface][favoriteMethod].isFavorite = true;
171198

172-
this.userData.favorites.add(favorite);
173-
}
199+
this.userData.favorites.add(favorite);
174200
}
175201
}
176-
catch (e) {
177-
console.error(e);
178-
}
202+
}
203+
catch (e) {
204+
console.error(e);
205+
}
179206

180-
for (const interfaceName in interfaces) {
181-
for (const methodName in interfaces[interfaceName]) {
182-
const method = interfaces[interfaceName][methodName];
207+
for (const interfaceName in this.interfaces) {
208+
for (const methodName in this.interfaces[interfaceName]) {
209+
const method = this.interfaces[interfaceName][methodName];
183210

184-
for (const parameter of method.parameters) {
185-
parameter._value = '';
211+
for (const parameter of method.parameters) {
212+
parameter._value = '';
186213

187-
if (parameter.type === 'bool') {
188-
parameter.manuallyToggled = false;
189-
}
214+
if (parameter.type === 'bool') {
215+
parameter.manuallyToggled = false;
190216
}
191-
192-
flattenedMethods.push({
193-
interface: interfaceName,
194-
method: methodName,
195-
} as FuseSearchType);
196217
}
197218

198-
const interfaceAppid = interfaceName.match(/_(?<appid>[0-9]+)$/);
199-
200-
if (interfaceAppid) {
201-
const appid = parseInt(interfaceAppid.groups!.appid, 10);
202-
203-
this.groupsMap.set(interfaceName, appid);
204-
205-
let group = this.groupsData.get(appid);
206-
207-
if (!group) {
208-
this.groupsData.set(appid, {
209-
name: `App ${appid}`,
210-
icon: 'steam.jpg',
211-
open: false,
212-
methods: {}
213-
});
214-
}
215-
}
219+
flattenedMethods.push({
220+
interface: interfaceName,
221+
method: methodName,
222+
} as FuseSearchType);
216223
}
224+
}
217225

218-
this.interfaces = interfaces;
226+
this.setInterface();
219227

228+
window.addEventListener('hashchange', () => {
220229
this.setInterface();
230+
}, false);
231+
232+
const fuseOptions: IFuseOptions<FuseSearchType> = {
233+
shouldSort: true,
234+
useExtendedSearch: true,
235+
includeMatches: true,
236+
minMatchCharLength: 3,
237+
threshold: 0.3,
238+
keys: [{
239+
name: 'interface',
240+
weight: 0.3
241+
}, {
242+
name: 'method',
243+
weight: 0.7
244+
}]
245+
};
246+
const fuse = new Fuse<FuseSearchType>(flattenedMethods, fuseOptions);
247+
this.fuzzy = markRaw(fuse);
221248

222-
window.addEventListener('hashchange', () => {
223-
this.setInterface();
224-
}, false);
225-
226-
const fuseOptions: IFuseOptions<FuseSearchType> = {
227-
shouldSort: true,
228-
useExtendedSearch: true,
229-
includeMatches: true,
230-
minMatchCharLength: 3,
231-
threshold: 0.3,
232-
keys: [{
233-
name: 'interface',
234-
weight: 0.3
235-
}, {
236-
name: 'method',
237-
weight: 0.7
238-
}]
239-
};
240-
const fuse = new Fuse<FuseSearchType>(flattenedMethods, fuseOptions);
241-
this.fuzzy = markRaw(fuse);
242-
243-
this.bindGlobalKeybind();
244-
245-
document.getElementById('loading')!.remove();
246-
});
249+
this.bindGlobalKeybind();
247250
},
248251
computed: {
249252
sidebarInterfaces(): Map<number, SidebarGroupData> {

src/index.html

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
<title>Steam Web API Documentation and Tester</title>
1111

12-
<link rel="preload" href="api.json" as="fetch">
1312
<script src="documentation.js" defer></script>
1413
<link rel="stylesheet" href="style.css">
1514
<link rel="shortcut icon" href="/favicon.ico">
@@ -23,32 +22,6 @@
2322
<link rel="canonical" href="https://steamapi.xpaw.me/">
2423
</head>
2524
<body>
26-
<div id="loading">
27-
<div class="py-2 no-select header">
28-
<div class="container">
29-
<div class="row">
30-
<div class="col-lg-3">
31-
<input type="search" class="search-input form-control me-sm-2" placeholder="Search methods… (type / to focus)">
32-
</div>
33-
<div class="col-lg-9">
34-
<h1><a href="#" class="title">Steam Web API Documentation</a></h1>
35-
with &hearts; by <a href="https://xpaw.me">xPaw</a>
36-
</div>
37-
</div>
38-
</div>
39-
</div>
40-
<div class="container">
41-
<div class="row text-center">
42-
<div class="col-lg-12">
43-
<div class="my-5">
44-
<div class="fs-1">Loading…</div>
45-
You need to have a modern browser with javascript enabled.
46-
</div>
47-
</div>
48-
</div>
49-
</div>
50-
</div>
51-
5225
<div id="app"></div>
5326
</body>
5427
</html>

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { createApp } from 'vue'
1+
import { createSSRApp } from 'vue'
22
import App from './App.vue';
33

44
if ('serviceWorker' in navigator && !('DEV_MODE' in window)) {
55
navigator.serviceWorker.register('serviceworker.js', { scope: './' });
66
}
77

8-
createApp(App).mount('#app');
8+
createSSRApp(App).mount('#app');

src/interfaces.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,3 @@ export interface ApiMethodParameter {
3535
optional: boolean
3636
extra?: ApiMethodParameter[]
3737
}
38-
39-
export async function getInterfaces() {
40-
const apiFetch = await fetch('api.json', {
41-
method: 'GET',
42-
credentials: 'include',
43-
mode: 'no-cors',
44-
});
45-
const interfaces: ApiServices = await apiFetch.json();
46-
47-
return interfaces;
48-
}

src/ssr.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createSSRApp } from 'vue'
2+
import { renderToString } from 'vue/server-renderer'
3+
import App from './App.vue';
4+
5+
const app = createSSRApp(App);
6+
7+
renderToString(app).then((html) => {
8+
console.log(html)
9+
})

src/style.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
@import "bootstrap/dist/css/bootstrap.css";
22

3-
#loading + #app {
4-
display: none;
5-
}
6-
73
a {
84
color: #1f8bff;
95
text-decoration: none;

0 commit comments

Comments
 (0)