├── .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 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
5 |
6 |
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 |
51 |
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 |
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 |
--------------------------------------------------------------------------------
/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 (
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 |
36 | )}
37 |
38 | {!minimal && (
39 |
40 |
48 |
49 | )}
50 |
51 |
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 |
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 |
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 |
24 | );
25 | });
26 |
27 | export const ChevronUp = React.forwardRef<
28 | HTMLElement,
29 | React.HTMLAttributes
30 | >((props, ref) => {
31 | return (
32 |
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 ;
79 | }
80 |
--------------------------------------------------------------------------------
/client/components/SearchBox/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchBox";
2 |
--------------------------------------------------------------------------------
/client/components/SuggestionPills/index.module.scss:
--------------------------------------------------------------------------------
1 | .suggestionsPillsContainer {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | flex-wrap: wrap;
6 | }
7 |
8 | .suggestionsPill {
9 | background: var(--primary-bg-color-highlight);
10 | padding: 0.3rem 1rem;
11 | border-radius: 2rem;
12 | color: var(--primary-fg-color);
13 | border: 0.5px solid var(--separator-color);
14 |
15 | & + & {
16 | margin-left: 1rem;
17 | }
18 |
19 | &:hover {
20 | background: var(--hover-bg);
21 | }
22 |
23 | @media (max-width: 480px) {
24 | padding: 0.2rem 1rem;
25 | margin: 0.2rem;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/components/SuggestionPills/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import Link from "next/link";
3 |
4 | import styles from "./index.module.scss";
5 |
6 | const allSuggestions = [
7 | "three",
8 | "execa",
9 | "globby",
10 | "solid-js",
11 | "playwright-core",
12 | "luxon",
13 | "redux",
14 | "lit",
15 | "@testing-library/react",
16 | "react-router-dom",
17 | "d3",
18 | "lodash-es",
19 | "@angular/core",
20 | ];
21 |
22 | function getRandomElements(array: string[], n: number) {
23 | let result = [];
24 | let taken = new Set();
25 |
26 | while (result.length < n && result.length < array.length) {
27 | let index = Math.floor(Math.random() * array.length);
28 | if (!taken.has(index)) {
29 | result.push(array[index]);
30 | taken.add(index);
31 | }
32 | }
33 |
34 | return result;
35 | }
36 |
37 | const SuggestionPills = () => {
38 | const [suggestions, setSuggestions] = React.useState([]);
39 | useEffect(() => {
40 | setSuggestions(getRandomElements(allSuggestions, 4));
41 | }, []);
42 |
43 | return (
44 |
45 | {suggestions.map((suggestion) => (
46 |
51 |
{suggestion}
52 |
53 | ))}
54 |
55 | );
56 | };
57 |
58 | export default SuggestionPills;
59 |
--------------------------------------------------------------------------------
/client/components/Toaster/Toaster.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/client/components/Toaster/Toaster.scss
--------------------------------------------------------------------------------
/client/components/ToggleMenu/ToggleMenu.module.scss:
--------------------------------------------------------------------------------
1 | .toggleButton {
2 | background: none;
3 | appearance: none;
4 | border: none;
5 |
6 | svg {
7 | width: 16px;
8 | height: 16px;
9 | fill: var(--font-color-subtext-dark);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/components/ToggleMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useCallback } from "react";
2 | import styles from "./ToggleMenu.module.scss";
3 |
4 | interface HamburgerMenuProps {
5 | menuSelector: string;
6 | onToggle: (isOpen: boolean) => void;
7 | }
8 |
9 | const ToggleMenu: React.FC = ({
10 | menuSelector,
11 | onToggle,
12 | }) => {
13 | const [isOpen, setIsOpen] = useState(false);
14 |
15 | const toggleMenu = useCallback(() => {
16 | setIsOpen(!isOpen);
17 | onToggle(!isOpen);
18 | }, [isOpen, onToggle]);
19 |
20 | const handleClickOutside = useCallback(
21 | (event: MouseEvent) => {
22 | const menu = document.querySelector(menuSelector);
23 | if (!menu.contains(event.target as Node)) {
24 | setIsOpen(false);
25 | onToggle(false);
26 | }
27 | },
28 | [onToggle],
29 | );
30 |
31 | useEffect(() => {
32 | document.addEventListener("mousedown", handleClickOutside);
33 |
34 | return () => {
35 | document.removeEventListener("mousedown", handleClickOutside);
36 | };
37 | }, [handleClickOutside]);
38 |
39 | return (
40 |
41 |
59 |
60 | );
61 | };
62 |
63 | export default ToggleMenu;
64 |
--------------------------------------------------------------------------------
/client/scripts/global-docs-main.ts:
--------------------------------------------------------------------------------
1 | let start = Date.now();
2 |
3 | function observeElement(
4 | root: HTMLElement,
5 | selector: string,
6 | callback: (element: Element) => void,
7 | ) {
8 | const observer = new MutationObserver((mutationsList, observer) => {
9 | // Look through all mutations that just occured
10 | for (let mutation of mutationsList) {
11 | // If the addedNodes property has one or more nodes
12 | if (mutation.addedNodes.length) {
13 | const element = root.querySelector(selector);
14 | if (element) {
15 | callback(element);
16 | observer.disconnect(); // Stop observing once element is found
17 | break;
18 | }
19 | }
20 | }
21 | });
22 |
23 | // Start observing the document with the configured parameters
24 | observer.observe(root, { childList: true, subtree: true });
25 | }
26 |
27 | function scrollNavigationIntoView() {
28 | console.log("Scrolling navigation into view");
29 | const matchedElement = (
30 | [...document.querySelectorAll(".tsd-navigation a")] as HTMLAnchorElement[]
31 | ).find((a: HTMLAnchorElement) => {
32 | return new URL(a.href).pathname === location.pathname;
33 | });
34 |
35 | matchedElement?.scrollIntoView({
36 | block: "center",
37 | inline: "center",
38 | });
39 | }
40 |
41 | document.addEventListener(
42 | "DOMContentLoaded",
43 | function () {
44 | observeElement(
45 | document.querySelector(".site-menu .tsd-navigation"),
46 | "li a",
47 | scrollNavigationIntoView,
48 | );
49 | },
50 | false,
51 | );
52 |
--------------------------------------------------------------------------------
/common/api/algolia.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import semver from "semver";
3 |
4 | const ALGOLIA_APP_ID = "OFCNCOG2CU";
5 | const ALGOLIA_API_KEY = "1fb64b9fde1959aacbe82000a34dd717";
6 |
7 | export async function getPackageVersions(packageName) {
8 | const packageDetails = await axios.get(
9 | `https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/npm-search/${encodeURIComponent(
10 | packageName,
11 | )}`,
12 | {
13 | params: {
14 | "x-algolia-application-id": ALGOLIA_APP_ID,
15 | "x-algolia-api-key": ALGOLIA_API_KEY,
16 | },
17 | },
18 | );
19 | return Object.keys(packageDetails.data.versions).map((version) =>
20 | semver.parse(version),
21 | );
22 | }
23 |
24 | export async function getPackageSuggestion(query: string, highlightClass) {
25 | const searchParams = {
26 | highlightPreTag: ``,
27 | highlightPostTag: "",
28 | hitsPerPage: 5,
29 | page: 0,
30 | attributesToRetrieve: [
31 | "description",
32 | "homepage",
33 | "keywords",
34 | "name",
35 | "repository",
36 | "types",
37 | "version",
38 | ],
39 | attributesToHighlight: ["name", "description", "keywords"],
40 | query,
41 | maxValuesPerFacet: 10,
42 | facets: ["keywords", "keywords", "owner.name"],
43 | };
44 |
45 | const searchParamsStringified = Object.entries(searchParams)
46 | .map(([key, value]) => {
47 | const stringified = JSON.stringify(value);
48 | const strQuotedRemoved = stringified.substring(1, stringified.length - 1);
49 | return `${key}=${
50 | typeof value === "string" ? strQuotedRemoved : stringified
51 | }`;
52 | })
53 | .join("&");
54 |
55 | const urlParams = new URLSearchParams({
56 | "x-algolia-application-id": ALGOLIA_APP_ID,
57 | "x-algolia-api-key": ALGOLIA_API_KEY,
58 | });
59 |
60 | const suggestions = await axios({
61 | url: `https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/*/queries?${urlParams.toString()}`,
62 | method: "POST",
63 | headers: {
64 | "content-type": "application/x-www-form-urlencoded",
65 | },
66 | data: JSON.stringify({
67 | requests: [
68 | {
69 | indexName: "npm-search",
70 | params: searchParamsStringified,
71 | },
72 | ],
73 | }),
74 | });
75 |
76 | const fixQuotes = (str) =>
77 | str
78 | ? str
79 | .replace(/\\"/g, '"')
80 | .replace(/""/, "span>")
82 | .replace(/>"/g, ">")
83 | .replace(/" ({
87 | name: hit.name,
88 | description: hit.description,
89 | repository: hit.repository,
90 | types: hit.types,
91 | version: hit.version,
92 | highlightedName: fixQuotes(hit._highlightResult?.name?.value || hit.name),
93 | highlightedDescription: fixQuotes(
94 | hit._highlightResult?.description?.value || hit.description,
95 | ),
96 | }));
97 | }
98 |
--------------------------------------------------------------------------------
/common/client-utils.ts:
--------------------------------------------------------------------------------
1 | import validatePackageName from "validate-npm-package-name";
2 | import { PackageNotFoundError } from "../server/package/CustomError";
3 |
4 | const validTypeDocFragmentPaths: string[] = [
5 | "index",
6 | "index.html",
7 | "assets",
8 | "classes",
9 | "functions",
10 | "interfaces",
11 | "modules",
12 | "modules.html",
13 | "types",
14 | "variables",
15 | ];
16 |
17 | /**
18 | * Extracts package name and version from a doc path string
19 | */
20 | export function packageFromPath(pathFragments: string) {
21 | const [pathFragment1, pathFragment2, pathFragment3, ...otherPathFragments] =
22 | pathFragments.split("/");
23 | const isScopedPackage = pathFragment1.startsWith("@");
24 | const defaultPackageVersion = "latest";
25 |
26 | if (!pathFragment1) {
27 | return null;
28 | }
29 |
30 | let packageName, packageVersion, docsFragment;
31 |
32 | if (isScopedPackage && !pathFragment2) {
33 | return null;
34 | }
35 |
36 | if (isScopedPackage) {
37 | packageName = [pathFragment1, pathFragment2].join("/");
38 | if (pathFragment3) {
39 | // /docs/@foo/bar/modules/index.html
40 | if (validTypeDocFragmentPaths.includes(pathFragment3)) {
41 | packageVersion = defaultPackageVersion;
42 | docsFragment = [pathFragment3, ...otherPathFragments]
43 | .filter(Boolean)
44 | .join("/");
45 | } else {
46 | // /docs/@foo/bar/1.0/modules/index.html
47 | packageVersion = pathFragment3;
48 | docsFragment = otherPathFragments.join("/");
49 | }
50 | } else {
51 | // /docs/@foo/bar
52 | packageVersion = defaultPackageVersion;
53 | docsFragment = "";
54 | }
55 | } else {
56 | packageName = pathFragment1;
57 | if (pathFragment2) {
58 | // /docs/foo/module/index.html
59 | if (validTypeDocFragmentPaths.includes(pathFragment2)) {
60 | packageVersion = defaultPackageVersion;
61 | docsFragment = [pathFragment2, pathFragment3, ...otherPathFragments]
62 | .filter(Boolean)
63 | .join("/");
64 | } else {
65 | // /docs/foo/1.0/module/index.html
66 | packageVersion = pathFragment2;
67 | docsFragment = [pathFragment3, ...otherPathFragments]
68 | .filter(Boolean)
69 | .join("/");
70 | }
71 | } else {
72 | // /docs/foo
73 | packageVersion = defaultPackageVersion;
74 | docsFragment = "";
75 | }
76 | }
77 |
78 | const packageValidity = validatePackageName(packageName);
79 | if (
80 | !packageValidity.validForNewPackages &&
81 | !packageValidity.validForOldPackages
82 | ) {
83 | throw new PackageNotFoundError(
84 | new Error(`Package name ${packageName} is invalid`),
85 | );
86 | }
87 |
88 | return {
89 | packageName,
90 | packageVersion,
91 | docsFragment,
92 | };
93 | }
94 |
--------------------------------------------------------------------------------
/common/logger.ts:
--------------------------------------------------------------------------------
1 | import winston from "winston";
2 | import { consoleFormat } from "winston-console-format";
3 |
4 | const logger = winston.createLogger({
5 | level: "info",
6 | format: winston.format.combine(
7 | winston.format.timestamp(),
8 | winston.format.ms(),
9 | winston.format.errors({ stack: true }),
10 | winston.format.splat(),
11 | winston.format.json(),
12 | ),
13 | transports: [
14 | //
15 | // - Write all logs with importance level of `error` or less to `error.log`
16 | // - Write all logs with importance level of `info` or less to `combined.log`
17 | //
18 | new winston.transports.File({ filename: "error.log", level: "error" }),
19 | new winston.transports.File({ filename: "combined.log" }),
20 | ],
21 | });
22 |
23 | const coloredJSON = winston.format.combine(
24 | winston.format.colorize({ all: true }),
25 | winston.format.padLevels(),
26 | consoleFormat({
27 | showMeta: true,
28 | metaStrip: ["timestamp", "service"],
29 | inspectOptions: {
30 | depth: 3,
31 | colors: true,
32 | maxArrayLength: 10,
33 | maxStringLength: 300,
34 | breakLength: 120,
35 | compact: 10,
36 | sorted: true,
37 | },
38 | }),
39 | );
40 |
41 | logger.add(
42 | new winston.transports.Console({
43 | format: coloredJSON,
44 | }),
45 | );
46 |
47 | export default logger;
48 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsdocs.dev",
3 | "private": true,
4 | "scripts": {
5 | "dev": "run-p --print-label 'shared:watch' 'next:watch'",
6 | "next:watch": "NODE_ENV=development node -r esbuild-register server.ts",
7 | "shared:watch": "vite build --watch",
8 | "build": "NODE_ENV=production next build && NODE_ENV=production vite build --mode production",
9 | "start": "NODE_ENV=production node -r esbuild-register server.ts",
10 | "start:production": "pm2 start process.yml",
11 | "prepare": "husky install"
12 | },
13 | "dependencies": {
14 | "@bull-board/api": "^6.3.3",
15 | "@bull-board/fastify": "^6.3.3",
16 | "@definitelytyped/header-parser": "^0.0.178",
17 | "@fastify/static": "^8.0.2",
18 | "@immobiliarelabs/fastify-sentry": "^8.0.2",
19 | "@monaco-editor/react": "^4.6.0",
20 | "@sentry/node": "^7.119.2",
21 | "@sentry/profiling-node": "^1.3.5",
22 | "@swc/core": "^1.7.42",
23 | "@swc/helpers": "^0.5.13",
24 | "@tanstack/react-query": "^4.36.1",
25 | "@types/semver": "^7.5.8",
26 | "@types/validate-npm-package-name": "^4.0.2",
27 | "@vitejs/plugin-react": "^4.3.3",
28 | "animejs": "^3.2.2",
29 | "axios": "^1.7.7",
30 | "bullmq": "patch:bullmq@npm%3A4.15.4#~/.yarn/patches/bullmq-npm-4.15.4-b55917dd70.patch",
31 | "chalk": "^4.1.2",
32 | "check-disk-space": "^3.4.0",
33 | "classnames": "^2.5.1",
34 | "debounce": "^1.2.1",
35 | "dotenv": "^16.4.5",
36 | "downshift": "^8.5.0",
37 | "enhanced-resolve": "^5.17.1",
38 | "esbuild-register": "^3.6.0",
39 | "fast-folder-size": "^2.3.0",
40 | "fastify": "^5.1.0",
41 | "fs-extra": "^11.2.0",
42 | "lru-cache": "^10.4.3",
43 | "monaco-editor": "^0.44.0",
44 | "mongodb": "6.2.0",
45 | "next": "15.0.2",
46 | "node-html-parser": "^6.1.13",
47 | "npm-run-all": "^4.1.5",
48 | "open-props": "^1.7.7",
49 | "pacote": "^17.0.7",
50 | "pino-pretty": "^9.4.1",
51 | "pkg-dir": "5.0.0",
52 | "pm2": "^5.4.2",
53 | "prettier": "^3.1.1",
54 | "pretty-bytes": "5.6.0",
55 | "react": "18.3.1",
56 | "react-dom": "18.3.1",
57 | "react-hotkeys-hook": "^3.4.7",
58 | "read-pkg-up": "7.0.1",
59 | "regenerator-runtime": "^0.14.1",
60 | "resolve-from": "^5.0.0",
61 | "resolve-pkg": "^2.0.0",
62 | "rimraf": "^3.0.2",
63 | "sanitize-filename": "^1.6.3",
64 | "sanitize.css": "^13.0.0",
65 | "sass": "^1.80.6",
66 | "semver": "^7.6.3",
67 | "sharp": "^0.33.5",
68 | "stacktrace-parser": "^0.1.10",
69 | "ts-morph": "^16.0.0",
70 | "typedoc": "patch:typedoc@npm%3A0.26.11#~/.yarn/patches/typedoc-npm-0.26.11-24fa09b154.patch",
71 | "typedoc-plugin-mdn-links": "^3.3.5",
72 | "typedoc-plugin-missing-exports": "^3.0.0",
73 | "typedoc-plugin-rename-defaults": "^0.7.1",
74 | "validate-npm-package-name": "5.0.0",
75 | "vite": "^5.4.10",
76 | "vite-plugin-externalize-dependencies": "^1.0.1",
77 | "winston": "^3.16.0",
78 | "winston-console-format": "^1.0.8",
79 | "workerpool": "^9.2.0"
80 | },
81 | "devDependencies": {
82 | "@fastify/basic-auth": "^6.0.1",
83 | "@types/animejs": "^3.1.12",
84 | "@types/fs-extra": "^11.0.4",
85 | "@types/node": "22.8.6",
86 | "@types/react": "18.2.9",
87 | "@types/regenerator-runtime": "^0.13.8",
88 | "fast-glob": "^3.3.2",
89 | "husky": "^8.0.3",
90 | "lint-staged": "^13.3.0",
91 | "typescript": "5.6.3"
92 | },
93 | "lint-staged": {
94 | "**/*": "prettier --write --ignore-unknown"
95 | },
96 | "packageManager": "yarn@4.0.2",
97 | "resolutions": {
98 | "typedoc@^0.25.3": "patch:typedoc@npm%3A0.25.3#./.yarn/patches/typedoc-npm-0.25.3-11902e45cc.patch",
99 | "@types/react": "18.2.9"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/process.yml:
--------------------------------------------------------------------------------
1 | # Write a generic process.yml
2 | # This is a template for the process.yml file
3 |
4 | apps:
5 | - script: "./server.ts"
6 | name: "server"
7 | env:
8 | NODE_ENV: "production"
9 | TS_NODE_TRANSPILE_ONLY: "true"
10 | interpreter: "node"
11 | interpreter_args: "-r ts-node/register --trace-warnings"
12 | wait_ready: true
13 | exec_mode: "cluster"
14 | instances: 2
15 | max_memory_restart: "500M"
16 | kill_timeout: 5000
17 | listen_timeout: 5000
18 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/public/favicon.ico
--------------------------------------------------------------------------------
/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/public/mstile-150x150.png
--------------------------------------------------------------------------------
/public/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pastelsky/tsdocs/744da56c879c8b4c3ce976089b1b3274eab64fb9/public/og-image.png
--------------------------------------------------------------------------------
/public/opensearch.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | tsdocs
4 | show tsdocs for
5 | https://tsdocs.dev/favicon-32x32.png
6 | tsdocs
7 |
8 |
9 | https://tsdocs.dev
10 |
--------------------------------------------------------------------------------
/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
16 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/obliterate-queues.ts:
--------------------------------------------------------------------------------
1 | import { allQueues } from "../server/queues";
2 |
3 | (() =>
4 | Promise.all(
5 | allQueues.map((queue) => {
6 | return queue.obliterate({ force: true });
7 | }),
8 | ))();
9 |
--------------------------------------------------------------------------------
/scripts/symlink-assets.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Symlink assets in `docs` with `docs-shared-assets`
3 | */
4 |
5 | import fs from "fs";
6 | import glob from "fast-glob";
7 | import path from "path";
8 | import { docsVersion } from "../server/package/utils";
9 |
10 | const assets = glob.sync(
11 | [
12 | "docs/**/assets/style.css",
13 | "docs/**/assets/custom.css",
14 | "docs/**/assets/highlight.css",
15 | "docs/**/assets/main.js",
16 | ],
17 | {
18 | cwd: process.cwd(),
19 | },
20 | );
21 |
22 | console.log(`Symlinking ${assets.length} assets to docs-shared-assets`);
23 |
24 | assets.forEach((asset) => {
25 | const target = path.join(
26 | "docs-shared-assets",
27 | docsVersion,
28 | path.basename(asset),
29 | );
30 | console.log("Symlinked: ", asset, " to ", target);
31 | if (fs.existsSync(asset)) {
32 | fs.unlinkSync(asset);
33 | }
34 | fs.symlinkSync(target, asset, "file");
35 | });
36 |
--------------------------------------------------------------------------------
/server.ts:
--------------------------------------------------------------------------------
1 | import "./server/init-sentry.js";
2 | import fastifyStart from "fastify";
3 | import fastifyStatic from "@fastify/static";
4 | import next from "next";
5 | import {
6 | handlerAPIDocsBuild,
7 | handlerAPIDocsPoll,
8 | handlerAPIDocsTrigger,
9 | handlerDocsHTML,
10 | } from "./server/package";
11 | import path from "path";
12 | import { docsRootPath } from "./server/package/utils";
13 | import { createBullBoard } from "@bull-board/api";
14 | import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
15 | import { FastifyAdapter } from "@bull-board/fastify";
16 | import { allQueues } from "./server/queues";
17 | import {
18 | CustomError,
19 | PackageNotFoundError,
20 | PackageVersionMismatchError,
21 | } from "./server/package/CustomError";
22 | import logger from "./common/logger";
23 | import fastifyBasicAuth from "@fastify/basic-auth";
24 | import "dotenv/config";
25 |
26 | console.log("Starting server...");
27 | const dev = process.env.NODE_ENV !== "production";
28 | const hostname = "localhost";
29 | const port = 3000;
30 |
31 | const fastify = fastifyStart({
32 | logger: {
33 | transport: {
34 | target: "pino-pretty",
35 | options: {
36 | translateTime: "HH:MM:ss Z",
37 | ignore: "pid,hostname",
38 | },
39 | },
40 | level: "warn",
41 | },
42 | });
43 |
44 | const app = next({ dev, hostname, port, turbopack: true });
45 | const nextHandle = app.getRequestHandler();
46 |
47 | const queueDashboardAdapter = new FastifyAdapter();
48 |
49 | createBullBoard({
50 | queues: allQueues.map((queue) => new BullMQAdapter(queue)),
51 | serverAdapter: queueDashboardAdapter,
52 | });
53 |
54 | queueDashboardAdapter.setBasePath("/queue/ui");
55 |
56 | console.log("Preparing next app...");
57 | app
58 | .prepare()
59 | .then(() => {
60 | fastify.register(fastifyStatic, {
61 | root: docsRootPath,
62 | index: ["index.html"],
63 | redirect: false,
64 | allowedPath: (pathName) => {
65 | if (pathName.includes("..")) {
66 | return false;
67 | }
68 | return true;
69 | },
70 | extensions: ["html", "js", "css"],
71 | list: false,
72 | // list: {
73 | // format: "html",
74 | // render: (dirs, files) => {
75 | // return `
76 | //
77 | //
78 | //
79 | //
80 | //
81 | //
82 | //
83 | // ${dirs
84 | // .map(
85 | // (dir) =>
86 | // `- ${dir.name}
`,
87 | // )
88 | // .join("\n ")}
89 | //
90 | //
100 | //
101 | // `;
102 | // },
103 | // },
104 | serve: false,
105 | });
106 |
107 | fastify.register(fastifyBasicAuth, {
108 | validate(username, password, _req, reply, done) {
109 | if (
110 | username === "tsdocs" &&
111 | password === process.env["TSDOCS_PASSWORD"]
112 | ) {
113 | done();
114 | } else {
115 | reply.status(401).send("Unauthorized");
116 | }
117 | },
118 | authenticate: true,
119 | });
120 |
121 | fastify.register(fastifyStatic, {
122 | root: path.join(__dirname, "shared-dist"),
123 | redirect: true,
124 | prefix: "/shared-dist/",
125 | allowedPath: (pathName) => {
126 | if (pathName.includes("..")) {
127 | return false;
128 | }
129 | return true;
130 | },
131 | extensions: ["js", "css"],
132 | list: false,
133 | serve: true,
134 | decorateReply: false,
135 | cacheControl: false,
136 | });
137 |
138 | queueDashboardAdapter.setBasePath("/queue/ui");
139 |
140 | fastify.register(queueDashboardAdapter.registerPlugin(), {
141 | basePath: "/",
142 | prefix: "/queue/ui",
143 | });
144 |
145 | fastify.route({
146 | method: "POST",
147 | url: `/api/docs/trigger/*`,
148 | handler: handlerAPIDocsTrigger,
149 | });
150 |
151 | fastify.route({
152 | method: "POST",
153 | url: `/api/docs/build/*`,
154 | handler: handlerAPIDocsBuild,
155 | });
156 |
157 | fastify.setErrorHandler(function (error, request, reply) {
158 | logger.error(error);
159 |
160 | if (error instanceof CustomError) {
161 | const payload = {
162 | name: error.name,
163 | extra: error.extra,
164 | };
165 |
166 | switch (error.name) {
167 | case PackageVersionMismatchError.name:
168 | case PackageNotFoundError.name:
169 | reply.status(404).send(payload);
170 | break;
171 | default:
172 | reply.status(500).send(payload);
173 | }
174 | }
175 | reply.status(500).send(error);
176 | });
177 |
178 | fastify.route({
179 | method: "GET",
180 | url: `/api/docs/poll/*`,
181 | handler: async (request, reply) => {
182 | return handlerAPIDocsPoll(request, reply);
183 | },
184 | });
185 |
186 | fastify.route({
187 | method: "GET",
188 | url: `/docs/*`,
189 | handler: async (request, reply) => {
190 | return handlerDocsHTML(request, reply);
191 | },
192 | });
193 |
194 | fastify.setNotFoundHandler((request, reply) =>
195 | nextHandle(request.raw, reply.raw),
196 | );
197 |
198 | fastify.addHook("onRequest", (req, reply, next) => {
199 | if (
200 | !req.url.startsWith("/queue/ui") ||
201 | !process.env["BULL_MQ_DASHBOARD_KEY"]
202 | ) {
203 | return next();
204 | }
205 | fastify.basicAuth(req, reply, function (error) {
206 | if (!error) {
207 | return next();
208 | }
209 |
210 | if (error.name === "FastifyError") {
211 | reply.redirect("/queue/ui", 401);
212 | }
213 | reply.code(500).send({ error: error.message });
214 | });
215 | });
216 |
217 | // Run the server!
218 | const start = async () => {
219 | try {
220 | await fastify.listen({ port });
221 | console.log("Server started at ", `http://localhost:${port}`);
222 | console.log("Queue UI at ", `http://localhost:${port}/queue/ui`);
223 | // For PM2 to know that our server is up
224 | if (process.send) process.send("ready");
225 | } catch (err) {
226 | console.error("server threw error on startup: ", err);
227 | fastify.log.error(err);
228 | process.exit(1);
229 | }
230 | };
231 | start();
232 | })
233 | .catch((err) => {
234 | console.log("Failed to prepare app with error: ", err);
235 | });
236 |
--------------------------------------------------------------------------------
/server/init-sentry.js:
--------------------------------------------------------------------------------
1 | const Sentry = require("@sentry/node");
2 | const { ProfilingIntegration } = require("@sentry/profiling-node");
3 | const path = require("path");
4 |
5 | require("dotenv").config({
6 | path: path.join(__dirname, "..", ".env"),
7 | });
8 |
9 | if (process.env.SENTRY_DSN) {
10 | Sentry.init({
11 | dsn: process.env.SENTRY_DSN,
12 | includeLocalVariables: true,
13 | integrations: [
14 | new Sentry.Integrations.Console(),
15 | new Sentry.Integrations.Http({ tracing: true }),
16 | new Sentry.Integrations.OnUncaughtException(),
17 | new Sentry.Integrations.OnUnhandledRejection(),
18 | new Sentry.Integrations.ContextLines(),
19 | new Sentry.Integrations.LocalVariables(),
20 | new Sentry.Integrations.Undici(),
21 | new Sentry.Integrations.RequestData(),
22 | new ProfilingIntegration(),
23 | ],
24 | tracesSampleRate: 1.0,
25 | profilesSampleRate: 1.0,
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/server/package/CustomError.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Wraps the original error with a identifiable
3 | * name.
4 | */
5 | export class CustomError extends Error {
6 | originalError: any;
7 | extra: any;
8 |
9 | constructor(name: string, originalError: Error, extra?: any) {
10 | super(name);
11 | this.name = name;
12 | this.cause = originalError;
13 | this.originalError = originalError;
14 | this.extra = extra;
15 | Object.setPrototypeOf(this, CustomError.prototype);
16 | }
17 |
18 | toJSON() {
19 | return {
20 | name: this.name,
21 | originalError: this.originalError,
22 | extra: this.extra,
23 | };
24 | }
25 | }
26 |
27 | export class BuildError extends CustomError {
28 | constructor(originalError: any, extra?: any) {
29 | super("BuildError", originalError, extra);
30 | Object.setPrototypeOf(this, BuildError.prototype);
31 | }
32 | }
33 |
34 | export class TypeDocBuildError extends CustomError {
35 | constructor(originalError: any, extra?: any) {
36 | super("TypeDocBuildError", originalError, extra);
37 | Object.setPrototypeOf(this, TypeDocBuildError.prototype);
38 | }
39 | }
40 |
41 | export class ResolutionError extends CustomError {
42 | constructor(originalError: any, extra?: any) {
43 | super("ResolutionError", originalError, extra);
44 | Object.setPrototypeOf(this, ResolutionError.prototype);
45 | }
46 | }
47 |
48 | export class EntryPointError extends CustomError {
49 | constructor(originalError: any, extra?: any) {
50 | super("EntryPointError", originalError, extra);
51 | Object.setPrototypeOf(this, EntryPointError.prototype);
52 | }
53 | }
54 |
55 | export class InstallError extends CustomError {
56 | constructor(originalError: any, extra?: any) {
57 | super("InstallError", originalError, extra);
58 | Object.setPrototypeOf(this, InstallError.prototype);
59 | }
60 | }
61 |
62 | export class PackageNotFoundError extends CustomError {
63 | constructor(originalError: any, extra?: any) {
64 | super("PackageNotFoundError", originalError, extra);
65 | Object.setPrototypeOf(this, PackageNotFoundError.prototype);
66 | }
67 | }
68 |
69 | export class PackageVersionMismatchError extends CustomError {
70 | constructor(originalError: any, validVersions: string[]) {
71 | super("PackageVersionMismatchError", originalError, validVersions);
72 | Object.setPrototypeOf(this, PackageVersionMismatchError.prototype);
73 | }
74 | }
75 |
76 | export class CLIBuildError extends CustomError {
77 | constructor(originalError: any, extra?: any) {
78 | super("CLIBuildError", originalError, extra);
79 | Object.setPrototypeOf(this, CLIBuildError.prototype);
80 | }
81 | }
82 |
83 | export class MinifyError extends CustomError {
84 | constructor(originalError: any, extra?: any) {
85 | super("MinifyError", originalError, extra);
86 | Object.setPrototypeOf(this, MinifyError.prototype);
87 | }
88 | }
89 |
90 | export class MissingDependencyError extends CustomError {
91 | missingModules: Array;
92 |
93 | constructor(originalError: any, extra: { missingModules: Array }) {
94 | super("MissingDependencyError", originalError, extra);
95 | this.missingModules = extra.missingModules;
96 | Object.setPrototypeOf(this, MissingDependencyError.prototype);
97 | }
98 | }
99 |
100 | export class TypeDefinitionResolveError extends CustomError {
101 | constructor(originalError: any, extra?: any) {
102 | super("TypeDefinitionResolveError", originalError, extra);
103 | Object.setPrototypeOf(this, TypeDefinitionResolveError.prototype);
104 | }
105 | }
106 |
107 | export class UnexpectedBuildError extends CustomError {
108 | constructor(originalError: any, extra?: any) {
109 | super("UnexpectedBuildError", originalError, extra);
110 | Object.setPrototypeOf(this, UnexpectedBuildError.prototype);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/server/package/DocsCache.ts:
--------------------------------------------------------------------------------
1 | import { docsCachePath, getDocsCachePath } from "./utils";
2 | import fs from "fs";
3 | import path from "path";
4 | import axios from "axios";
5 | import logger from "../../common/logger";
6 | import "dotenv/config";
7 |
8 | const baseDocsCachePathDisk = path.join(__dirname, "..", "..");
9 |
10 | export class DocsCache {
11 | static async getFromDisk(packageName: string, packageVersion: string) {
12 | try {
13 | const docsCachePath = getDocsCachePath({
14 | packageName,
15 | packageVersion,
16 | basePath: baseDocsCachePathDisk,
17 | });
18 | await fs.promises.mkdir(path.dirname(docsCachePath), { recursive: true });
19 | const data = await fs.promises.readFile(docsCachePath, "utf-8");
20 |
21 | return JSON.parse(data);
22 | } catch (err) {
23 | return null;
24 | }
25 | }
26 |
27 | static async getFromDB(packageName: string, packageVersion: string) {
28 | try {
29 | const docsCachePath = getDocsCachePath({
30 | packageName,
31 | packageVersion,
32 | basePath: "",
33 | });
34 |
35 | const response = await axios.get(
36 | `https://cf-tsdocs-worker.binalgo.workers.dev/${docsCachePath}`,
37 | {
38 | headers: {
39 | "X-CF-WORKERS-KEY": process.env["X-CF-WORKERS-KEY"],
40 | },
41 | },
42 | );
43 |
44 | return JSON.parse(response.data);
45 | } catch (err) {
46 | return null;
47 | }
48 | }
49 |
50 | static async get(packageName: string, packageVersion: string) {
51 | const docsFromDisk = await DocsCache.getFromDisk(
52 | packageName,
53 | packageVersion,
54 | );
55 |
56 | if (docsFromDisk) {
57 | logger.info(
58 | "Docs cache hit for %s %s from disk",
59 | packageName,
60 | packageVersion,
61 | );
62 | return docsFromDisk;
63 | }
64 |
65 | const docsFromDB = await DocsCache.getFromDB(packageName, packageVersion);
66 |
67 | if (docsFromDB) {
68 | logger.info(
69 | "Docs cache hit for %s %s from DB",
70 | packageName,
71 | packageVersion,
72 | );
73 | return docsFromDB;
74 | }
75 |
76 | logger.warn("Docs cache miss for %s %s", packageName, packageVersion);
77 | return null;
78 | }
79 |
80 | static async setToDisk(packageName: string, packageVersion: string, data) {
81 | const docsPath = getDocsCachePath({
82 | packageName,
83 | packageVersion,
84 | basePath: baseDocsCachePathDisk,
85 | });
86 | await fs.promises.mkdir(path.dirname(docsPath), { recursive: true });
87 | await fs.promises.writeFile(docsPath, data, "utf-8");
88 | }
89 |
90 | static async setToDB(packageName: string, packageVersion: string, data) {
91 | try {
92 | const docsPath = getDocsCachePath({
93 | packageName,
94 | packageVersion,
95 | basePath: "",
96 | });
97 |
98 | await axios.put(
99 | `https://cf-tsdocs-worker.binalgo.workers.dev/${docsPath}`,
100 | data,
101 | {
102 | headers: {
103 | "X-CF-WORKERS-KEY": process.env["X-CF-WORKERS-KEY"],
104 | },
105 | },
106 | );
107 | } catch (error) {
108 | if (process.env["X-CF-WORKERS-KEY"]) {
109 | throw error;
110 | } else {
111 | throw new Error("X-CF-WORKERS-KEY is not set");
112 | }
113 | }
114 | }
115 |
116 | static async set(packageName: string, packageVersion: string, typedoc) {
117 | const data = JSON.stringify(typedoc);
118 |
119 | Promise.all([
120 | DocsCache.setToDisk(packageName, packageVersion, data)
121 | .then(() => {
122 | logger.info(
123 | "Disk: Docs cache set for %s %s",
124 | packageName,
125 | packageVersion,
126 | );
127 | })
128 | .catch((err) => {
129 | logger.error("Error writing docs cache to disk", err);
130 | }),
131 | DocsCache.setToDB(packageName, packageVersion, data)
132 | .then(() => {
133 | logger.info(
134 | "DB: Docs cache set for %s %s",
135 | packageName,
136 | packageVersion,
137 | );
138 | })
139 | .catch((err) => {
140 | logger.error("Error writing docs cache to db", err);
141 | }),
142 | ]);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/server/package/common.utils.ts:
--------------------------------------------------------------------------------
1 | import childProcess from "child_process";
2 | import os from "os";
3 |
4 | const homeDirectory = os.homedir();
5 |
6 | export function exec(command: string, options: any, timeout?: number) {
7 | let timerId: NodeJS.Timeout;
8 | return new Promise((resolve, reject) => {
9 | const child = childProcess.exec(
10 | command,
11 | options,
12 | (error, stdout, stderr) => {
13 | if (error) {
14 | reject(stderr);
15 | } else {
16 | resolve(stdout);
17 | }
18 |
19 | if (timerId) {
20 | clearTimeout(timerId);
21 | }
22 | },
23 | );
24 |
25 | if (timeout) {
26 | timerId = setTimeout(() => {
27 | process.kill(child.pid);
28 | reject(
29 | `Execution of ${command.substring(
30 | 0,
31 | 40,
32 | )}... cancelled as it exceeded a timeout of ${timeout} ms`,
33 | );
34 | }, timeout);
35 | }
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/server/package/extractor/augment-extract.ts:
--------------------------------------------------------------------------------
1 | import { promises } from "fs";
2 | import * as prettier from "prettier";
3 | import { Project } from "ts-morph";
4 |
5 | const { readFile, writeFile } = promises;
6 |
7 | const transformPrettier = (fileContent: string) => {
8 | return prettier.format(fileContent, {
9 | semi: false,
10 | singleQuote: true,
11 | parser: "typescript",
12 | });
13 | };
14 |
15 | /**
16 | * API extractor doesn't work well with exports of the form `export = Foo`,
17 | * so we transform it to the near-equivalent export default Foo
18 | * @see https://github.com/microsoft/rushstack/issues/3998
19 | **/
20 | export const transformCommonJSExport = (fileContent: string) => {
21 | const project = new Project({
22 | useInMemoryFileSystem: true,
23 | });
24 |
25 | const sourceFile = project.createSourceFile("file.ts", fileContent);
26 |
27 | const exportAssignments = sourceFile.getExportAssignments();
28 |
29 | if (exportAssignments.length == 1) {
30 | exportAssignments[0].setIsExportEquals(false);
31 | }
32 |
33 | return sourceFile.getFullText();
34 | };
35 |
36 | export default async function augmentExtract(filePath: string) {
37 | const fileContent = await readFile(filePath, "utf8");
38 |
39 | let augmentedContent = fileContent;
40 | const transforms = [transformPrettier];
41 |
42 | for (let transform of transforms) {
43 | augmentedContent = await transform(augmentedContent);
44 | }
45 |
46 | await writeFile(filePath, augmentedContent);
47 | }
48 |
--------------------------------------------------------------------------------
/server/package/extractor/css-overrides.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-family-code: "JetBrains Mono", Menlo, Consolas, Monaco, Liberation Mono,
3 | Lucida Console, monospace;
4 | --font-family-system: Inter, Roboto, "Helvetica Neue", "Arial Nova",
5 | "Nimbus Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
6 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
7 | "Segoe UI Symbol";
8 | --shadow-overlay: 0px 10px 15px rgba(32, 37, 46, 0.15),
9 | 0px 3px 5px rgba(23, 26, 33, 0.21);
10 | }
11 |
12 | body {
13 | font-size: 15px;
14 | font-family: var(--font-family-system);
15 | }
16 |
17 | @media (prefers-color-scheme: light), (prefers-color-scheme: dark) {
18 | :root[data-theme="light"],
19 | :root[data-theme="dark"],
20 | :root[data-theme="os"] {
21 | color-scheme: light !important;
22 | --light-color-active-menu-item: #efeff0;
23 | --dark-color-active-menu-item: #efeff0;
24 |
25 | --light-color-text: #222;
26 | --dark-color-text: #222;
27 |
28 | --light-color-ts-project: #2548b6;
29 | --dark-color-ts-project: #2548b6;
30 |
31 | --light-color-ts-enum: #0585ae;
32 | --dark-color-ts-enum: #0585ae;
33 | --light-color-ts-enum-background: #dcf2f2;
34 | --dark-color-ts-enum-background: #dcf2f2;
35 |
36 | --light-color-ts-namespace: #1305ae;
37 | --dark-color-ts-namespace: #1305ae;
38 | --light-color-ts-namespace-background: #ececf4;
39 | --dark-color-ts-namespace-background: #ececf4;
40 |
41 | --light-color-ts-variable: #78059e;
42 | --dark-color-ts-variable: #78059e;
43 | --light-color-ts-variable-background: #eee1f2;
44 | --dark-color-ts-variable-background: #eee1f2;
45 |
46 | --light-color-ts-type-parameter: #403e48;
47 | --dark-color-ts-type-parameter: #403e48;
48 |
49 | --light-color-ts-function: #6639ba;
50 | --light-color-ts-function-background: #e9e9f2;
51 | --dark-color-ts-function: #6639ba;
52 | --dark-color-ts-function-background: #e9e9f2;
53 |
54 | --light-color-ts-class: #0550ae;
55 | --light-color-ts-class-background: #dce7f2;
56 | --dark-color-ts-class: #0550ae;
57 | --dark-color-ts-class-background: #dce7f2;
58 |
59 | --light-color-ts-interface: #04679f;
60 | --light-color-ts-interface-background: #e1ecf2;
61 | --dark-color-ts-interface: #04679f;
62 | --dark-color-ts-interface-background: #e1ecf2;
63 |
64 | --light-color-ts-type-alias: #ae05a7;
65 | --light-color-ts-type-alias-background: #f4ebf4;
66 | --dark-color-ts-type-alias: #ae05a7;
67 | --dark-color-ts-type-alias-background: #f4ebf4;
68 |
69 | --light-color-accent: rgb(237, 237, 242);
70 | --light-code-background: rgb(245, 245, 247);
71 | --light-color-background: #f7f7f8;
72 | --light-color-background-secondary: #f5f5f7;
73 | --dark-color-accent: rgb(237, 237, 242);
74 | --dark-code-background: rgb(245, 245, 247);
75 | --dark-color-background: #f7f7f8;
76 | --dark-color-background-secondary: #f5f5f7;
77 |
78 | /* Highlight Colors */
79 | --light-hl-0: #2f626c;
80 | --light-hl-1: #000000;
81 | --light-hl-2: #0070c1;
82 | --light-hl-3: #181818;
83 | --light-hl-4: #4e17b8;
84 | --light-hl-5: #2f626c;
85 | --light-hl-6: #2f626c;
86 |
87 | --dark-hl-0: #2f626c;
88 | --dark-hl-1: #000000;
89 | --dark-hl-2: #0070c1;
90 | --dark-hl-3: #181818;
91 | --dark-hl-4: #4e17b8;
92 | --dark-hl-5: #2f626c;
93 | --dark-hl-6: #2f626c;
94 |
95 | --light-color-text-aside: #82868a;
96 | --light-color-link: #1f59c2;
97 |
98 | --dark-color-text-aside: #82868a;
99 | --dark-color-link: #1f59c2;
100 |
101 | /* Custom */
102 | --light-color-text-light: #62636a;
103 | --light-color-separator: #f7f7f7;
104 | --light-color-separator-dark: #e4e4e4;
105 |
106 | --dark-color-text-light: #62636a;
107 | --dark-color-separator: #f7f7f7;
108 | --dark-color-separator-dark: #e4e4e4;
109 | }
110 | }
111 |
112 | @media (prefers-color-scheme: light) {
113 | :root {
114 | --color-ts-function-background: var(--light-color-ts-function-background);
115 | --color-ts-class-background: var(--light-color-ts-class-background);
116 | --color-ts-variable-background: var(--light-color-ts-variable-background);
117 | --color-ts-interface-background: var(--light-color-ts-interface-background);
118 | --color-ts-enum-background: var(--light-color-ts-enum-background);
119 | --color-ts-type-alias-background: var(
120 | --light-color-ts-type-alias-background
121 | );
122 | --color-ts-namespace-background: var(--light-color-ts-namespace-background);
123 | }
124 | }
125 |
126 | @media (prefers-color-scheme: dark) {
127 | :root {
128 | --color-ts-function-background: var(--dark-color-ts-function-background);
129 | --color-ts-class-background: var(--dark-color-ts-class-background);
130 | --color-ts-variable-background: var(--dark-color-ts-variable-background);
131 | --color-ts-interface-background: var(--dark-color-ts-interface-background);
132 | --color-ts-enum-background: var(--dark-color-ts-enum-background);
133 | --color-ts-type-alias-background: var(
134 | --dark-color-ts-type-alias-background
135 | );
136 | --color-ts-namespace-background: var(--dark-color-ts-namespace-background);
137 | }
138 | }
139 |
140 | @media (prefers-color-scheme: dark) {
141 | :root {
142 | color-scheme: light !important;
143 | }
144 | }
145 |
146 | :root {
147 | color-scheme: only light;
148 | --primary-bg-color-transparent: rgba(247, 249, 250, 0.85);
149 | }
150 |
151 | html {
152 | color-scheme: light;
153 | }
154 |
155 | pre {
156 | border: none;
157 | }
158 |
159 | hr {
160 | display: block;
161 | height: 1px;
162 | border: 0;
163 | border-top: 1px solid var(--light-color-separator);
164 | margin: 1em 0;
165 | padding: 0;
166 | }
167 |
168 | *::-webkit-scrollbar {
169 | display: none;
170 | }
171 |
172 | h4 {
173 | margin: 1.2rem 0;
174 | }
175 |
176 | .container {
177 | background: white;
178 | }
179 |
180 | .container-main {
181 | min-height: calc(100vh - 4rem);
182 | }
183 |
184 | @media (min-width: 770px) {
185 | #docs-header {
186 | height: 61px;
187 | background: var(--primary-bg-color-transparent);
188 | }
189 | }
190 |
191 | @media (max-width: 480px) {
192 | #docs-header {
193 | height: 95px;
194 | }
195 | }
196 |
197 | .container-main .col-content {
198 | padding: 0 2rem;
199 | margin-top: 2rem;
200 | }
201 |
202 | @media (min-width: 770px) {
203 | .container-main {
204 | margin: 0;
205 | }
206 | }
207 |
208 | .tsd-page-toolbar {
209 | }
210 |
211 | a.tsd-index-link {
212 | font-size: 0.9rem;
213 | }
214 |
215 | .tsd-navigation {
216 | font-size: 14px;
217 | padding-top: 1rem;
218 | }
219 |
220 | .tsd-nested-navigation {
221 | margin-left: 2.2rem;
222 | }
223 |
224 | .tsd-index-content > :not(:first-child) {
225 | padding: 0 1rem;
226 | }
227 |
228 | /* Hide visibility selector and themer */
229 | .tsd-theme-toggle {
230 | display: none;
231 | }
232 |
233 | .tsd-navigation .tsd-kind-icon {
234 | height: 20px;
235 | width: 20px;
236 | min-height: 20px;
237 | min-width: 20px;
238 | margin-right: 0.4rem;
239 | }
240 |
241 | .tsd-navigation a,
242 | .tsd-navigation summary > span,
243 | .tsd-page-navigation a {
244 | display: block;
245 | }
246 |
247 | .container .site-menu {
248 | height: 100%;
249 | border-right: 1px solid var(--light-color-separator);
250 | overflow: visible;
251 | }
252 |
253 | .container .site-menu > div {
254 | height: 100%;
255 | }
256 |
257 | @media (min-width: 770px) and (max-width: 1399px) {
258 | .site-menu {
259 | margin-top: 0;
260 | }
261 | }
262 |
263 | .page-menu {
264 | border-left: 1px solid var(--light-color-separator);
265 | background-image: var(--sidebar-background);
266 | }
267 |
268 | .tsd-signature,
269 | .tsd-kind-parameter,
270 | .tsd-kind-property,
271 | .tsd-parameter h5,
272 | .tsd-signature-type,
273 | .tsd-signature-symbol,
274 | code,
275 | pre {
276 | font-family: var(--font-family-code);
277 | }
278 |
279 | .tsd-kind-icon ~ span {
280 | font-family: var(--font-family-system);
281 | }
282 |
283 | #tsd-search .results span.parent {
284 | color: var(--light-color-text-light);
285 | }
286 |
287 | .tsd-navigation .tsd-accordion-summary,
288 | .tsd-accordion-summary > * {
289 | display: flex;
290 | align-items: center;
291 | }
292 |
293 | .tsd-signatures .tsd-signature,
294 | .tsd-signature {
295 | border: none;
296 | border-radius: 10px;
297 | background: var(--light-code-background);
298 | }
299 |
300 | .tsd-signature {
301 | padding: 1rem;
302 | }
303 |
304 | a.tsd-signature-type {
305 | text-decoration: underline;
306 | text-decoration-thickness: from-font;
307 | font-style: normal;
308 | }
309 |
310 | a.tsd-signature-type:hover {
311 | text-decoration: none;
312 | }
313 |
314 | .tsd-returns-title,
315 | ul.tsd-parameter-list h5,
316 | ul.tsd-type-parameter-list h5,
317 | .tsd-parameters h5 {
318 | font-size: 0.9rem;
319 | }
320 |
321 | .tsd-parameters h5 {
322 | margin: 0.5rem 0;
323 | }
324 |
325 | .tsd-accordion-details .tsd-index-heading {
326 | display: flex;
327 | align-items: center;
328 | text-transform: uppercase;
329 | font-size: 0.9rem;
330 | letter-spacing: 0.3px;
331 | }
332 |
333 | .tsd-index-heading svg {
334 | margin-right: 5px;
335 | }
336 |
337 | ul.tsd-hierarchy li {
338 | margin-top: 0.4rem;
339 | }
340 |
341 | .tsd-panel.tsd-member {
342 | margin-bottom: 2rem;
343 | }
344 |
345 | .tsd-panel h4 {
346 | font-weight: 600;
347 | }
348 |
349 | .tsd-panel-group {
350 | margin: 2.5rem 0;
351 | }
352 |
353 | code.tsd-tag {
354 | border: none;
355 | background: var(--color-accent);
356 | margin-bottom: -5px;
357 | font-size: 75%;
358 | }
359 |
360 | .site-menu .tsd-navigation {
361 | overflow-y: scroll;
362 | height: calc(100% - 65px);
363 | }
364 |
365 | .tsd-navigation a,
366 | .tsd-navigation summary > span,
367 | .tsd-page-navigation a {
368 | display: flex;
369 | }
370 |
371 | #tsd-search {
372 | position: sticky;
373 | top: 0;
374 | z-index: 1;
375 | }
376 |
377 | @media (min-width: 770px) {
378 | #tsd-search {
379 | padding: 1rem 1rem 1rem 0;
380 | }
381 | }
382 |
383 | #tsd-search.has-focus {
384 | background: none;
385 | }
386 |
387 | #tsd-search input {
388 | box-shadow: none;
389 | font-size: var(--font-size-regular);
390 | border: 1.5px solid var(--separator-color);
391 | font-weight: 400;
392 | cursor: text;
393 | padding: 0.4rem 1rem;
394 | width: 100%;
395 | }
396 |
397 | #tsd-search .results li:nth-child(even),
398 | #tsd-search .results li {
399 | font-size: 0.8rem;
400 | background-color: unset;
401 | padding: 0.1rem 0.4rem;
402 | }
403 |
404 | #tsd-search .results li a {
405 | color: inherit;
406 | text-decoration: none;
407 | }
408 |
409 | #tsd-search .results li.current:not(.no-results),
410 | #tsd-search .results li:hover:not(.no-results) {
411 | background: var(--light-color-separator);
412 | }
413 |
414 | #tsd-search .results li:not(:last-of-type) {
415 | border-bottom: 0.5px solid var(--light-color-separator);
416 | }
417 |
418 | #tsd-search .results li:not(:last-of-type) {
419 | border-bottom: 0.5px solid var(--light-color-separator);
420 | }
421 |
422 | #tsd-search .results {
423 | max-width: 800px;
424 | border-radius: 0 0 4px 4px;
425 | overflow: hidden;
426 | background: rgba(255, 255, 255, 0.99);
427 | top: 56px;
428 | }
429 |
430 | @media (min-width: 770px) {
431 | #tsd-search .results {
432 | min-width: 380px;
433 | }
434 | }
435 |
436 | #tsd-search .results .no-results {
437 | padding: 1rem;
438 | }
439 |
440 | .tsd-page-navigation {
441 | font-size: 14px;
442 | }
443 |
444 | .tsd-page-navigation h3 {
445 | margin-bottom: 1rem;
446 | }
447 |
448 | .tsd-internal-warning-banner {
449 | border-radius: 10px;
450 | background: var(--light-code-background);
451 | margin-bottom: 1rem;
452 | padding: 1rem;
453 | }
454 |
455 | #tsd-search {
456 | transition: background-color 0.1s;
457 | }
458 |
459 | #tsd-search .field input,
460 | #tsd-search .title,
461 | #tsd-toolbar-links a {
462 | transition: opacity 0.1s;
463 | }
464 |
465 | #tsd-search input:focus {
466 | border: 1.5px solid var(--selected-stroke-color);
467 | }
468 |
469 | .tsd-navigation a.current {
470 | font-weight: 600;
471 | border-radius: 5px;
472 | }
473 |
474 | .tsd-page-toolbar {
475 | background: rgba(250, 250, 250, 0.9);
476 | backdrop-filter: blur(10px);
477 | transition: transform 0.1s ease-in-out;
478 | }
479 |
480 | #tsd-search-field {
481 | font-family: var(--font-family-code);
482 | font-size: 1rem;
483 | }
484 |
485 | /* Remove nested padding */
486 | ul > li:only-child > ul:only-child {
487 | padding: 0;
488 | }
489 |
490 | .tsd-page-navigation ul {
491 | padding-left: 1rem;
492 | }
493 |
494 | .tsd-typography {
495 | }
496 |
497 | .header-iframe {
498 | }
499 |
500 | .tsd-kind-icon-custom {
501 | background: #ecdceb;
502 | padding: 0 5px;
503 | border-radius: 3px;
504 | font-family: var(--font-family-code);
505 | font-weight: bold;
506 | color: #9f0499;
507 | line-height: 1.4;
508 | margin-right: 2px;
509 | }
510 |
511 | [id^="icon-"] rect {
512 | stroke: none;
513 | stroke-width: 0;
514 | rx: 8px;
515 | }
516 |
517 | [id^="icon-"] path {
518 | stroke-width: 0.5px;
519 | }
520 |
521 | #icon-chevronDown path {
522 | fill: #4b4e5c;
523 | stroke: white;
524 | stroke-width: 1.5px;
525 | }
526 |
527 | .tsd-breadcrumb {
528 | font-family: var(--font-family-code);
529 | font-size: 14px;
530 | }
531 |
532 | .container {
533 | padding: 0 1rem;
534 | }
535 |
536 | @media (min-width: 770px) {
537 | .tsd-filter-visibility {
538 | padding-left: 1.5rem;
539 | }
540 | }
541 |
--------------------------------------------------------------------------------
/server/package/extractor/generate-tsconfig.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import fs from "fs";
3 |
4 | const extractorTSConfig = {
5 | compilerOptions: {
6 | target: "es2020",
7 | lib: ["dom", "dom.iterable", "esnext"],
8 | allowJs: true,
9 | skipLibCheck: true,
10 | strict: false,
11 | forceConsistentCasingInFileNames: true,
12 | noEmit: true,
13 | incremental: true,
14 | esModuleInterop: true,
15 | module: "esnext",
16 | moduleResolution: "node",
17 | resolveJsonModule: true,
18 | isolatedModules: true,
19 | jsx: "preserve",
20 | },
21 | include: ["./**/*.ts", "./**/*.d.ts", "./*.ts", "./*.d.ts"],
22 | exclude: [],
23 | };
24 |
25 | export async function generateTSConfig(packageDir: string) {
26 | const tsConfigPath = path.join(packageDir, "tsconfig.json");
27 |
28 | await fs.promises.writeFile(
29 | tsConfigPath,
30 | JSON.stringify(extractorTSConfig, null, 2),
31 | );
32 | return tsConfigPath;
33 | }
34 |
--------------------------------------------------------------------------------
/server/package/extractor/plugin-add-missing-exports.ts:
--------------------------------------------------------------------------------
1 | // From https://github.com/Gerrit0/typedoc-plugin-missing-exports/blob/48c65892e05923cab8dd63c92d1ea153e8089c5a/index.ts
2 |
3 | import path from "path";
4 | import {
5 | Application,
6 | Context,
7 | Converter,
8 | ReflectionKind,
9 | TypeScript as ts,
10 | ReferenceType,
11 | Reflection,
12 | DeclarationReflection,
13 | ProjectReflection,
14 | ParameterType,
15 | JSX,
16 | Renderer,
17 | ContainerReflection,
18 | } from "typedoc";
19 | import fs from "fs";
20 | import logger from "../../../common/logger";
21 |
22 | declare module "typedoc" {
23 | export interface TypeDocOptionMap {
24 | internalModule: string;
25 | collapseInternalModule: boolean;
26 | placeInternalsInOwningModule: boolean;
27 | }
28 |
29 | export interface Reflection {
30 | [InternalModule]?: boolean;
31 | }
32 | }
33 |
34 | let hasMonkeyPatched = false;
35 | const InternalModule = Symbol();
36 | const ModuleLike: ReflectionKind =
37 | ReflectionKind.Project | ReflectionKind.Module;
38 |
39 | // https://github.com/Gerrit0/typedoc-plugin-missing-exports/issues/12
40 | function patchEscapedName(escapedName: string) {
41 | if (escapedName.includes("node_modules")) {
42 | const startIndex =
43 | escapedName.lastIndexOf("node_modules") + "node_modules".length + 1;
44 | const packagePath = escapedName.substring(startIndex);
45 | const fragments = packagePath.split(path.sep);
46 |
47 | if (packagePath.startsWith("@")) {
48 | return [fragments[0], fragments[1]].join(path.sep);
49 | } else {
50 | return fragments[0];
51 | }
52 | }
53 | return escapedName;
54 | }
55 |
56 | const HOOK_JS = `
57 |
58 | `.trim();
59 |
60 | export function load(app: Application) {
61 | app["missingExportsPlugin"] = {
62 | activeReflection: undefined,
63 | referencedSymbols: new Map>(),
64 | symbolToOwningModule: new Map(),
65 | knownPrograms: new Map(),
66 | };
67 |
68 | function discoverMissingExports(
69 | owningModule: Reflection,
70 | context: Context,
71 | program: ts.Program,
72 | ): Set {
73 | // An export is missing if if was referenced
74 | // Is not contained in the documented
75 | // And is "owned" by the active reflection
76 | const referenced =
77 | app["missingExportsPlugin"].referencedSymbols.get(program) || new Set();
78 | const ownedByOther = new Set();
79 | app["missingExportsPlugin"].referencedSymbols.set(program, ownedByOther);
80 |
81 | for (const s of [...referenced]) {
82 | // Patch package name:
83 | if (s.escapedName) {
84 | // @ts-ignore
85 | s.escapedName = patchEscapedName(s.escapedName);
86 | }
87 | if (context.project.getReflectionFromSymbol(s)) {
88 | referenced.delete(s);
89 | } else if (
90 | app["missingExportsPlugin"].symbolToOwningModule.get(s) !==
91 | owningModule
92 | ) {
93 | referenced.delete(s);
94 | ownedByOther.add(s);
95 | }
96 | }
97 |
98 | return referenced;
99 | }
100 |
101 | // Monkey patch the constructor for references so that we can get every
102 | const origCreateSymbolReference = ReferenceType.createSymbolReference;
103 | app["missingExportsPlugin"].createSymbolReference = function (
104 | symbol,
105 | context,
106 | name,
107 | ) {
108 | const owningModule = getOwningModule(context);
109 | console.log(
110 | "Created ref",
111 | symbol.name,
112 | "owner",
113 | owningModule.getFullName(),
114 | );
115 |
116 | if (!app["missingExportsPlugin"].activeReflection) {
117 | logger.error(
118 | "active reflection has not been set for " + symbol.escapedName,
119 | );
120 | return origCreateSymbolReference.call(this, symbol, context, name);
121 | }
122 | const set = app["missingExportsPlugin"].referencedSymbols.get(
123 | context.program,
124 | );
125 | app["missingExportsPlugin"].symbolToOwningModule.set(
126 | symbol,
127 | app["missingExportsPlugin"].owningModule,
128 | );
129 | if (set) {
130 | set.add(symbol);
131 | } else {
132 | app["missingExportsPlugin"].referencedSymbols.set(
133 | context.program,
134 | new Set([symbol]),
135 | );
136 | }
137 | return origCreateSymbolReference.call(this, symbol, context, name);
138 | };
139 |
140 | ReferenceType.createSymbolReference = (symbol, context, name) => {
141 | return app["missingExportsPlugin"].createSymbolReference(
142 | symbol,
143 | context,
144 | name,
145 | );
146 | };
147 |
148 | app.options.addDeclaration({
149 | name: "internalModule",
150 | help: "[typedoc-plugin-missing-exports] Define the name of the module that internal symbols which are not exported should be placed into.",
151 | defaultValue: "",
152 | });
153 |
154 | app.options.addDeclaration({
155 | name: "placeInternalsInOwningModule",
156 | help: "[typedoc-plugin-missing-exports] If set internal symbols will not be placed into an internals module, but directly into the module which references them.",
157 | defaultValue: false,
158 | type: ParameterType.Boolean,
159 | });
160 | app.converter.on(Converter.EVENT_BEGIN, () => {
161 | if (
162 | app.options.getValue("placeInternalsInOwningModule") &&
163 | app.options.isSet("internalModule")
164 | ) {
165 | app.logger.warn(
166 | `[typedoc-plugin-missing-exports] Both placeInternalsInOwningModule and internalModule are set, the internalModule option will be ignored.`,
167 | );
168 | }
169 | });
170 |
171 | app.options.addDeclaration({
172 | name: "collapseInternalModule",
173 | help: "[typedoc-plugin-missing-exports] Include JS in the page to collapse all entries in the navigation on page load.",
174 | defaultValue: false,
175 | type: ParameterType.Boolean,
176 | });
177 |
178 | app.converter.on(
179 | Converter.EVENT_CREATE_DECLARATION,
180 | (context: Context, refl: Reflection) => {
181 | // TypeDoc 0.26 doesn't fire EVENT_CREATE_DECLARATION for project
182 | // We need to ensure the project has a program attached to it, so
183 | // do that when the first declaration is created.
184 | if (app["missingExportsPlugin"].knownPrograms.size === 0) {
185 | app["missingExportsPlugin"].knownPrograms.set(refl.project, context.program);
186 | }
187 |
188 | if (refl.kindOf(ModuleLike)) {
189 | app["missingExportsPlugin"].knownPrograms.set(refl, context.program);
190 | app["missingExportsPlugin"].activeReflection = refl;
191 | }
192 | },
193 | );
194 |
195 | const basePath = path.join(app.options.getValue("basePath"), "..", "..");
196 |
197 | app.converter.on(
198 | Converter.EVENT_RESOLVE_BEGIN,
199 | function onResolveBegin(context: Context) {
200 | const modules: (DeclarationReflection | ProjectReflection)[] =
201 | context.project.getChildrenByKind(ReflectionKind.Module);
202 | if (modules.length === 0) {
203 | // Single entry point, just target the project.
204 | modules.push(context.project);
205 | }
206 |
207 | for (const mod of modules) {
208 | const program = app["missingExportsPlugin"].knownPrograms.get(mod);
209 | if (!program) continue;
210 | let missing = discoverMissingExports(mod, context, program);
211 | if (!missing.size) continue;
212 |
213 | // Nasty hack here that will almost certainly break in future TypeDoc versions.
214 | context.setActiveProgram(program);
215 |
216 | let internalContext: Context;
217 | if (app.options.getValue("placeInternalsInOwningModule")) {
218 | internalContext = context.withScope(mod);
219 | } else {
220 | const internalNs = context
221 | .withScope(mod)
222 | .createDeclarationReflection(
223 | ReflectionKind.Module,
224 | void 0,
225 | void 0,
226 | app.options.getValue("internalModule"),
227 | );
228 | internalNs[InternalModule] = true;
229 | context.finalizeDeclarationReflection(internalNs);
230 | internalContext = context.withScope(internalNs);
231 | }
232 |
233 |
234 | const internalNs = context
235 | .withScope(mod)
236 | .createDeclarationReflection(
237 | ReflectionKind.Module,
238 | void 0,
239 | void 0,
240 | context.converter.application.options.getValue("internalModule"),
241 | );
242 | internalNs[InternalModule] = true;
243 | context.finalizeDeclarationReflection(internalNs);
244 |
245 | // Keep track of which symbols we've tried to convert. If they don't get converted
246 | // when calling convertSymbol, then the user has excluded them somehow, don't go into
247 | // an infinite loop when converting.
248 | const tried = new Set();
249 |
250 | do {
251 | for (const s of missing) {
252 | if (shouldConvertSymbol(s, context.checker, basePath)) {
253 | internalContext.converter.convertSymbol(internalContext, s);
254 | }
255 | tried.add(s);
256 | }
257 |
258 | missing = discoverMissingExports(mod, context, program);
259 | for (const s of tried) {
260 | missing.delete(s);
261 | }
262 | } while (missing.size > 0);
263 |
264 | // All the missing symbols were excluded, so get rid of our namespace.
265 | if (!internalNs.children?.length) {
266 | context.project.removeReflection(internalNs);
267 | }
268 |
269 | context.setActiveProgram(void 0);
270 | }
271 |
272 | app["missingExportsPlugin"].knownPrograms.clear();
273 | app["missingExportsPlugin"].referencedSymbols.clear();
274 | app["missingExportsPlugin"].symbolToOwningModule.clear();
275 |
276 | },
277 | 1e9,
278 | );
279 |
280 | app.renderer.on(Renderer.EVENT_BEGIN, () => {
281 | if (app.options.getValue("collapseInternalModule")) {
282 | app.renderer.hooks.on("head.end", () =>
283 | JSX.createElement(JSX.Raw, {
284 | html: HOOK_JS.replace(
285 | "NAME",
286 | JSON.stringify(app.options.getValue("internalModule")),
287 | ),
288 | }),
289 | );
290 | }
291 | });
292 | }
293 |
294 | // open a new file for appending
295 | const fd = fs.openSync("./accessed", "a");
296 |
297 | function getOwningModule(context: Context): Reflection {
298 | let refl = context.scope;
299 | // Go up the reflection hierarchy until we get to a module
300 | while (!refl.kindOf(ModuleLike)) {
301 | refl = refl.parent!;
302 | }
303 | // The module cannot be an owning module.
304 | if (refl[InternalModule]) {
305 | return refl.parent!;
306 | }
307 | return refl;
308 | }
309 |
310 | // append string to file
311 | function shouldConvertSymbol(
312 | symbol: ts.Symbol,
313 | checker: ts.TypeChecker,
314 | basePath: string,
315 | ) {
316 | while (symbol.flags & ts.SymbolFlags.Alias) {
317 | symbol = checker.getAliasedSymbol(symbol);
318 | }
319 |
320 | // We're looking at an unknown symbol which is declared in some package without
321 | // type declarations. We know nothing about it, so don't convert it.
322 | if (symbol.flags & ts.SymbolFlags.Transient) {
323 | return false;
324 | }
325 |
326 | // This is something inside the special Node `Globals` interface. Don't convert it
327 | // because TypeDoc will reasonably assert that "Property" means that a symbol should be
328 | // inside something that can have properties.
329 | if (symbol.flags & ts.SymbolFlags.Property && symbol.name !== "default") {
330 | return false;
331 | }
332 |
333 | const isOutsideBase = (symbol.getDeclarations() ?? []).some(
334 | (node) => !node.getSourceFile().fileName.startsWith(basePath),
335 | );
336 |
337 | if (isOutsideBase) {
338 | return false;
339 | }
340 |
341 | return true;
342 | }
343 |
--------------------------------------------------------------------------------
/server/package/index.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 |
3 | import { docsRootPath, getDocsPath } from "./utils";
4 | import { resolvePackageJSON } from "./resolvers";
5 | import semver from "semver";
6 | import { generateDocsQueue, generateDocsQueueEvents } from "../queues";
7 | import { packageFromPath } from "../../common/client-utils";
8 | import { PackageNotFoundError } from "./CustomError";
9 | import logger from "../../common/logger";
10 | import { parse } from "node-html-parser";
11 | import fs from "fs";
12 | import * as stackTraceParser from "stacktrace-parser";
13 | import { LRUCache } from "lru-cache";
14 | import workerpool from "workerpool";
15 |
16 | const docsWorkerPool = workerpool.pool(
17 | path.join(__dirname, "..", "workers", "docs-builder-worker-pool.js"),
18 | {
19 | workerType: "process",
20 | maxQueueSize: 10,
21 | forkOpts: {
22 | stdio: "inherit",
23 | },
24 | forkArgs: ["--max-old-space-size=1024"],
25 | },
26 | );
27 |
28 | export async function resolveDocsRequest({
29 | packageName,
30 | packageVersion,
31 | force,
32 | }: {
33 | packageName: string;
34 | packageVersion: string;
35 | force: boolean;
36 | }): Promise<
37 | | {
38 | type: "hit";
39 | packageName: string;
40 | packageVersion: string;
41 | docsPathDisk: string;
42 | }
43 | | {
44 | type: "miss";
45 | packageName: string;
46 | packageVersion: string;
47 | packageJSON: { [key: string]: any };
48 | docsPathDisk: string;
49 | }
50 | > {
51 | if (force) {
52 | const packageJSON = await resolvePackageJSON({
53 | packageName,
54 | packageVersion,
55 | });
56 |
57 | const docsPathDisk = getDocsPath({
58 | packageName: packageName,
59 | packageVersion: packageVersion,
60 | });
61 |
62 | return {
63 | type: "miss",
64 | packageName: packageJSON.name,
65 | packageVersion: packageJSON.version,
66 | packageJSON,
67 | docsPathDisk,
68 | };
69 | }
70 |
71 | if (semver.valid(packageVersion)) {
72 | const docsPathDisk = getDocsPath({
73 | packageName: packageName,
74 | packageVersion: packageVersion,
75 | });
76 |
77 | if (fs.existsSync(path.join(docsPathDisk, "index.html"))) {
78 | return {
79 | type: "hit",
80 | packageName,
81 | packageVersion,
82 | docsPathDisk,
83 | };
84 | }
85 | }
86 |
87 | const packageJSON = await resolvePackageJSON({
88 | packageName,
89 | packageVersion,
90 | });
91 |
92 | const docsPathDisk = getDocsPath({
93 | packageName: packageJSON.name,
94 | packageVersion: packageJSON.version,
95 | });
96 |
97 | if (fs.existsSync(path.join(docsPathDisk, "index.html"))) {
98 | return {
99 | type: "hit",
100 | packageName: packageJSON.name,
101 | packageVersion: packageJSON.version,
102 | docsPathDisk,
103 | };
104 | }
105 |
106 | return {
107 | type: "miss",
108 | packageName: packageJSON.name,
109 | packageVersion: packageJSON.version,
110 | packageJSON,
111 | docsPathDisk,
112 | };
113 | }
114 |
115 | export async function handlerAPIDocsTrigger(req, res) {
116 | const paramsPath = req.params["*"];
117 | const { force } = req.query;
118 | const routePackageDetails = packageFromPath(paramsPath);
119 | logger.info("routePackageDetails is ", routePackageDetails);
120 |
121 | if (!routePackageDetails) {
122 | logger.error("Route package details not found in " + paramsPath);
123 | return res.status(404).send({
124 | name: PackageNotFoundError.name,
125 | });
126 | }
127 |
128 | const { packageName, packageVersion, docsFragment } = routePackageDetails;
129 |
130 | const resolvedRequest = await resolveDocsRequest({
131 | packageName,
132 | packageVersion,
133 | force,
134 | });
135 |
136 | if (resolvedRequest.type === "hit") {
137 | return res.send({ status: "success" });
138 | } else {
139 | logger.info(
140 | 'Docs job for "%s" at version %s queued for building',
141 | resolvedRequest.packageName,
142 | resolvedRequest.packageVersion,
143 | );
144 | const generateJob = await generateDocsQueue.add(
145 | `generate docs ${packageName}`,
146 | { packageJSON: resolvedRequest.packageJSON, force },
147 | {
148 | jobId: `${resolvedRequest.packageJSON.name}@${resolvedRequest.packageJSON.version}`,
149 | priority: 100,
150 | attempts: 1,
151 | },
152 | );
153 |
154 | return res.send({
155 | status: "queued",
156 | jobId: generateJob.id,
157 | pollInterval: 2000,
158 | });
159 | }
160 | }
161 |
162 | export async function handlerAPIDocsBuild(req, res) {
163 | const paramsPath = req.params["*"];
164 | const { force } = req.query;
165 | const routePackageDetails = packageFromPath(paramsPath);
166 | logger.info("routePackageDetails is ", routePackageDetails);
167 |
168 | if (!routePackageDetails) {
169 | logger.error("Route package details not found in " + paramsPath);
170 | return res.status(404).send({
171 | name: PackageNotFoundError.name,
172 | });
173 | }
174 |
175 | const { packageName, packageVersion, docsFragment } = routePackageDetails;
176 |
177 | const resolvedRequest = await resolveDocsRequest({
178 | packageName,
179 | packageVersion,
180 | force,
181 | });
182 |
183 | if (resolvedRequest.type === "hit") {
184 | return res.send({ status: "success" });
185 | } else {
186 | logger.info(
187 | 'Docs job for "%s" at version %s queued for building',
188 | resolvedRequest.packageName,
189 | resolvedRequest.packageVersion,
190 | );
191 |
192 | try {
193 | await docsWorkerPool.exec("generateDocs", [
194 | { packageJSON: resolvedRequest.packageJSON, force },
195 | ]);
196 |
197 | return res.send({ status: "success" });
198 | } catch (err) {
199 | return res.send({
200 | status: "failed",
201 | errorCode: err.message,
202 | errorMessage: err.originalError?.message,
203 | errorStack:
204 | cleanStackTrace(err.originalError?.stacktrace) ||
205 | err.originalError?.message,
206 | });
207 | }
208 | }
209 | }
210 |
211 | function cleanStackTrace(stackTrace: string | undefined) {
212 | if (!stackTrace) return "";
213 |
214 | let parsedStackTrace = [];
215 | try {
216 | const parsed = stackTraceParser.parse(stackTrace);
217 | parsedStackTrace = parsed.map((stack) => ({
218 | ...stack,
219 | file: stack.file.split("node_modules/").pop().replace(process.cwd(), ""),
220 | }));
221 | } catch (err) {
222 | logger.error("Failed to parse stack trace", err);
223 | parsedStackTrace = [];
224 | }
225 |
226 | return parsedStackTrace
227 | .map(
228 | (stack) => `at ${stack.methodName} in ${stack.file}:${stack.lineNumber}`,
229 | )
230 | .join("\n");
231 | }
232 |
233 | const priorityCache = new LRUCache({
234 | ttl: 1000 * 60 * 3,
235 | max: 500,
236 | });
237 |
238 | export async function handlerAPIDocsPoll(req, res) {
239 | const jobId = req.params["*"];
240 | const job = await generateDocsQueue.getJob(jobId);
241 |
242 | if (!job) {
243 | logger.error(`Job ${jobId} not found in queue`);
244 | return res.status(404);
245 | }
246 |
247 | if (priorityCache.has(jobId)) {
248 | await job.changePriority({
249 | priority: priorityCache.get(jobId) - 1,
250 | });
251 | priorityCache.set(jobId, priorityCache.get(jobId) - 1);
252 | } else {
253 | await job.changePriority({
254 | priority: 99,
255 | });
256 | priorityCache.set(jobId, 99);
257 | }
258 |
259 | if (await job.isCompleted()) {
260 | return { status: "success" };
261 | } else if (await job.isFailed()) {
262 | return res.send({
263 | status: "failed",
264 | errorCode: job.failedReason,
265 | errorMessage: job.data.originalError?.message,
266 | errorStack: cleanStackTrace(job.data.originalError?.stacktrace),
267 | });
268 | }
269 |
270 | return { status: "queued" };
271 | }
272 |
273 | const preloadCache = new LRUCache<
274 | string,
275 | { url: string; rel: string; as: string }[]
276 | >({
277 | max: 500,
278 | });
279 |
280 | function extractPreloadResources(htmlPath: string) {
281 | if (preloadCache.get(htmlPath)) {
282 | return preloadCache.get(htmlPath);
283 | }
284 |
285 | const htmlContent = fs.readFileSync(htmlPath, "utf8");
286 | const root = parse(htmlContent);
287 | const scriptAssets = root
288 | .querySelectorAll("script")
289 | .map((script) => script.getAttribute("src"))
290 | .filter(Boolean)
291 | .map((src) => {
292 | if (src.startsWith("/")) {
293 | return {
294 | url: src,
295 | rel: "preload",
296 | as: "script",
297 | };
298 | }
299 |
300 | if (!src.startsWith("http") && !src.startsWith("//")) {
301 | const relativeDocsPath = path.join(
302 | "/docs",
303 | path.relative(docsRootPath, path.join(path.dirname(htmlPath), src)),
304 | );
305 | return {
306 | url: relativeDocsPath,
307 | rel: "preload",
308 | as: "script",
309 | };
310 | }
311 | return null;
312 | })
313 | .filter(Boolean);
314 |
315 | const linkAssets = root
316 | .querySelectorAll("link")
317 | .map((link) => link.getAttribute("href"))
318 | .map((href) => {
319 | const pathName = href.split("?")[0];
320 | if (pathName.endsWith(".css")) {
321 | if (href.startsWith("/")) {
322 | return {
323 | url: href,
324 | rel: "preload",
325 | as: "style",
326 | };
327 | }
328 |
329 | if (!href.startsWith("http") && !href.startsWith("//")) {
330 | const relativeDocsPath = path.join(
331 | "/docs",
332 | path.relative(
333 | docsRootPath,
334 | path.join(path.dirname(htmlPath), href),
335 | ),
336 | );
337 | return {
338 | url: relativeDocsPath,
339 | rel: "preload",
340 | as: "style",
341 | };
342 | }
343 | return null;
344 | }
345 | })
346 | .filter(Boolean);
347 |
348 | const jsAssets = {
349 | url: "/shared-dist/header.umd.js",
350 | rel: "preload",
351 | as: "script",
352 | };
353 | const preloadAssets = [...linkAssets, ...scriptAssets, jsAssets];
354 | preloadCache.set(htmlPath, preloadAssets);
355 | return preloadAssets;
356 | }
357 |
358 | export async function handlerDocsHTML(req, res) {
359 | const paramsPath = req.params["*"];
360 | const { force } = req.query;
361 | const routePackageDetails = packageFromPath(paramsPath);
362 |
363 | if (!routePackageDetails) {
364 | return res.status(404);
365 | }
366 |
367 | const { packageName, packageVersion, docsFragment } = routePackageDetails;
368 |
369 | const resolvedRequest = await resolveDocsRequest({
370 | packageName,
371 | packageVersion,
372 | force,
373 | });
374 |
375 | if (resolvedRequest.type === "miss") {
376 | const generateJob = await generateDocsQueue.add(
377 | `generate docs ${packageName}`,
378 | { packageJSON: resolvedRequest.packageJSON, force },
379 | {
380 | jobId: `${resolvedRequest.packageJSON.name}@${resolvedRequest.packageJSON.version}`,
381 | priority: 100,
382 | attempts: 1,
383 | },
384 | );
385 | await generateJob.waitUntilFinished(generateDocsQueueEvents);
386 | } else if (docsFragment.endsWith("index.html")) {
387 | logger.info(
388 | 'Hit cache for "%s" at version %s since HTML already exists',
389 | packageName,
390 | packageVersion,
391 | );
392 | }
393 |
394 | const resolvedPath = path.join(
395 | resolvedRequest.packageName,
396 | resolvedRequest.packageVersion,
397 | docsFragment,
398 | );
399 |
400 | if (paramsPath !== resolvedPath) {
401 | return res.redirect(`/docs/${resolvedPath}`);
402 | }
403 |
404 | const resolvedAbsolutePath = path.join(
405 | resolvedRequest.docsPathDisk,
406 | docsFragment,
407 | );
408 | const relativeDocsPath = path.relative(docsRootPath, resolvedAbsolutePath);
409 |
410 | if (relativeDocsPath.endsWith(".html")) {
411 | // Cache HTML for 2 hours
412 |
413 | res.header("Cache-Control", "public, max-age=3600");
414 | const linkHeaderContent = extractPreloadResources(resolvedAbsolutePath)
415 | .map(
416 | ({ url, rel, as }) =>
417 | `; rel="${rel}"; as="${as}"`,
418 | )
419 | .join(", ");
420 | res.header("Link", linkHeaderContent);
421 | } else {
422 | // Cache rest for 8 hours
423 | res.header("Cache-Control", `public, max-age=${60 * 60 * 8}`);
424 | }
425 |
426 | return res.sendFile(relativeDocsPath);
427 | }
428 |
--------------------------------------------------------------------------------
/server/package/installation.utils.ts:
--------------------------------------------------------------------------------
1 | import rimraf from "rimraf";
2 | import path from "path";
3 | import { promises as fs } from "fs";
4 | import sanitize from "sanitize-filename";
5 |
6 | import { InstallError, PackageNotFoundError } from "./CustomError";
7 | import { exec } from "./common.utils";
8 | import { InstallPackageOptions } from "./types";
9 | import { performance } from "perf_hooks";
10 | import { fileExists } from "next/dist/lib/file-exists";
11 | import logger from "../../common/logger";
12 | import "dotenv/config";
13 |
14 | // When operating on a local directory, force npm to copy directory structure
15 | // and all dependencies instead of just symlinking files
16 | const wrapPackCommand = (packagePath: string) =>
17 | `$(npm pack --ignore-scripts ${packagePath} | tail -1)`;
18 |
19 | const tmpFolder = path.join("/tmp", "tmp-build");
20 |
21 | const InstallationUtils = {
22 | getInstallPath({
23 | packageName,
24 | packageVersion,
25 | basePath,
26 | }: {
27 | packageName: string;
28 | packageVersion: string;
29 | basePath: string;
30 | }) {
31 | return path.join(
32 | basePath,
33 | "packages",
34 | sanitize(`build-${packageName}-${packageVersion}`),
35 | );
36 | },
37 |
38 | async preparePath(packageName: string, packageVersion: string) {
39 | const installPath = InstallationUtils.getInstallPath({
40 | packageName,
41 | packageVersion,
42 | basePath: tmpFolder,
43 | });
44 |
45 | await fs.mkdir(tmpFolder, { recursive: true });
46 | await fs.mkdir(installPath, { recursive: true });
47 |
48 | const packageJSON = path.join(installPath, "package.json");
49 |
50 | const packageJSONExists = await fileExists(packageJSON);
51 |
52 | if (!packageJSONExists)
53 | await fs.writeFile(
54 | path.join(installPath, "package.json"),
55 | JSON.stringify({
56 | name: "build-package",
57 | dependencies: {},
58 | browserslist: [
59 | "last 5 Chrome versions",
60 | "last 5 Firefox versions",
61 | "Safari >= 9",
62 | "edge >= 12",
63 | ],
64 | }),
65 | );
66 |
67 | return installPath;
68 | },
69 |
70 | async installPackage(
71 | packageStrings: string[],
72 | installPath: string,
73 | installOptions: InstallPackageOptions,
74 | ) {
75 | let flags, command;
76 | let installStartTime = performance.now();
77 |
78 | const {
79 | client = "npm",
80 | limitConcurrency,
81 | networkConcurrency,
82 | installTimeout = 600000,
83 | } = installOptions;
84 |
85 | if (client === "yarn") {
86 | flags = [
87 | "ignore-flags",
88 | "ignore-engines",
89 | "skip-integrity-check",
90 | "exact",
91 | "json",
92 | "no-progress",
93 | "silent",
94 | "no-lockfile",
95 | "no-bin-links",
96 | "no-audit",
97 | "no-fund",
98 | "ignore-optional",
99 | ];
100 | if (limitConcurrency) {
101 | flags.push("mutex network");
102 | }
103 |
104 | if (networkConcurrency) {
105 | flags.push(`network-concurrency ${networkConcurrency}`);
106 | }
107 | command = `yarn add ${packageStrings.join(" ")} --${flags.join(" --")}`;
108 | } else if (client === "npm") {
109 | flags = [
110 | // Setting cache is required for concurrent `npm install`s to work
111 | `cache=${path.join(tmpFolder, "cache")}`,
112 | "no-package-lock",
113 | "no-shrinkwrap",
114 | "no-optional",
115 | "no-bin-links",
116 | "progress false",
117 | "loglevel error",
118 | "ignore-scripts",
119 | "save-exact",
120 | "production",
121 | "json",
122 | ];
123 |
124 | command = `npm install ${packageStrings.join(" ")} --${flags.join(
125 | " --",
126 | )}`;
127 | } else if (client === "pnpm") {
128 | flags = [
129 | "no-optional",
130 | "loglevel error",
131 | "ignore-scripts",
132 | "save-exact",
133 | `store-dir ${path.join(tmpFolder, "cache")}`,
134 | ];
135 |
136 | command = `pnpm add ${packageStrings.join(" ")} --${flags.join(" --")}`;
137 | } else if (client === "bun") {
138 | const cacheDir = path.join(tmpFolder, "cache")
139 | flags = [
140 | "no-save",
141 | "exact",
142 | "no-progress",
143 | `cache-dir ${cacheDir}`,
144 | ];
145 |
146 | // Set the BUN_INSTALL_CACHE_DIR environment variable
147 | // See https://github.com/oven-sh/bun/issues/6423
148 | process.env.BUN_INSTALL_CACHE_DIR = cacheDir;
149 | command = `bun install ${packageStrings.join(" ")} --${flags.join(
150 | " --",
151 | )}`;
152 | } else {
153 | console.error("No valid client specified");
154 | process.exit(1);
155 | }
156 |
157 | logger.info("install start %s using %s", packageStrings.join(" "), client);
158 |
159 | try {
160 | await exec(
161 | command,
162 | {
163 | cwd: installPath,
164 | maxBuffer: 1024 * 500,
165 | },
166 | installTimeout,
167 | );
168 |
169 | logger.info("install finish %s", packageStrings.join(" "));
170 | } catch (err) {
171 | logger.error(err);
172 | if (typeof err === "string" && err.includes("404")) {
173 | throw new PackageNotFoundError(err);
174 | } else {
175 | throw new InstallError(err);
176 | }
177 | }
178 | },
179 |
180 | async cleanupPath(installPath: string) {
181 | const noop = () => {};
182 | try {
183 | await rimraf(installPath, noop);
184 | } catch (err) {
185 | console.error("cleaning up path ", installPath, " failed due to ", err);
186 | }
187 | },
188 | };
189 |
190 | export default InstallationUtils;
191 |
--------------------------------------------------------------------------------
/server/package/resolvers.ts:
--------------------------------------------------------------------------------
1 | import resolve, { CachedInputFileSystem } from "enhanced-resolve";
2 | import fs from "fs";
3 | import path from "path";
4 | import logger from "../../common/logger";
5 | import semver from "semver";
6 | import InstallationUtils from "./installation.utils";
7 | import pacote from "pacote";
8 | import { LRUCache } from "lru-cache";
9 | import {
10 | PackageNotFoundError,
11 | PackageVersionMismatchError,
12 | ResolutionError,
13 | } from "./CustomError";
14 | import { InstallPackageOptions } from "./types";
15 | import { config } from "dotenv";
16 |
17 | config({
18 | path: path.join(__dirname, "../../.env"),
19 | });
20 |
21 | const getTypeResolver = () =>
22 | resolve.create({
23 | conditionNames: [
24 | "types",
25 | "import",
26 | // APF: https://angular.io/guide/angular-package-format
27 | "esm2020",
28 | "es2020",
29 | "es2015",
30 | "require",
31 | "node",
32 | "node-addons",
33 | "browser",
34 | "default",
35 | ],
36 | extensions: [".js"],
37 | mainFields: [
38 | "types",
39 | "typings",
40 | // APF: https://angular.io/guide/angular-package-format
41 | "fesm2020",
42 | "fesm2015",
43 | "esm2020",
44 | "es2020",
45 | "module",
46 | "jsnext:main",
47 | "main",
48 | ],
49 | fileSystem: new CachedInputFileSystem(fs, 5 * 1000),
50 | symlinks: false,
51 | });
52 |
53 | export type TypeResolveResult = {
54 | packagePath: string;
55 | packageName: string;
56 | typePath: string;
57 | };
58 |
59 | function definitionPath(filePath: string) {
60 | const filename = path.parse(filePath).name;
61 | const fileDir = path.dirname(filePath);
62 | return path.join(fileDir, `${filename}.d.ts`);
63 | }
64 |
65 | export async function resolveTypePathInbuilt(
66 | packageContainingPath,
67 | packageName,
68 | ): Promise {
69 | const packagePath = path.join(
70 | packageContainingPath,
71 | "node_modules",
72 | packageName,
73 | );
74 |
75 | const packageJSON = await getPackageJSON(packagePath);
76 |
77 | if (packageJSON.types) {
78 | return {
79 | packagePath: packagePath,
80 | packageName: packageName,
81 | typePath: path.resolve(packagePath, packageJSON.types),
82 | };
83 | }
84 |
85 | return new Promise((resolve, reject) => {
86 | getTypeResolver()(
87 | packageContainingPath,
88 | packageJSON.name,
89 | (err, resolvedPath) => {
90 | if (err || resolvedPath === false) {
91 | logger.warn(
92 | "Failed to resolve inbuilt types for %s",
93 | packageJSON.name,
94 | );
95 | return resolve(null);
96 | } else {
97 | if (resolvedPath.endsWith(".d.ts")) {
98 | return resolve({
99 | packagePath: packagePath,
100 | packageName: packageJSON.name,
101 | typePath: resolvedPath,
102 | });
103 | } else {
104 | const exists = fs.existsSync(definitionPath(resolvedPath));
105 |
106 | if (exists) {
107 | return resolve({
108 | packagePath: packagePath,
109 | packageName: packageJSON.name,
110 | typePath: definitionPath(resolvedPath),
111 | });
112 | } else {
113 | logger.warn(
114 | "Failed to resolve inbuilt types for %s",
115 | packageJSON.name,
116 | );
117 | return resolve(null);
118 | }
119 | }
120 | }
121 | },
122 | );
123 | });
124 | }
125 |
126 | type PackageJSON = {
127 | name: string;
128 | version: string;
129 | types?: string;
130 | };
131 |
132 | async function getPackageJSON(packagePath: string) {
133 | const packageJSONPath = path.join(packagePath, "package.json");
134 | const packageJSONContents = await fs.promises.readFile(
135 | packageJSONPath,
136 | "utf-8",
137 | );
138 | return JSON.parse(packageJSONContents);
139 | }
140 |
141 | const packageVersionsCache = new LRUCache({
142 | max: 100,
143 | ttl: 1000 * 60 * 60,
144 | });
145 |
146 | async function getPackageVersions(packageName: string) {
147 | if (packageVersionsCache.has(packageName)) {
148 | return packageVersionsCache.get(packageName);
149 | }
150 | const { versions } = await pacote.packument(packageName, {
151 | fullMetadata: false,
152 | });
153 | packageVersionsCache.set(packageName, versions);
154 | return versions;
155 | }
156 |
157 | export async function resolveTypePathDefinitelyTyped(packageJSON: PackageJSON) {
158 | let typeVersions = [];
159 | if (!packageJSON.name) {
160 | throw new Error("No name!");
161 | }
162 | const typesPackageName = `@types/${packageJSON.name.replace("/", "__")}`;
163 | const parsedPackageVersion = semver.parse(packageJSON.version);
164 | try {
165 | const versions = await getPackageVersions(typesPackageName);
166 | typeVersions = Object.keys(versions);
167 | } catch (err) {
168 | logger.warn(
169 | "Failed to resolve definitely typed definitions for ",
170 | { name: packageJSON.name, version: packageJSON.version },
171 | err,
172 | );
173 | return null;
174 | }
175 |
176 | const matchingMajors = typeVersions.filter(
177 | (version) => semver.parse(version).major === parsedPackageVersion.major,
178 | );
179 |
180 | const matchingMinors = typeVersions.filter(
181 | (version) =>
182 | semver.parse(version).major === parsedPackageVersion.major &&
183 | semver.parse(version).minor === parsedPackageVersion.minor,
184 | );
185 |
186 | let matchingTypeVersion = null;
187 |
188 | if (matchingMinors.length > 0) {
189 | matchingTypeVersion = matchingMinors
190 | .sort((versionA, versionB) => semver.compare(versionA, versionB))
191 | .pop();
192 | } else if (matchingMajors.length > 0) {
193 | matchingTypeVersion = matchingMajors
194 | .sort((versionA, versionB) => semver.compare(versionA, versionB))
195 | .pop();
196 | }
197 |
198 | logger.info("Trying definitely typed versions for " + packageJSON.name, {
199 | matchingTypeVersion,
200 | });
201 |
202 | if (!matchingTypeVersion) {
203 | logger.warn(
204 | "No matching minor version found for a definitely typed definition for",
205 | { name: packageJSON.name, version: packageJSON.version },
206 | );
207 |
208 | return null;
209 | }
210 |
211 | const installPath = await InstallationUtils.preparePath(
212 | packageJSON.name,
213 | packageJSON.version,
214 | );
215 | await InstallationUtils.installPackage(
216 | [`${typesPackageName}@${matchingTypeVersion}`],
217 | installPath,
218 | {
219 | client:
220 | (process.env.INSTALL_CLIENT as InstallPackageOptions["client"]) ||
221 | "npm",
222 | },
223 | );
224 |
225 | const typeResolveResult = await resolveTypePathInbuilt(
226 | installPath,
227 | typesPackageName,
228 | );
229 |
230 | if (!typeResolveResult) {
231 | logger.error(
232 | 'Could not resolve type path for "%s" at version %s',
233 | typesPackageName,
234 | matchingTypeVersion,
235 | );
236 | return null;
237 | }
238 |
239 | logger.info("Found matching minor version in definitely typed", {
240 | name: packageJSON.name,
241 | matchingTypeVersion,
242 | });
243 | return typeResolveResult;
244 | }
245 |
246 | function handleFailedResolve(err: any, packageName: string) {
247 | if (err.statusCode === 404) {
248 | logger.error("Package not found for %s, error %o", packageName, err);
249 | throw new PackageNotFoundError(err);
250 | } else if (err.code === "ETARGET" && err.type === "version") {
251 | logger.error("Package version not found %s, error %o", packageName, err);
252 | throw new PackageVersionMismatchError(err, err.versions);
253 | } else {
254 | logger.error("Failed to resolve version %s, error %o", packageName, err);
255 | throw new ResolutionError(err);
256 | }
257 | }
258 |
259 | const npmResolveCache = new LRUCache({
260 | max: 1000,
261 | // how long to live in ms
262 | ttl: 1000 * 60,
263 | });
264 |
265 | export async function resolvePackageJSON({
266 | packageName,
267 | packageVersion,
268 | }: {
269 | packageName: string;
270 | packageVersion: string;
271 | }) {
272 | const cacheKey = `${packageName}@${packageVersion}`;
273 | const cachedEntry = npmResolveCache.get(cacheKey);
274 |
275 | if (cachedEntry) {
276 | return cachedEntry;
277 | }
278 |
279 | try {
280 | const manifest = await pacote.manifest(`${packageName}@${packageVersion}`);
281 | npmResolveCache.set(cacheKey, manifest);
282 | return manifest;
283 | } catch (err) {
284 | handleFailedResolve(err, packageName);
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/server/package/types.ts:
--------------------------------------------------------------------------------
1 | export type InstallPackageOptions = {
2 | client?: "npm" | "yarn" | "pnpm" | "bun";
3 | limitConcurrency?: boolean;
4 | networkConcurrency?: number;
5 | installTimeout?: number;
6 | };
7 |
--------------------------------------------------------------------------------
/server/package/utils.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import sanitize from "sanitize-filename";
4 |
5 | export const docsVersion = "1.1";
6 | export const docsRootPath = path.join(
7 | __dirname,
8 | "..",
9 | "..",
10 | "docs",
11 | docsVersion,
12 | );
13 |
14 | export const docsCachePath = (basePath: string) =>
15 | path.join(basePath, "docs-cache", docsVersion);
16 |
17 | export const getDocsPath = ({ packageName, packageVersion }) => {
18 | return path.join(
19 | docsRootPath,
20 | sanitize(packageName.replace("/", "___")).replace("___", "/"),
21 | sanitize(packageVersion),
22 | );
23 | };
24 |
25 | export const getDocsCachePath = ({ packageName, packageVersion, basePath }) => {
26 | return path.join(
27 | docsCachePath(basePath),
28 | sanitize(packageName) + "@" + sanitize(packageVersion) + ".json",
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/server/queues.ts:
--------------------------------------------------------------------------------
1 | import { Queue, QueueEvents } from "bullmq";
2 | import { Worker, Job } from "bullmq";
3 | import InstallationUtils from "./package/installation.utils";
4 | import os from "os";
5 | import path from "path";
6 | import logger from "../common/logger";
7 | import { config } from "dotenv";
8 | import { InstallPackageOptions } from "./package/types";
9 | import { execSync } from "node:child_process";
10 |
11 | config({
12 | path: path.join(__dirname, "../.env"),
13 | });
14 |
15 | /**
16 | * When pm2 restarts the server, its possible for worker processes to be left behind.
17 | * Here we force kill all such processes
18 | */
19 | function killAllBullMQProcesses(processName: string) {
20 | if (process.env.NODE_ENV !== "production") return;
21 |
22 | const ageInSeconds = 30;
23 | const command = `ps aux | grep '${processName}' | grep -v grep | awk '{split($10,a,":"); if (a[1] * 60 + a[2] > ${ageInSeconds}) print $2}' | xargs -r kill -9`;
24 | try {
25 | execSync(command);
26 | console.log(`Killed processes with name containing '${processName}'`);
27 | } catch (error) {
28 | console.error(`Error killing processes: ${error}`);
29 | }
30 | }
31 |
32 | // killAllBullMQProcesses("bullmq");
33 |
34 | const redisOptions = {
35 | port: 6379,
36 | host: "localhost",
37 | password: "",
38 | };
39 |
40 | type InstallWorkerOptions = {
41 | packageName: string;
42 | packageVersion: string;
43 | installPath: string;
44 | additionalTypePackages: string;
45 | };
46 |
47 | export const installQueue = new Queue(
48 | "install-package-q",
49 | {
50 | connection: redisOptions,
51 | },
52 | );
53 |
54 | export const installQueueEvents = new QueueEvents(installQueue.name, {
55 | connection: redisOptions,
56 | });
57 |
58 | installQueue.on("error", (err) => {
59 | logger.error("Error install queue:", err);
60 | });
61 |
62 | const installWorker = new Worker(
63 | installQueue.name,
64 | async (job: Job) => {
65 | await InstallationUtils.installPackage(
66 | [
67 | `${job.data.packageName}@${job.data.packageVersion}`,
68 | job.data.additionalTypePackages,
69 | ].filter(Boolean),
70 | job.data.installPath,
71 | {
72 | client:
73 | (process.env.INSTALL_CLIENT as InstallPackageOptions["client"]) ||
74 | "npm",
75 | },
76 | );
77 | },
78 | {
79 | concurrency: os.cpus().length - 1,
80 | connection: redisOptions,
81 | },
82 | );
83 |
84 | type GenerateDocsWorkerOptions = {
85 | // Added by worker if there is an error
86 | originalError?: {
87 | code?: string;
88 | message?: string;
89 | stacktrace?: string;
90 | };
91 | packageJSON: object;
92 | force: boolean;
93 | };
94 |
95 | export const generateDocsQueue = new Queue(
96 | "generate-docs-package-q",
97 | {
98 | connection: redisOptions,
99 | },
100 | );
101 | export const generateDocsQueueEvents = new QueueEvents(generateDocsQueue.name, {
102 | connection: redisOptions,
103 | });
104 |
105 | generateDocsQueue.on("error", (err) => {
106 | logger.error("Error generating docs:", err);
107 | });
108 |
109 | const generateDocsWorker = new Worker(
110 | generateDocsQueue.name,
111 | path.join(__dirname, "./workers/docs-builder-worker.js"),
112 | {
113 | concurrency: os.cpus().length - 1,
114 | connection: redisOptions,
115 | useWorkerThreads: false,
116 | limiter: {
117 | max: 2,
118 | duration: 10000,
119 | },
120 | },
121 | );
122 |
123 | const cleanupCacheQueue = new Queue("cleanup-cache-q", {
124 | connection: redisOptions,
125 | });
126 |
127 | if (process.env.NODE_ENV === "production") {
128 | cleanupCacheQueue.add("cleanup", null, {
129 | repeat: {
130 | // Every hour
131 | pattern: "0 * * * *",
132 | },
133 | });
134 | }
135 |
136 | const cleanupCacheWorker = new Worker(
137 | cleanupCacheQueue.name,
138 | path.join(__dirname, "./workers/cleanup-cache.js"),
139 | {
140 | concurrency: 1,
141 | connection: redisOptions,
142 | useWorkerThreads: false,
143 | },
144 | );
145 |
146 | export const appQueues = [installQueue, generateDocsQueue];
147 | export const scheduledQueues = [cleanupCacheQueue];
148 | export const allQueues = [...scheduledQueues, ...appQueues];
149 | const workers = [installWorker, generateDocsWorker, cleanupCacheWorker];
150 |
151 | async function shutdownWorkers(): Promise {
152 | await Promise.all(workers.map((worker) => worker.close()));
153 | logger.warn("Shutdown all workers complete");
154 | process.exit(0);
155 | }
156 |
157 | async function handleSignal() {
158 | try {
159 | await shutdownWorkers();
160 | process.exit(0);
161 | } catch (err) {
162 | console.error("Error during shutdown", err);
163 | process.exit(1);
164 | }
165 | }
166 |
167 | process.on("SIGTERM", handleSignal);
168 | process.on("SIGINT", handleSignal);
169 | process.on("SIGUSR2", handleSignal);
170 | process.on("SIGUSR1", handleSignal);
171 | process.on("beforeExit", handleSignal);
172 |
173 | const FINISHED_JOB_EXPIRY = 30 * 1000;
174 | const FAILED_JOB_EXPIRY =
175 | process.env.NODE_ENV === "development" ? 1000 : 5 * 60 * 1000;
176 | const UNFINISHED_JOB_EXPIRY = 2 * 60 * 1000;
177 |
178 | setInterval(async () => {
179 | for (const queue of appQueues) {
180 | const finishedJobs = await queue.getJobs(["completed"]);
181 | const failedJobs = await queue.getJobs(["failed"]);
182 | const unfinishedJobs = await queue.getJobs([
183 | "active",
184 | "wait",
185 | "waiting",
186 | "delayed",
187 | "prioritized",
188 | ]);
189 |
190 | for (let job of finishedJobs) {
191 | const finishedExpiryAgo = Date.now() - FINISHED_JOB_EXPIRY;
192 | if (job.finishedOn < finishedExpiryAgo) {
193 | logger.warn(`Removing finished job ${job.id} because its too old`);
194 | await job.remove();
195 | }
196 | }
197 |
198 | for (let job of failedJobs) {
199 | const failedExpiryAgo = Date.now() - FAILED_JOB_EXPIRY;
200 | if (job.finishedOn < failedExpiryAgo) {
201 | logger.warn(`Removing failed job ${job.id} because its too old`);
202 | await job.remove();
203 | }
204 | }
205 |
206 | for (let job of unfinishedJobs.filter(Boolean)) {
207 | const unfinishedExpiryAgo = Date.now() - UNFINISHED_JOB_EXPIRY;
208 | if (job.timestamp < unfinishedExpiryAgo) {
209 | logger.warn(
210 | `Removing ${await job.getState()} job ${job.id} because its too old`,
211 | );
212 | try {
213 | await job.remove();
214 | } catch (err) {
215 | logger.error(
216 | `Failed to remove ${await job.getState()} job ${job.id}`,
217 | err,
218 | );
219 | }
220 | }
221 | }
222 | }
223 | }, 5000);
224 |
--------------------------------------------------------------------------------
/server/workers/cleanup-cache.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs").promises;
2 | const child_process = require("child_process");
3 | const checkDiskSpace = require("check-disk-space").default;
4 | const path = require("path");
5 | const glob = require("fast-glob");
6 | const fastFolderSizeSync = require("fast-folder-size/sync");
7 | const fsExtra = require("fs-extra");
8 | const prettyBytes = require("pretty-bytes");
9 |
10 | // Clean if disk space is less than this percentage
11 | const CLEAN_THRESHOLD = 30;
12 |
13 | const cacheLocations = [
14 | ...glob.sync(`${path.join("/tmp", "tmp-build")}/packages/*`, {
15 | onlyDirectories: true,
16 | }),
17 | ...glob.sync(`${path.join(__dirname, "..", "..", "docs-cache")}/*/*`, {
18 | onlyFiles: true,
19 | }),
20 | ...glob.sync(`${path.join(__dirname, "..", "..", "docs")}/*/*`, {
21 | onlyDirectories: true,
22 | }),
23 | ];
24 |
25 | async function getFileOrDirInfo(dirPath) {
26 | const stats = await fs.stat(dirPath);
27 | let totalSize = stats.size;
28 | let lastAccessSecondsAgo =
29 | (new Date().getTime() - new Date(stats.atime).getTime()) / 60;
30 |
31 | if (stats.isDirectory()) {
32 | totalSize = fastFolderSizeSync(dirPath);
33 | }
34 |
35 | return {
36 | lastAccessSecondsAgo,
37 | totalSize,
38 | };
39 | }
40 |
41 | function getCurrentDisk() {
42 | const cwd = process.cwd();
43 |
44 | const disk = child_process.execSync(
45 | `df -P "${cwd}" | tail -1 | awk '{print $1}'`,
46 | );
47 |
48 | return disk.toString().trim();
49 | }
50 |
51 | async function getFreeDiskPercent() {
52 | const cwd = process.cwd();
53 |
54 | const { free, size } = await checkDiskSpace(getCurrentDisk());
55 | return (free / size) * 100;
56 | }
57 |
58 | async function shouldFreeSpace() {
59 | const freeSpace = await getFreeDiskPercent();
60 | return freeSpace < CLEAN_THRESHOLD;
61 | }
62 |
63 | async function cleanupSpaceWork(job) {
64 | let result = "";
65 |
66 | if (!(await shouldFreeSpace())) {
67 | result += `Cleanup space: Free disk space is greater than ${CLEAN_THRESHOLD}%, skipping.`;
68 | return result;
69 | }
70 |
71 | result += `Cleanup space: Free disk space is less than ${CLEAN_THRESHOLD}%, cleaning up`;
72 | const locationsToClean = await Promise.all(
73 | cacheLocations.map(async (location) => {
74 | return {
75 | location,
76 | ...(await getFileOrDirInfo(location)),
77 | };
78 | }),
79 | );
80 |
81 | const oldestCaches = locationsToClean.sort(
82 | (a, b) => a.lastAccessSecondsAgo - b.lastAccessSecondsAgo,
83 | );
84 |
85 | const removedDirs = [];
86 | let removedSize = 0;
87 |
88 | for (let oldest of oldestCaches) {
89 | try {
90 | await fsExtra.remove(oldest.location);
91 |
92 | removedDirs.push(oldest.location);
93 | removedSize += oldest.totalSize;
94 | } catch (err) {
95 | console.error(`Cleanup Space: Error removing ${oldest.location}`, err);
96 | }
97 |
98 | if (!(await shouldFreeSpace())) {
99 | break;
100 | }
101 | }
102 |
103 | result +=
104 | "Cleanup Space: Freed " +
105 | prettyBytes(removedSize) +
106 | " from " +
107 | JSON.stringify(removedDirs);
108 |
109 | if (await shouldFreeSpace()) {
110 | result +=
111 | "Cleanup Space: Still need to free space, cleaning up node modules cache";
112 | await fsExtra.remove(path.join("/tmp", "tmp-build", "cache"));
113 | }
114 | return result;
115 | }
116 |
117 | module.exports = async (job) => {
118 | return await cleanupSpaceWork(job);
119 | };
120 |
--------------------------------------------------------------------------------
/server/workers/docs-builder-worker-pool.js:
--------------------------------------------------------------------------------
1 | require("esbuild-register/dist/node").register();
2 |
3 | const {
4 | generateDocsForPackage,
5 | } = require("../package/extractor/doc-generator");
6 | const workerpool = require("workerpool");
7 |
8 | async function generateDocs(job) {
9 | return await generateDocsForPackage(job.packageJSON, {
10 | force: job.force,
11 | });
12 | }
13 |
14 | workerpool.worker({
15 | generateDocs,
16 | });
17 |
--------------------------------------------------------------------------------
/server/workers/docs-builder-worker.js:
--------------------------------------------------------------------------------
1 | require("esbuild-register/dist/node").register();
2 | require("../init-sentry");
3 |
4 | const {
5 | generateDocsForPackage,
6 | } = require("../package/extractor/doc-generator");
7 | const logger = require("../../common/logger").default;
8 |
9 | let workerActiveTime = Date.now();
10 |
11 | setInterval(() => {
12 | if (Date.now() - workerActiveTime > 1000 * 60 * 5) {
13 | console.log(
14 | "Worker remained idle for too long without doing much...",
15 | process.pid,
16 | " alive for ",
17 | process.uptime(),
18 | "s. Without work for ",
19 | Date.now() - workerActiveTime,
20 | "s",
21 | );
22 |
23 | throw new Error("Worker exited because it was jobless for too long.");
24 | }
25 |
26 | const gbInBytes = 1.5 * 1024 * 1024 * 1024;
27 |
28 | if (process.memoryUsage.rss() > gbInBytes) {
29 | console.log(
30 | "Worker memory usage is too high, exiting...",
31 | process.pid,
32 | " alive for ",
33 | process.uptime(),
34 | "s. Memory usage ",
35 | process.memoryUsage.rss(),
36 | " bytes",
37 | );
38 |
39 | throw new Error("Worker exited because it was taking up too much memory.");
40 | }
41 | }, 5000);
42 |
43 | function promiseTimeout(promise, ms = 10000) {
44 | let timeout = new Promise((resolve, reject) => {
45 | let id = setTimeout(() => {
46 | clearTimeout(id);
47 | reject("Promise timed out in " + ms + "ms.");
48 | }, ms);
49 | });
50 |
51 | return Promise.race([promise, timeout]).then(
52 | (result) => result,
53 | (error) => Promise.reject(error),
54 | );
55 | }
56 |
57 | process.on("unhandledRejection", (error) => {
58 | throw error;
59 | });
60 |
61 | module.exports = async (job) => {
62 | try {
63 | logger.info(
64 | "Docs Worker: Starting to build in worker %s %s %o",
65 | job.data.packageJSON.name,
66 | job.data.packageJSON.version,
67 | { pid: process.pid },
68 | );
69 | workerActiveTime = Date.now();
70 | const results = await promiseTimeout(
71 | generateDocsForPackage(job.data.packageJSON, {
72 | force: job.data.force,
73 | }),
74 | 120 * 1000,
75 | );
76 | return results;
77 | } catch (err) {
78 | const isWorkerTimeout = err?.message?.includes("PROMISE_TIMEOUT");
79 |
80 | const errorCode = isWorkerTimeout ? "DOCS_BUILD_TIMEOUT" : err.message;
81 |
82 | logger.error("Docs Worker: Error building docs %o", {
83 | errorCode,
84 | stack: err.stack,
85 | message: err.message,
86 | });
87 |
88 | job.updateData({
89 | ...job.data,
90 | originalError: {
91 | code: errorCode,
92 | stacktrace: err?.originalError?.stack,
93 | message: err?.originalError?.message,
94 | },
95 | });
96 | throw err;
97 | }
98 | };
99 |
--------------------------------------------------------------------------------
/server/workers/install-worker-pool.js:
--------------------------------------------------------------------------------
1 | require("esbuild-register/dist/node").register();
2 | const workerpool = require("workerpool");
3 | const { default: InstallationUtils } = require("../package/installation.utils");
4 |
5 | async function installPackage({
6 | packageName,
7 | packageVersion,
8 | additionalTypePackages,
9 | installPath,
10 | }) {
11 | await InstallationUtils.installPackage(
12 | [`${packageName}@${packageVersion}`, additionalTypePackages].filter(
13 | Boolean,
14 | ),
15 | installPath,
16 | {
17 | client: process.env.INSTALL_CLIENT || "npm",
18 | },
19 | );
20 | }
21 |
22 | workerpool.worker({
23 | installPackage,
24 | });
25 |
--------------------------------------------------------------------------------
/stylesheets/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin popup {
2 | background: var(--primary-bg-color);
3 | box-shadow: var(
4 | --shadow-overlay,
5 | 0 4px 8px -2px rgba(9, 30, 66, 0.25),
6 | 0 0 1px rgba(9, 30, 66, 0.31)
7 | );
8 | border-radius: 4px;
9 | }
10 |
11 | @mixin selectedLabel {
12 | background: var(--selected-bg);
13 | color: var(--selected-text-color);
14 | text-shadow: 0 0 2px var(--primary-bg-color);
15 | }
16 |
17 | @mixin hideScrollbar {
18 | -ms-overflow-style: none;
19 | scrollbar-width: none;
20 | &::-webkit-scrollbar {
21 | display: none;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "incremental": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ]
22 | },
23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
24 | "ts-node": {
25 | "swc": true,
26 | "compilerOptions": {
27 | "module": "commonjs"
28 | }
29 | },
30 | "exclude": ["node_modules", "archived", "cloud-function"]
31 | }
32 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import { resolve } from "path";
3 | import react from "@vitejs/plugin-react";
4 | import externalize from "vite-plugin-externalize-dependencies";
5 |
6 | export default defineConfig({
7 | css: {
8 | preprocessorOptions: {
9 | scss: {
10 | api: "modern-compiler", // or "modern"
11 | },
12 | },
13 | },
14 | plugins: [
15 | react(),
16 | // externalize({ externals: ["react", "react-dom"] })
17 | ],
18 | publicDir: false,
19 | define: {
20 | "process.env.NODE_ENV": JSON.stringify("production"),
21 | },
22 | build: {
23 | outDir: resolve(__dirname, "./shared-dist"),
24 | lib: {
25 | entry: resolve(__dirname, "./client/components/HeaderIframe/index.tsx"),
26 | formats: ["umd"],
27 | name: "header",
28 | fileName: "header",
29 | },
30 | },
31 | });
32 |
--------------------------------------------------------------------------------