From aec03294d6535f32ac396d93e3693a867c114dce Mon Sep 17 00:00:00 2001 From: venashial Date: Sat, 25 Jun 2022 00:17:42 -0700 Subject: [PATCH] Create `send` function for API requests --- docs/dummyStore.ts | 4 ++ docs/routes/getting-started/utils.md | 18 ++++++ package.json | 1 + pnpm-lock.yaml | 37 +++++++++-- src/plugins/generator/index.js | 3 + src/plugins/generator/outputs/openapi.js | 22 +++++++ src/utils/index.ts | 1 + src/utils/send.ts | 82 ++++++++++++++++++++++++ svelte.config.js | 17 ++--- tsconfig.json | 10 +-- 10 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 docs/dummyStore.ts create mode 100644 src/plugins/generator/outputs/openapi.js create mode 100644 src/utils/send.ts diff --git a/docs/dummyStore.ts b/docs/dummyStore.ts new file mode 100644 index 00000000..1fbc5955 --- /dev/null +++ b/docs/dummyStore.ts @@ -0,0 +1,4 @@ +import { writable } from 'svelte/store' + +// Used in `src/utils/send.ts` +export const token = writable('') diff --git a/docs/routes/getting-started/utils.md b/docs/routes/getting-started/utils.md index b0f021b4..727ace43 100644 --- a/docs/routes/getting-started/utils.md +++ b/docs/routes/getting-started/utils.md @@ -2,6 +2,24 @@ title: Built-in utilities --- +## API requests + +Use the `send` function to make API requests. + +```svelte example raised + + +{#await project} + fetching... +{:then project} + {project.downloads} downloads +{/await} +``` + ## Markdown Use the markdown utilities to parse markdown text into HTML. Both markdown parsers have HTML sanitization built-in. diff --git a/package.json b/package.json index da9fd42f..00536d1b 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "insane": "^2.6.2", "jimp": "^0.16.1", "marked": "^4.0.12", + "openapi-typescript": "^5.4.0", "postcss": "^8.4.8", "postcss-easy-import": "^4.0.0", "postcss-extend-rule": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0489a2d5..87c1e9a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,7 @@ specifiers: mdsvex: ^0.10.5 mdsvexamples: ^0.3.0 nodemon: ^2.0.15 + openapi-typescript: ^5.4.0 postcss: ^8.4.8 postcss-easy-import: ^4.0.0 postcss-extend-rule: ^4.0.0 @@ -68,6 +69,7 @@ dependencies: insane: 2.6.2 jimp: 0.16.1 marked: 4.0.12 + openapi-typescript: 5.4.0 postcss: 8.4.8 postcss-easy-import: 4.0.0_postcss@8.4.8 postcss-extend-rule: 4.0.0_postcss@8.4.8 @@ -1444,7 +1446,6 @@ packages: /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true /array-union/1.0.2: resolution: {integrity: sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=} @@ -2746,7 +2747,6 @@ packages: /globalyzer/0.1.0: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - dev: true /globby/11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -2773,7 +2773,6 @@ packages: /globrex/0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - dev: true /got/9.6.0: resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} @@ -3038,7 +3037,6 @@ packages: hasBin: true dependencies: argparse: 2.0.1 - dev: true /jsesc/2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} @@ -3266,6 +3264,12 @@ packages: hasBin: true dev: false + /mime/3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: false + /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -3467,6 +3471,19 @@ packages: mimic-fn: 2.1.0 dev: false + /openapi-typescript/5.4.0: + resolution: {integrity: sha512-YUtsWQ01igvx0xRmJvtzpHsxtvnSORrGMDTXFY1zs3znljsJDvhsUe7XrKoNP2eUmiOIwBXQMoM8M5V+nEnIrw==} + engines: {node: '>= 14.0.0', npm: '>= 7.0.0'} + hasBin: true + dependencies: + js-yaml: 4.1.0 + mime: 3.0.0 + prettier: 2.6.2 + tiny-glob: 0.2.9 + undici: 5.5.1 + yargs-parser: 21.0.1 + dev: false + /optionator/0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -4393,7 +4410,6 @@ packages: resolution: {integrity: sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==} engines: {node: '>=10.13.0'} hasBin: true - dev: true /prism-svelte/0.4.7: resolution: {integrity: sha512-yABh19CYbM24V7aS7TuPYRNMqthxwbvx6FF/Rw920YbyBWO3tnyPIqRMgHuSVsLmuHkkBS1Akyof463FVdkeDQ==} @@ -5075,7 +5091,6 @@ packages: dependencies: globalyzer: 0.1.0 globrex: 0.1.2 - dev: true /tinycolor2/1.4.2: resolution: {integrity: sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==} @@ -5169,6 +5184,11 @@ packages: engines: {node: '>=12.18'} dev: false + /undici/5.5.1: + resolution: {integrity: sha512-MEvryPLf18HvlCbLSzCW0U00IMftKGI5udnjrQbC5D4P0Hodwffhv+iGfWuJwg16Y/TK11ZFK8i+BPVW2z/eAw==} + engines: {node: '>=12.18'} + dev: false + /unique-string/2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -5471,6 +5491,11 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + /yargs-parser/21.0.1: + resolution: {integrity: sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==} + engines: {node: '>=12'} + dev: false + /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/src/plugins/generator/index.js b/src/plugins/generator/index.js index 1feb2b16..19e4b678 100644 --- a/src/plugins/generator/index.js +++ b/src/plugins/generator/index.js @@ -3,6 +3,7 @@ import { landingPage } from './outputs/landingPage.js' import { projectColors } from './outputs/projectColors.js' import { gameVersions } from './outputs/gameVersions.js' import { tags } from './outputs/tags.js' +import { openapi } from './outputs/openapi.js' const API_URL = process.env.VITE_API_URL && process.env.VITE_API_URL === 'https://staging-api.modrinth.com/v2/' @@ -25,6 +26,7 @@ const TTL = 7 * 24 * 60 * 60 * 1000 * @param {boolean} options.landingPage * @param {boolean} options.gameVersions * @param {boolean} options.tags + * @param {boolean} options.openapi * @returns {PluginResult} */ export default function Generator(options) { @@ -57,6 +59,7 @@ export default function Generator(options) { if (options.tags) await tags(API_URL) if (options.landingPage) await landingPage(API_URL) if (options.gameVersions) await gameVersions(API_URL) + if (options.openapi) await openapi(API_URL) if (options.projectColors) await projectColors(API_URL) }, } diff --git a/src/plugins/generator/outputs/openapi.js b/src/plugins/generator/outputs/openapi.js new file mode 100644 index 00000000..546c648e --- /dev/null +++ b/src/plugins/generator/outputs/openapi.js @@ -0,0 +1,22 @@ +import { promises as fs } from 'fs' +import cliProgress from 'cli-progress' +import openapiTS from 'openapi-typescript' + +export async function openapi() { + const progressBar = new cliProgress.SingleBar({ + format: 'Generating openapi types | {bar} | {percentage}%', + barCompleteChar: '\u2588', + barIncompleteChar: '\u2591', + hideCursor: true, + }) + progressBar.start(2, 0) + + const output = await openapiTS('https://docs.modrinth.com/redocusaurus/plugin-redoc-0.yaml') + progressBar.increment() + + // Write JSON file + await fs.writeFile('./generated/openapi.ts', output) + progressBar.increment() + + progressBar.stop() +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 864b961a..d0b4517a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export { ago } from './ago' export { Permissions } from './permissions' export { formatVersions, getPrimary, downloadUrl } from './versions' export { markdown, markdownInline } from './parse' +export { send } from './send' diff --git a/src/utils/send.ts b/src/utils/send.ts new file mode 100644 index 00000000..44609123 --- /dev/null +++ b/src/utils/send.ts @@ -0,0 +1,82 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /* `svelte-check` doesn't find issues but VSCode does */ +// @ts-ignore: `$stores/account` needs to be created in consumer package +import { token as tokenStore } from '$stores/account' +import { get, writable } from 'svelte/store' +import type { operations } from '$generated/openapi' + +export const fetching = writable(0) + +type method = 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'HEAD' + +/* On get requests with query params, pass them in data */ +export async function send( + method: method, + route: string, + data: // @ts-ignore: Not always present + | (operations[Operation]['requestBody']['content']['application/json'] & + // @ts-ignore + operations[Operation]['requestBody']['content']['multipart/form-data'] & + // @ts-ignore + operations[Operation]['parameters']['query']) + | FormData + | null = null, + options: { + token?: string + fetch?: (info: RequestInfo, init?: RequestInit) => Promise + file?: File + } = { + token: '', + } +): Promise< + // @ts-ignore: On some API routes, a response body is available, if not, defaults to `unknown` + operations[Operation]['responses'][200]['content']['application/json'] +> { + fetching.set(get(fetching) + 1) + + const fetchOptions: RequestInit = { + method, + } + + const token = get(tokenStore) || options.token + if (token) { + fetchOptions.headers['Authorization'] = token + } + + let url = (import.meta.env.VITE_API_URL || 'https://api.modrinth.com/v2/') + route + + if (data) { + if (data instanceof FormData) { + fetchOptions.body = data + } else { + if (method === 'GET' || options.file) { + url += '?' + new URLSearchParams(data as Record).toString() + } else { + fetchOptions.headers['Content-Type'] = 'application/json' + fetchOptions.body = JSON.stringify(data) + } + if (options.file) { + fetchOptions.headers['Content-Type'] = options.file.type + fetchOptions.body = options.file + } + } + } + + const response = await (options.fetch || fetch)(url, fetchOptions) + + fetching.set(get(fetching) - 1) + + if (!response.ok) { + throw response + } + + let parsed: any + if (response.status !== 204) { + try { + parsed = await response.json() + } catch { + console.error('Could not parse API response') + } + } + + return parsed +} diff --git a/svelte.config.js b/svelte.config.js index 5dba7a10..cc0d7758 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -20,10 +20,18 @@ export default { default: true, onError: 'continue', }, + + alias: { + $generated: path.resolve('./generated'), + omorphia: path.resolve('./src'), + ['$stores/account']: path.resolve('./docs/dummyStore.ts'), + }, + vite: { plugins: [ Generator({ gameVersions: true, + openapi: true, }), ...plugins, examples, @@ -31,16 +39,9 @@ export default { precompileIntl('locales'), ], - resolve: { - alias: { - $generated: path.resolve('./generated'), - omorphia: path.resolve('./src'), - }, - }, - build: { rollupOptions: { - external: '/_app/COMPONENT_API.json', + external: ['/_app/COMPONENT_API.json'], }, }, diff --git a/tsconfig.json b/tsconfig.json index 55bd8fe7..cb98ae9f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,5 @@ { "compilerOptions": { - "paths": { - "omorphia/utils": ["src/utils"], - "omorphia/*": ["src/*"], - "omorphia": ["src"], - "$generated/*": ["generated/*"], - "$lib": ["src"], - "$lib/*": ["src/*"] - }, "resolveJsonModule": true, "esModuleInterop": true }, @@ -19,6 +11,6 @@ "./src/**/*.ts", "./src/**/*.svelte" ], - "exclude": ["./node_modules/**", "./.svelte-kit/**"], + "exclude": ["./node_modules/**", "./generated/**", ".svelte-kit/**"], "extends": "./.svelte-kit/tsconfig.json" }