├── .env.example ├── .fleet └── settings.json ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .idea ├── .gitignore ├── GitLink.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── didapichange.iml ├── modules.xml ├── prettier.xml └── vcs.xml ├── .node-dev.json ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .yarn ├── patches │ ├── bullmq-npm-4.15.4-b55917dd70.patch │ └── typedoc-npm-0.26.11-24fa09b154.patch └── releases │ └── yarn-4.0.2.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── accessed ├── app ├── global.css ├── home │ ├── home.module.scss │ └── home.tsx ├── layout.tsx ├── page.tsx └── search │ └── docs │ └── [...pkg] │ ├── page.tsx │ ├── search.module.scss │ └── search.tsx ├── archived └── package │ ├── PackageAPIBrowser │ ├── PackagePage.tsx │ ├── VersionSlider │ │ ├── VersionSlider.module.scss │ │ ├── VersionSlider.tsx │ │ └── index.ts │ ├── Viewer.tsx │ └── index.ts │ └── [...packageName].ts ├── cf-tsdocs-worker ├── package-lock.json ├── package.json ├── src │ └── index.ts ├── tsconfig.json ├── wrangler.toml └── yarn.lock ├── client ├── api │ └── get-package-docs.ts ├── assets │ ├── heart.svg │ ├── toaster-new.png │ ├── toaster.jpeg │ ├── toaster.jpg │ └── toaster.png ├── components │ ├── Footer │ │ ├── Footer.module.scss │ │ └── index.tsx │ ├── Header │ │ ├── Header.module.scss │ │ └── index.tsx │ ├── HeaderIframe │ │ ├── HeaderIframe.module.scss │ │ └── index.tsx │ ├── Logo │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Placeholder │ │ ├── Placeholder.module.scss │ │ └── index.tsx │ ├── SearchBox │ │ ├── Chevron.tsx │ │ ├── SearchBox.module.scss │ │ ├── SearchBox.tsx │ │ ├── VersionDropdown.tsx │ │ └── index.ts │ ├── SuggestionPills │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Toaster │ │ ├── Toaster.scss │ │ └── index.tsx │ └── ToggleMenu │ │ ├── ToggleMenu.module.scss │ │ └── index.tsx └── scripts │ └── global-docs-main.ts ├── common ├── api │ └── algolia.ts ├── client-utils.ts └── logger.ts ├── next-env.d.ts ├── package.json ├── process.yml ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── mstile-150x150.png ├── og-image.png ├── opensearch.xml ├── safari-pinned-tab.svg └── site.webmanifest ├── scripts ├── obliterate-queues.ts └── symlink-assets.ts ├── server.ts ├── server ├── init-sentry.js ├── package │ ├── CustomError.ts │ ├── DocsCache.ts │ ├── common.utils.ts │ ├── extractor │ │ ├── augment-extract.ts │ │ ├── css-overrides.css │ │ ├── doc-generator.tsx │ │ ├── generate-tsconfig.ts │ │ └── plugin-add-missing-exports.ts │ ├── index.ts │ ├── installation.utils.ts │ ├── resolvers.ts │ ├── types.ts │ └── utils.ts ├── queues.ts └── workers │ ├── cleanup-cache.js │ ├── docs-builder-worker-pool.js │ ├── docs-builder-worker.js │ └── install-worker-pool.js ├── stylesheets └── mixins.scss ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | TSDOCS_PASSWORD=password 2 | -------------------------------------------------------------------------------- /.fleet/settings.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/.fleet/settings.json -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: pastelsky 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: yarn install 30 | - run: yarn build 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | shared-dist 15 | .env 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | /docs-cache 35 | /docs 36 | 37 | **/.yarn/cache 38 | **/.yarn/install-state.gz 39 | .fleet 40 | 41 | docs-shared-assets 42 | 43 | combined.log 44 | error.log 45 | err.log 46 | 47 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/GitLink.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 16 | 17 | 19 | 20 | 27 | 28 | 31 | 32 | 34 | 35 | 42 | 43 | 50 | 51 | 58 | 59 | 64 | 65 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/didapichange.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.node-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [".next", "shared/module.js"], 3 | "extensions": { 4 | "ts": "esbuild-register" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 23.1.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.yarn/patches/bullmq-npm-4.15.4-b55917dd70.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/cjs/classes/child.js b/dist/cjs/classes/child.js 2 | index d47d14bbf5762a2412b842e183fa7ada157df7c8..fddbe1383a7fb8172d58c6d2dfc7367f0e185195 100644 3 | --- a/dist/cjs/classes/child.js 4 | +++ b/dist/cjs/classes/child.js 5 | @@ -74,11 +74,15 @@ class Child extends events_1.EventEmitter { 6 | stdin: true, 7 | stdout: true, 8 | stderr: true, 9 | + // Limit worker memory to not blow up the machine 10 | + resourceLimits: { 11 | + maxOldGenerationSizeMb: 1500, 12 | + } 13 | }); 14 | } 15 | else { 16 | this.childProcess = parent = (0, child_process_1.fork)(this.mainFile, [], { 17 | - execArgv, 18 | + execArgv: [...execArgv, '--max-old-space-size=1200'], 19 | stdio: 'pipe', 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /.yarn/patches/typedoc-npm-0.26.11-24fa09b154.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/lib/converter/types.js b/dist/lib/converter/types.js 2 | index cc8beef359a61cfc0e05c73db612eca27173e3aa..8e1b9242853f0af19e2bbaff676061d838cc9da2 100644 3 | --- a/dist/lib/converter/types.js 4 | +++ b/dist/lib/converter/types.js 5 | @@ -239,7 +239,7 @@ const importType = { 6 | convert(context, node) { 7 | const name = node.qualifier?.getText() ?? "__module"; 8 | const symbol = context.checker.getSymbolAtLocation(node); 9 | - (0, assert_1.default)(symbol, "Missing symbol when converting import type node"); 10 | + (0, assert_1.default)(symbol, "Missing symbol when converting import type node at: " + node.getText()); 11 | return models_1.ReferenceType.createSymbolReference(context.resolveAliasedSymbol(symbol), context, name); 12 | }, 13 | convertType(context, type) { 14 | diff --git a/dist/lib/output/plugins/JavascriptIndexPlugin.js b/dist/lib/output/plugins/JavascriptIndexPlugin.js 15 | index 1b5dcd6abe289f6c5a0df6237c5ec2799137b02b..a88ff111f4c3614e8cb0ff040631160ceda045fb 100644 16 | --- a/dist/lib/output/plugins/JavascriptIndexPlugin.js 17 | +++ b/dist/lib/output/plugins/JavascriptIndexPlugin.js 18 | @@ -122,8 +122,11 @@ let JavascriptIndexPlugin = (() => { 19 | return ((refl instanceof models_1.DeclarationReflection || 20 | refl instanceof models_1.DocumentReflection) && 21 | refl.url && 22 | - refl.name && 23 | - !refl.flags.isExternal); 24 | + refl.name 25 | + // Include externals in search because they include re-exports for e.g. 26 | + // && 27 | + // !refl.flags.isExternal 28 | + ); 29 | }); 30 | const indexEvent = new events_1.IndexEvent(initialSearchResults); 31 | this.owner.trigger(events_1.IndexEvent.PREPARE_INDEX, indexEvent); 32 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | npmRegistryServer: "https://registry.npmjs.org/" 8 | 9 | yarnPath: .yarn/releases/yarn-4.0.2.cjs 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Shubham Kanodia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsdocs.dev 2 | 3 |

4 | 5 |

6 | 7 | [TSDocs.dev](https://tsdocs.dev) is a service that lets you browse type reference documentation 8 | for Javascript packages. 9 | 10 | It works even with packages that aren't written in Typescript (sourced from DefinitelyTyped) 11 | or when packages re-export types from other packages. 12 | 13 | Its depends heavily on a customized version of [typedoc](https://github.com/TypeStrong/typedoc) 14 | for generating API docs documentation. 15 | 16 | ## Writing good documentation for your library 17 | 18 | `tsdocs.dev` extracts documentation from the type definitions that ships with libraries. In case a type definition is 19 | unavailable, it searches [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) for the closest equivalent. 20 | 21 | For an example, see documentation for d3 — 22 | https://tsdocs.dev/docs/d3/7.8.5/classes/FormatSpecifier.html 23 | 24 | Internally tsdocs.dev uses a customized version of typedoc to parse 25 | and render documentation, which works on docstrings and markdown 26 | https://typedoc.org/guides/doccomments/ 27 | 28 | 29 | ## Development 30 | 31 | 1. Ensure that you have [redis installed](https://redis.io/docs/install/install-redis/) and running locally 32 | 2. Run `yarn install` 33 | 3. Run `yarn dev` 34 | 35 | ## Sponsors 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/global.css: -------------------------------------------------------------------------------- 1 | @import "sanitize.css"; 2 | @import "open-props/open-props.min.css"; 3 | 4 | :root { 5 | --primary-bg-color: #f9fafb; 6 | --primary-bg-color-transparent: rgba(247, 249, 250, 0.85); 7 | --primary-bg-color-highlight: white; 8 | --selected-stroke-color: #2c75d5; 9 | --selected-border-color: #0e2d57; 10 | --selected-bg: rgb(238, 245, 255); 11 | --hover-bg: rgba(161, 204, 247, 0.2); 12 | --primary-fg-color: #37373a; 13 | --primary-fg-color-faded: rgb(242, 244, 246); 14 | --secondary-color: #536390; 15 | --separator-color: #e6e6ea; 16 | --separator-color-dark: #dedee3; 17 | --separator-size: 0.5px; 18 | --bg-color: #fff; 19 | --bg-color-shaded: #f9fafb; 20 | 21 | --selected-text-color: #1064c9; 22 | --selected-text-color-subdued: #508dde; 23 | --selected-text-color-subdued-more: #84b6f3; 24 | --font-color-subtext: #86868f; 25 | --font-color-subtext-dark: #536473; 26 | 27 | --shadow-overlay: 0px 10px 15px rgba(32, 37, 46, 0.05), 28 | 0px 3px 5px rgba(23, 26, 33, 0.1); 29 | --font-family-code: "JetBrains Mono", Menlo, Consolas, Monaco, Liberation Mono, 30 | Lucida Console, monospace; 31 | --font-family-system: Inter, Roboto, "Helvetica Neue", "Arial Nova", 32 | "Nimbus Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 33 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 34 | "Segoe UI Symbol"; 35 | 36 | --input-bg-color: var(--primary-bg-color-highlight); 37 | 38 | --font-weight-light: 200; 39 | --font-weight-regular: 400; 40 | --font-weight-bold: 600; 41 | 42 | --font-size-xs: 0.75rem; 43 | --font-size-sm: 0.8rem; 44 | --font-size-lg: 1.2rem; 45 | --font-size-xl: 1.5rem; 46 | --font-size-regular: 0.9rem; 47 | 48 | --header-height: 2rem; 49 | } 50 | 51 | @media (max-width: 480px) { 52 | :root { 53 | --font-size-xl: 1.4rem; 54 | } 55 | } 56 | 57 | body { 58 | color: var(--primary-fg-color); 59 | background: var(--primary-bg-color); 60 | font-family: var(--font-family-system); 61 | font-size: var(--font-size-regular); 62 | } 63 | 64 | input { 65 | font-weight: 300; 66 | appearance: none; 67 | color: var(--primary-fg-color); 68 | background: var(--input-bg-color); 69 | font-family: var(--font-family-code); 70 | border-radius: 5px; 71 | outline: none; 72 | border: none; 73 | caret-color: var(--selected-stroke-color); 74 | } 75 | 76 | ::placeholder { 77 | font-weight: var(--font-weight-light); 78 | color: var(--font-color-subtext); 79 | /* font-family: var(--font-family-system);*/ 80 | } 81 | 82 | a { 83 | text-decoration: none; 84 | color: var(--selected-text-color); 85 | } 86 | -------------------------------------------------------------------------------- /app/home/home.module.scss: -------------------------------------------------------------------------------- 1 | .homePageContainer { 2 | width: 100%; 3 | min-height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .homePageContent { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | min-height: 100vh; 14 | margin-top: -9rem; // offset header and logo 15 | margin-bottom: 6rem; 16 | } 17 | 18 | .tagline { 19 | margin-bottom: 2rem; 20 | color: var(--font-color-subtext); 21 | text-align: center; 22 | 23 | @media (max-width: 480px) { 24 | margin-bottom: 1.5rem; 25 | } 26 | } 27 | 28 | .searchContainer { 29 | margin-bottom: 2rem; 30 | width: 35vw; 31 | width: clamp(30rem, 35vw, 40rem); 32 | max-width: 85vw; 33 | } 34 | -------------------------------------------------------------------------------- /app/home/home.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import SearchBox from "../../client/components/SearchBox"; 4 | import styles from "./home.module.scss"; 5 | import Logo from "../../client/components/Logo"; 6 | import SuggestionPills from "../../client/components/SuggestionPills"; 7 | import { useEffect, useState } from "react"; 8 | import Header from "../../client/components/Header"; 9 | import Footer from "../../client/components/Footer"; 10 | import { useRouter } from "next/navigation"; 11 | 12 | export default function Index() { 13 | const router = useRouter(); 14 | 15 | const handleSearchSubmit = async (pkg: string) => { 16 | router.push(`/search/docs/${pkg}`); 17 | }; 18 | 19 | useEffect(() => { 20 | router.prefetch("/search/docs/foo"); 21 | }, []); 22 | 23 | return ( 24 |
25 |
30 |
31 | <> 32 |
33 | 40 |
41 |
42 | browse type documentation for npm packages 43 |
44 | 45 |
46 | handleSearchSubmit(pkg)} autoFocus /> 47 |
48 | 49 |
50 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./global.css"; 2 | import { Metadata, Viewport } from "next"; 3 | import { JetBrains_Mono } from "next/font/google"; 4 | 5 | const jetbrainsMono = JetBrains_Mono({ 6 | subsets: ["latin"], 7 | display: "swap", 8 | weight: ["200", "400", "600"], 9 | preload: true, 10 | }); 11 | 12 | export const metadata: Metadata = { 13 | metadataBase: new URL("https://tsdocs.dev"), 14 | title: "TS Docs | Reference docs for npm packages", 15 | applicationName: "TS Docs", 16 | description: "Generate type documentation for npm libraries", 17 | openGraph: { 18 | type: "website", 19 | url: "https://tsdocs.dev/", 20 | title: "TS Docs | Reference docs for npm packages", 21 | description: "Generate type documentation for npm libraries", 22 | images: [ 23 | { 24 | url: "https://tsdocs.dev/og-image.png", 25 | }, 26 | ], 27 | }, 28 | twitter: { 29 | card: "summary_large_image", 30 | site: "https://tsdocs.dev/", 31 | title: "TS Docs | Reference docs for npm packages", 32 | description: "Generate type documentation for npm libraries", 33 | images: [ 34 | { 35 | url: "https://tsdocs.dev/og-image.png", 36 | }, 37 | ], 38 | }, 39 | manifest: "/site.webmanifest", 40 | icons: [ 41 | { 42 | url: "/apple-touch-icon.png", 43 | sizes: "180x180", 44 | rel: "apple-touch-icon", 45 | type: "image/png", 46 | }, 47 | { 48 | url: "/favicon-32x32.png", 49 | rel: "icon", 50 | sizes: "32x32", 51 | type: "image/png", 52 | }, 53 | { 54 | url: "/favicon-16x16.png", 55 | rel: "icon", 56 | sizes: "16x16", 57 | type: "image/png", 58 | }, 59 | ], 60 | }; 61 | 62 | export const viewport: Viewport = { 63 | themeColor: "#2C75D5", 64 | width: "device-width", 65 | initialScale: 1, 66 | }; 67 | 68 | function Layout({ children }) { 69 | return ( 70 | 71 | 77 | {children} 78 | 79 | ); 80 | } 81 | 82 | export default Layout; 83 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from "./home/home"; 2 | 3 | export default function Home() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/search/docs/[...pkg]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Search from "./search"; 3 | 4 | const SearchPage = async ({ 5 | params, 6 | }: { 7 | params: Promise<{ pkg: string | string[] }>; 8 | }) => { 9 | const { pkg } = await params; 10 | return ; 11 | }; 12 | 13 | export default SearchPage; 14 | -------------------------------------------------------------------------------- /app/search/docs/[...pkg]/search.module.scss: -------------------------------------------------------------------------------- 1 | .searchContainer { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .searchPageLoaderContainer { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | min-height: 100vh; 13 | } 14 | -------------------------------------------------------------------------------- /app/search/docs/[...pkg]/search.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import styles from "./search.module.scss"; 5 | import { 6 | getPackageDocs, 7 | getPackageDocsSync, 8 | } from "../../../../client/api/get-package-docs"; 9 | import Placeholder from "../../../../client/components/Placeholder"; 10 | import Header from "../../../../client/components/Header"; 11 | import Footer from "../../../../client/components/Footer"; 12 | import { packageFromPath } from "../../../../common/client-utils"; 13 | import { useRouter } from "next/navigation"; 14 | import { useSearchParams } from "next/navigation"; 15 | 16 | export default function Search({ pkg }) { 17 | const pkgArray = Array.isArray(pkg) ? pkg : [pkg]; 18 | const pkgPath = pkgArray.map(decodeURIComponent).join("/").split(/[#?]/)[0]; 19 | 20 | const [status, setStatus] = useState<"loading" | "error">("loading"); 21 | const [error, setError] = useState<{ 22 | errorCode: string; 23 | errorMessage: string; 24 | } | null>(null); 25 | 26 | const router = useRouter(); 27 | const searchParams = useSearchParams(); 28 | const force = !!searchParams.get("force"); 29 | const withForce = force ? "?force=true" : ""; 30 | 31 | let packageName = ""; 32 | let packageVersion = ""; 33 | 34 | if (!pkgArray.length) { 35 | setStatus("error"); 36 | setError({ 37 | errorCode: "NO_PACKAGE_SPECIFIED", 38 | errorMessage: "No package name was specified", 39 | }); 40 | } 41 | 42 | const pathFragments = packageFromPath(pkgPath); 43 | packageName = pathFragments.packageName; 44 | packageVersion = pathFragments.packageVersion; 45 | 46 | const searchAndRedirect = async (pkg: string, version: string | null) => { 47 | try { 48 | const result = await getPackageDocsSync(pkg, version, { force }); 49 | 50 | if (result.status === "success") { 51 | window.location.href = `/docs/${ 52 | version ? [pkg, version].join("/") : pkg 53 | }/index.html${withForce}`; 54 | } else { 55 | console.error("Getting package docs failed", result); 56 | setStatus("error"); 57 | setError({ 58 | errorMessage: result.errorMessage, 59 | errorCode: result.errorCode, 60 | }); 61 | } 62 | } catch (err) { 63 | console.error("Getting package docs failed", err); 64 | setStatus("error"); 65 | setError({ 66 | errorMessage: "UNKNOWN_ERROR", 67 | errorCode: "Unexpected error when building the package", 68 | }); 69 | } 70 | }; 71 | 72 | const handleSearchSubmit = async (pkg: string) => { 73 | setStatus("loading"); 74 | router.replace(`/search/docs/${pkg}${withForce}`); 75 | searchAndRedirect(pkg, null); 76 | }; 77 | 78 | const handleVersionChange = async (version: string) => { 79 | router.replace( 80 | `/search/docs/${[packageName, version].join("/")}${withForce}`, 81 | ); 82 | searchAndRedirect(pkg, version); 83 | }; 84 | 85 | useEffect(() => { 86 | searchAndRedirect(packageName, packageVersion); 87 | }, []); 88 | 89 | return ( 90 |
91 |
98 |
99 | 100 |
101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /archived/package/PackageAPIBrowser/PackagePage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery } from "@tanstack/react-query"; 3 | import { useRouter } from "next/router"; 4 | import axios from "axios"; 5 | import Viewer from "./Viewer"; 6 | import VersionSlider from "./VersionSlider"; 7 | import { getPackageVersions } from "../../../common/api/algolia"; 8 | import SearchBox from "../../../client/components/SearchBox"; 9 | 10 | const PackagePage = () => { 11 | const { query, pathname, isReady, push } = useRouter(); 12 | 13 | console.log("pathname is ", { pathname }); 14 | 15 | const packageName = Array.isArray(query.packageName) 16 | ? query.packageName.join("/") 17 | : query.packageName; 18 | 19 | console.log("query.packageName", query.packageName); 20 | 21 | const { 22 | isLoading: isPackageAPILoading, 23 | isError: isPackageAPIError, 24 | error: packageAPIError, 25 | data: packageAPIData, 26 | } = useQuery(["package-api", { packageName }], () => 27 | axios.get(`/api/package?package=${packageName}`).then((res) => res.data), 28 | ); 29 | 30 | const { 31 | isLoading: isPackageVersionsLoading, 32 | isError: isPackageVersionsError, 33 | error: packageVersionsError, 34 | data: packageVersionsData, 35 | } = useQuery(["package-versions", { packageName }], () => 36 | getPackageVersions(packageName), 37 | ); 38 | 39 | return ( 40 |
41 | push(`/package/${item}`)} /> 42 | {isPackageAPILoading &&
Loading...
} 43 | {isPackageAPIError && ( 44 |
Error... {JSON.stringify({ packageAPIError }, null, 2)}
45 | )} 46 | {packageAPIData && } 47 | {packageVersionsData && ( 48 | 53 | )} 54 |
55 | ); 56 | }; 57 | 58 | PackagePage.getInitialProps = async (ctx) => { 59 | return {}; 60 | }; 61 | 62 | export default PackagePage; 63 | -------------------------------------------------------------------------------- /archived/package/PackageAPIBrowser/VersionSlider/VersionSlider.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../stylesheets/mixins"; 2 | 3 | .versionSlider { 4 | @include popup; 5 | position: fixed; 6 | bottom: 4rem; 7 | right: 4rem; 8 | width: clamp(20rem, 25vw, 30rem); 9 | height: 123px; 10 | padding-top: 10px; 11 | font-family: var(--font-family-code); 12 | user-select: none; 13 | 14 | &::after { 15 | content: ""; 16 | position: absolute; 17 | left: 0; 18 | right: 0; 19 | top: 0; 20 | bottom: 0; 21 | box-shadow: 22 | inset 12px 0 15px -4px var(--primary-bg-color), 23 | inset -12px 0 8px -4px var(--primary-bg-color); 24 | border-radius: 4px; 25 | width: 100%; 26 | height: 100%; 27 | pointer-events: none; 28 | } 29 | } 30 | 31 | .versionCellContainer { 32 | display: flex; 33 | list-style: none; 34 | margin: 0; 35 | position: relative; 36 | overflow-x: auto; 37 | scroll-snap-type: x mandatory; 38 | padding: 8px clamp(10rem, 12.5vw, 15rem) 0; 39 | height: 100%; 40 | -ms-overflow-style: none; /* Internet Explorer 10+ */ 41 | scrollbar-width: none; /* Firefox */ 42 | 43 | &::-webkit-scrollbar { 44 | display: none; /* Safari and Chrome */ 45 | } 46 | 47 | &::after { 48 | content: ""; 49 | position: absolute; 50 | top: 0; 51 | width: 100%; 52 | height: 100%; 53 | //background: linear-gradient(to right, var(--primary-bg-color) 1%, transparent 10%, transparent 90%, var(--primary-bg-color)); 54 | pointer-events: none; 55 | } 56 | } 57 | 58 | .versionCellContent { 59 | display: flex; 60 | white-space: nowrap; 61 | } 62 | 63 | .versionCell { 64 | color: var(--primary-fg-color-faded); 65 | font-weight: var(--font-weight-light); 66 | position: relative; 67 | height: 40px; 68 | opacity: 0.9; 69 | scroll-snap-align: center; 70 | scroll-snap-stop: always; 71 | font-size: var(--font-size-xs); 72 | 73 | button { 74 | appearance: none; 75 | background: inherit; 76 | border: none; 77 | color: inherit; 78 | font-family: inherit; 79 | font-weight: inherit; 80 | font-size: inherit; 81 | cursor: pointer; 82 | transition: color 0.1s; 83 | outline: none; 84 | border-radius: 1px; 85 | padding: 3px 8px; 86 | letter-spacing: -1.2px; 87 | 88 | &:focus { 89 | background: var(--selected-bg); 90 | } 91 | } 92 | 93 | &::after { 94 | content: ""; 95 | display: block; 96 | width: 1px; 97 | height: 8px; 98 | position: absolute; 99 | margin-top: 5px; 100 | left: 0; 101 | right: 0; 102 | margin-left: auto; 103 | margin-right: auto; 104 | background: var(--primary-fg-color); 105 | opacity: 0.5; 106 | } 107 | 108 | &:hover { 109 | opacity: 1; 110 | } 111 | } 112 | 113 | .versionCellSelected { 114 | font-weight: var(--font-weight-bold); 115 | opacity: 1; 116 | 117 | button { 118 | @include selectedLabel; 119 | 120 | &:focus { 121 | background: var(--selected-bg); 122 | } 123 | } 124 | } 125 | 126 | .versionPointer { 127 | position: absolute; 128 | left: 0; 129 | right: 0; 130 | bottom: 0; 131 | width: 2px; 132 | background: var(--selected-stroke-color); 133 | height: 60px; 134 | margin-left: auto; 135 | margin-right: auto; 136 | } 137 | 138 | .versionLabel { 139 | text-transform: uppercase; 140 | font-weight: var(--font-weight-regular); 141 | font-size: var(--font-size-xs); 142 | text-align: center; 143 | letter-spacing: 1.5px; 144 | color: var(--primary-fg-color); 145 | opacity: 0.4; 146 | } 147 | 148 | .dimmed { 149 | opacity: 0.65; 150 | } 151 | -------------------------------------------------------------------------------- /archived/package/PackageAPIBrowser/VersionSlider/VersionSlider.tsx: -------------------------------------------------------------------------------- 1 | import React, { SyntheticEvent, useEffect, useRef, useState } from "react"; 2 | import styles from "./VersionSlider.module.scss"; 3 | import cx from "classnames"; 4 | import { getPackageVersions } from "../../../../common/api/getPackageVersions"; 5 | import { useQuery } from "@tanstack/react-query"; 6 | import axios from "axios"; 7 | import semver, { SemVer } from "semver"; 8 | import debounce from "debounce"; 9 | import { useHotkeys } from "react-hotkeys-hook"; 10 | 11 | const VersionCell = ({ 12 | version, 13 | isSelected, 14 | onSelect, 15 | }: { 16 | version: SemVer; 17 | isSelected: boolean; 18 | onSelect: ( 19 | version: string, 20 | e: React.MouseEvent, 21 | ) => void; 22 | }) => { 23 | const versionCellRef = useRef(null); 24 | 25 | useEffect(() => { 26 | if (isSelected) { 27 | versionCellRef.current.focus({ preventScroll: true }); 28 | // versionCellRef.current.scrollIntoView({ 29 | // behavior: "smooth", 30 | // inline: "center", 31 | // }); 32 | } 33 | }, [isSelected]); 34 | 35 | return ( 36 |
  • 41 | 50 |
  • 51 | ); 52 | }; 53 | 54 | const sumAll = (arr: number[]) => arr.reduce((a, b) => a + b, 0); 55 | 56 | const VersionSlider = ({ 57 | selectedVersion, 58 | versions, 59 | includePrerelease = false, 60 | includePatch = false, 61 | includeMinor = false, 62 | }: { 63 | selectedVersion?: string; 64 | versions: SemVer[]; 65 | includePrerelease: boolean; 66 | includePatch: boolean; 67 | includeMinor: boolean; 68 | }) => { 69 | const filteredVersions = versions 70 | .sort((versionA, versionB) => semver.compare(versionA, versionB)) 71 | .filter((version, index) => { 72 | if (!includePrerelease && version.prerelease.length) { 73 | return false; 74 | } 75 | 76 | if ( 77 | !includePatch && 78 | version.minor === versions[index + 1]?.minor && 79 | version.major === versions[index + 1]?.major 80 | ) { 81 | return false; 82 | } 83 | 84 | if (!includeMinor && version.major === versions[index + 1]?.major) { 85 | return false; 86 | } 87 | 88 | return true; 89 | }); 90 | 91 | const initialSelected = 92 | semver.parse(selectedVersion) || 93 | filteredVersions[filteredVersions.length - 1]; 94 | const [selected, setSelected] = useState(initialSelected); 95 | const versionListRef = useRef(null); 96 | const versionCellContentRef = useRef(null); 97 | const midpointsRef = useRef([]); 98 | const isScrolling = useRef(false); 99 | let scrollTimer = useRef(null); 100 | let isClickScroll = useRef(false); 101 | let isDrag = useRef(false); 102 | const selectedStateRef = useRef(selected); 103 | 104 | let widthMeasurements = []; 105 | 106 | const getClosestMid = (scrollX: number) => { 107 | const { current: midpoints } = midpointsRef; 108 | if (!midpoints.length) return 0; 109 | if (scrollX <= midpoints[0]) return 0; 110 | if (scrollX >= midpoints[midpoints.length - 1]) return midpoints.length - 1; 111 | return midpoints.findIndex((midpoint) => midpoint >= scrollX); 112 | }; 113 | 114 | useEffect(() => { 115 | widthMeasurements = [ 116 | ...versionListRef.current.querySelectorAll(`.${styles.versionCell}`), 117 | ].map((element) => element.scrollWidth); 118 | 119 | let tempMidpoints = []; 120 | widthMeasurements.forEach((width, index) => { 121 | tempMidpoints.push(sumAll(widthMeasurements.slice(0, index + 1))); 122 | }); 123 | 124 | midpointsRef.current = tempMidpoints; 125 | }, [includePrerelease]); 126 | 127 | useHotkeys("left", (...args) => { 128 | const currentIndex = filteredVersions.findIndex( 129 | (v) => v === selectedStateRef.current, 130 | ); 131 | console.log("left", currentIndex); 132 | if (currentIndex > 0) { 133 | isClickScroll.current = true; 134 | setSelected(filteredVersions[currentIndex - 1]); 135 | selectedStateRef.current = filteredVersions[currentIndex - 1]; 136 | } 137 | }); 138 | 139 | useHotkeys("right", (...args) => { 140 | const currentIndex = filteredVersions.findIndex( 141 | (v) => v === selectedStateRef.current, 142 | ); 143 | console.log("right", currentIndex); 144 | if (currentIndex < filteredVersions.length - 1) { 145 | isClickScroll.current = true; 146 | setSelected(filteredVersions[currentIndex + 1]); 147 | selectedStateRef.current = filteredVersions[currentIndex + 1]; 148 | } 149 | }); 150 | 151 | const handleVersionSelect = ( 152 | version, 153 | e: React.MouseEvent, 154 | ) => { 155 | isClickScroll.current = true; 156 | setSelected(version); 157 | selectedStateRef.current = version; 158 | 159 | e.target.scrollIntoView({ 160 | behavior: "smooth", 161 | inline: "center", 162 | }); 163 | }; 164 | 165 | const handleSlide = (event) => { 166 | const onScrollStop = () => { 167 | console.log("setting scroll to false"); 168 | isClickScroll.current = false; 169 | isScrolling.current = false; 170 | 171 | const closestIndex = getClosestMid(event.target.scrollLeft); 172 | console.log( 173 | "closestIndex is ", 174 | closestIndex, 175 | filteredVersions.length, 176 | filteredVersions[closestIndex], 177 | ); 178 | 179 | if (closestIndex === -1) { 180 | console.error("invalid index", event.target.scrollLeft); 181 | } 182 | 183 | setSelected(filteredVersions[closestIndex]); 184 | selectedStateRef.current = filteredVersions[closestIndex]; 185 | }; 186 | 187 | console.log("scrolling"); 188 | if (!isScrolling.current) { 189 | scrollTimer.current = setTimeout(onScrollStop, 100); 190 | isScrolling.current = true; 191 | } else { 192 | clearTimeout(scrollTimer.current); 193 | scrollTimer.current = setTimeout(onScrollStop, 100); 194 | } 195 | }; 196 | 197 | let pos; 198 | 199 | const handleMouseUp = () => { 200 | if (!pos) return; 201 | const offset = versionCellContentRef.current.style.transform.match( 202 | /translateX\(([\d.]+)px\)/, 203 | ); 204 | 205 | if (offset && offset[1]) 206 | versionListRef.current.scrollTo({ 207 | left: pos.left - offset[1], 208 | behaviour: "smooth", 209 | }); 210 | versionCellContentRef.current.style.transform = "none"; 211 | 212 | versionListRef.current.style.cursor = "initial"; 213 | console.log("mouse up", versionCellContentRef.current.style.transform); 214 | pos = null; 215 | }; 216 | 217 | const handleMouseMove = (e) => { 218 | if (!pos) return; 219 | versionListRef.current.style.cursor = "grabbing"; 220 | const dx = e.clientX - pos.x; 221 | console.log("mouse moving...", dx); 222 | isDrag.current = true; 223 | 224 | // Scroll the element 225 | versionCellContentRef.current.style.transform = "translateX(" + dx + "px)"; 226 | }; 227 | 228 | const handleMouseDown = (e) => { 229 | console.log("mouse down"); 230 | versionListRef.current.style.cursor = "grab"; 231 | pos = { 232 | // The current scroll 233 | left: e.currentTarget.scrollLeft, 234 | // Get the current mouse position 235 | x: e.clientX, 236 | }; 237 | 238 | console.log("set current pos to ", pos); 239 | }; 240 | 241 | return ( 242 |
    243 |
    Version
    244 |
      252 |
      253 | {filteredVersions.map((version, index) => ( 254 | 260 | ))} 261 |
      262 |
    263 |
    264 |
    265 | ); 266 | }; 267 | 268 | export default VersionSlider; 269 | -------------------------------------------------------------------------------- /archived/package/PackageAPIBrowser/VersionSlider/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./VersionSlider"; 2 | -------------------------------------------------------------------------------- /archived/package/PackageAPIBrowser/Viewer.tsx: -------------------------------------------------------------------------------- 1 | import Editor from "@monaco-editor/react"; 2 | import { useRef } from "react"; 3 | 4 | import React from "react"; 5 | 6 | const Viewer = ({ content }: { content: string }) => { 7 | const editorRef = useRef(null); 8 | 9 | function handleEditorDidMount(editor, monaco) { 10 | // here is the editor instance 11 | // you can store it in `useRef` for further usage 12 | editorRef.current = editor; 13 | console.log("theme", editor.theme); 14 | monaco.editor.setTheme("vs-dark"); 15 | editor.trigger("fold", "editor.foldImports"); 16 | } 17 | 18 | return ( 19 | 52 | ); 53 | }; 54 | 55 | export default Viewer; 56 | -------------------------------------------------------------------------------- /archived/package/PackageAPIBrowser/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./PackagePage"; 2 | -------------------------------------------------------------------------------- /archived/package/[...packageName].ts: -------------------------------------------------------------------------------- 1 | export { default } from "./PackageAPIBrowser"; 2 | -------------------------------------------------------------------------------- /cf-tsdocs-worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cf-tsdocs-worker", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev" 9 | }, 10 | "devDependencies": { 11 | "@cloudflare/workers-types": "^4.20230419.0", 12 | "itty-router": "^3.0.12", 13 | "typescript": "^5.0.4", 14 | "wrangler": "^3.19.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cf-tsdocs-worker/src/index.ts: -------------------------------------------------------------------------------- 1 | interface Env { 2 | tsdocsReflections: any; // Replace 'any' with the actual type of MY_BUCKET if available 3 | R2_ACCESS_SECRET: any; 4 | } 5 | 6 | export default { 7 | async fetch(request: Request, env: Env): Promise { 8 | const url = new URL(request.url); 9 | const key = url.pathname.slice(1); 10 | 11 | if (request.headers.get("X-CF-WORKERS-KEY") !== env.R2_ACCESS_SECRET) { 12 | return new Response("Forbidden", { status: 403 }); 13 | } 14 | 15 | switch (request.method) { 16 | case "PUT": 17 | await env.tsdocsReflections.put(key, request.body); 18 | return new Response(`Put ${key} successfully!`); 19 | case "GET": 20 | const object = await env.tsdocsReflections.get(key); 21 | 22 | if (object === null) { 23 | return new Response("Object Not Found", { status: 404 }); 24 | } 25 | 26 | const headers = new Headers(); 27 | object.writeHttpMetadata(headers); 28 | headers.set("etag", object.httpEtag); 29 | 30 | return new Response(object.body, { 31 | headers, 32 | }); 33 | case "DELETE": 34 | await env.tsdocsReflections.delete(key); 35 | return new Response("Deleted!"); 36 | 37 | default: 38 | return new Response("Method Not Allowed", { 39 | status: 405, 40 | headers: { 41 | Allow: "PUT, GET, DELETE", 42 | }, 43 | }); 44 | } 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /cf-tsdocs-worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "es2021" 17 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 18 | "jsx": "react" /* Specify what JSX code is generated. */, 19 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 24 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | 28 | /* Modules */ 29 | "module": "es2022" /* Specify what module code is generated. */, 30 | // "rootDir": "./", /* Specify the root folder within your source files. */ 31 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 32 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 33 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 36 | "types": [ 37 | "@cloudflare/workers-types" 38 | ] /* Specify type package names to be included without being referenced in a source file. */, 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | "resolveJsonModule": true /* Enable importing .json files */, 41 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 42 | 43 | /* JavaScript Support */ 44 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 45 | "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, 46 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 47 | 48 | /* Emit */ 49 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 50 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 51 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 52 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 53 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 54 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 55 | // "removeComments": true, /* Disable emitting comments. */ 56 | "noEmit": true /* Disable emitting files from a compilation. */, 57 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 58 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 59 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 60 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 65 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 66 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 69 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 71 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 72 | 73 | /* Interop Constraints */ 74 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 75 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 76 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 77 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 78 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 79 | 80 | /* Type Checking */ 81 | "strict": true /* Enable all strict type-checking options. */, 82 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 83 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 84 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 85 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 86 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 87 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 88 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 89 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 90 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 91 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 92 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 93 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 94 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 95 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 96 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 97 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 98 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 99 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 100 | 101 | /* Completeness */ 102 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 103 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cf-tsdocs-worker/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "cf-tsdocs-worker" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-10-30" 4 | 5 | account_id = "0e0b1211b8db62c7912b714b599972e0" # ← Replace with your Account ID. 6 | workers_dev = true 7 | 8 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. 9 | # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ 10 | [[r2_buckets]] 11 | binding = "tsdocsReflections" 12 | bucket_name = "tsdocs-reflections" 13 | 14 | # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. 15 | # Docs: https://developers.cloudflare.com/queues/get-started 16 | # [[queues.producers]] 17 | # binding = "MY_QUEUE" 18 | # queue = "my-queue" 19 | 20 | # Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. 21 | # Docs: https://developers.cloudflare.com/queues/get-started 22 | # [[queues.consumers]] 23 | # queue = "my-queue" 24 | 25 | # Bind another Worker service. Use this binding to call another Worker without network overhead. 26 | # Docs: https://developers.cloudflare.com/workers/platform/services 27 | # [[services]] 28 | # binding = "MY_SERVICE" 29 | # service = "my-service" 30 | 31 | # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. 32 | # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. 33 | # Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects 34 | # [[durable_objects.bindings]] 35 | # name = "MY_DURABLE_OBJECT" 36 | # class_name = "MyDurableObject" 37 | 38 | # Durable Object migrations. 39 | # Docs: https://developers.cloudflare.com/workers/learning/using-durable-objects#configure-durable-object-classes-with-migrations 40 | # [[migrations]] 41 | # tag = "v1" 42 | # new_classes = ["MyDurableObject"] 43 | -------------------------------------------------------------------------------- /client/api/get-package-docs.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from "axios"; 2 | import { 3 | PackageNotFoundError, 4 | PackageVersionMismatchError, 5 | TypeDefinitionResolveError, 6 | TypeDocBuildError, 7 | } from "../../server/package/CustomError"; 8 | 9 | type TriggerAPIResponse = 10 | | { 11 | status: "success"; 12 | } 13 | | { 14 | status: "queued"; 15 | jobId: string; 16 | pollInterval: number; 17 | }; 18 | 19 | type BuildAPIResponse = 20 | | { 21 | status: "success"; 22 | } 23 | | { 24 | status: "failed"; 25 | errorCode: string; 26 | errorMessage: string; 27 | errorStack: string; 28 | }; 29 | 30 | type PollAPIResponse = 31 | | { 32 | status: "success" | "queued"; 33 | } 34 | | { 35 | status: "failed"; 36 | errorCode: string; 37 | errorMessage: string; 38 | errorStack: string; 39 | }; 40 | 41 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 42 | const POLL_TIMEOUT = 200_000; 43 | 44 | type PackageDocsResponse = 45 | | { 46 | status: "success"; 47 | } 48 | | { 49 | status: "failure"; 50 | errorCode: string; 51 | errorMessage: string; 52 | }; 53 | 54 | async function pollQueue( 55 | { 56 | jobId, 57 | pollInterval, 58 | }: { 59 | jobId: string; 60 | pollInterval: number; 61 | }, 62 | timeElapsed = 0, 63 | ): Promise { 64 | if (timeElapsed > POLL_TIMEOUT) { 65 | return { 66 | status: "failure", 67 | errorCode: "DocsBuildTimeout", 68 | errorMessage: 69 | "Building docs took longer than expected. Check back in sometime to see if the load on the server has reduced? If this persists, file an issue.", 70 | }; 71 | } 72 | 73 | const start = Date.now(); 74 | 75 | let pollResponse: AxiosResponse = null; 76 | 77 | try { 78 | pollResponse = await axios.get(`/api/docs/poll/${jobId}`); 79 | } catch (err) { 80 | let errorMessage = ""; 81 | if (err.response) { 82 | if (err.response.status === 404) { 83 | errorMessage = 84 | "Building docs for this package failed, because the job to build the docs was removed from queue."; 85 | } else { 86 | errorMessage = `${err.response.status} ${ 87 | err.response.data ? JSON.stringify(err.response.data) : "" 88 | }`; 89 | } 90 | } 91 | 92 | return { 93 | status: "failure", 94 | errorCode: "UNEXPECTED_DOCS_POLL_FAILURE", 95 | errorMessage: errorMessage, 96 | }; 97 | } 98 | 99 | const status = pollResponse.data.status; 100 | 101 | if (status === "success") { 102 | return { status: "success" }; 103 | } 104 | 105 | if (status === "queued") { 106 | await sleep(pollInterval); 107 | return pollQueue({ jobId, pollInterval }, timeElapsed + Date.now() - start); 108 | } 109 | 110 | if (status === "failed") { 111 | return { 112 | status: "failure", 113 | errorCode: pollResponse.data.errorCode, 114 | errorMessage: getErrorMessage({ 115 | name: pollResponse.data.errorCode, 116 | extra: pollResponse.data.errorMessage, 117 | errorStack: pollResponse.data.errorStack, 118 | }), 119 | }; 120 | } 121 | 122 | return { 123 | status: "failure", 124 | errorCode: "UNEXPECTED_DOCS_POLL_STATUS", 125 | errorMessage: 126 | "Failed because the polling API returned an unknown status: " + status, 127 | }; 128 | } 129 | 130 | export function getErrorMessage(error: { 131 | name: string; 132 | extra: any; 133 | errorStack?: string; 134 | }) { 135 | switch (error.name) { 136 | case "PackageNotFoundError": 137 | return "This package could not be found on the npm registry. Did you get the name right?"; 138 | 139 | case "PackageVersionMismatchError": 140 | return `The given version for this package was not found on the npm registry.\n Found versions: \n${error.extra.join( 141 | ", ", 142 | )}`; 143 | 144 | case "TypeDefinitionResolveError": 145 | return ( 146 | "Failed to resolve types for this package. " + 147 | "This package likely does not ship with types, and it does not have a corresponding package `@types` package " + 148 | "from which reference documentation for its APIs can be built." 149 | ); 150 | case "TypeDocBuildError": 151 | return `Failed to generate documentation for this package.
    152 |
    153 | See stack trace 154 |
    155 |                 ${error.extra}${
    156 |                   "\n" + error.errorStack || ""
    157 |                 }
    158 |               
    159 |
    `; 160 | default: 161 | console.warn("Could not get error message for error: ", error); 162 | return ""; 163 | } 164 | } 165 | 166 | export async function getPackageDocs( 167 | pkg: string, 168 | pkgVersion: string, 169 | { force }: { force: boolean }, 170 | ): Promise { 171 | let triggerResponse: AxiosResponse = null; 172 | const withForce = force ? "?force=true" : ""; 173 | 174 | try { 175 | triggerResponse = await axios.post( 176 | `/api/docs/trigger/${ 177 | pkgVersion ? [pkg, pkgVersion].join("/") : pkg 178 | }${withForce}`, 179 | ); 180 | } catch (err) { 181 | let errorMessage = ""; 182 | if (err.response?.data) { 183 | return { 184 | status: "failure", 185 | errorCode: err.response.data.name, 186 | errorMessage: getErrorMessage(err.response.data), 187 | }; 188 | } 189 | 190 | return { 191 | status: "failure", 192 | errorCode: "UNEXPECTED_DOCS_TRIGGER_FAILURE", 193 | errorMessage: errorMessage, 194 | }; 195 | } 196 | 197 | let triggerResult = triggerResponse.data; 198 | const { status } = triggerResult; 199 | 200 | if (status === "success") { 201 | return { 202 | status: "success", 203 | }; 204 | } 205 | 206 | if (status === "queued") { 207 | return await pollQueue({ 208 | jobId: triggerResult.jobId, 209 | pollInterval: triggerResult.pollInterval, 210 | }); 211 | } 212 | 213 | return { 214 | status: "failure", 215 | errorCode: "UNEXPECTED_DOCS_TRIGGER_STATUS", 216 | errorMessage: 217 | "Failed because the polling API returned an unknown status: " + status, 218 | }; 219 | } 220 | 221 | export async function getPackageDocsSync( 222 | pkg: string, 223 | pkgVersion: string, 224 | { force }: { force: boolean }, 225 | ): Promise { 226 | let triggerResponse: AxiosResponse = null; 227 | const withForce = force ? "?force=true" : ""; 228 | 229 | try { 230 | triggerResponse = await axios.post( 231 | `/api/docs/build/${ 232 | pkgVersion ? [pkg, pkgVersion].join("/") : pkg 233 | }${withForce}`, 234 | ); 235 | } catch (err) { 236 | let errorMessage = ""; 237 | if (err.response?.data) { 238 | return { 239 | status: "failure", 240 | errorCode: err.response.data.name, 241 | errorMessage: getErrorMessage(err.response.data), 242 | }; 243 | } 244 | 245 | return { 246 | status: "failure", 247 | errorCode: "UNEXPECTED_DOCS_TRIGGER_FAILURE", 248 | errorMessage: errorMessage, 249 | }; 250 | } 251 | 252 | let triggerResult = triggerResponse.data; 253 | const { status } = triggerResult; 254 | 255 | if (status === "success") { 256 | return { 257 | status: "success", 258 | }; 259 | } 260 | 261 | return { 262 | status: "failure", 263 | errorCode: triggerResult.errorCode, 264 | errorMessage: getErrorMessage({ 265 | name: triggerResult.errorCode, 266 | extra: triggerResult.errorMessage, 267 | errorStack: triggerResult.errorStack, 268 | }), 269 | }; 270 | } 271 | -------------------------------------------------------------------------------- /client/assets/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/assets/toaster-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/client/assets/toaster-new.png -------------------------------------------------------------------------------- /client/assets/toaster.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/client/assets/toaster.jpeg -------------------------------------------------------------------------------- /client/assets/toaster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/client/assets/toaster.jpg -------------------------------------------------------------------------------- /client/assets/toaster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/client/assets/toaster.png -------------------------------------------------------------------------------- /client/components/Footer/Footer.module.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | background: #222; 3 | width: 100%; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | padding: 1rem 2rem 3rem 2rem; 8 | flex-direction: column; 9 | color: var(--font-color-subtext); 10 | 11 | a { 12 | color: white; 13 | transition: color 0.2s; 14 | display: block; 15 | text-decoration: none; 16 | 17 | &:hover { 18 | color: gray; 19 | } 20 | } 21 | 22 | p { 23 | text-align: center; 24 | } 25 | } 26 | 27 | .footerCredits { 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | 32 | p { 33 | margin: 0; 34 | } 35 | } 36 | 37 | .footerDescription { 38 | p { 39 | text-align: center; 40 | line-height: 1.4; 41 | 42 | a { 43 | display: inline; 44 | } 45 | } 46 | } 47 | 48 | .footerForkButton { 49 | cursor: pointer; 50 | margin-top: 1rem; 51 | border: 2px solid var(--font-color-subtext); 52 | background: transparent; 53 | border-radius: 10px; 54 | padding: 0.5rem 1rem; 55 | display: block; 56 | transition: background 0.2s; 57 | text-transform: uppercase; 58 | letter-spacing: 1.5px; 59 | font-size: 10px; 60 | font-weight: bold; 61 | color: white; 62 | 63 | &:hover { 64 | background: white; 65 | border: 2px solid white; 66 | color: #212121; 67 | } 68 | } 69 | 70 | .footerCreditsInner { 71 | display: flex; 72 | align-items: center; 73 | justify-content: center; 74 | } 75 | 76 | .footerHostingCredits { 77 | display: flex; 78 | flex-direction: column; 79 | align-items: center; 80 | justify-content: center; 81 | margin-top: 1rem; 82 | margin-bottom: 1rem; 83 | } 84 | 85 | .footerSponsorLogo { 86 | margin-top: 1rem; 87 | } 88 | -------------------------------------------------------------------------------- /client/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Footer.module.scss"; 3 | 4 | function DigitalOceanLogo(props: { className: string }) { 5 | return ( 6 | 8 | 10 | 11 | ); 12 | } 13 | 14 | const Footer = () => { 15 | return ( 16 | 51 | ); 52 | }; 53 | 54 | export default Footer; 55 | -------------------------------------------------------------------------------- /client/components/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | padding: 0.8rem 2rem; 4 | position: relative; 5 | z-index: 10; 6 | display: grid; 7 | grid-template-columns: auto 1fr auto; /* Adjust the ratio as needed */ 8 | grid-template-areas: "logo search links"; 9 | gap: 0.5rem; 10 | } 11 | 12 | .headerMinimal { 13 | border: none; 14 | } 15 | 16 | .headerLogo { 17 | grid-area: logo; 18 | display: flex; 19 | align-items: center; 20 | } 21 | 22 | .headerLinks { 23 | margin-left: auto; 24 | display: flex; 25 | align-items: center; 26 | grid-area: links; 27 | } 28 | 29 | .headerCenter { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | grid-area: search; 34 | } 35 | 36 | .headerSearchContainer { 37 | max-width: 20rem; 38 | max-width: clamp(30rem, 35vw, 40rem); 39 | width: 100%; 40 | } 41 | 42 | @media (max-width: 768px) { 43 | /* Adjust the breakpoint as needed */ 44 | .header { 45 | grid-template-columns: 1fr; /* Stack elements in one column */ 46 | grid-template-rows: auto auto; /* Two rows */ 47 | grid-template-areas: 48 | "logo links" 49 | "search search"; 50 | } 51 | } 52 | 53 | @media (max-width: 480px) { 54 | .header { 55 | padding: 0.8rem 1rem; 56 | } 57 | /* Search */ 58 | .headerCenter { 59 | margin-top: 0.5rem; 60 | } 61 | 62 | .headerLinks { 63 | margin-left: auto; 64 | } 65 | 66 | .headerLinkGithub { 67 | width: 22px; 68 | height: 22px; 69 | margin-right: 0.5rem; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Header.module.scss"; 3 | import Logo from "../Logo"; 4 | import SearchBox from "../SearchBox"; 5 | import cx from "classnames"; 6 | 7 | const Header = ({ 8 | minimal, 9 | initialSearchValue, 10 | initialSearchVersion, 11 | onSearchSubmit, 12 | onVersionChange, 13 | linksSection, 14 | }: { 15 | minimal: boolean; 16 | initialSearchValue: string; 17 | initialSearchVersion?: string; 18 | onSearchSubmit: (pkg: string) => void; 19 | onVersionChange?: (value: string) => void; 20 | linksSection?: React.ReactNode; 21 | }) => { 22 | return ( 23 |
    24 | {!minimal && ( 25 |
    26 | 27 | 34 | 35 |
    36 | )} 37 |
    38 | {!minimal && ( 39 |
    40 | 48 |
    49 | )} 50 |
    51 |
    52 | 53 | 59 | Github Link 60 | 64 | 65 | 66 | {linksSection} 67 |
    68 |
    69 | ); 70 | }; 71 | 72 | export default Header; 73 | -------------------------------------------------------------------------------- /client/components/HeaderIframe/HeaderIframe.module.scss: -------------------------------------------------------------------------------- 1 | .docsHeaderContainer { 2 | position: fixed; 3 | z-index: 10; 4 | width: 100%; 5 | margin-bottom: 1rem; 6 | background: var(--primary-bg-color-transparent); 7 | backdrop-filter: blur(10px); 8 | } 9 | 10 | .docsHeaderToggle { 11 | display: none; 12 | 13 | @media (max-width: 480px) { 14 | display: block; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/components/HeaderIframe/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import Header from "../Header"; 4 | import "../../../app/global.css"; 5 | import styles from "./HeaderIframe.module.scss"; 6 | import { packageFromPath } from "../../../common/client-utils"; 7 | import ToggleMenu from "../ToggleMenu"; 8 | import "../../scripts/global-docs-main"; 9 | 10 | const HeaderIframe = () => { 11 | const searchParams = new URL(window.document.location.href).searchParams; 12 | 13 | const force = !!searchParams.get("force"); 14 | 15 | const { 16 | packageName: initialPackageName, 17 | packageVersion: initialPackageVersion, 18 | } = packageFromPath(window.location.pathname.split("/docs/")[1]); 19 | 20 | const handleSearchSubmit = async (pkg: string) => { 21 | const withForce = force ? "?force=true" : ""; 22 | window.location.pathname = `/search/docs/${pkg}${withForce}`; 23 | }; 24 | 25 | const handleVersionChange = async (version: string) => { 26 | const withForce = force ? "?force=true" : ""; 27 | window.location.pathname = `/search/docs/${[ 28 | initialPackageName, 29 | version, 30 | ].join("/")}${withForce}`; 31 | }; 32 | 33 | const handleMenuToggle = (open: boolean) => { 34 | if (open) { 35 | document.documentElement.classList.add("has-menu"); 36 | } else { 37 | document.documentElement.classList.remove("has-menu"); 38 | } 39 | }; 40 | 41 | return ( 42 |
    43 |
    51 | 55 |
    56 | } 57 | /> 58 |
    59 | ); 60 | }; 61 | 62 | document.addEventListener("DOMContentLoaded", (event) => { 63 | const rootElement = document.getElementById("docs-header"); 64 | const root = ReactDOM.createRoot(rootElement); 65 | root.render( 66 | 67 | 68 | , 69 | ); 70 | }); 71 | -------------------------------------------------------------------------------- /client/components/Logo/index.module.scss: -------------------------------------------------------------------------------- 1 | .logoContainer { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .logoText { 8 | display: flex; 9 | max-width: 80%; 10 | } 11 | 12 | .logoImg { 13 | width: 33%; 14 | height: 100%; 15 | margin-bottom: 2rem; 16 | filter: drop-shadow(0 16px 33px rgb(185 221 255 / 72%)); 17 | } 18 | 19 | .alpha { 20 | color: var(--selected-text-color-subdued); 21 | margin-top: 2%; 22 | } 23 | -------------------------------------------------------------------------------- /client/components/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./index.module.scss"; 3 | import ToasterImg from "../../assets/toaster-new.png"; 4 | 5 | const Index = ({ minWidth, maxWidth, fluidWidth, showAlpha, showImage }) => { 6 | return ( 7 |
    15 | {showImage && } 16 |
    17 | 23 | 27 | 31 | 35 | 39 | 40 | {showAlpha &&
    alpha
    } 41 |
    42 |
    43 | ); 44 | }; 45 | 46 | export default Index; 47 | -------------------------------------------------------------------------------- /client/components/Placeholder/Placeholder.module.scss: -------------------------------------------------------------------------------- 1 | .placeholderContainer { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | text-align: center; 7 | margin-top: -4rem; // offset header and logo 8 | max-width: 35rem; 9 | } 10 | 11 | .placeholderLoader .toaster { 12 | animation: shimmer-dark 0.5s alternate infinite; 13 | } 14 | 15 | .label { 16 | margin-top: 1rem; 17 | } 18 | 19 | .placeholderLoader .label { 20 | animation: shimmer-light 0.5s alternate infinite; 21 | } 22 | 23 | .errorCode { 24 | font-size: var(--font-size-lg); 25 | font-weight: bold; 26 | } 27 | 28 | .errorMessage { 29 | pre { 30 | text-align: start; 31 | } 32 | } 33 | 34 | .placeholderError .label p { 35 | overflow: hidden; 36 | display: -webkit-box; 37 | -webkit-line-clamp: 3; 38 | line-clamp: 3; 39 | -webkit-box-orient: vertical; 40 | color: var(--font-color-subtext); 41 | line-height: 1.5; 42 | margin-top: 0.5rem; 43 | } 44 | 45 | @keyframes shimmer-light { 46 | from { 47 | opacity: 0.3; 48 | } 49 | 50 | to { 51 | opacity: 0.5; 52 | } 53 | } 54 | 55 | @keyframes shimmer-dark { 56 | from { 57 | opacity: 0.5; 58 | } 59 | 60 | to { 61 | opacity: 0.7; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/components/Placeholder/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cx from "classnames"; 3 | import styles from "./Placeholder.module.scss"; 4 | import Toaster from "../Toaster"; 5 | 6 | const Placeholder = ({ 7 | status, 8 | error, 9 | }: { 10 | status: "loading" | "error"; 11 | error?: { 12 | errorCode: string; 13 | errorMessage: string; 14 | }; 15 | }) => { 16 | return ( 17 |
    23 |
    24 | 25 |
    26 | 27 | {status === "loading" && ( 28 |
    29 | Installing package and extracting docs... 30 |
    31 | )} 32 | 33 | {status === "error" && ( 34 |
    35 | {error.errorCode} 36 |

    40 |

    41 | )} 42 |
    43 | ); 44 | }; 45 | 46 | export default Placeholder; 47 | -------------------------------------------------------------------------------- /client/components/SearchBox/Chevron.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ChevronDown = React.forwardRef< 4 | HTMLElement, 5 | React.HTMLAttributes 6 | >((props, ref) => { 7 | return ( 8 | 16 | 23 | 24 | ); 25 | }); 26 | 27 | export const ChevronUp = React.forwardRef< 28 | HTMLElement, 29 | React.HTMLAttributes 30 | >((props, ref) => { 31 | return ( 32 | 40 | 47 | 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /client/components/SearchBox/SearchBox.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../../stylesheets/mixins"; 2 | 3 | .searchBox { 4 | z-index: 1; 5 | top: 3rem; 6 | width: 100%; 7 | } 8 | 9 | .searchInput { 10 | box-shadow: var(--shadow-overlay); 11 | font-size: var(--font-size-xl); 12 | padding: 0.8rem 1.2rem; 13 | border: var(--separator-size) solid var(--separator-color); 14 | cursor: text; 15 | 16 | @media (max-width: 480px) { 17 | padding: 0.5rem 1rem; 18 | } 19 | } 20 | 21 | .searchBoxCompact .searchInput { 22 | box-shadow: none; 23 | font-size: var(--font-size-regular); 24 | padding: 0.5rem 1rem; 25 | border: 1.5px solid var(--separator-color); 26 | font-weight: 400; 27 | 28 | &::placeholder { 29 | color: var(--font-color-subtext-dark); 30 | } 31 | 32 | @media (max-width: 480px) { 33 | padding: 0.3rem 0.8rem; 34 | } 35 | } 36 | 37 | .searchBoxCompact .searchInput:focus { 38 | border: 1.5px solid var(--selected-stroke-color); 39 | } 40 | 41 | .searchInput, 42 | .searchSuggestionMenu { 43 | width: 100%; 44 | } 45 | 46 | .searchInput::placeholder { 47 | text-align: center; 48 | letter-spacing: -0.2px; 49 | } 50 | 51 | .searchSuggestionMenu, 52 | .versionMenu { 53 | @include mixins.popup; 54 | @include mixins.hideScrollbar; 55 | border-radius: 0 0 4px 4px; 56 | list-style: none; 57 | box-shadow: var(--shadow-overlay); 58 | position: absolute; 59 | backdrop-filter: blur(10px); 60 | background: var(--primary-bg-color-highlight); 61 | 62 | li { 63 | cursor: pointer; 64 | border-radius: 5px; 65 | } 66 | } 67 | 68 | .inputWrapper { 69 | display: flex; 70 | align-items: center; 71 | } 72 | 73 | .searchSuggestionMenu { 74 | width: clamp(30rem, 35vw, 40rem); 75 | max-width: 85vw; 76 | margin: -4px 0 0; 77 | 78 | &:not(:empty) { 79 | padding: 15px 10px 10px; 80 | } 81 | 82 | li { 83 | padding: 10px 10px; 84 | 85 | &[aria-selected="true"] { 86 | background: var(--hover-bg); 87 | } 88 | } 89 | } 90 | 91 | .versionDropdown { 92 | justify-self: flex-end; 93 | position: absolute; 94 | right: 0; 95 | height: 100%; 96 | display: flex; 97 | align-items: center; 98 | 99 | &:before { 100 | content: ""; 101 | position: absolute; 102 | left: 0; 103 | top: 50%; 104 | transform: translateY(-50%); 105 | width: 1px; 106 | height: 50%; 107 | background: var(--separator-color); 108 | } 109 | } 110 | 111 | .versionDropdownToggle { 112 | display: flex; 113 | align-items: center; 114 | font-variant-numeric: tabular-nums; 115 | 116 | padding: 0 1.2rem; 117 | 118 | @media (max-width: 480px) { 119 | padding: 0 1rem; 120 | } 121 | } 122 | 123 | .versionMenu { 124 | max-height: 30vh; 125 | overflow-y: scroll; 126 | font-variant-numeric: tabular-nums; 127 | top: 75%; 128 | 129 | &:not(:empty) { 130 | padding: 10px 5px 5px; 131 | } 132 | 133 | li { 134 | padding: 5px 8px; 135 | 136 | &:hover { 137 | background: var(--hover-bg); 138 | } 139 | 140 | &[aria-selected="true"] { 141 | color: var(--selected-text-color); 142 | font-weight: var(--font-weight-bold); 143 | } 144 | } 145 | } 146 | 147 | .versionDropdownLabel { 148 | color: var(--selected-text-color-subdued-more); 149 | font-family: var(--font-family-code); 150 | margin-right: 5px; 151 | } 152 | 153 | .resultName { 154 | font-weight: var(--font-weight-bold); 155 | 156 | .searchHighlight { 157 | @include mixins.selectedLabel; 158 | } 159 | } 160 | 161 | .resultDescription { 162 | //font-weight: var(--font-weight-light); 163 | font-size: var(--font-size-sm); 164 | color: var(--font-color-subtext); 165 | font-family: var(--font-family-system); 166 | 167 | overflow: hidden; 168 | text-overflow: ellipsis; 169 | white-space: nowrap; 170 | 171 | max-width: 80vw; 172 | 173 | .searchHighlight { 174 | color: var(--selected-text-color-subdued); 175 | } 176 | } 177 | 178 | .comboBoxContainer { 179 | position: relative; 180 | z-index: 1; 181 | } 182 | -------------------------------------------------------------------------------- /client/components/SearchBox/SearchBox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useCombobox } from "downshift"; 3 | import { getPackageSuggestion } from "../../../common/api/algolia"; 4 | import cx from "classnames"; 5 | import styles from "./SearchBox.module.scss"; 6 | import VersionDropdown from "./VersionDropdown"; 7 | 8 | type SearchBoxProps = { 9 | onSelect: (value: string) => void; 10 | onVersionChange?: (value: string) => void; 11 | initialValue?: string; 12 | initialVersion?: string; 13 | compact?: boolean; 14 | autoFocus?: boolean; 15 | showVersionDropdown?: boolean; 16 | }; 17 | 18 | export default function SearchBox({ 19 | onSelect, 20 | onVersionChange, 21 | showVersionDropdown, 22 | initialValue, 23 | initialVersion, 24 | compact, 25 | autoFocus, 26 | }: SearchBoxProps) { 27 | const [suggestions, setSuggestions] = React.useState([]); 28 | 29 | const stateReducer = React.useCallback((state, actionAndChanges) => { 30 | const { type, changes } = actionAndChanges; 31 | switch (type) { 32 | case useCombobox.stateChangeTypes.InputKeyDownEnter: 33 | onSelect(state.inputValue); 34 | default: 35 | return changes; 36 | } 37 | }, []); 38 | 39 | const { 40 | isOpen, 41 | getToggleButtonProps, 42 | getLabelProps, 43 | getMenuProps, 44 | getInputProps, 45 | highlightedIndex, 46 | getItemProps, 47 | selectedItem, 48 | } = useCombobox({ 49 | initialInputValue: initialValue, 50 | onSelectedItemChange: ({ selectedItem }) => { 51 | onSelect(selectedItem.name); 52 | }, 53 | stateReducer, 54 | 55 | onInputValueChange({ inputValue }) { 56 | getPackageSuggestion(inputValue, styles.searchHighlight).then( 57 | (suggestions) => { 58 | setSuggestions(suggestions); 59 | }, 60 | ); 61 | }, 62 | items: suggestions, 63 | itemToString(item) { 64 | return item.name; 65 | }, 66 | }); 67 | 68 | return ( 69 |
    72 |
    73 |
    74 | { 80 | // prevent key hijack by typedoc search 81 | // https://github.com/pastelsky/tsdocs/issues/2 82 | event.stopPropagation(); 83 | 84 | // usable Home/End buttons 85 | // https://github.com/pastelsky/tsdocs/issues/3 86 | // reference: https://github.com/downshift-js/downshift#customizing-handlers 87 | if (event.key == "Home" || event.key == "End") 88 | event.nativeEvent["preventDownshiftDefault"] = true; 89 | }, 90 | })} 91 | autoFocus={autoFocus} 92 | /> 93 | {showVersionDropdown && initialVersion && ( 94 | 99 | )} 100 |
    101 |
    102 |
      103 | {isOpen && 104 | suggestions.map((item, index) => ( 105 |
    • 106 |
      110 |
      116 |
    • 117 | ))} 118 |
    119 |
    120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /client/components/SearchBox/VersionDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { useSelect } from "downshift"; 2 | import React from "react"; 3 | import semver from "semver"; 4 | import styles from "./SearchBox.module.scss"; 5 | import { getPackageVersions } from "../../../common/api/algolia"; 6 | import { ChevronDown, ChevronUp } from "./Chevron"; 7 | 8 | export default function VersionDropdown({ 9 | pkg, 10 | initialVersion, 11 | onSelect, 12 | }: { 13 | pkg: string; 14 | initialVersion: string; 15 | onSelect: (value: string) => void; 16 | }) { 17 | const [versions, setVersions] = React.useState([]); 18 | 19 | React.useEffect(() => { 20 | getPackageVersions(pkg).then((versions) => { 21 | setVersions( 22 | versions 23 | .filter((v) => semver.valid(v) && !semver.prerelease(v)) 24 | .map((v) => v.version) 25 | .sort(semver.compare), 26 | ); 27 | }); 28 | }, []); 29 | 30 | function itemToString(item) { 31 | return item ? item.title : ""; 32 | } 33 | 34 | const versionItems = versions.map((v) => ({ id: v, title: v })); 35 | 36 | function Select() { 37 | const { 38 | isOpen, 39 | selectedItem, 40 | getToggleButtonProps, 41 | getLabelProps, 42 | getMenuProps, 43 | highlightedIndex, 44 | getItemProps, 45 | } = useSelect({ 46 | items: versionItems, 47 | itemToString, 48 | initialSelectedItem: versionItems.find((v) => v.id === initialVersion), 49 | onSelectedItemChange: (changes) => { 50 | onSelect(changes.selectedItem.id); 51 | }, 52 | }); 53 | 54 | return ( 55 |
    56 |
    57 |
    61 | v 62 | {selectedItem?.title || initialVersion} 63 | {isOpen ? : } 64 |
    65 |
    66 |
      67 | {isOpen && 68 | versionItems.map((item, index) => ( 69 |
    • 70 | {item.title} 71 |
    • 72 | ))} 73 |
    74 |
    75 | ); 76 | } 77 | 78 | return