From bf9ae6173b69e8059eb49597fb0a9b992b27ab84 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen <30383579+Adammatthiesen@users.noreply.github.com> Date: Thu, 14 Mar 2024 07:59:22 -0700 Subject: [PATCH] New Error handling & logging (#51) Co-authored-by: Jacob Jenkins <7649031+jdtjenkins@users.noreply.github.com> --- .changeset/tall-rice-brush.md | 5 + package/README.md | 4 +- package/package.json | 5 +- .../ExpressiveCode/components/page-data.ts | 2 + package/src/ExpressiveCode/index.ts | 2 + package/src/components/GetGist.astro | 3 + package/src/components/GetGistGroup.astro | 4 + package/src/index.ts | 6 +- package/src/integration.ts | 84 +++++++++--- package/src/lib/gist-processor.ts | 30 ----- package/src/lib/index.ts | 3 +- package/src/lib/octokit.ts | 125 +++++++++++++----- playground/astro.config.mjs | 2 +- pnpm-lock.yaml | 35 ++++- 14 files changed, 214 insertions(+), 96 deletions(-) create mode 100644 .changeset/tall-rice-brush.md delete mode 100644 package/src/lib/gist-processor.ts diff --git a/.changeset/tall-rice-brush.md b/.changeset/tall-rice-brush.md new file mode 100644 index 0000000..198263b --- /dev/null +++ b/.changeset/tall-rice-brush.md @@ -0,0 +1,5 @@ +--- +"@matthiesenxyz/astro-gists": patch +--- + +adds better logging and a new Error handling system, Only user facing change is a verbose switch in config options diff --git a/package/README.md b/package/README.md index 6ab3500..f7b9c5a 100644 --- a/package/README.md +++ b/package/README.md @@ -56,7 +56,9 @@ import { defineConfig } from "astro/config"; export default defineConfig({ + integrations: [astroGist({ // Allows you to set the default theme - theme: ['catppuccin-macchiato'] + theme: ['catppuccin-macchiato'], + // Allows you to enable verbose logging + verbose: false, + })] }); ``` diff --git a/package/package.json b/package/package.json index aaac7ca..4590c97 100644 --- a/package/package.json +++ b/package/package.json @@ -40,12 +40,15 @@ "peerDependencies": { "astro": "^4.4.1" }, + "devDependencies": { + "@octokit/types": "^12.6.0" + }, "dependencies": { "@expressive-code/plugin-line-numbers": "^0.33.4", - "@octokit/types": "^12.6.0", "astro-integration-kit": "^0.5.1", "expressive-code": "^0.33.4", "hast-util-to-html": "8.0.4", + "p-retry": "6.2.0", "octokit": "^3.1.2", "vite": "^5.1.6" } diff --git a/package/src/ExpressiveCode/components/page-data.ts b/package/src/ExpressiveCode/components/page-data.ts index f332b3f..8c7ca4f 100644 --- a/package/src/ExpressiveCode/components/page-data.ts +++ b/package/src/ExpressiveCode/components/page-data.ts @@ -3,8 +3,10 @@ export type PageData = { blockGroupIndex: number } +// Map of request to page data const pageDataMap = new Map() +// Get the page data for a request export function getPageData(request: Request): PageData { let data = pageDataMap.get(request) if (!data) { diff --git a/package/src/ExpressiveCode/index.ts b/package/src/ExpressiveCode/index.ts index cb0b267..3db5bd9 100644 --- a/package/src/ExpressiveCode/index.ts +++ b/package/src/ExpressiveCode/index.ts @@ -2,8 +2,10 @@ import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers' import { ExpressiveCode, loadShikiTheme, type BundledShikiTheme } from 'expressive-code' import config from "virtual:astro-gists/config"; +// Export defined components export { default as Code } from './components/Code.astro' +// Create a custom instance of ExpressiveCode export const engine = new ExpressiveCode({ themes: [ config.theme ? diff --git a/package/src/components/GetGist.astro b/package/src/components/GetGist.astro index 4778481..aeb6f9f 100644 --- a/package/src/components/GetGist.astro +++ b/package/src/components/GetGist.astro @@ -17,11 +17,14 @@ export interface Props { wrap?: boolean; } +// Extracting the Props const { gistId, filename, wrap, showLineNumbers } = Astro.props as Props; +// Setting the Defaults const WRAP = wrap ? wrap : wrap === undefined ? true : false; const SLN = showLineNumbers ? showLineNumbers : showLineNumbers === undefined ? true : false; +// Fetching the Gist const Gist = await getGistFile( gistId, filename); --- diff --git a/package/src/components/GetGistGroup.astro b/package/src/components/GetGistGroup.astro index 5d1f060..c36e6b4 100644 --- a/package/src/components/GetGistGroup.astro +++ b/package/src/components/GetGistGroup.astro @@ -15,13 +15,17 @@ export interface Props { wrap?: boolean; } +// extract the props const { gistId, wrap, showLineNumbers } = Astro.props as Props; +// set the defaults const WRAP = wrap ? wrap : wrap === undefined ? true : false; const SLN = showLineNumbers ? showLineNumbers : showLineNumbers === undefined ? true : false; +// get the Gist const Gist = await getGistGroup(gistId); +// extract the files const files = Gist.files; --- { Gist && ( diff --git a/package/src/index.ts b/package/src/index.ts index 45cb777..81466d1 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -14,6 +14,10 @@ export const optionsSchema = z.object({ * All available themes are listed in the [Shiki documentation](https://shiki.matsu.io/docs/themes). */ theme: z.custom().optional(), -}); + /** + * Optional: Allows the user to enable verbose logging. + */ + verbose: z.boolean().default(false), +}).optional().default({}); export type astroGistsUserConfig = z.infer \ No newline at end of file diff --git a/package/src/integration.ts b/package/src/integration.ts index 921c1f3..ab5bc2b 100644 --- a/package/src/integration.ts +++ b/package/src/integration.ts @@ -1,9 +1,24 @@ import { defineIntegration, createResolver } from "astro-integration-kit" import { corePlugins } from "astro-integration-kit/plugins" -import { isThereAToken, TOKEN_MISSING_ERROR } from "./lib" import { optionsSchema } from "./index" import { readFileSync } from "node:fs"; +import type { AstroIntegrationLogger } from "astro"; +import { loadEnv } from "vite"; +// Load environment variables +const { GITHUB_PERSONAL_TOKEN } = loadEnv("all", process.cwd(), "GITHUB_"); + +// Check if there is a GitHub Personal Token +export const isThereAToken = () => { + if (!GITHUB_PERSONAL_TOKEN) { + return false; + } + return true; + } + +// Error message if the token is missing +export const TOKEN_MISSING_ERROR = "GITHUB_PERSONAL_TOKEN not found. Please add it to your .env file. Without it, you will be limited to 60 requests per hour."; + /** * Astro-Gist - An Astro.js integration for embedding GitHub Gists in your Astro.js project. */ @@ -12,49 +27,74 @@ export default defineIntegration({ optionsSchema, plugins: [...corePlugins], setup({ options }) { - const { resolve } = createResolver(import.meta.url) + // Create resolve helper + const { resolve } = createResolver(import.meta.url); + + // Check if verbose logging is enabled + const isVerbose = options.verbose; + + // Create Gist Logger interface + const gistLogger = async ( + logger: AstroIntegrationLogger, + type: "info"|"warn"|"error", + message: string, + checkVerbose: boolean, + ) => { + // if checkVerbose is true and isVerbose is true, log the message + if (!checkVerbose || checkVerbose && isVerbose) { + if (type === "info") { + logger.info(message); + } else if (type === "warn") { + logger.warn(message); + } else if (type === "error") { + logger.error(message); + } + } + }; return { "astro:config:setup": ({ watchIntegration, addVirtualImports, logger, addDts }) => { - logger.info("Setting up Astro Gists Integration.") - const configSetup = logger.fork("astro-gists/config:setup") - + + // Create a logger for the setup events + const configLogger = logger.fork("astro-gists : setup"); + const configDone = logger.fork("astro-gists : setup-done") + + gistLogger(configLogger, "info", "Setting up Astro Gists Integration.", false); + + gistLogger(configLogger, "warn", "Verbose logging is enabled.", true); + // WATCH INTEGRATION FOR CHANGES watchIntegration(resolve()) // Check for GITHUB_PERSONAL_TOKEN - if (!isThereAToken) {configSetup.warn(TOKEN_MISSING_ERROR)} + if (!isThereAToken()) { + gistLogger(configLogger,"error",TOKEN_MISSING_ERROR, false) + } // Add virtual imports + gistLogger(configLogger, "info", "Adding virtual imports.", true); addVirtualImports({ "virtual:astro-gists/config": `export default ${JSON.stringify(options)}`, "astro-gists:components": `export * from "@matthiesenxyz/astro-gists/components";` }); + // Add .d.ts file + gistLogger(configLogger, "info", "Injecting astro-gists.d.ts file.", true); addDts({ name: "astro-gists", content: readFileSync(resolve("./stubs/astro-gists.d.ts"), "utf-8") }) + // Log that the configuration is complete + gistLogger( + configDone, + "info", + "Configuration for Astro Gists Integration is complete.", + false + ); }, - "astro:config:done": ({ logger }) => { - const configDone = logger.fork("astro-gists/config:done") - configDone.info("Astro Gists Integration Loaded.") - }, - "astro:server:setup": ({ logger }) => { - const serverSetup = logger.fork("astro-gists/server:setup") - serverSetup.info("Setting up Astro Gists Integration for Development.") - }, - "astro:build:start": ({ logger }) => { - const buildStart = logger.fork("astro-gists/build:start") - buildStart.info("Building Astro Gists Integration.") - }, - "astro:build:done": ({ logger }) => { - const buildDone = logger.fork("astro-gists/build:done") - buildDone.info("Astro Gists Integration Built.") - } } } }) diff --git a/package/src/lib/gist-processor.ts b/package/src/lib/gist-processor.ts deleted file mode 100644 index 259f6e9..0000000 --- a/package/src/lib/gist-processor.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { requestRetry } from './octokit'; - -// Get a Gist by ID -export const getGist = async (gistId: string) => { - /** @ts-ignore-error */ - const { data: response } = await requestRetry('GET /gists/{gist_id}', { gist_id: gistId }); - const data = response as { - // biome-ignore lint/suspicious/noExplicitAny: we don't know the shape of the data returned from the API -files: any; data: unknown -}; - return data; -}; - -// Get a file from a Gist by ID and filename -export const getGistFile = async ( - gistId: string, - filename: string - ) => { - const gist = await getGist(gistId); - if (gist?.files) { - const file = gist.files[filename]; - return file ? file : null; - } - return null; -}; - -export const getGistGroup = async (gistId: string) => { - const gist = await getGist(gistId); - return gist; -}; \ No newline at end of file diff --git a/package/src/lib/index.ts b/package/src/lib/index.ts index b7e2afd..8a420de 100644 --- a/package/src/lib/index.ts +++ b/package/src/lib/index.ts @@ -1,2 +1 @@ -export * from "./gist-processor"; -export * from "./octokit"; \ No newline at end of file +export * from "./octokit"; diff --git a/package/src/lib/octokit.ts b/package/src/lib/octokit.ts index d75a5e5..ac4629d 100644 --- a/package/src/lib/octokit.ts +++ b/package/src/lib/octokit.ts @@ -1,43 +1,102 @@ import { Octokit } from "octokit"; +import type { OctokitResponse } from "@octokit/types"; import { loadEnv } from "vite"; -import type { Route, RequestParameters, OctokitResponse } from "@octokit/types" +import pRretry from 'p-retry'; +import config from "virtual:astro-gists/config"; + +// Load config options to check if verbose logging is enabled +const isVerbose = config.verbose; + +// Create Gist Logger interface +const gistLogger = async ( + type: "info"|"warn"|"error", + message: string, + VerboseCheck: boolean + ) => { + // if checkVerbose is true and isVerbose is true, log the message + if (!VerboseCheck || VerboseCheck && isVerbose) { + if (type === "info") { + console.log(`[astro-gists : octokit] ${message}`); + } else if (type === "warn") { + console.log(`[WARN] [astro-gists : octokit] ${message}`); + } else if (type === "error") { + console.log(`[ERROR] [astro-gists : octokit] ${message}`); + } + } + }; // Load environment variables const { GITHUB_PERSONAL_TOKEN } = loadEnv("all", process.cwd(), "GITHUB_"); -export const isThereAToken = () => { - if (!GITHUB_PERSONAL_TOKEN) { - return false; - } - return true; -} - -export const TOKEN_MISSING_ERROR = "GITHUB_PERSONAL_TOKEN not found. Please add it to your .env file. Without it, you will be limited to 60 requests per hour."; - // Create an Octokit instance const octokit = new Octokit({ auth: GITHUB_PERSONAL_TOKEN }); -// Retry requests if rate limited -export async function requestRetry(route: Route, parameters: RequestParameters) { - try { - const response: OctokitResponse = await octokit.request(route, parameters); - return response; - } catch (error) { - /** @ts-ignore-error */ - if (error.response && error.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') { - /** @ts-ignore-error */ - const resetTimeEpochSeconds = error.response.headers['x-ratelimit-reset']; - const currentTimeEpochSeconds = Math.floor(new Date().getTime() / 1000); - const secondsToWait = resetTimeEpochSeconds - currentTimeEpochSeconds; - console.log(`Rate limit reached. Waiting ${secondsToWait} seconds before retrying.`); - return new Promise((resolve) => { - setTimeout(async () => { - const retryResponse = await requestRetry(route, parameters); - resolve(retryResponse); - }, secondsToWait * 1000); - }); - } - // Return a rejected Promise - return Promise.reject(error); +// Retry failed requests +const retry: typeof pRretry = (fn, opts) => + pRretry(fn, { + onFailedAttempt: (e) => + gistLogger("warn", + `Attempt ${e.attemptNumber} failed. There are ${e.retriesLeft} retries left.\n ${e.message}`, + false), + retries: 3, + ...opts, + }); + +// Handle the response from the Octokit API +// biome-ignore lint/suspicious/noExplicitAny: any is used to handle the response from the Octokit API +function getStatusCode(response: OctokitResponse) { + switch (response.status) { + case 200: + return response.data; + case 404: + return "E404"; + case 403: + return "E403"; + case 500: + return "E500"; + default: + return "E000"; } -} \ No newline at end of file +} +// Gist Grabber +const gistGrabber = async (gistId: string) => { + const response = await retry(() => octokit.request('GET /gists/{gist_id}', { gist_id: gistId })); + const statusCode = getStatusCode(response); + + if (statusCode === "E404") { + gistLogger("error", `Gist ${gistId} not found.`, false); + return null; + } + if (statusCode === "E403") { + gistLogger("error", "Rate limit exceeded. Please try again later.", false); + return null; + } + if (statusCode === "E500") { + gistLogger("error", "Internal server error. Please try again later.", false); + return null; + } + if (statusCode === "E000") { + gistLogger("error", "An unknown error occurred. Please try again later.", false); + return null; + } + if (statusCode === response.data) { + gistLogger("info", `Gist ${gistId} found.`, true); + } + return statusCode; +} + +// Get a file from a Gist by ID and filename +export const getGistFile = async ( + gistId: string, + filename: string + ) => { + const gist = await gistGrabber(gistId); + const file = gist.files[filename]; + return file ? file : null; +}; + +// Get a Group of Gist files by ID +export const getGistGroup = async (gistId: string) => { + const gist = await gistGrabber(gistId); + return gist; +}; \ No newline at end of file diff --git a/playground/astro.config.mjs b/playground/astro.config.mjs index aa66aa3..81e887d 100644 --- a/playground/astro.config.mjs +++ b/playground/astro.config.mjs @@ -4,6 +4,6 @@ import mdx from "@astrojs/mdx" // https://astro.build/config export default defineConfig({ - integrations: [astroGist(), mdx()] + integrations: [astroGist({ verbose: true }), mdx()] }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afc3eb5..a808d2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@expressive-code/plugin-line-numbers': specifier: ^0.33.4 version: 0.33.4 - '@octokit/types': - specifier: ^12.6.0 - version: 12.6.0 astro: specifier: ^4.4.1 version: 4.4.11 @@ -38,9 +35,16 @@ importers: octokit: specifier: ^3.1.2 version: 3.1.2 + p-retry: + specifier: 6.2.0 + version: 6.2.0 vite: specifier: ^5.1.6 version: 5.1.6(@types/node@20.11.25) + devDependencies: + '@octokit/types': + specifier: ^12.6.0 + version: 12.6.0 playground: dependencies: @@ -1186,7 +1190,6 @@ packages: /@octokit/openapi-types@20.0.0: resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==} - dev: false /@octokit/plugin-paginate-graphql@4.0.0(@octokit/core@5.1.0): resolution: {integrity: sha512-7HcYW5tP7/Z6AETAPU14gp5H5KmCPT3hmJrS/5tO7HIgbwenYmgw4OY9Ma54FDySuxMwD+wsJlxtuGWwuZuItA==} @@ -1263,7 +1266,6 @@ packages: resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} dependencies: '@octokit/openapi-types': 20.0.0 - dev: false /@octokit/webhooks-methods@4.1.0: resolution: {integrity: sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==} @@ -1486,6 +1488,10 @@ packages: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: false + /@types/retry@0.12.2: + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + dev: false + /@types/semver@7.5.7: resolution: {integrity: sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==} dev: true @@ -3341,6 +3347,11 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-network-error@1.0.1: + resolution: {integrity: sha512-OwQXkwBJeESyhFw+OumbJVD58BFBJJI5OM5S1+eyrDKlgDZPX2XNT5gXS56GSD3NPbbwUuMlR1Q71SRp5SobuQ==} + engines: {node: '>=16'} + dev: false + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -4395,6 +4406,15 @@ packages: eventemitter3: 5.0.1 p-timeout: 6.1.2 + /p-retry@6.2.0: + resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} + engines: {node: '>=16.17'} + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.0.1 + retry: 0.13.1 + dev: false + /p-timeout@6.1.2: resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} engines: {node: '>=14.16'} @@ -4838,6 +4858,11 @@ packages: retext-stringify: 3.1.0 unified: 10.1.2 + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}