diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index be8d1d5eec..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,9 +0,0 @@ -lib -build -dist -.yarn -.husky -generated -bundle.c?js -.pnp* -**/generated diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 431cf9c7b5..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,153 +0,0 @@ -/** - * @type {import("eslint").Linter.Config} - */ -module.exports = { - root: true, - extends: [ - "prettier", - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/strict", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:vitest/recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "plugin:tailwindcss/recommended", - "plugin:@next/next/recommended", - ], - plugins: ["deprecation", "import", "eslint-plugin-tailwindcss"], - env: { - browser: true, - es2021: true, - }, - settings: { - react: { - version: "^18.2.0", - }, - settings: { - next: { - rootDir: "packages/docs-bundle/", - }, - }, - }, - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 12, - sourceType: "module", - project: ["./tsconfig.eslint.json", "./packages/**/tsconfig.json"], - allowAutomaticSingleRunInference: true, - tsconfigRootDir: __dirname, - }, - ignorePatterns: ["*.js", "*.jsx"], - rules: { - "@typescript-eslint/no-unused-vars": [ - "error", - { - varsIgnorePattern: "^_", - argsIgnorePattern: "^_", - ignoreRestSiblings: true, - }, - ], - "@typescript-eslint/no-namespace": [ - "error", - { - allowDeclarations: true, - }, - ], - "@typescript-eslint/explicit-module-boundary-types": ["off"], - "@typescript-eslint/no-floating-promises": ["error"], - "@typescript-eslint/no-empty-function": [ - "error", - { - allow: ["private-constructors", "protected-constructors", "decoratedFunctions"], - }, - ], - "@typescript-eslint/await-thenable": "error", - "@typescript-eslint/no-base-to-string": "error", - "@typescript-eslint/no-extraneous-class": "off", - "@typescript-eslint/no-invalid-void-type": "off", - "@typescript-eslint/prefer-optional-chain": "off", - "@typescript-eslint/strict-boolean-expressions": "off", - "linebreak-style": ["error", "unix"], - "no-console": "error", - "no-empty": [ - "error", - { - allowEmptyCatch: true, - }, - ], - "no-unused-vars": "off", - "tailwindcss/classnames-order": "off", - quotes: [ - "error", - "double", - { - avoidEscape: true, - }, - ], - semi: ["error", "always"], - indent: "off", - "object-shorthand": ["error"], - "deprecation/deprecation": "warn", - // "import/no-internal-modules": ["error"], - eqeqeq: [ - "error", - "always", - { - null: "never", - }, - ], - curly: "error", - "react/react-in-jsx-scope": "off", - "react/prop-types": "off", - "tailwindcss/no-custom-classname": "off", - "@next/next/no-html-link-for-pages": "off", - "@next/next/no-img-element": "off", - "react-hooks/exhaustive-deps": [ - "warn", - { - additionalHooks: "(useMemoOne|useCallbackOne)", - }, - ], - }, - overrides: [ - { - // enable the rule specifically for TypeScript files - files: ["*.ts", "*.mts", "*.cts", "*.tsx"], - rules: { - "@typescript-eslint/explicit-module-boundary-types": [ - "error", - { - allowHigherOrderFunctions: false, - }, - ], - }, - }, - { - files: ["packages/fdr-sdk/**/*", "servers/fdr-deploy/**/*", "servers/fdr/**/*"], - rules: { - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-floating-promises": "off", - "@typescript-eslint/no-base-to-string": "off", - }, - }, - { - files: ["servers/fdr/**/*", "servers/fern-bot/**/*"], - rules: { - eqeqeq: "off", - "no-console": "off", - }, - }, - { - files: ["packages/ui/**/*"], - rules: { - "@typescript-eslint/no-explicit-any": "off", - }, - }, - ], -}; diff --git a/.eslintrc.lint-staged.js b/.eslintrc.lint-staged.js deleted file mode 100644 index 5a13eac140..0000000000 --- a/.eslintrc.lint-staged.js +++ /dev/null @@ -1,46 +0,0 @@ -const DEFAULT_CONFIG = require("./.eslintrc.js"); - -const TYPESCRIPT_ESLINT = "@typescript-eslint"; -const TYPESCRIPT_ESLINT_PARSER_OPTIONS = new Set(["project", "allowAutomaticSingleRunInference", "tsconfigRootDir"]); - -module.exports = { - ...DEFAULT_CONFIG, - extends: [ - ...DEFAULT_CONFIG.extends.filter((extended) => !extended.startsWith(`plugin:${TYPESCRIPT_ESLINT}/`)), - // needed to disable recommended eslint rules that don't apply - "plugin:@typescript-eslint/eslint-recommended", - ], - parserOptions: Object.entries(DEFAULT_CONFIG.parserOptions).reduce( - (newParserOptions, [parserOptionKey, parserOption]) => { - if (!TYPESCRIPT_ESLINT_PARSER_OPTIONS.has(parserOptionKey)) { - newParserOptions[parserOptionKey] = parserOption; - } - return newParserOptions; - }, - {}, - ), - rules: Object.entries(DEFAULT_CONFIG.rules).reduce( - (newRules, [ruleId, rule]) => { - if (!doesRuleRequireTypeInformation(ruleId) || rule === "off") { - newRules[ruleId] = rule; - } - return newRules; - }, - { - "no-unused-vars": "off", - }, - ), -}; - -function doesRuleRequireTypeInformation(ruleId) { - if (ruleId.startsWith(`${TYPESCRIPT_ESLINT}/`)) { - return true; - } - switch (ruleId) { - case "deprecation/deprecation": - case "jest/unbound-method": - return true; - default: - return false; - } -} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9fe7e3bd35..49c6a3440a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,3 @@ * @dsinghvi -packages/template-resolver/** @armandobelardo -packages/ui/** @abvthecity -packages/ui/fern-dashboard/** @armandobelardo +packages/fern-docs/** @abvthecity +packages/parsers/** @RohinBhargava \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff53a1d76d..7b4b85172b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,14 +67,14 @@ jobs: uses: ./.github/actions/install - name: Build components - run: pnpm turbo --filter=@fern-ui/components compile + run: pnpm turbo --filter=@fern-docs/components compile - name: Run Chromatic uses: chromaui/action@latest # Chromatic CI config: https://www.chromatic.com/docs/github-actions/ with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - workingDir: packages/ui/components + workingDir: packages/fern-docs/components onlyChanged: true fern-generate: diff --git a/.github/workflows/deploy-docs-bundle-dev.yml b/.github/workflows/deploy-docs-bundle-dev.yml index 9e7d74d86f..bd9cbbf053 100644 --- a/.github/workflows/deploy-docs-bundle-dev.yml +++ b/.github/workflows/deploy-docs-bundle-dev.yml @@ -1,4 +1,4 @@ -name: Deploy @fern-ui/docs-bundle (dev) +name: Deploy @fern-docs/bundle (dev) on: push: @@ -23,7 +23,7 @@ jobs: with: token: ${{ secrets.VERCEL_TOKEN }} project: "app-dev.buildwithfern.com" - package: "@fern-ui/docs-bundle" + package: "@fern-docs/bundle" environment: "production" branch: main diff --git a/.github/workflows/deploy-docs-bundle-preview.yml b/.github/workflows/deploy-docs-bundle-preview.yml index 0506955728..f0977f045a 100644 --- a/.github/workflows/deploy-docs-bundle-preview.yml +++ b/.github/workflows/deploy-docs-bundle-preview.yml @@ -1,4 +1,4 @@ -name: Preview @fern-ui/docs-bundle +name: Preview @fern-docs/bundle on: pull_request: @@ -34,7 +34,7 @@ jobs: with: token: ${{ secrets.VERCEL_TOKEN }} project: "app.buildwithfern.com" - package: "@fern-ui/docs-bundle" + package: "@fern-docs/bundle" environment: "preview" branch: ${{ github.event.pull_request.head.ref || github.ref_name || github.ref }} @@ -100,7 +100,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: bundle - path: packages/ui/docs-bundle/.next/analyze/__bundle_analysis.json + path: packages/fern-docs/bundle/.next/analyze/__bundle_analysis.json - name: Download base branch bundle stats uses: dawidd6/action-download-artifact@v6 @@ -108,18 +108,18 @@ jobs: with: workflow: deploy-docs-bundle-preview.yml branch: ${{ github.event.pull_request.base.ref }} - path: packages/ui/docs-bundle/.next/analyze/base + path: packages/fern-docs/bundle/.next/analyze/base # https://infrequently.org/2021/03/the-performance-inequality-gap/ - name: Compare with base branch bundle if: success() && github.event.number - run: ls -laR packages/ui/docs-bundle/.next/analyze/base && pnpm --package=nextjs-bundle-analysis dlx compare + run: ls -laR packages/fern-docs/bundle/.next/analyze/base && pnpm --package=nextjs-bundle-analysis dlx compare - name: Comment PR Bundle Analysis if: github.event_name == 'pull_request' uses: thollander/actions-comment-pull-request@v2 with: - filePath: packages/ui/docs-bundle/.next/analyze/__bundle_analysis_comment.txt + filePath: packages/fern-docs/bundle/.next/analyze/__bundle_analysis_comment.txt comment_tag: bundle_analysis deploy-dev: diff --git a/.github/workflows/deploy-docs-bundle-prod.yml b/.github/workflows/deploy-docs-bundle-prod.yml index 2482cb6dc2..5cf23d45e7 100644 --- a/.github/workflows/deploy-docs-bundle-prod.yml +++ b/.github/workflows/deploy-docs-bundle-prod.yml @@ -1,4 +1,4 @@ -name: Deploy @fern-ui/docs-bundle +name: Deploy @fern-docs/bundle on: push: @@ -88,6 +88,8 @@ jobs: if: success() runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install - name: Revalidate all app.buildwithfern.com deployments run: pnpm vercel-scripts revalidate-all app.buildwithfern.com --token ${{ secrets.VERCEL_TOKEN }} @@ -98,6 +100,8 @@ jobs: env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install - name: Rollback on failure # remove this step once we switch back to pre-promotion testing run: | echo "E2E tests failed. Rolling back deployment" diff --git a/.github/workflows/deploy-fdr-dev.yml b/.github/workflows/deploy-fdr-dev.yml index df89129a6d..e9a65311fd 100644 --- a/.github/workflows/deploy-fdr-dev.yml +++ b/.github/workflows/deploy-fdr-dev.yml @@ -13,7 +13,7 @@ on: - ".github/workflows/publish-fdr-sdk.yml" - "packages/fdr-sdk/**" # Remove this once fdr is no longer needed in the generation - - "packages/ui/fern-docs-search-server/**" + - "packages/fern-docs/search-server/**" - "pnpm-lock.yaml" env: diff --git a/.github/workflows/deploy-fontawesome-cdn.yml b/.github/workflows/deploy-fontawesome-cdn.yml index 59c3c5a4f1..4c00681ab6 100644 --- a/.github/workflows/deploy-fontawesome-cdn.yml +++ b/.github/workflows/deploy-fontawesome-cdn.yml @@ -1,4 +1,4 @@ -name: Deploy @fern-ui/fontawesome-cdn +name: Deploy @fern-docs/icons-cdn on: push: @@ -23,7 +23,7 @@ jobs: with: token: ${{ secrets.VERCEL_TOKEN }} project: "icons.ferndocs.com" - package: "@fern-ui/fontawesome-cdn" + package: "@fern-docs/icons-cdn" environment: "production" branch: main diff --git a/.github/workflows/deploy-local-preview-bundle-dryrun.yml b/.github/workflows/deploy-local-preview-bundle-dryrun.yml index 4d80389481..e23f5421e5 100644 --- a/.github/workflows/deploy-local-preview-bundle-dryrun.yml +++ b/.github/workflows/deploy-local-preview-bundle-dryrun.yml @@ -1,4 +1,4 @@ -name: Deploy @fern-ui/local-preview-bundle (Dry Run) +name: Deploy @fern-docs/local-preview-bundle (Dry Run) on: pull_request: @@ -26,9 +26,9 @@ jobs: - name: Install uses: ./.github/actions/install - name: Build local preview bundle - run: pnpm turbo --filter=@fern-ui/local-preview-bundle build + run: pnpm turbo --filter=@fern-docs/local-preview-bundle build - name: Synthesize local preview bundle - run: pnpm --filter=@fern-ui/cdk run synth:dev2 + run: pnpm --filter=@fern-platform/cdk run synth:dev2 prod: runs-on: ubuntu-latest @@ -44,6 +44,6 @@ jobs: - name: Install uses: ./.github/actions/install - name: Build local preview bundle - run: pnpm turbo --filter=@fern-ui/local-preview-bundle build + run: pnpm turbo --filter=@fern-docs/local-preview-bundle build - name: Synthesize local preview bundle - run: pnpm --filter=@fern-ui/cdk run synth:prod + run: pnpm --filter=@fern-platform/cdk run synth:prod diff --git a/.github/workflows/deploy-local-preview-bundle.yml b/.github/workflows/deploy-local-preview-bundle.yml index adab40050e..25392edd4f 100644 --- a/.github/workflows/deploy-local-preview-bundle.yml +++ b/.github/workflows/deploy-local-preview-bundle.yml @@ -1,4 +1,4 @@ -name: Deploy @fern-ui/local-preview-bundle +name: Deploy @fern-docs/local-preview-bundle on: workflow_dispatch: {} @@ -31,9 +31,9 @@ jobs: - name: Build local preview bundle run: | pnpm compile - ENABLE_SOURCE_MAPS=true pnpm turbo --filter=@fern-ui/local-preview-bundle build + ENABLE_SOURCE_MAPS=true pnpm turbo --filter=@fern-docs/local-preview-bundle build - name: Deploy local preview bundle - run: pnpm --filter=@fern-ui/cdk run deploy:dev2 + run: pnpm --filter=@fern-platform/cdk run deploy:dev2 prod: runs-on: ubuntu-latest @@ -50,6 +50,6 @@ jobs: - name: Install uses: ./.github/actions/install - name: Build local preview bundle - run: pnpm turbo --filter=@fern-ui/local-preview-bundle build + run: pnpm turbo --filter=@fern-docs/local-preview-bundle build - name: Deploy local preview bundle - run: pnpm --filter=@fern-ui/cdk run deploy:prod + run: pnpm --filter=@fern-platform/cdk run deploy:prod diff --git a/.github/workflows/publish-template-resolver.yml b/.github/workflows/publish-template-resolver.yml index 1cd32522f5..29dd85b4b8 100644 --- a/.github/workflows/publish-template-resolver.yml +++ b/.github/workflows/publish-template-resolver.yml @@ -25,7 +25,7 @@ jobs: - name: 📥 Install uses: ./.github/actions/install - + - name: 🧪 Build and test run: pnpm turbo codegen build test --filter=${{ env.PACKAGE_NAME }} @@ -42,7 +42,7 @@ jobs: VERSION="${tag#$prefix}" cd packages/template-resolver - node scripts/preparePackageJson.cjs $VERSION + node preparePackageJson.cjs $VERSION echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc npm publish --access public diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..72e0ee2a60 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,64 @@ +.DS_Store + + +lib +build +dist +.yarn +.husky +generated +bundle.c?js +.pnp* +.next +../tests/**/* +.circleci +.github +.vscode +**/fern/** +out +pnpm-lock.yaml +**/generated/ +**/generated/** +**/.serverless/** +**/__test__/**/*.mdx +**/__test__/**/*.md +**/__test__/**/*.snap +**/__test__/**/*.json +**/__test__/fixtures/ +**/__test__/**/fixtures/ +**/__test__/outputs/ +**/tsconfig*tsbuildinfo +docker/build + +# environments +.env*.local +.vercel + +# Autogenerated css +*.frag + +# Autogenerated route tree file +**/routeTree.gen.ts + +.turbo + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +#ide +.idea/ +*.cookie + +# script outputs +domains.txt +preview.txt +last-deploy.txt +deployment-url.txt + +# next.js analysis +analyze + +test-results \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index e17fd7560a..57317be3cb 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,18 +1,5 @@ { - "printWidth": 120, - "tabWidth": 4, - "overrides": [ - { - "files": "*.{yml,yaml,json,md,mdx}", - "options": { - "tabWidth": 2 - } - }, - { - "files": "*.{json,cjs}", - "options": { - "trailingComma": "none" - } - } - ] + "trailingComma": "es5", + "plugins": ["prettier-plugin-packagejson", "prettier-plugin-tailwindcss"], + "tailwindFunctions": ["clsx", "cn", "cva"] } diff --git a/.vscode/launch.json b/.vscode/launch.json index ab97a0301f..a699984206 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,7 +31,7 @@ "type": "node-terminal", "request": "launch", "command": "pnpm docs:dev", - "cwd": "${workspaceFolder}/packages/ui/docs-bundle", + "cwd": "${workspaceFolder}/packages/fern-docs/bundle", "serverReadyAction": { "pattern": "- Local:.+(https?://.+)", "uriFormat": "%s", diff --git a/.vscode/settings.json b/.vscode/settings.json index 995a830e67..3f6d6f7871 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,11 @@ }, "typescript.enablePromptUseWorkspaceTsdk": true, "editor.formatOnSave": true, - "editor.codeActionsOnSave": ["source.fixAll", "source.organizeImports", "source.sortMembers"], + "editor.codeActionsOnSave": [ + "source.fixAll", + "source.organizeImports", + "source.sortMembers", + ], "editor.defaultFormatter": "esbenp.prettier-vscode", "[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d909ce2a2c..dc7c185353 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ To build and run the NextJS docs UI, run: The frontend is served at `localhost:3000`. You can configure which docs are loaded by using `.env.local`: ```bash -# packages/ui/docs-bundle/.env.local +# packages/fern-docs/bundle/.env.local # uncomment the next line when targeting the production cloud environment # NEXT_PUBLIC_DOCS_DOMAIN=proficientai.docs.buildwithfern.com @@ -103,8 +103,8 @@ Then link vercel to the project: - When prompted to link to the project, say `yes` Then, run `vercel pull`, which will create `/fern-platform/.vercel/.env.development.local` -Then, copy that file (creating if necessary) to `/fern-platform/packages/ui/docs-bundle/.env.local` -Finally, to run the dev server, `cd /packages/ui/docs-bundle` and run `pnpm docs:dev`, which should begin running on `localhost:3000` +Then, copy that file (creating if necessary) to `/fern-platform/packages/fern-docs/bundle/.env.local` +Finally, to run the dev server, `cd /packages/fern-docs/bundle` and run `pnpm docs:dev`, which should begin running on `localhost:3000` Optionally, to reroute to a different docs domain, add a `NEXT_PUBLIC_DOCS_DOMAIN` to `.env.local` diff --git a/clis/generator-cli/.depcheckrc.json b/clis/generator-cli/.depcheckrc.json index 55fa88d975..d400c9eec8 100644 --- a/clis/generator-cli/.depcheckrc.json +++ b/clis/generator-cli/.depcheckrc.json @@ -1,4 +1,9 @@ { - "ignores": ["@types/jest", "@types/node", "esbuild", "@yarnpkg/esbuild-plugin-pnp"], + "ignores": [ + "@types/jest", + "@types/node", + "esbuild", + "@yarnpkg/esbuild-plugin-pnp" + ], "ignore-patterns": ["lib", "dist"] } diff --git a/clis/generator-cli/.env-cmdrc.cjs b/clis/generator-cli/.env-cmdrc.cjs deleted file mode 100644 index f053ebf797..0000000000 --- a/clis/generator-cli/.env-cmdrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/clis/generator-cli/.prettierrc.cjs b/clis/generator-cli/.prettierrc.cjs deleted file mode 100644 index 2b5cf5b0c0..0000000000 --- a/clis/generator-cli/.prettierrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("../../.prettierrc.json"); diff --git a/clis/generator-cli/package.json b/clis/generator-cli/package.json index d853f007d7..61868e4dd4 100644 --- a/clis/generator-cli/package.json +++ b/clis/generator-cli/package.json @@ -1,28 +1,31 @@ { "name": "@fern-api/generator-cli", "version": "0.0.0", - "files": [ - "dist" - ], "type": "module", - "source": "src/index.ts", "main": "dist/cli.cjs", + "source": "src/index.ts", "bin": { "generator-cli": "dist/cli.cjs" }, + "files": [ + "dist" + ], "scripts": { "clean": "rm -rf ./dist && tsc --build --clean", "compile": "tsup ./src/cli.ts --format cjs && echo '#!/usr/bin/env node' | cat - dist/cli.cjs > dist/tmp && mv dist/tmp dist/cli.cjs", - "test": "vitest --run --passWithNoTests --globals --disable-console-intercept", - "test:update": "vitest -u --run --passWithNoTests --globals --disable-console-intercept", - "lint:eslint": "eslint --max-warnings 0 . --ignore-path=../../.eslintignore", + "depcheck": "depcheck", + "format": "prettier --write --ignore-unknown \"**\"", + "format:check": "prettier --check --ignore-unknown \"**\"", + "lint:eslint": "eslint --max-warnings 0 .", "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:style": "stylelint 'src/**/*.scss' --allow-empty-input --max-warnings 0", "lint:style:fix": "pnpm lint:style --fix", - "format": "prettier --write --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"", - "format:check": "prettier --check --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"", "organize-imports": "organize-imports-cli tsconfig.json", - "depcheck": "depcheck" + "test": "vitest --run --passWithNoTests --globals --disable-console-intercept", + "test:update": "vitest -u --run --passWithNoTests --globals --disable-console-intercept" + }, + "dependencies": { + "es-toolkit": "^1.30.0" }, "devDependencies": { "@fern-api/fs-utils": "0.15.0-rc63", @@ -33,17 +36,14 @@ "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.15", "depcheck": "^1.4.3", "esbuild": "0.20.2", - "eslint": "^8.56.0", + "eslint": "^9", "execa": "^9.5.1", "organize-imports-cli": "^0.10.0", - "prettier": "^3.3.2", + "prettier": "^3.4.2", "tmp-promise": "^3.0.3", "tsup": "^8.3.5", "typescript": "4.9.5", "vitest": "^2.1.4", "yargs": "^17.4.1" - }, - "dependencies": { - "es-toolkit": "^1.30.0" } } diff --git a/clis/generator-cli/src/__test__/advanced.test.ts b/clis/generator-cli/src/__test__/advanced.test.ts index 7c3255b084..99b04659cd 100644 --- a/clis/generator-cli/src/__test__/advanced.test.ts +++ b/clis/generator-cli/src/__test__/advanced.test.ts @@ -2,8 +2,8 @@ import { default as CONFIG } from "./fixtures/advanced/readme"; import { testGenerateReadme } from "./testGenerateReadme"; describe("advanced", () => { - testGenerateReadme({ - fixtureName: "advanced", - config: CONFIG, - }); + testGenerateReadme({ + fixtureName: "advanced", + config: CONFIG, + }); }); diff --git a/clis/generator-cli/src/__test__/basic-go.test.ts b/clis/generator-cli/src/__test__/basic-go.test.ts index b909295ecb..9b00f339e1 100644 --- a/clis/generator-cli/src/__test__/basic-go.test.ts +++ b/clis/generator-cli/src/__test__/basic-go.test.ts @@ -2,8 +2,8 @@ import { default as CONFIG } from "./fixtures/basic-go/readme"; import { testGenerateReadme } from "./testGenerateReadme"; describe("basic-go", () => { - testGenerateReadme({ - fixtureName: "basic-go", - config: CONFIG, - }); + testGenerateReadme({ + fixtureName: "basic-go", + config: CONFIG, + }); }); diff --git a/clis/generator-cli/src/__test__/basic-reference.test.ts b/clis/generator-cli/src/__test__/basic-reference.test.ts index 11bb428b27..e488628484 100644 --- a/clis/generator-cli/src/__test__/basic-reference.test.ts +++ b/clis/generator-cli/src/__test__/basic-reference.test.ts @@ -2,8 +2,8 @@ import { default as CONFIG } from "./fixtures/basic-reference/reference"; import { testGenerateReference } from "./testGenerateReference"; describe("basic-reference", () => { - testGenerateReference({ - fixtureName: "basic-reference", - config: CONFIG, - }); + testGenerateReference({ + fixtureName: "basic-reference", + config: CONFIG, + }); }); diff --git a/clis/generator-cli/src/__test__/cohere-go-merged.test.ts b/clis/generator-cli/src/__test__/cohere-go-merged.test.ts index e5c9199f08..ec2b591a83 100644 --- a/clis/generator-cli/src/__test__/cohere-go-merged.test.ts +++ b/clis/generator-cli/src/__test__/cohere-go-merged.test.ts @@ -2,9 +2,9 @@ import { default as CONFIG } from "./fixtures/cohere-go-merged/readme"; import { testGenerateReadme } from "./testGenerateReadme"; describe("cohere-go-merged", () => { - testGenerateReadme({ - fixtureName: "cohere-go-merged", - config: CONFIG, - originalReadme: "README.md", - }); + testGenerateReadme({ + fixtureName: "cohere-go-merged", + config: CONFIG, + originalReadme: "README.md", + }); }); diff --git a/clis/generator-cli/src/__test__/cohere-go.test.ts b/clis/generator-cli/src/__test__/cohere-go.test.ts index 8b4a49aa84..2ff87d7e17 100644 --- a/clis/generator-cli/src/__test__/cohere-go.test.ts +++ b/clis/generator-cli/src/__test__/cohere-go.test.ts @@ -2,8 +2,8 @@ import { default as CONFIG } from "./fixtures/cohere-go/readme"; import { testGenerateReadme } from "./testGenerateReadme"; describe("cohere-go", () => { - testGenerateReadme({ - fixtureName: "cohere-go", - config: CONFIG, - }); + testGenerateReadme({ + fixtureName: "cohere-go", + config: CONFIG, + }); }); diff --git a/clis/generator-cli/src/__test__/empty-go.test.ts b/clis/generator-cli/src/__test__/empty-go.test.ts index 4394899f8a..18124ddafe 100644 --- a/clis/generator-cli/src/__test__/empty-go.test.ts +++ b/clis/generator-cli/src/__test__/empty-go.test.ts @@ -2,8 +2,8 @@ import { default as CONFIG } from "./fixtures/empty-go/readme"; import { testGenerateReadme } from "./testGenerateReadme"; describe("empty-go", () => { - testGenerateReadme({ - fixtureName: "empty-go", - config: CONFIG, - }); + testGenerateReadme({ + fixtureName: "empty-go", + config: CONFIG, + }); }); diff --git a/clis/generator-cli/src/__test__/reference-md.test.ts b/clis/generator-cli/src/__test__/reference-md.test.ts index fbcb1806fe..905d82efcd 100644 --- a/clis/generator-cli/src/__test__/reference-md.test.ts +++ b/clis/generator-cli/src/__test__/reference-md.test.ts @@ -2,8 +2,8 @@ import { default as CONFIG } from "./fixtures/reference-md/readme"; import { testGenerateReadme } from "./testGenerateReadme"; describe("reference-md", () => { - testGenerateReadme({ - fixtureName: "reference-md", - config: CONFIG, - }); + testGenerateReadme({ + fixtureName: "reference-md", + config: CONFIG, + }); }); diff --git a/clis/generator-cli/src/__test__/testGenerateReadme.ts b/clis/generator-cli/src/__test__/testGenerateReadme.ts index fd941c4353..4748befb5d 100644 --- a/clis/generator-cli/src/__test__/testGenerateReadme.ts +++ b/clis/generator-cli/src/__test__/testGenerateReadme.ts @@ -8,33 +8,54 @@ import * as serializers from "../configuration/generated/serialization"; const FIXTURES_PATH = path.join(__dirname, "fixtures"); export function testGenerateReadme({ - fixtureName, - config, - originalReadme, + fixtureName, + config, + originalReadme, }: { - fixtureName: string; - config: FernGeneratorCli.ReadmeConfig; - originalReadme?: string; + fixtureName: string; + config: FernGeneratorCli.ReadmeConfig; + originalReadme?: string; }): void { - // eslint-disable-next-line vitest/valid-title - describe(fixtureName, () => { - it("generate readme", async () => { - const file = await tmp.file(); - const json = JSON.stringify(await serializers.ReadmeConfig.jsonOrThrow(config), undefined, 2); - await writeFile(file.path, json); + describe(fixtureName, () => { + it("generate readme", async () => { + const file = await tmp.file(); + const json = JSON.stringify( + await serializers.ReadmeConfig.jsonOrThrow(config), + undefined, + 2 + ); + await writeFile(file.path, json); - const args = [path.join(__dirname, "../../dist/cli.cjs"), "generate", "readme", "--config", file.path]; - if (originalReadme != null) { - args.push( - ...["--original-readme", getAbsolutePathToFixtureFile({ fixtureName, filepath: originalReadme })], - ); - } - const { stdout } = await execa("node", args); - expect(stdout).toMatchSnapshot(); - }); + const args = [ + path.join(__dirname, "../../dist/cli.cjs"), + "generate", + "readme", + "--config", + file.path, + ]; + if (originalReadme != null) { + args.push( + ...[ + "--original-readme", + getAbsolutePathToFixtureFile({ + fixtureName, + filepath: originalReadme, + }), + ] + ); + } + const { stdout } = await execa("node", args); + expect(stdout).toMatchSnapshot(); }); + }); } -function getAbsolutePathToFixtureFile({ fixtureName, filepath }: { fixtureName: string; filepath: string }): string { - return path.join(FIXTURES_PATH, fixtureName, filepath); +function getAbsolutePathToFixtureFile({ + fixtureName, + filepath, +}: { + fixtureName: string; + filepath: string; +}): string { + return path.join(FIXTURES_PATH, fixtureName, filepath); } diff --git a/clis/generator-cli/src/__test__/testGenerateReference.ts b/clis/generator-cli/src/__test__/testGenerateReference.ts index 67c92e48ed..b479811794 100644 --- a/clis/generator-cli/src/__test__/testGenerateReference.ts +++ b/clis/generator-cli/src/__test__/testGenerateReference.ts @@ -6,22 +6,30 @@ import { FernGeneratorCli } from "../configuration/generated"; import * as serializers from "../configuration/generated/serialization"; export function testGenerateReference({ - fixtureName, - config, + fixtureName, + config, }: { - fixtureName: string; - config: FernGeneratorCli.ReferenceConfig; + fixtureName: string; + config: FernGeneratorCli.ReferenceConfig; }): void { - // eslint-disable-next-line vitest/valid-title - describe(fixtureName, () => { - it("generate readme", async () => { - const file = await tmp.file(); - const json = JSON.stringify(await serializers.ReferenceConfig.jsonOrThrow(config), undefined, 2); - await writeFile(file.path, json); + describe(fixtureName, () => { + it("generate readme", async () => { + const file = await tmp.file(); + const json = JSON.stringify( + await serializers.ReferenceConfig.jsonOrThrow(config), + undefined, + 2 + ); + await writeFile(file.path, json); - const args = [path.join(__dirname, "../../dist/cli.cjs"), "generate-reference", "--config", file.path]; - const { stdout } = await execa("node", args); - expect(stdout).toMatchSnapshot(); - }); + const args = [ + path.join(__dirname, "../../dist/cli.cjs"), + "generate-reference", + "--config", + file.path, + ]; + const { stdout } = await execa("node", args); + expect(stdout).toMatchSnapshot(); }); + }); } diff --git a/clis/generator-cli/src/cli.ts b/clis/generator-cli/src/cli.ts index 33b6e966dc..969b5711fd 100644 --- a/clis/generator-cli/src/cli.ts +++ b/clis/generator-cli/src/cli.ts @@ -1,4 +1,9 @@ -import { AbsoluteFilePath, cwd, doesPathExist, resolve } from "@fern-api/fs-utils"; +import { + AbsoluteFilePath, + cwd, + doesPathExist, + resolve, +} from "@fern-api/fs-utils"; import fs from "fs"; import { mkdir, readFile } from "fs/promises"; import path from "path"; @@ -11,89 +16,100 @@ import { ReadmeParser } from "./readme/ReadmeParser"; import { ReferenceGenerator } from "./reference/ReferenceGenerator"; void yargs(hideBin(process.argv)) - .scriptName(process.env.CLI_NAME ?? "generator-cli") - .strict() - .command( - "generate readme", - "Generate a README.md using the provided configuration file.", - (argv) => - argv - .option("config", { - string: true, - requred: true, - }) - .option("original-readme", { - string: true, - requred: false, - }) - .option("output", { - string: true, - requred: false, - }), - async (argv) => { - if (argv.config == null) { - process.stderr.write("missing required arguments; please specify the --config flag\n"); - process.exit(1); - } - const wd = cwd(); - const readmeConfig = await loadReadmeConfig({ - absolutePathToConfig: resolve(wd, argv.config), - }); - const generator = new ReadmeGenerator({ - readmeParser: new ReadmeParser(), - readmeConfig, - originalReadme: argv.originalReadme != null ? await readFile(argv.originalReadme, "utf8") : undefined, - }); - await generator.generateReadme({ - output: await createWriteStream(argv.output), - }); - process.exit(0); - }, - ) - .command( - "generate-reference", - "Generate an SDK reference (`reference.md`) using the provided configuration file.", - (argv) => - argv - .option("config", { - string: true, - requred: true, - }) - .option("output", { - string: true, - requred: false, - }), - async (argv) => { - if (argv.config == null) { - process.stderr.write("missing required arguments; please specify the --config flag\n"); - process.exit(1); - } - const wd = cwd(); - const referenceConfig = await loadReferenceConfig({ - absolutePathToConfig: resolve(wd, argv.config), - }); - const generator = new ReferenceGenerator({ - referenceConfig, - }); - await generator.generate({ - output: await createWriteStream(argv.output), - }); - process.exit(0); - }, - ) - .demandCommand() - .showHelpOnFail(true) - .parse(); + .scriptName(process.env.CLI_NAME ?? "generator-cli") + .strict() + .command( + "generate readme", + "Generate a README.md using the provided configuration file.", + (argv) => + argv + .option("config", { + string: true, + requred: true, + }) + .option("original-readme", { + string: true, + requred: false, + }) + .option("output", { + string: true, + requred: false, + }), + async (argv) => { + if (argv.config == null) { + process.stderr.write( + "missing required arguments; please specify the --config flag\n" + ); + process.exit(1); + } + const wd = cwd(); + const readmeConfig = await loadReadmeConfig({ + absolutePathToConfig: resolve(wd, argv.config), + }); + const generator = new ReadmeGenerator({ + readmeParser: new ReadmeParser(), + readmeConfig, + originalReadme: + argv.originalReadme != null + ? await readFile(argv.originalReadme, "utf8") + : undefined, + }); + await generator.generateReadme({ + output: await createWriteStream(argv.output), + }); + process.exit(0); + } + ) + .command( + "generate-reference", + "Generate an SDK reference (`reference.md`) using the provided configuration file.", + (argv) => + argv + .option("config", { + string: true, + requred: true, + }) + .option("output", { + string: true, + requred: false, + }), + async (argv) => { + if (argv.config == null) { + process.stderr.write( + "missing required arguments; please specify the --config flag\n" + ); + process.exit(1); + } + const wd = cwd(); + const referenceConfig = await loadReferenceConfig({ + absolutePathToConfig: resolve(wd, argv.config), + }); + const generator = new ReferenceGenerator({ + referenceConfig, + }); + await generator.generate({ + output: await createWriteStream(argv.output), + }); + process.exit(0); + } + ) + .demandCommand() + .showHelpOnFail(true) + .parse(); -async function createWriteStream(outputPath: string | undefined): Promise { - return outputPath != null - ? await createWriteStreamFromFile(resolve(cwd(), outputPath)) - : (process.stdout as unknown as fs.WriteStream); +async function createWriteStream( + outputPath: string | undefined +): Promise { + return outputPath != null + ? await createWriteStreamFromFile(resolve(cwd(), outputPath)) + : (process.stdout as unknown as fs.WriteStream); } -async function createWriteStreamFromFile(filepath: AbsoluteFilePath): Promise { - if (!doesPathExist(filepath)) { - await mkdir(path.dirname(filepath), { recursive: true }); - } - return fs.createWriteStream(filepath); +async function createWriteStreamFromFile( + filepath: AbsoluteFilePath +): Promise { + if (!doesPathExist(filepath)) { + await mkdir(path.dirname(filepath), { recursive: true }); + } + return fs.createWriteStream(filepath); } diff --git a/clis/generator-cli/src/configuration/loadReadmeConfig.ts b/clis/generator-cli/src/configuration/loadReadmeConfig.ts index 0f546aaa4b..60f473f8a4 100644 --- a/clis/generator-cli/src/configuration/loadReadmeConfig.ts +++ b/clis/generator-cli/src/configuration/loadReadmeConfig.ts @@ -3,10 +3,10 @@ import { readFile } from "fs/promises"; import { FernGeneratorCli } from "./generated"; export async function loadReadmeConfig({ - absolutePathToConfig, + absolutePathToConfig, }: { - absolutePathToConfig: AbsoluteFilePath; + absolutePathToConfig: AbsoluteFilePath; }): Promise { - const rawContents = await readFile(absolutePathToConfig, "utf8"); - return JSON.parse(rawContents); + const rawContents = await readFile(absolutePathToConfig, "utf8"); + return JSON.parse(rawContents); } diff --git a/clis/generator-cli/src/configuration/loadReferenceConfig.ts b/clis/generator-cli/src/configuration/loadReferenceConfig.ts index f0cccf9f2d..30d771e436 100644 --- a/clis/generator-cli/src/configuration/loadReferenceConfig.ts +++ b/clis/generator-cli/src/configuration/loadReferenceConfig.ts @@ -3,10 +3,10 @@ import { readFile } from "fs/promises"; import { FernGeneratorCli } from "./generated"; export async function loadReferenceConfig({ - absolutePathToConfig, + absolutePathToConfig, }: { - absolutePathToConfig: AbsoluteFilePath; + absolutePathToConfig: AbsoluteFilePath; }): Promise { - const rawContents = await readFile(absolutePathToConfig, "utf8"); - return JSON.parse(rawContents); + const rawContents = await readFile(absolutePathToConfig, "utf8"); + return JSON.parse(rawContents); } diff --git a/clis/generator-cli/src/readme/Block.ts b/clis/generator-cli/src/readme/Block.ts index 45b9263274..535d225c9e 100644 --- a/clis/generator-cli/src/readme/Block.ts +++ b/clis/generator-cli/src/readme/Block.ts @@ -1,18 +1,18 @@ import { Writer } from "../utils/Writer"; export class Block { - public id: string; - public content: string; + public id: string; + public content: string; - constructor({ id, content }: { id: string; content: string }) { - this.id = id; - this.content = content; - } + constructor({ id, content }: { id: string; content: string }) { + this.id = id; + this.content = content; + } - public write(writer: Writer): void { - writer.write(this.content); - if (!this.content.endsWith("\n")) { - writer.writeLine(); - } + public write(writer: Writer): void { + writer.write(this.content); + if (!this.content.endsWith("\n")) { + writer.writeLine(); } + } } diff --git a/clis/generator-cli/src/readme/BlockMerger.ts b/clis/generator-cli/src/readme/BlockMerger.ts index bc3b9e034d..526f880dc8 100644 --- a/clis/generator-cli/src/readme/BlockMerger.ts +++ b/clis/generator-cli/src/readme/BlockMerger.ts @@ -1,114 +1,117 @@ import { Block } from "./Block"; export class BlockMerger { - private original: Block[]; - private originalByID: Record = {}; - private updated: Block[]; - private updatedByID: Record = {}; - - constructor({ original, updated }: { original: Block[]; updated: Block[] }) { - this.original = original; - this.original.map((block) => { - this.originalByID[block.id] = block; - }); - - this.updated = updated; - this.updated.map((block) => { - this.updatedByID[block.id] = block; - }); - } - - public merge(): Block[] { - let originalIndex = 0; - let updatedIndex = 0; - - const merged: BlockList = new BlockList(); - while (originalIndex < this.original.length && updatedIndex < this.updated.length) { - const originalBlock = this.getOriginalBlock(originalIndex); - const updatedBlock = this.getUpdatedBlock(updatedIndex); - if (originalBlock.id === updatedBlock.id) { - merged.addBlock(updatedBlock); - originalIndex++; - updatedIndex++; - continue; - } - - if (originalIndex <= updatedIndex) { - while (originalIndex < this.original.length) { - const nextOriginalBlock = this.getOriginalBlock(originalIndex); - originalIndex++; - if (!this.blockExistsInUpdated(nextOriginalBlock)) { - merged.addBlock(nextOriginalBlock); - } else { - break; - } - } - continue; - } - - merged.addBlock(updatedBlock); - updatedIndex++; - } - + private original: Block[]; + private originalByID: Record = {}; + private updated: Block[]; + private updatedByID: Record = {}; + + constructor({ original, updated }: { original: Block[]; updated: Block[] }) { + this.original = original; + this.original.map((block) => { + this.originalByID[block.id] = block; + }); + + this.updated = updated; + this.updated.map((block) => { + this.updatedByID[block.id] = block; + }); + } + + public merge(): Block[] { + let originalIndex = 0; + let updatedIndex = 0; + + const merged: BlockList = new BlockList(); + while ( + originalIndex < this.original.length && + updatedIndex < this.updated.length + ) { + const originalBlock = this.getOriginalBlock(originalIndex); + const updatedBlock = this.getUpdatedBlock(updatedIndex); + if (originalBlock.id === updatedBlock.id) { + merged.addBlock(updatedBlock); + originalIndex++; + updatedIndex++; + continue; + } + + if (originalIndex <= updatedIndex) { while (originalIndex < this.original.length) { - const block = this.getOriginalBlock(originalIndex); - if (!this.blockExistsInUpdated(block)) { - merged.addBlock(block); - } - originalIndex++; + const nextOriginalBlock = this.getOriginalBlock(originalIndex); + originalIndex++; + if (!this.blockExistsInUpdated(nextOriginalBlock)) { + merged.addBlock(nextOriginalBlock); + } else { + break; + } } + continue; + } - while (updatedIndex < this.updated.length) { - merged.addBlock(this.getUpdatedBlock(updatedIndex)); - updatedIndex++; - } - - return merged.getBlocks(); + merged.addBlock(updatedBlock); + updatedIndex++; } - private getOriginalBlock(index: number): Block { - return this.getBlockOrThrow(this.original, index); + while (originalIndex < this.original.length) { + const block = this.getOriginalBlock(originalIndex); + if (!this.blockExistsInUpdated(block)) { + merged.addBlock(block); + } + originalIndex++; } - private getUpdatedBlock(index: number): Block { - return this.getBlockOrThrow(this.updated, index); + while (updatedIndex < this.updated.length) { + merged.addBlock(this.getUpdatedBlock(updatedIndex)); + updatedIndex++; } - private blockExistsInUpdated(block: Block): boolean { - return this.updatedByID[block.id] !== undefined; - } + return merged.getBlocks(); + } - private getBlockOrThrow(blocks: Block[], index: number): Block { - const block = blocks[index]; - if (block == null) { - throw new Error(`index out of bounds: ${index}`); - } - return block; + private getOriginalBlock(index: number): Block { + return this.getBlockOrThrow(this.original, index); + } + + private getUpdatedBlock(index: number): Block { + return this.getBlockOrThrow(this.updated, index); + } + + private blockExistsInUpdated(block: Block): boolean { + return this.updatedByID[block.id] !== undefined; + } + + private getBlockOrThrow(blocks: Block[], index: number): Block { + const block = blocks[index]; + if (block == null) { + throw new Error(`index out of bounds: ${index}`); } + return block; + } } class BlockList { - private ids: Set; - private blocks: Block[]; + private ids: Set; + private blocks: Block[]; - constructor() { - this.ids = new Set(); - this.blocks = []; - } + constructor() { + this.ids = new Set(); + this.blocks = []; + } - public addBlock(block: Block): void { - if (this.hasBlock(block.id)) { - throw new Error(`block with id "${block.id}" already exists`); - } - this.ids.add(block.id); - this.blocks.push(block); + public addBlock(block: Block): void { + if (this.hasBlock(block.id)) { + throw new Error(`block with id "${block.id}" already exists`); } + this.ids.add(block.id); + this.blocks.push(block); + } - public hasBlock(id: string): boolean { - return this.ids.has(id); - } + public hasBlock(id: string): boolean { + return this.ids.has(id); + } - public getBlocks(): Block[] { - return this.blocks; - } + public getBlocks(): Block[] { + return this.blocks; + } } diff --git a/clis/generator-cli/src/readme/ReadmeGenerator.ts b/clis/generator-cli/src/readme/ReadmeGenerator.ts index 4f581f20e1..a17d3b8c3a 100644 --- a/clis/generator-cli/src/readme/ReadmeGenerator.ts +++ b/clis/generator-cli/src/readme/ReadmeGenerator.ts @@ -9,539 +9,649 @@ import { BlockMerger } from "./BlockMerger"; import { ReadmeParser } from "./ReadmeParser"; export class ReadmeGenerator { - private ADVANCED_FEATURE_ID = "ADVANCED"; - private ADVANCED_FEATURES: Set = new Set([ - FernGeneratorCli.StructuredFeatureId.Retries, - FernGeneratorCli.StructuredFeatureId.Timeouts, - FernGeneratorCli.StructuredFeatureId.CustomClient, - ]); - - private readmeParser: ReadmeParser; - private readmeConfig: FernGeneratorCli.ReadmeConfig; - private originalReadme: string | undefined; - private languageTitle: string; - private organizationPascalCase: string; - - constructor({ - readmeParser, - readmeConfig, - originalReadme, - }: { - readmeParser: ReadmeParser; - readmeConfig: FernGeneratorCli.ReadmeConfig; - originalReadme: string | undefined; - }) { - this.readmeParser = readmeParser; - this.readmeConfig = readmeConfig; - this.originalReadme = originalReadme; - this.languageTitle = languageToTitle(this.readmeConfig.language); - this.organizationPascalCase = pascalCase(this.readmeConfig.organization); + private ADVANCED_FEATURE_ID = "ADVANCED"; + private ADVANCED_FEATURES = new Set([ + FernGeneratorCli.StructuredFeatureId.Retries, + FernGeneratorCli.StructuredFeatureId.Timeouts, + FernGeneratorCli.StructuredFeatureId.CustomClient, + ]); + + private readmeParser: ReadmeParser; + private readmeConfig: FernGeneratorCli.ReadmeConfig; + private originalReadme: string | undefined; + private languageTitle: string; + private organizationPascalCase: string; + + constructor({ + readmeParser, + readmeConfig, + originalReadme, + }: { + readmeParser: ReadmeParser; + readmeConfig: FernGeneratorCli.ReadmeConfig; + originalReadme: string | undefined; + }) { + this.readmeParser = readmeParser; + this.readmeConfig = readmeConfig; + this.originalReadme = originalReadme; + this.languageTitle = languageToTitle(this.readmeConfig.language); + this.organizationPascalCase = pascalCase(this.readmeConfig.organization); + } + + public async generateReadme({ + output, + }: { + output: fs.WriteStream; + }): Promise { + const blocks = this.generateBlocks(); + + const writer = new StreamWriter(output); + this.writeHeader({ writer }); + this.writeBlocks({ + writer, + blocks: await this.mergeBlocks({ blocks }), + }); + writer.end(); + + return; + } + + private generateBlocks(): Block[] { + const blocks: Block[] = []; + + if (this.readmeConfig.apiReferenceLink != null) { + blocks.push( + this.generateDocumentation({ + docsLink: this.readmeConfig.apiReferenceLink, + }) + ); + } + if (this.readmeConfig.requirements != null) { + blocks.push( + this.generateRequirements({ + requirements: this.readmeConfig.requirements, + }) + ); + } + if (this.readmeConfig.language?.publishInfo != null) { + blocks.push( + this.generateInstallation({ + language: this.readmeConfig.language, + }) + ); + } + if (this.readmeConfig.referenceMarkdownPath != null) { + blocks.push( + this.generateReference({ + referenceFile: this.readmeConfig.referenceMarkdownPath, + }) + ); + } + + const coreFeatures = + this.readmeConfig.features?.filter((feat) => !this.isAdvanced(feat)) ?? + []; + const advancedFeatures = + this.readmeConfig.features?.filter((feat) => this.isAdvanced(feat)) ?? []; + + for (const feature of coreFeatures) { + if (this.shouldSkipFeature({ feature })) { + continue; + } + blocks.push( + this.generateFeatureBlock({ + feature, + }) + ); + } + + const advancedFeatureBlock = this.generateNestedFeatureBlock({ + featureId: this.ADVANCED_FEATURE_ID, + features: advancedFeatures, + }); + if (advancedFeatureBlock != null) { + blocks.push(advancedFeatureBlock); + } + + blocks.push(this.generateContributing()); + + return blocks; + } + + private isAdvanced(feat: ReadmeFeature): boolean { + if (this.ADVANCED_FEATURES.has(feat.id)) { + return true; + } + return feat.advanced ?? false; + } + + private generateNestedFeatureBlock({ + featureId, + features, + }: { + featureId: string; + features: FernGeneratorCli.ReadmeFeature[]; + }): Block | undefined { + if (!this.shouldGenerateFeatures({ features })) { + return undefined; + } + + const writer = new StringWriter(); + writer.writeLine(`## ${featureIDToTitle(featureId)}`); + writer.writeLine(); + + for (const feature of features) { + if (this.shouldSkipFeature({ feature })) { + continue; + } + this.generateFeatureBlock({ + feature, + heading: "###", + maybeWriter: writer, + }); + } + return new Block({ + id: featureId, + content: writer.toString(), + }); + } + + private generateFeatureBlock({ + feature, + heading = "##", + maybeWriter, + }: { + feature: FernGeneratorCli.ReadmeFeature; + heading?: "##" | "###"; + maybeWriter?: StringWriter; + }): Block { + const writer = maybeWriter ?? new StringWriter(); + writer.writeLine(`${heading} ${featureIDToTitle(feature.id)}`); + writer.writeLine(); + if (feature.description != null) { + writer.writeLine(feature.description); + } + feature.snippets?.forEach((snippet, index) => { + if (index > 0) { + writer.writeLine(); + } + writer.writeCodeBlock(this.readmeConfig.language.type, snippet); + }); + if (feature.addendum != null) { + writer.writeLine(feature.addendum); + } + writer.writeLine(); + return new Block({ + id: feature.id, + content: writer.toString(), + }); + } + + private async mergeBlocks({ blocks }: { blocks: Block[] }): Promise { + const originalReadmeContent = await this.getOriginalReadmeContent(); + if (originalReadmeContent == null) { + return blocks; + } + const parsed = this.readmeParser.parse({ + content: originalReadmeContent, + }); + const merger = new BlockMerger({ + original: parsed.blocks, + updated: blocks, + }); + return merger.merge(); + } + + private async getOriginalReadmeContent(): Promise { + if (this.originalReadme != null) { + return this.originalReadme; + } + if (this.readmeConfig.remote != null) { + const clonedRepository = await cloneRepository({ + githubRepository: this.readmeConfig.remote.repoUrl, + installationToken: this.readmeConfig.remote.installationToken, + }); + return await clonedRepository.getReadme(); + } + return undefined; + } + + private writeBlocks({ + writer, + blocks, + }: { + writer: Writer; + blocks: Block[]; + }): void { + for (const block of blocks) { + block.write(writer); + } + } + + private writeHeader({ writer }: { writer: Writer }): void { + writer.writeLine( + `# ${this.organizationPascalCase} ${this.languageTitle} Library` + ); + writer.writeLine(); + if (this.readmeConfig.bannerLink != null) { + this.writeBanner({ + writer, + bannerLink: this.readmeConfig.bannerLink, + }); } - - public async generateReadme({ output }: { output: fs.WriteStream }): Promise { - const blocks = this.generateBlocks(); - - const writer = new StreamWriter(output); - this.writeHeader({ writer }); - this.writeBlocks({ - writer, - blocks: await this.mergeBlocks({ blocks }), + this.writeFernShield({ writer }); + if (this.readmeConfig.language != null) { + this.writeShield({ + writer, + language: this.readmeConfig.language, + }); + } + writer.writeLine(); + this.writeIntroudction({ writer }); + } + + private writeBanner({ + writer, + bannerLink, + }: { + writer: Writer; + bannerLink: string; + }): void { + writer.writeLine(`![](${bannerLink})`); + writer.writeLine(); + } + + private writeFernShield({ writer }: { writer: Writer }): void { + const repoSource = + this.readmeConfig.remote?.repoUrl ?? + `${this.organizationPascalCase}/${this.languageTitle}`; + writer.writeLine( + `[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=${encodeURIComponent(repoSource)})` + ); + } + + private writeIntroudction({ writer }: { writer: Writer }): void { + writer.writeLine( + this.readmeConfig.introduction != null + ? this.readmeConfig.introduction + : `The ${this.organizationPascalCase} ${this.languageTitle} library provides convenient access to the ${this.organizationPascalCase} API from ${this.languageTitle}.` + ); + writer.writeLine(); + } + + private generateDocumentation({ docsLink }: { docsLink: string }): Block { + const writer = new StringWriter(); + writer.writeLine("## Documentation"); + writer.writeLine(); + writer.writeLine( + `API reference documentation is available [here](${docsLink}).` + ); + writer.writeLine(); + return new Block({ + id: "DOCUMENTATION", + content: writer.toString(), + }); + } + + private generateReference({ + referenceFile, + }: { + referenceFile: string; + }): Block { + const writer = new StringWriter(); + writer.writeLine("## Reference"); + writer.writeLine(); + writer.writeLine( + `A full reference for this library is available [here](${referenceFile}).` + ); + writer.writeLine(); + return new Block({ + id: "REFERENCE", + content: writer.toString(), + }); + } + + private generateRequirements({ + requirements, + }: { + requirements: string[]; + }): Block { + const writer = new StringWriter(); + writer.writeLine("## Requirements"); + writer.writeLine(); + if (requirements.length === 1) { + writer.writeLine(`This SDK requires ${requirements[0]}.`); + } else { + writer.writeLine("This SDK requires:"); + for (const requirement of requirements) { + writer.writeLine(`- ${requirement}`); + } + } + writer.writeLine(); + return new Block({ + id: "REQUIREMENTS", + content: writer.toString(), + }); + } + + private generateInstallation({ + language, + }: { + language: FernGeneratorCli.LanguageInfo; + }): Block { + if (language.publishInfo == null) { + // This should be unreachable. + throw new Error("publish information is required for installation block"); + } + const writer = new StringWriter(); + writer.writeLine("## Installation"); + writer.writeLine(); + switch (language.type) { + case "typescript": + this.writeInstallationForNPM({ + writer, + npm: language.publishInfo, }); - writer.end(); - - return; - } - - private generateBlocks(): Block[] { - const blocks: Block[] = []; - - if (this.readmeConfig.apiReferenceLink != null) { - blocks.push(this.generateDocumentation({ docsLink: this.readmeConfig.apiReferenceLink })); - } - if (this.readmeConfig.requirements != null) { - blocks.push(this.generateRequirements({ requirements: this.readmeConfig.requirements })); - } - if (this.readmeConfig.language != null && this.readmeConfig.language.publishInfo != null) { - blocks.push(this.generateInstallation({ language: this.readmeConfig.language })); - } - if (this.readmeConfig.referenceMarkdownPath != null) { - blocks.push(this.generateReference({ referenceFile: this.readmeConfig.referenceMarkdownPath })); - } - - const coreFeatures = this.readmeConfig.features?.filter((feat) => !this.isAdvanced(feat)) ?? []; - const advancedFeatures = this.readmeConfig.features?.filter((feat) => this.isAdvanced(feat)) ?? []; - - for (const feature of coreFeatures) { - if (this.shouldSkipFeature({ feature })) { - continue; - } - blocks.push( - this.generateFeatureBlock({ - feature, - }), - ); - } - - const advancedFeatureBlock = this.generateNestedFeatureBlock({ - featureId: this.ADVANCED_FEATURE_ID, - features: advancedFeatures, + break; + case "python": + this.writeInstallationForPyPi({ + writer, + pypi: language.publishInfo, }); - if (advancedFeatureBlock != null) { - blocks.push(advancedFeatureBlock); - } - - blocks.push(this.generateContributing()); - - return blocks; - } - - private isAdvanced(feat: ReadmeFeature): boolean { - if (this.ADVANCED_FEATURES.has(feat.id)) { - return true; - } - return feat.advanced ?? false; - } - - private generateNestedFeatureBlock({ - featureId, - features, - }: { - featureId: string; - features: FernGeneratorCli.ReadmeFeature[]; - }): Block | undefined { - if (!this.shouldGenerateFeatures({ features })) { - return undefined; - } - - const writer = new StringWriter(); - writer.writeLine(`## ${featureIDToTitle(featureId)}`); - writer.writeLine(); - - for (const feature of features) { - if (this.shouldSkipFeature({ feature })) { - continue; - } - this.generateFeatureBlock({ - feature, - heading: "###", - maybeWriter: writer, - }); - } - return new Block({ - id: featureId, - content: writer.toString(), + break; + case "java": + this.writeInstallationForMaven({ + writer, + maven: language.publishInfo, }); - } - - private generateFeatureBlock({ - feature, - heading = "##", - maybeWriter, - }: { - feature: FernGeneratorCli.ReadmeFeature; - heading?: "##" | "###"; - maybeWriter?: StringWriter; - }): Block { - const writer = maybeWriter ?? new StringWriter(); - writer.writeLine(`${heading} ${featureIDToTitle(feature.id)}`); - writer.writeLine(); - if (feature.description != null) { - writer.writeLine(feature.description); - } - feature.snippets?.forEach((snippet, index) => { - if (index > 0) { - writer.writeLine(); - } - writer.writeCodeBlock(this.readmeConfig.language.type, snippet); + break; + case "go": + this.writeInstallationForGo({ + writer, + go: language.publishInfo, }); - if (feature.addendum != null) { - writer.writeLine(feature.addendum); - } - writer.writeLine(); - return new Block({ - id: feature.id, - content: writer.toString(), + break; + case "ruby": + this.writeInstallationForRubyGems({ + writer, + rubyGems: language.publishInfo, }); - } - - private async mergeBlocks({ blocks }: { blocks: Block[] }): Promise { - const originalReadmeContent = await this.getOriginalReadmeContent(); - if (originalReadmeContent == null) { - return blocks; - } - const parsed = this.readmeParser.parse({ content: originalReadmeContent }); - const merger = new BlockMerger({ - original: parsed.blocks, - updated: blocks, + break; + case "csharp": + this.writeInstallationForNuget({ + writer, + nuget: language.publishInfo, }); - return merger.merge(); - } - - private async getOriginalReadmeContent(): Promise { - if (this.originalReadme != null) { - return this.originalReadme; - } - if (this.readmeConfig.remote != null) { - const clonedRepository = await cloneRepository({ - githubRepository: this.readmeConfig.remote.repoUrl, - installationToken: this.readmeConfig.remote.installationToken, - }); - return await clonedRepository.getReadme(); - } - return undefined; - } - - private writeBlocks({ writer, blocks }: { writer: Writer; blocks: Block[] }): void { - for (const block of blocks) { - block.write(writer); - } - } - - private writeHeader({ writer }: { writer: Writer }): void { - writer.writeLine(`# ${this.organizationPascalCase} ${this.languageTitle} Library`); - writer.writeLine(); - if (this.readmeConfig.bannerLink != null) { - this.writeBanner({ writer, bannerLink: this.readmeConfig.bannerLink }); - } - this.writeFernShield({ writer }); - if (this.readmeConfig.language != null) { - this.writeShield({ - writer, - language: this.readmeConfig.language, - }); + break; + default: + assertNever(language); + } + return new Block({ + id: "INSTALLATION", + content: writer.toString(), + }); + } + + private writeInstallationForNPM({ + writer, + npm, + }: { + writer: Writer; + npm: FernGeneratorCli.NpmPublishInfo; + }): void { + writer.writeLine("```sh"); + writer.writeLine(`npm i -s ${npm.packageName}`); + writer.writeLine("```"); + writer.writeLine(); + } + + private writeInstallationForPyPi({ + writer, + pypi, + }: { + writer: Writer; + pypi: FernGeneratorCli.PypiPublishInfo; + }): void { + writer.writeLine("```sh"); + writer.writeLine(`pip install ${pypi.packageName}`); + writer.writeLine("```"); + writer.writeLine(); + } + + private writeInstallationForMaven({ + writer, + maven, + }: { + writer: Writer; + maven: FernGeneratorCli.MavenPublishInfo; + }): void { + writer.writeLine("### Gradle"); + writer.writeLine(); + writer.writeLine("Add the dependency in your `build.gradle` file:"); + writer.writeLine(); + writer.writeLine("```groovy"); + writer.writeLine("dependencies {"); + writer.writeLine(` implementation '${maven.group}:${maven.artifact}'`); + writer.writeLine("}"); + writer.writeLine("```"); + writer.writeLine(); + + writer.writeLine("### Maven"); + writer.writeLine(); + writer.writeLine("Add the dependency in your `pom.xml` file:"); + writer.writeLine(); + writer.writeLine("```xml"); + writer.writeLine(""); + writer.writeLine(` ${maven.group}`); + writer.writeLine(` ${maven.artifact}`); + writer.writeLine(` ${maven.version}`); + writer.writeLine(""); + writer.writeLine("```"); + writer.writeLine(); + } + + private writeInstallationForGo({ + writer, + go, + }: { + writer: Writer; + go: FernGeneratorCli.GoPublishInfo; + }): void { + writer.writeLine("```sh"); + writer.write(`go get github.com/${go.owner}/${go.repo}`); + const majorVersion = getMajorVersion(go.version); + if (!majorVersion.startsWith("0") && !majorVersion.startsWith("1")) { + // For Go, we need to append the major version to the module path for any release greater than v1.X.X. + writer.write(`/v${majorVersion}`); + } + writer.writeLine(); + writer.writeLine("```"); + writer.writeLine(); + } + + private writeInstallationForRubyGems({ + writer, + rubyGems, + }: { + writer: Writer; + rubyGems: FernGeneratorCli.RubyGemsPublishInfo; + }): void { + writer.writeLine("```sh"); + writer.writeLine(`gem install ${rubyGems.packageName}`); + writer.writeLine("```"); + writer.writeLine(); + } + + private writeInstallationForNuget({ + writer, + nuget, + }: { + writer: Writer; + nuget: FernGeneratorCli.NugetPublishInfo; + }): void { + writer.writeLine("```sh"); + writer.writeLine(`nuget install ${nuget.packageName}`); + writer.writeLine("```"); + writer.writeLine(); + } + + private writeShield({ + writer, + language, + }: { + writer: Writer; + language: FernGeneratorCli.LanguageInfo; + }): void { + switch (language.type) { + case "typescript": { + const npm = language.publishInfo; + if (npm == null) { + return; } - writer.writeLine(); - this.writeIntroudction({ writer }); - } - - private writeBanner({ writer, bannerLink }: { writer: Writer; bannerLink: string }): void { - writer.writeLine(`![](${bannerLink})`); - writer.writeLine(); - } - - private writeFernShield({ writer }: { writer: Writer }): void { - const repoSource = this.readmeConfig.remote?.repoUrl ?? `${this.organizationPascalCase}/${this.languageTitle}`; - writer.writeLine( - `[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=${encodeURIComponent(repoSource)})`, - ); - } - - private writeIntroudction({ writer }: { writer: Writer }): void { - writer.writeLine( - this.readmeConfig.introduction != null - ? this.readmeConfig.introduction - : `The ${this.organizationPascalCase} ${this.languageTitle} library provides convenient access to the ${this.organizationPascalCase} API from ${this.languageTitle}.`, - ); - writer.writeLine(); - } - - private generateDocumentation({ docsLink }: { docsLink: string }): Block { - const writer = new StringWriter(); - writer.writeLine("## Documentation"); - writer.writeLine(); - writer.writeLine(`API reference documentation is available [here](${docsLink}).`); - writer.writeLine(); - return new Block({ - id: "DOCUMENTATION", - content: writer.toString(), - }); - } - - private generateReference({ referenceFile }: { referenceFile: string }): Block { - const writer = new StringWriter(); - writer.writeLine("## Reference"); - writer.writeLine(); - writer.writeLine(`A full reference for this library is available [here](${referenceFile}).`); - writer.writeLine(); - return new Block({ - id: "REFERENCE", - content: writer.toString(), + this.writeShieldForNPM({ + writer, + npm, }); - } - - private generateRequirements({ requirements }: { requirements: string[] }): Block { - const writer = new StringWriter(); - writer.writeLine("## Requirements"); - writer.writeLine(); - if (requirements.length === 1) { - writer.writeLine(`This SDK requires ${requirements[0]}.`); - } else { - writer.writeLine("This SDK requires:"); - for (const requirement of requirements) { - writer.writeLine(`- ${requirement}`); - } + return; + } + case "python": { + const pypi = language.publishInfo; + if (pypi == null) { + return; } - writer.writeLine(); - return new Block({ - id: "REQUIREMENTS", - content: writer.toString(), + this.writeShieldForPyPi({ + writer, + pypi, }); - } - - private generateInstallation({ language }: { language: FernGeneratorCli.LanguageInfo }): Block { - if (language.publishInfo == null) { - // This should be unreachable. - throw new Error("publish information is required for installation block"); + return; + } + case "java": { + const maven = language.publishInfo; + if (maven == null) { + return; } - const writer = new StringWriter(); - writer.writeLine("## Installation"); - writer.writeLine(); - switch (language.type) { - case "typescript": - this.writeInstallationForNPM({ - writer, - npm: language.publishInfo, - }); - break; - case "python": - this.writeInstallationForPyPi({ - writer, - pypi: language.publishInfo, - }); - break; - case "java": - this.writeInstallationForMaven({ - writer, - maven: language.publishInfo, - }); - break; - case "go": - this.writeInstallationForGo({ - writer, - go: language.publishInfo, - }); - break; - case "ruby": - this.writeInstallationForRubyGems({ - writer, - rubyGems: language.publishInfo, - }); - break; - case "csharp": - this.writeInstallationForNuget({ - writer, - nuget: language.publishInfo, - }); - break; - default: - assertNever(language); + this.writeShieldForMaven({ + writer, + maven, + }); + return; + } + case "go": { + const go = language.publishInfo; + if (go == null) { + return; } - return new Block({ - id: "INSTALLATION", - content: writer.toString(), + this.writeShieldForGo({ + writer, + go, }); - } - - private writeInstallationForNPM({ writer, npm }: { writer: Writer; npm: FernGeneratorCli.NpmPublishInfo }): void { - writer.writeLine("```sh"); - writer.writeLine(`npm i -s ${npm.packageName}`); - writer.writeLine("```"); - writer.writeLine(); - } - - private writeInstallationForPyPi({ - writer, - pypi, - }: { - writer: Writer; - pypi: FernGeneratorCli.PypiPublishInfo; - }): void { - writer.writeLine("```sh"); - writer.writeLine(`pip install ${pypi.packageName}`); - writer.writeLine("```"); - writer.writeLine(); - } - - private writeInstallationForMaven({ - writer, - maven, - }: { - writer: Writer; - maven: FernGeneratorCli.MavenPublishInfo; - }): void { - writer.writeLine("### Gradle"); - writer.writeLine(); - writer.writeLine("Add the dependency in your `build.gradle` file:"); - writer.writeLine(); - writer.writeLine("```groovy"); - writer.writeLine("dependencies {"); - writer.writeLine(` implementation '${maven.group}:${maven.artifact}'`); - writer.writeLine("}"); - writer.writeLine("```"); - writer.writeLine(); - - writer.writeLine("### Maven"); - writer.writeLine(); - writer.writeLine("Add the dependency in your `pom.xml` file:"); - writer.writeLine(); - writer.writeLine("```xml"); - writer.writeLine(""); - writer.writeLine(` ${maven.group}`); - writer.writeLine(` ${maven.artifact}`); - writer.writeLine(` ${maven.version}`); - writer.writeLine(""); - writer.writeLine("```"); - writer.writeLine(); - } - - private writeInstallationForGo({ writer, go }: { writer: Writer; go: FernGeneratorCli.GoPublishInfo }): void { - writer.writeLine("```sh"); - writer.write(`go get github.com/${go.owner}/${go.repo}`); - const majorVersion = getMajorVersion(go.version); - if (!majorVersion.startsWith("0") && !majorVersion.startsWith("1")) { - // For Go, we need to append the major version to the module path for any release greater than v1.X.X. - writer.write(`/v${majorVersion}`); + return; + } + case "ruby": { + const rubyGems = language.publishInfo; + if (rubyGems == null) { + return; } - writer.writeLine(); - writer.writeLine("```"); - writer.writeLine(); - } - - private writeInstallationForRubyGems({ - writer, - rubyGems, - }: { - writer: Writer; - rubyGems: FernGeneratorCli.RubyGemsPublishInfo; - }): void { - writer.writeLine("```sh"); - writer.writeLine(`gem install ${rubyGems.packageName}`); - writer.writeLine("```"); - writer.writeLine(); - } - - private writeInstallationForNuget({ - writer, - nuget, - }: { - writer: Writer; - nuget: FernGeneratorCli.NugetPublishInfo; - }): void { - writer.writeLine("```sh"); - writer.writeLine(`nuget install ${nuget.packageName}`); - writer.writeLine("```"); - writer.writeLine(); - } - - private writeShield({ writer, language }: { writer: Writer; language: FernGeneratorCli.LanguageInfo }): void { - switch (language.type) { - case "typescript": { - const npm = language.publishInfo; - if (npm == null) { - return; - } - this.writeShieldForNPM({ - writer, - npm, - }); - return; - } - case "python": { - const pypi = language.publishInfo; - if (pypi == null) { - return; - } - this.writeShieldForPyPi({ - writer, - pypi, - }); - return; - } - case "java": { - const maven = language.publishInfo; - if (maven == null) { - return; - } - this.writeShieldForMaven({ - writer, - maven, - }); - return; - } - case "go": { - const go = language.publishInfo; - if (go == null) { - return; - } - this.writeShieldForGo({ - writer, - go, - }); - return; - } - case "ruby": { - const rubyGems = language.publishInfo; - if (rubyGems == null) { - return; - } - this.writeShieldForRubyGems({ - writer, - rubyGems, - }); - return; - } - case "csharp": { - const nuget = language.publishInfo; - if (nuget == null) { - return; - } - this.writeShieldForNuget({ - writer, - nuget, - }); - return; - } - default: - assertNever(language); + this.writeShieldForRubyGems({ + writer, + rubyGems, + }); + return; + } + case "csharp": { + const nuget = language.publishInfo; + if (nuget == null) { + return; } - } - - private writeShieldForNPM({ writer, npm }: { writer: Writer; npm: FernGeneratorCli.NpmPublishInfo }): void { - writer.write("[![npm shield]"); - writer.write(`(https://img.shields.io/npm/v/${npm.packageName})]`); - writer.writeLine(`(https://www.npmjs.com/package/${npm.packageName})`); - } - - private writeShieldForPyPi({ writer, pypi }: { writer: Writer; pypi: FernGeneratorCli.PypiPublishInfo }): void { - writer.write("[![pypi]"); - writer.write(`(https://img.shields.io/pypi/v/${pypi.packageName})]`); - writer.writeLine(`(https://pypi.python.org/pypi/${pypi.packageName})`); - } - - private writeShieldForMaven({ writer, maven }: { writer: Writer; maven: FernGeneratorCli.MavenPublishInfo }): void { - writer.write("[![Maven Central]"); - writer.write(`(https://img.shields.io/maven-central/v/${maven.artifact})]`); - writer.writeLine(`(https://central.sonatype.com/artifact/${maven.group}/${maven.artifact})`); - } - - private writeShieldForGo({ writer, go }: { writer: Writer; go: FernGeneratorCli.GoPublishInfo }): void { - writer.write("[![go shield]"); - writer.write("(https://img.shields.io/badge/go-docs-blue)]"); - writer.writeLine(`(https://pkg.go.dev/github.com/${go.owner}/${go.repo})`); - } - - private writeShieldForRubyGems({ - writer, - rubyGems, - }: { - writer: Writer; - rubyGems: FernGeneratorCli.RubyGemsPublishInfo; - }): void { - writer.write("[![gems shield]"); - writer.write(`(https://img.shields.io/gem/v/${rubyGems.packageName})]`); - writer.writeLine(`(https://rubygems.org/gems/${rubyGems.packageName})`); - } - - private writeShieldForNuget({ writer, nuget }: { writer: Writer; nuget: FernGeneratorCli.NugetPublishInfo }): void { - writer.write("[![nuget shield]"); - writer.write(`(https://img.shields.io/nuget/v/${nuget.packageName})]`); - writer.writeLine(`(https://nuget.org/packages/${nuget.packageName})`); - } - - private generateContributing(): Block { - return new Block({ - id: "contributing", - content: `## Contributing + this.writeShieldForNuget({ + writer, + nuget, + }); + return; + } + default: + assertNever(language); + } + } + + private writeShieldForNPM({ + writer, + npm, + }: { + writer: Writer; + npm: FernGeneratorCli.NpmPublishInfo; + }): void { + writer.write("[![npm shield]"); + writer.write(`(https://img.shields.io/npm/v/${npm.packageName})]`); + writer.writeLine(`(https://www.npmjs.com/package/${npm.packageName})`); + } + + private writeShieldForPyPi({ + writer, + pypi, + }: { + writer: Writer; + pypi: FernGeneratorCli.PypiPublishInfo; + }): void { + writer.write("[![pypi]"); + writer.write(`(https://img.shields.io/pypi/v/${pypi.packageName})]`); + writer.writeLine(`(https://pypi.python.org/pypi/${pypi.packageName})`); + } + + private writeShieldForMaven({ + writer, + maven, + }: { + writer: Writer; + maven: FernGeneratorCli.MavenPublishInfo; + }): void { + writer.write("[![Maven Central]"); + writer.write(`(https://img.shields.io/maven-central/v/${maven.artifact})]`); + writer.writeLine( + `(https://central.sonatype.com/artifact/${maven.group}/${maven.artifact})` + ); + } + + private writeShieldForGo({ + writer, + go, + }: { + writer: Writer; + go: FernGeneratorCli.GoPublishInfo; + }): void { + writer.write("[![go shield]"); + writer.write("(https://img.shields.io/badge/go-docs-blue)]"); + writer.writeLine(`(https://pkg.go.dev/github.com/${go.owner}/${go.repo})`); + } + + private writeShieldForRubyGems({ + writer, + rubyGems, + }: { + writer: Writer; + rubyGems: FernGeneratorCli.RubyGemsPublishInfo; + }): void { + writer.write("[![gems shield]"); + writer.write(`(https://img.shields.io/gem/v/${rubyGems.packageName})]`); + writer.writeLine(`(https://rubygems.org/gems/${rubyGems.packageName})`); + } + + private writeShieldForNuget({ + writer, + nuget, + }: { + writer: Writer; + nuget: FernGeneratorCli.NugetPublishInfo; + }): void { + writer.write("[![nuget shield]"); + writer.write(`(https://img.shields.io/nuget/v/${nuget.packageName})]`); + writer.writeLine(`(https://nuget.org/packages/${nuget.packageName})`); + } + + private generateContributing(): Block { + return new Block({ + id: "contributing", + content: `## Contributing While we value open-source contributions to this SDK, this library is generated programmatically. Additions made directly to this library would have to be moved over to our generation code, @@ -551,52 +661,63 @@ an issue first to discuss with us! On the other hand, contributions to the README are always very welcome! `, - }); - } - - private shouldSkipFeature({ feature }: { feature: FernGeneratorCli.ReadmeFeature }): boolean { - return !feature.snippetsAreOptional && (feature.snippets == null || feature.snippets.length === 0); - } - - private shouldGenerateFeatures({ features }: { features: FernGeneratorCli.ReadmeFeature[] }): boolean { - return features.some((feature) => !this.shouldSkipFeature({ feature })); - } + }); + } + + private shouldSkipFeature({ + feature, + }: { + feature: FernGeneratorCli.ReadmeFeature; + }): boolean { + return ( + !feature.snippetsAreOptional && + (feature.snippets == null || feature.snippets.length === 0) + ); + } + + private shouldGenerateFeatures({ + features, + }: { + features: FernGeneratorCli.ReadmeFeature[]; + }): boolean { + return features.some((feature) => !this.shouldSkipFeature({ feature })); + } } function languageToTitle(language: FernGeneratorCli.LanguageInfo): string { - switch (language.type) { - case "typescript": - return "TypeScript"; - case "python": - return "Python"; - case "go": - return "Go"; - case "java": - return "Java"; - case "ruby": - return "Ruby"; - case "csharp": - return "C#"; - default: - assertNever(language); - } + switch (language.type) { + case "typescript": + return "TypeScript"; + case "python": + return "Python"; + case "go": + return "Go"; + case "java": + return "Java"; + case "ruby": + return "Ruby"; + case "csharp": + return "C#"; + default: + assertNever(language); + } } function featureIDToTitle(featureID: string): string { - return featureID - .split("_") - .map((s) => pascalCase(s)) - .join(" "); + return featureID + .split("_") + .map((s) => pascalCase(s)) + .join(" "); } function pascalCase(s: string): string { - return upperFirst(camelCase(s)); + return upperFirst(camelCase(s)); } function getMajorVersion(version: string): string { - return version.split(".")[0] ?? "0"; + return version.split(".")[0] ?? "0"; } function assertNever(x: never): never { - throw new Error("unexpected value: " + JSON.stringify(x)); + throw new Error("unexpected value: " + JSON.stringify(x)); } diff --git a/clis/generator-cli/src/readme/ReadmeParser.ts b/clis/generator-cli/src/readme/ReadmeParser.ts index 6ab32ce9aa..c8ee0d909b 100644 --- a/clis/generator-cli/src/readme/ReadmeParser.ts +++ b/clis/generator-cli/src/readme/ReadmeParser.ts @@ -2,50 +2,50 @@ import { snakeCase } from "es-toolkit/string"; import { Block } from "./Block"; export interface ParseResult { - header: string; - blocks: Block[]; + header: string; + blocks: Block[]; } export class ReadmeParser { - public parse({ content }: { content: string }): ParseResult { - let header: string = ""; - let currentBlock: Block | undefined; - const blocks: Block[] = []; - const lines = content.split("\n"); - for (const line of lines) { - const h2Match = line.match(/^##\s+(.*)/); - if (h2Match) { - if (currentBlock) { - blocks.push(currentBlock); - } - currentBlock = new Block({ - id: sectionNameToID(h2Match[1] ?? ""), - content: "", - }); - } - if (currentBlock == null) { - header += line; - continue; - } - currentBlock.content += line + "\n"; + public parse({ content }: { content: string }): ParseResult { + let header: string = ""; + let currentBlock: Block | undefined; + const blocks: Block[] = []; + const lines = content.split("\n"); + for (const line of lines) { + const h2Match = line.match(/^##\s+(.*)/); + if (h2Match) { + if (currentBlock) { + blocks.push(currentBlock); } - return { - header, - blocks, - }; + currentBlock = new Block({ + id: sectionNameToID(h2Match[1] ?? ""), + content: "", + }); + } + if (currentBlock == null) { + header += line; + continue; + } + currentBlock.content += line + "\n"; } + return { + header, + blocks, + }; + } } function sectionNameToID(sectionName: string): string { - return snakeCase( - sectionName - .split(" ") - .map((word, index) => { - if (index === 0) { - return word; - } - return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); - }) - .join(""), - ).toUpperCase(); + return snakeCase( + sectionName + .split(" ") + .map((word, index) => { + if (index === 0) { + return word; + } + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }) + .join("") + ).toUpperCase(); } diff --git a/clis/generator-cli/src/reference/ReferenceGenerator.ts b/clis/generator-cli/src/reference/ReferenceGenerator.ts index eb6a8a34cb..9c168f375a 100644 --- a/clis/generator-cli/src/reference/ReferenceGenerator.ts +++ b/clis/generator-cli/src/reference/ReferenceGenerator.ts @@ -1,105 +1,142 @@ import fs from "fs"; import { FernGeneratorCli } from "../configuration/generated"; import { - EndpointReference, - LinkedText, - ParameterReference, - ReferenceSection, - RelativeLocation, - RootPackageReferenceSection, + EndpointReference, + LinkedText, + ParameterReference, + ReferenceSection, + RelativeLocation, + RootPackageReferenceSection, } from "../configuration/generated/api"; import { StreamWriter, StringWriter, Writer } from "../utils/Writer"; export class ReferenceGenerator { - private referenceConfig: FernGeneratorCli.ReferenceConfig; + private referenceConfig: FernGeneratorCli.ReferenceConfig; - constructor({ referenceConfig }: { referenceConfig: FernGeneratorCli.ReferenceConfig }) { - this.referenceConfig = referenceConfig; - } + constructor({ + referenceConfig, + }: { + referenceConfig: FernGeneratorCli.ReferenceConfig; + }) { + this.referenceConfig = referenceConfig; + } - public async generate({ output }: { output: fs.WriteStream }): Promise { - const writer = new StreamWriter(output); - writer.writeLine("# Reference"); + public async generate({ output }: { output: fs.WriteStream }): Promise { + const writer = new StreamWriter(output); + writer.writeLine("# Reference"); - if (this.referenceConfig.rootSection != null) { - this.writeRootSection({ section: this.referenceConfig.rootSection, writer }); - } - for (const section of this.referenceConfig.sections) { - this.writeSection({ section, writer }); - } - writer.end(); + if (this.referenceConfig.rootSection != null) { + this.writeRootSection({ + section: this.referenceConfig.rootSection, + writer, + }); } - - private writeRootSection({ section, writer }: { section: RootPackageReferenceSection; writer: Writer }): void { - if (section.description != null) { - writer.writeLine(`${section.description}`); - } - for (const endpoint of section.endpoints) { - this.writeEndpoint({ endpoint, writer }); - } + for (const section of this.referenceConfig.sections) { + this.writeSection({ section, writer }); } + writer.end(); + } - private writeSection({ section, writer }: { section: ReferenceSection; writer: Writer }): void { - writer.writeLine(`## ${section.title}`); - if (section.description != null) { - writer.writeLine(`${section.description}`); - } - for (const endpoint of section.endpoints) { - this.writeEndpoint({ endpoint, writer }); - } + private writeRootSection({ + section, + writer, + }: { + section: RootPackageReferenceSection; + writer: Writer; + }): void { + if (section.description != null) { + writer.writeLine(section.description); } - - private writeEndpoint({ endpoint, writer }: { endpoint: EndpointReference; writer: Writer }): void { - const stringWriter = new StringWriter(); - if (endpoint.description != null) { - stringWriter.writeLine( - `#### 📝 Description\n\n${this.writeIndentedBlock(this.writeIndentedBlock(endpoint.description))}\n`, - ); - } - stringWriter.writeLine( - `#### 🔌 Usage\n\n${this.writeIndentedBlock( - this.writeIndentedBlock( - "```" + this.referenceConfig.language.toLowerCase() + "\n" + endpoint.snippet + "\n```", - ), - )}\n`, - ); - if (endpoint.parameters.length > 0) { - stringWriter.writeLine( - `#### ⚙️ Parameters\n\n${this.writeIndentedBlock( - endpoint.parameters - .map((parameter) => this.writeIndentedBlock(this.writeParameter(parameter))) - .join("\n\n"), - )}\n`, - ); - } - - let linkedSnippet = this.wrapInLinksAndJoin(endpoint.title.snippetParts); - if (endpoint.title.returnValue != null) { - linkedSnippet += ` -> ${this.wrapInLink(endpoint.title.returnValue.text, endpoint.title.returnValue.location)}`; - } - writer.writeLine(`
${linkedSnippet}`); - writer.writeLine(this.writeIndentedBlock(stringWriter.toString())); - writer.writeLine("
\n"); + for (const endpoint of section.endpoints) { + this.writeEndpoint({ endpoint, writer }); } + } - private writeParameter(parameter: ParameterReference): string { - const desc = parameter.description?.match(/[^\r\n]+/g)?.length; - const containsLineBreak = desc != null && desc > 1; - return `**${parameter.name}:** \`${this.wrapInLink(parameter.type, parameter.location)}\` ${ - parameter.description != null ? (containsLineBreak ? "\n\n" : "— ") + parameter.description : "" - } - `; + private writeSection({ + section, + writer, + }: { + section: ReferenceSection; + writer: Writer; + }): void { + writer.writeLine(`## ${section.title}`); + if (section.description != null) { + writer.writeLine(section.description); + } + for (const endpoint of section.endpoints) { + this.writeEndpoint({ endpoint, writer }); } + } - private writeIndentedBlock(content: string): string { - return `
\n
\n\n${content}\n
\n
`; + private writeEndpoint({ + endpoint, + writer, + }: { + endpoint: EndpointReference; + writer: Writer; + }): void { + const stringWriter = new StringWriter(); + if (endpoint.description != null) { + stringWriter.writeLine( + `#### 📝 Description\n\n${this.writeIndentedBlock(this.writeIndentedBlock(endpoint.description))}\n` + ); + } + stringWriter.writeLine( + `#### 🔌 Usage\n\n${this.writeIndentedBlock( + this.writeIndentedBlock( + "```" + + this.referenceConfig.language.toLowerCase() + + "\n" + + endpoint.snippet + + "\n```" + ) + )}\n` + ); + if (endpoint.parameters.length > 0) { + stringWriter.writeLine( + `#### ⚙️ Parameters\n\n${this.writeIndentedBlock( + endpoint.parameters + .map((parameter) => + this.writeIndentedBlock(this.writeParameter(parameter)) + ) + .join("\n\n") + )}\n` + ); } - private wrapInLinksAndJoin(content: LinkedText[]): string { - return content.map(({ text, location }) => this.wrapInLink(text, location)).join(""); + let linkedSnippet = this.wrapInLinksAndJoin(endpoint.title.snippetParts); + if (endpoint.title.returnValue != null) { + linkedSnippet += ` -> ${this.wrapInLink(endpoint.title.returnValue.text, endpoint.title.returnValue.location)}`; } + writer.writeLine( + `
${linkedSnippet}` + ); + writer.writeLine(this.writeIndentedBlock(stringWriter.toString())); + writer.writeLine("
\n"); + } - private wrapInLink(content: string, link?: RelativeLocation) { - return link != null ? `${content}` : content; + private writeParameter(parameter: ParameterReference): string { + const desc = parameter.description?.match(/[^\r\n]+/g)?.length; + const containsLineBreak = desc != null && desc > 1; + return `**${parameter.name}:** \`${this.wrapInLink(parameter.type, parameter.location)}\` ${ + parameter.description != null + ? (containsLineBreak ? "\n\n" : "— ") + parameter.description + : "" } + `; + } + + private writeIndentedBlock(content: string): string { + return `
\n
\n\n${content}\n
\n
`; + } + + private wrapInLinksAndJoin(content: LinkedText[]): string { + return content + .map(({ text, location }) => this.wrapInLink(text, location)) + .join(""); + } + + private wrapInLink(content: string, link?: RelativeLocation) { + return link != null ? `${content}` : content; + } } diff --git a/clis/generator-cli/src/utils/Writer.ts b/clis/generator-cli/src/utils/Writer.ts index d0b4b6bcf0..a7ad2ed2d0 100644 --- a/clis/generator-cli/src/utils/Writer.ts +++ b/clis/generator-cli/src/utils/Writer.ts @@ -1,56 +1,56 @@ import fs from "fs"; export abstract class Writer { - public writeCodeBlock(language: string, content: string): void { - this.writeLine("```" + language); - this.write(content); - this.writeLine("```"); - } - - public writeLine(content?: string): void { - if (content === undefined) { - this.write("\n"); - } else { - this.write(`${content}\n`); - } - } - - public abstract write(content: string): void; - public abstract end(): void; + public writeCodeBlock(language: string, content: string): void { + this.writeLine("```" + language); + this.write(content); + this.writeLine("```"); + } + + public writeLine(content?: string): void { + if (content === undefined) { + this.write("\n"); + } else { + this.write(`${content}\n`); + } + } + + public abstract write(content: string): void; + public abstract end(): void; } export class StreamWriter extends Writer { - constructor(private stream: fs.WriteStream) { - super(); - this.stream = stream; - } - - public write(content: string): void { - this.stream.write(content); - } - - public end(): void { - this.stream.end(); - } + constructor(private stream: fs.WriteStream) { + super(); + this.stream = stream; + } + + public write(content: string): void { + this.stream.write(content); + } + + public end(): void { + this.stream.end(); + } } export class StringWriter extends Writer { - private content: string; + private content: string; - constructor() { - super(); - this.content = ""; - } + constructor() { + super(); + this.content = ""; + } - public write(content: string): void { - this.content += content; - } + public write(content: string): void { + this.content += content; + } - public end(): void { - return; - } + public end(): void { + return; + } - public toString(): string { - return this.content; - } + public toString(): string { + return this.content; + } } diff --git a/clis/generator-cli/tsconfig.eslint.json b/clis/generator-cli/tsconfig.eslint.json new file mode 100644 index 0000000000..08c2772f9e --- /dev/null +++ b/clis/generator-cli/tsconfig.eslint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + } +} diff --git a/clis/vercel-scripts/package.json b/clis/vercel-scripts/package.json index 1ec79a7d7c..8c7f2d04f6 100644 --- a/clis/vercel-scripts/package.json +++ b/clis/vercel-scripts/package.json @@ -3,32 +3,32 @@ "version": "0.0.0", "type": "module", "scripts": { - "vercel-scripts": "pnpm tsx src/cli.ts", - "lint:eslint": "eslint --max-warnings 0 . --ignore-path=../../.eslintignore", + "depcheck": "depcheck", + "format": "prettier --write --ignore-unknown \"**\"", + "format:check": "prettier --check --ignore-unknown \"**\"", + "lint:eslint": "eslint --max-warnings 0 .", "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:style": "stylelint 'src/**/*.scss' --allow-empty-input --max-warnings 0", "lint:style:fix": "pnpm lint:style --fix", - "format": "prettier --write --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"", - "format:check": "prettier --check --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"", "organize-imports": "organize-imports-cli tsconfig.json", - "depcheck": "depcheck" + "vercel-scripts": "pnpm tsx src/cli.ts" + }, + "dependencies": { + "@fern-fern/fern-docs-sdk": "0.0.5", + "@fern-fern/vercel": "0.0.4655", + "execa": "^9.5.1", + "ts-essentials": "^10.0.1" }, "devDependencies": { "@fern-platform/configs": "workspace:*", "@types/node": "^18.7.18", "@types/yargs": "^17.0.32", "depcheck": "^1.4.3", - "eslint": "^8.56.0", + "eslint": "^9", "organize-imports-cli": "^0.10.0", - "prettier": "^3.3.2", + "prettier": "^3.4.2", "tsx": "^4.7.1", "typescript": "4.9.5", "yargs": "^17.4.1" - }, - "dependencies": { - "@fern-fern/fern-docs-sdk": "0.0.5", - "@fern-fern/vercel": "0.0.4655", - "execa": "^9.5.1", - "ts-essentials": "^10.0.1" } } diff --git a/clis/vercel-scripts/src/cli.ts b/clis/vercel-scripts/src/cli.ts index d0e03d2c24..23a409b220 100644 --- a/clis/vercel-scripts/src/cli.ts +++ b/clis/vercel-scripts/src/cli.ts @@ -12,188 +12,265 @@ import { getVercelTokenFromGlobalConfig } from "./utils/global-config.js"; import { FernDocsRevalidator } from "./utils/revalidator.js"; void yargs(hideBin(process.argv)) - .scriptName("vercel-scripts") - .strict() - .options("token", { + .scriptName("vercel-scripts") + .strict() + .options("token", { + type: "string", + description: "The Vercel API token", + demandOption: true, + default: process.env.VERCEL_TOKEN ?? getVercelTokenFromGlobalConfig(), + }) + .options("teamId", { + type: "string", + description: "The Vercel team ID", + demandOption: false, + default: process.env.VERCEL_ORG_ID ?? "team_6FKOM5nw037hv8g2mTk3gaH7", + }) + .options("teamName", { + type: "string", + description: "The Vercel team name", + demandOption: false, + default: "buildwithfern", + }) + .command( + "deploy ", + "Deploy a project to Vercel", + (argv) => + argv + .positional("project", { + type: "string", + demandOption: true, + description: "The project ID or project name to deploy", + }) + .options("environment", { + type: "string", + description: "The environment to deploy to", + demandOption: true, + default: "preview", + choices: ["preview" as const, "production" as const], + }) + .option("skip-deploy", { + type: "boolean", + description: "Skip the deploy step", + }) + .option("output", { + type: "string", + description: "The output file to write the preview URLs to", + default: "deployment-url.txt", + }), + async ({ + project, + environment, + token, + teamName, + teamId, + output, + skipDeploy, + }) => { + await deployCommand({ + project, + environment, + token, + teamName, + teamId, + output, + skipDeploy, + }); + + process.exit(0); + } + ) + .command( + "promote ", + "Promote a deployment to production", + (argv) => + argv + .positional("deploymentUrl", { + type: "string", + demandOption: true, + }) + .option("revalidate-all", { + type: "boolean", + description: "Revalidate the deployment (if it's fern docs)", + }), + async ({ deploymentUrl, token, teamId, revalidateAll }) => { + await promoteCommand({ + deploymentIdOrUrl: deploymentUrl, + token, + teamId, + revalidateAll, + }); + process.exit(0); + } + ) + .command( + "rollback ", + "Rollback to the previous deployment", + (argv) => + argv.positional("projectId", { type: "string", - description: "The Vercel API token", demandOption: true, - default: process.env.VERCEL_TOKEN ?? getVercelTokenFromGlobalConfig(), - }) - .options("teamId", { - type: "string", - description: "The Vercel team ID", - demandOption: false, - default: process.env.VERCEL_ORG_ID ?? "team_6FKOM5nw037hv8g2mTk3gaH7", - }) - .options("teamName", { + }), + async ({ projectId, token }) => { + await rollbackCommand({ projectId, token }); + process.exit(0); + } + ) + .command( + "revalidate-all ", + "Revalidate all docs for a deployment", + (argv) => + argv.positional("deploymentUrl", { type: "string", - description: "The Vercel team name", - demandOption: false, - default: "buildwithfern", - }) - .command( - "deploy ", - "Deploy a project to Vercel", - (argv) => - argv - .positional("project", { - type: "string", - demandOption: true, - description: "The project ID or project name to deploy", - }) - .options("environment", { - type: "string", - description: "The environment to deploy to", - demandOption: true, - default: "preview", - choices: ["preview" as const, "production" as const], - }) - .option("skip-deploy", { type: "boolean", description: "Skip the deploy step" }) - .option("output", { - type: "string", - description: "The output file to write the preview URLs to", - default: "deployment-url.txt", - }), - async ({ project, environment, token, teamName, teamId, output, skipDeploy }) => { - await deployCommand({ project, environment, token, teamName, teamId, output, skipDeploy }); - - process.exit(0); - }, - ) - .command( - "promote ", - "Promote a deployment to production", - (argv) => - argv.positional("deploymentUrl", { type: "string", demandOption: true }).option("revalidate-all", { - type: "boolean", - description: "Revalidate the deployment (if it's fern docs)", - }), - async ({ deploymentUrl, token, teamId, revalidateAll }) => { - await promoteCommand({ deploymentIdOrUrl: deploymentUrl, token, teamId, revalidateAll }); - process.exit(0); - }, - ) - .command( - "rollback ", - "Rollback to the previous deployment", - (argv) => argv.positional("projectId", { type: "string", demandOption: true }), - async ({ projectId, token }) => { - await rollbackCommand({ projectId, token }); - process.exit(0); - }, - ) - .command( - "revalidate-all ", - "Revalidate all docs for a deployment", - (argv) => argv.positional("deploymentUrl", { type: "string", demandOption: true }), - async ({ deploymentUrl, token, teamId }) => { - await revalidateAllCommand({ token, teamId, deploymentIdOrUrl: deploymentUrl }); - process.exit(0); - }, - ) - .command( - "preview.txt ", - "Get preview URLs for a deployment", - (argv) => - argv.positional("deploymentUrl", { type: "string", demandOption: true }).option("output", { - type: "string", - description: "The output file to write the preview URLs to", - default: "preview.txt", - }), - async ({ deploymentUrl, token, teamId, output }) => { - const deployment = await new VercelClient({ token }).deployments.getDeployment( - cleanDeploymentId(deploymentUrl), - { teamId, withGitRepoInfo: "false" }, - ); + demandOption: true, + }), + async ({ deploymentUrl, token, teamId }) => { + await revalidateAllCommand({ + token, + teamId, + deploymentIdOrUrl: deploymentUrl, + }); + process.exit(0); + } + ) + .command( + "preview.txt ", + "Get preview URLs for a deployment", + (argv) => + argv + .positional("deploymentUrl", { + type: "string", + demandOption: true, + }) + .option("output", { + type: "string", + description: "The output file to write the preview URLs to", + default: "preview.txt", + }), + async ({ deploymentUrl, token, teamId, output }) => { + const deployment = await new VercelClient({ + token, + }).deployments.getDeployment(cleanDeploymentId(deploymentUrl), { + teamId, + withGitRepoInfo: "false", + }); - if (!deployment.project) { - throw new Error("Deployment does not have a project"); - } + if (!deployment.project) { + throw new Error("Deployment does not have a project"); + } - const revalidator = new FernDocsRevalidator({ token, project: deployment.project.name, teamId }); + const revalidator = new FernDocsRevalidator({ + token, + project: deployment.project.name, + teamId, + }); - const urls = await revalidator.getPreviewUrls(deploymentUrl); + const urls = await revalidator.getPreviewUrls(deploymentUrl); - await writefs(output, `## PR Preview\n\n${urls.map((d) => `- [ ] [${d.name}](${d.url})`).join("\n")}`); + await writefs( + output, + `## PR Preview\n\n${urls.map((d) => `- [ ] [${d.name}](${d.url})`).join("\n")}` + ); - process.exit(0); - }, - ) - .command( - "domains.txt ", - "Get domains for a deployment", - (argv) => - argv.positional("deploymentUrl", { type: "string", demandOption: true }).option("output", { - type: "string", - description: "The output file to write the preview URLs to", - default: "domains.txt", - }), - async ({ deploymentUrl, token, teamId, output }) => { - if (!token) { - throw new Error("VERCEL_TOKEN is required"); - } + process.exit(0); + } + ) + .command( + "domains.txt ", + "Get domains for a deployment", + (argv) => + argv + .positional("deploymentUrl", { + type: "string", + demandOption: true, + }) + .option("output", { + type: "string", + description: "The output file to write the preview URLs to", + default: "domains.txt", + }), + async ({ deploymentUrl, token, teamId, output }) => { + if (!token) { + throw new Error("VERCEL_TOKEN is required"); + } - const deployment = await new VercelClient({ token }).deployments.getDeployment( - cleanDeploymentId(deploymentUrl), - { teamId, withGitRepoInfo: "false" }, - ); + const deployment = await new VercelClient({ + token, + }).deployments.getDeployment(cleanDeploymentId(deploymentUrl), { + teamId, + withGitRepoInfo: "false", + }); - if (!deployment.project) { - throw new Error("Deployment does not have a project"); - } + if (!deployment.project) { + throw new Error("Deployment does not have a project"); + } - const revalidator = new FernDocsRevalidator({ token, project: deployment.project.name, teamId }); + const revalidator = new FernDocsRevalidator({ + token, + project: deployment.project.name, + teamId, + }); - const urls = await revalidator.getDomains(); + const urls = await revalidator.getDomains(); - await writefs(output, urls.join("\n")); + await writefs(output, urls.join("\n")); - process.exit(0); - }, - ) - .command( - "last-deploy.txt ", - "Get the last deployment", - (argv) => - argv - .positional("project", { type: "string", demandOption: true }) - .option("branch", { - type: "string", - description: "The branch to get the last deployment for", - }) - .options("environment", { - type: "string", - description: "The environment to deploy to", - demandOption: true, - default: "preview", - choices: ["preview" as const, "production" as const], - }) - .option("output", { - type: "string", - description: "The output file to write the preview URLs to", - default: "last-deploy.txt", - }), - async ({ project, token, branch, output, environment }) => { - await getLastDeployCommand({ project, token, branch, output, environment }); - process.exit(0); - }, - ) - .command( - "get-deployment ", - "Get a deployment", - (argv) => argv.positional("deploymentId", { type: "string", demandOption: true }), - async ({ deploymentId, token, teamId }) => { - const deployment = await new VercelClient({ token }).deployments.getDeployment(deploymentId, { - teamId, - withGitRepoInfo: "false", - }); + process.exit(0); + } + ) + .command( + "last-deploy.txt ", + "Get the last deployment", + (argv) => + argv + .positional("project", { type: "string", demandOption: true }) + .option("branch", { + type: "string", + description: "The branch to get the last deployment for", + }) + .options("environment", { + type: "string", + description: "The environment to deploy to", + demandOption: true, + default: "preview", + choices: ["preview" as const, "production" as const], + }) + .option("output", { + type: "string", + description: "The output file to write the preview URLs to", + default: "last-deploy.txt", + }), + async ({ project, token, branch, output, environment }) => { + await getLastDeployCommand({ + project, + token, + branch, + output, + environment, + }); + process.exit(0); + } + ) + .command( + "get-deployment ", + "Get a deployment", + (argv) => + argv.positional("deploymentId", { + type: "string", + demandOption: true, + }), + async ({ deploymentId, token, teamId }) => { + const deployment = await new VercelClient({ + token, + }).deployments.getDeployment(deploymentId, { + teamId, + withGitRepoInfo: "false", + }); - // eslint-disable-next-line no-console - console.log(JSON.stringify(deployment, null, 2)); + console.log(JSON.stringify(deployment, null, 2)); - process.exit(0); - }, - ) - .showHelpOnFail(false) - .parse(); + process.exit(0); + } + ) + .showHelpOnFail(false) + .parse(); diff --git a/clis/vercel-scripts/src/commands/deploy.ts b/clis/vercel-scripts/src/commands/deploy.ts index 42ae490dca..36c63eb28f 100644 --- a/clis/vercel-scripts/src/commands/deploy.ts +++ b/clis/vercel-scripts/src/commands/deploy.ts @@ -3,40 +3,39 @@ import { VercelDeployer } from "../utils/deployer.js"; import { assertValidEnvironment } from "../utils/valid-env.js"; interface DeployArgs { - project: string; - environment: string; - token: string; - teamName: string; - teamId: string; - output: string; - skipDeploy?: boolean; + project: string; + environment: string; + token: string; + teamName: string; + teamId: string; + output: string; + skipDeploy?: boolean; } export async function deployCommand({ - project, - environment, - token, - teamName, - teamId, - output, - skipDeploy, + project, + environment, + token, + teamName, + teamId, + output, + skipDeploy, }: DeployArgs): Promise { - assertValidEnvironment(environment); + assertValidEnvironment(environment); - // eslint-disable-next-line no-console - console.log(`Deploying project ${project} to ${environment} environment`); + console.log(`Deploying project ${project} to ${environment} environment`); - const cli = new VercelDeployer({ - token, - teamName, - teamId, - environment, - cwd: await cwd(), - }); + const cli = new VercelDeployer({ + token, + teamName, + teamId, + environment, + cwd: await cwd(), + }); - const result = await cli.buildAndDeployToVercel(project, { skipDeploy }); + const result = await cli.buildAndDeployToVercel(project, { skipDeploy }); - if (result) { - await writefs(output, `https://${result.url}`); - } + if (result) { + await writefs(output, `https://${result.url}`); + } } diff --git a/clis/vercel-scripts/src/commands/last-deploy.ts b/clis/vercel-scripts/src/commands/last-deploy.ts index 6147131dcc..34d4f9c1bc 100644 --- a/clis/vercel-scripts/src/commands/last-deploy.ts +++ b/clis/vercel-scripts/src/commands/last-deploy.ts @@ -3,47 +3,45 @@ import { writefs } from "../cwd.js"; import { assertValidEnvironment } from "../utils/valid-env.js"; interface LastDeployArgs { - project: string; - token: string; - environment: string; - output: string; - branch?: string; + project: string; + token: string; + environment: string; + output: string; + branch?: string; } const GIT_COMMIT_REF = "githubCommitRef"; const GIT_COMMIT_SHA = "githubCommitSha"; export async function getLastDeployCommand({ - project, - token, - environment, - branch, - output, + project, + token, + environment, + branch, + output, }: LastDeployArgs): Promise { - assertValidEnvironment(environment); + assertValidEnvironment(environment); - const vercel = new VercelClient({ token }); + const vercel = new VercelClient({ token }); - const { deployments } = await vercel.deployments.getDeployments({ - projectId: project, - limit: 1, - state: "BUILDING,ERROR,INITIALIZING,QUEUED,READY", - target: environment, - branch, - }); + const { deployments } = await vercel.deployments.getDeployments({ + projectId: project, + limit: 1, + state: "BUILDING,ERROR,INITIALIZING,QUEUED,READY", + target: environment, + branch, + }); - const meta = deployments[0]?.meta; - const sha = meta?.[GIT_COMMIT_SHA]; - const ref = meta?.[GIT_COMMIT_REF]; + const meta = deployments[0]?.meta; + const sha = meta?.[GIT_COMMIT_SHA]; + const ref = meta?.[GIT_COMMIT_REF]; - if (sha) { - // eslint-disable-next-line no-console - console.log(`Last deploy ref: ${sha} (${ref ?? branch})`, deployments[0]); - await writefs(output, sha); - } else { - // eslint-disable-next-line no-console - console.error( - `No ${environment} deployment found for project ${project} ${branch != null ? `on branch ${branch}` : ""}`, - ); - } + if (sha) { + console.log(`Last deploy ref: ${sha} (${ref ?? branch})`, deployments[0]); + await writefs(output, sha); + } else { + console.error( + `No ${environment} deployment found for project ${project} ${branch != null ? `on branch ${branch}` : ""}` + ); + } } diff --git a/clis/vercel-scripts/src/commands/promote.ts b/clis/vercel-scripts/src/commands/promote.ts index a5b080745f..cf61f2e1d8 100644 --- a/clis/vercel-scripts/src/commands/promote.ts +++ b/clis/vercel-scripts/src/commands/promote.ts @@ -4,23 +4,31 @@ import { requestPromote } from "../utils/promoter.js"; import { revalidateAllCommand } from "./revalidate-all.js"; interface PromoteArgs { - deploymentIdOrUrl: string; - token: string; - teamId: string; - revalidateAll?: boolean; + deploymentIdOrUrl: string; + token: string; + teamId: string; + revalidateAll?: boolean; } -export async function promoteCommand({ deploymentIdOrUrl, token, teamId, revalidateAll }: PromoteArgs): Promise { - const vercel = new VercelClient({ token }); +export async function promoteCommand({ + deploymentIdOrUrl, + token, + teamId, + revalidateAll, +}: PromoteArgs): Promise { + const vercel = new VercelClient({ token }); - const deployment = await vercel.deployments.getDeployment(cleanDeploymentId(deploymentIdOrUrl), { - teamId, - withGitRepoInfo: "false", - }); + const deployment = await vercel.deployments.getDeployment( + cleanDeploymentId(deploymentIdOrUrl), + { + teamId, + withGitRepoInfo: "false", + } + ); - await requestPromote(token, deployment); + await requestPromote(token, deployment); - if (revalidateAll) { - await revalidateAllCommand({ token, teamId, deployment }); - } + if (revalidateAll) { + await revalidateAllCommand({ token, teamId, deployment }); + } } diff --git a/clis/vercel-scripts/src/commands/revalidate-all.ts b/clis/vercel-scripts/src/commands/revalidate-all.ts index 6d8c4e901a..217aa2f105 100644 --- a/clis/vercel-scripts/src/commands/revalidate-all.ts +++ b/clis/vercel-scripts/src/commands/revalidate-all.ts @@ -4,31 +4,39 @@ import { cleanDeploymentId } from "../utils/clean-id.js"; import { FernDocsRevalidator } from "../utils/revalidator.js"; interface RevalidateAllArgs { - token: string; - teamId: string; - deployment?: GetDeploymentResponse; - deploymentIdOrUrl?: string; + token: string; + teamId: string; + deployment?: GetDeploymentResponse; + deploymentIdOrUrl?: string; } export async function revalidateAllCommand({ - token, - teamId, - deployment, - deploymentIdOrUrl, + token, + teamId, + deployment, + deploymentIdOrUrl, }: RevalidateAllArgs): Promise { - if (!deployment) { - if (!deploymentIdOrUrl) { - throw new Error("Either deployment or deploymentIdOrUrl must be provided"); - } - - const vercel = new VercelClient({ token }); - deployment = await vercel.deployments.getDeployment(cleanDeploymentId(deploymentIdOrUrl)); + if (!deployment) { + if (!deploymentIdOrUrl) { + throw new Error( + "Either deployment or deploymentIdOrUrl must be provided" + ); } - if (!deployment.project) { - throw new Error("Deployment does not have a project"); - } + const vercel = new VercelClient({ token }); + deployment = await vercel.deployments.getDeployment( + cleanDeploymentId(deploymentIdOrUrl) + ); + } - const revalidator = new FernDocsRevalidator({ token, project: deployment.project.id, teamId }); - await revalidator.revalidateAll(); + if (!deployment.project) { + throw new Error("Deployment does not have a project"); + } + + const revalidator = new FernDocsRevalidator({ + token, + project: deployment.project.id, + teamId, + }); + await revalidator.revalidateAll(); } diff --git a/clis/vercel-scripts/src/commands/rollback.ts b/clis/vercel-scripts/src/commands/rollback.ts index 0f4da3af97..8d0ca0ad26 100644 --- a/clis/vercel-scripts/src/commands/rollback.ts +++ b/clis/vercel-scripts/src/commands/rollback.ts @@ -5,36 +5,41 @@ import { VercelClient } from "@fern-fern/vercel"; * * This should be used ONLY when a production deployment has failed. */ -export async function rollbackCommand({ projectId, token }: { projectId: string; token: string }): Promise { - const vercel = new VercelClient({ token }); +export async function rollbackCommand({ + projectId, + token, +}: { + projectId: string; + token: string; +}): Promise { + const vercel = new VercelClient({ token }); - const { deployments } = await vercel.deployments.getDeployments({ - projectId, - rollbackCandidate: true, - state: "READY", - target: "production", - limit: 2, - }); + const { deployments } = await vercel.deployments.getDeployments({ + projectId, + rollbackCandidate: true, + state: "READY", + target: "production", + limit: 2, + }); - if (!deployments[1]) { - throw new Error("No rollback candidate found."); - } + if (!deployments[1]) { + throw new Error("No rollback candidate found."); + } - const deployment = deployments[1]; + const deployment = deployments[1]; - /** - * Vercel's OpenAPI doesn't have a rollback endpoint, so we have to use the API directly - */ - await fetch(`/v9/projects/${projectId}/rollback/${deployment.uid}`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - // required - body: JSON.stringify({}), - }); + /** + * Vercel's OpenAPI doesn't have a rollback endpoint, so we have to use the API directly + */ + await fetch(`/v9/projects/${projectId}/rollback/${deployment.uid}`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + // required + body: JSON.stringify({}), + }); - // eslint-disable-next-line no-console - console.log(`Successfully rolled back ${projectId} to ${deployment.url}`); + console.log(`Successfully rolled back ${projectId} to ${deployment.url}`); } diff --git a/clis/vercel-scripts/src/cwd.ts b/clis/vercel-scripts/src/cwd.ts index ef7f78c575..4fcc2ad53d 100644 --- a/clis/vercel-scripts/src/cwd.ts +++ b/clis/vercel-scripts/src/cwd.ts @@ -5,26 +5,28 @@ import { loggingExeca } from "./utils/loggingExeca.js"; let _cwd: string | undefined; export async function cwd(): Promise { - if (_cwd) { - return _cwd; - } + if (_cwd) { + return _cwd; + } - const result = await loggingExeca("Get git project root", "git", ["rev-parse", "--show-toplevel"]); + const result = await loggingExeca("Get git project root", "git", [ + "rev-parse", + "--show-toplevel", + ]); - const cwd = String(result.stdout).trim(); + const cwd = String(result.stdout).trim(); - if (!cwd.startsWith("/")) { - throw new Error("Could not detect git project root directory"); - } + if (!cwd.startsWith("/")) { + throw new Error("Could not detect git project root directory"); + } - _cwd = cwd; + _cwd = cwd; - // eslint-disable-next-line no-console - console.log("Detected monorepo root directory:", cwd); + console.log("Detected monorepo root directory:", cwd); - return cwd; + return cwd; } export async function writefs(output: string, content: string): Promise { - writeFileSync(join(await cwd(), output), content); + writeFileSync(join(await cwd(), output), content); } diff --git a/clis/vercel-scripts/src/utils/clean-id.ts b/clis/vercel-scripts/src/utils/clean-id.ts index 6ab5887012..cc8f1faeba 100644 --- a/clis/vercel-scripts/src/utils/clean-id.ts +++ b/clis/vercel-scripts/src/utils/clean-id.ts @@ -1,7 +1,7 @@ export function cleanDeploymentId(deploymentIdOrUrl: string): string { - const toReplace = deploymentIdOrUrl.replace("https://", ""); - if (toReplace.length === 0) { - throw new Error(`Invalid deployment ID or URL: ${deploymentIdOrUrl}`); - } - return toReplace; + const toReplace = deploymentIdOrUrl.replace("https://", ""); + if (toReplace.length === 0) { + throw new Error(`Invalid deployment ID or URL: ${deploymentIdOrUrl}`); + } + return toReplace; } diff --git a/clis/vercel-scripts/src/utils/deployer.ts b/clis/vercel-scripts/src/utils/deployer.ts index 2b5aa4c164..e69bb79a8a 100644 --- a/clis/vercel-scripts/src/utils/deployer.ts +++ b/clis/vercel-scripts/src/utils/deployer.ts @@ -7,156 +7,190 @@ import { loggingExeca } from "./loggingExeca.js"; import { requestPromote } from "./promoter.js"; export class VercelDeployer { - private token: string; - private teamName: string; - private teamId: string; - private environment: "preview" | "production"; - private cwd: string; - public vercel: VercelClient; - constructor({ - token, - teamName, - teamId, - environment, - cwd, - }: { - token: string; - teamName: string; - teamId: string; - environment: "preview" | "production"; - cwd: string; - }) { - this.token = token; - this.teamName = teamName; - this.teamId = teamId; - this.environment = environment; - this.cwd = cwd; - this.vercel = new VercelClient({ token }); + private token: string; + private teamName: string; + private teamId: string; + private environment: "preview" | "production"; + private cwd: string; + public vercel: VercelClient; + constructor({ + token, + teamName, + teamId, + environment, + cwd, + }: { + token: string; + teamName: string; + teamId: string; + environment: "preview" | "production"; + cwd: string; + }) { + this.token = token; + this.teamName = teamName; + this.teamId = teamId; + this.environment = environment; + this.cwd = cwd; + this.vercel = new VercelClient({ token }); + } + + get environmentName(): string { + if (this.environment === "preview") { + return "Preview"; + } else if (this.environment === "production") { + return "Production"; + } else if (this.environment === "production-dev2") { + return "Production Dev2"; + } else { + throw new UnreachableCaseError(this.environment); } - - get environmentName(): string { - if (this.environment === "preview") { - return "Preview"; - } else if (this.environment === "production") { - return "Production"; - } else if (this.environment === "production-dev2") { - return "Production Dev2"; - } else { - throw new UnreachableCaseError(this.environment); - } + } + + private env(projectId: string): Record { + return { + TURBO_TOKEN: this.token, + TURBO_TEAM: this.teamName, + VERCEL_ORG_ID: this.teamId, + VERCEL_PROJECT_ID: projectId, + }; + } + + private async pull(project: { id: string; name: string }): Promise { + const args = [ + "vercel", + "pull", + "--yes", + `--environment=${this.environment}`, + `--token=${this.token}`, + ]; + await loggingExeca( + `[${this.environmentName}] Pull ${project.name} from Vercel (${project.id})`, + "pnpx", + args, + { + env: this.env(project.id), + cwd: this.cwd, + } + ); + } + + private async build(project: { id: string; name: string }): Promise { + // let command = `pnpx vercel build --yes --token=${this.token} --debug`; + const args = [ + "vercel", + "build", + "--yes", + `--token=${this.token}`, + "--debug", + ]; + if (this.environment === "production") { + args.push("--prod"); } - - private env(projectId: string): Record { - return { - TURBO_TOKEN: this.token, - TURBO_TEAM: this.teamName, - VERCEL_ORG_ID: this.teamId, - VERCEL_PROJECT_ID: projectId, - }; + await loggingExeca( + `[${this.environmentName}] Build bundle for ${project.name}`, + "pnpx", + args, + { + env: this.env(project.id), + cwd: this.cwd, + } + ); + } + + private async deploy( + project: { id: string; name: string }, + opts?: { prebuilt?: boolean } + ): Promise { + // let command = `pnpx vercel deploy --yes --token=${this.token} --archive=tgz`; + const args = [ + "vercel", + "deploy", + "--yes", + `--token=${this.token}`, + "--archive=tgz", + ]; + + if (opts?.prebuilt) { + args.push("--prebuilt"); } - private async pull(project: { id: string; name: string }): Promise { - const args = ["vercel", "pull", "--yes", `--environment=${this.environment}`, `--token=${this.token}`]; - await loggingExeca(`[${this.environmentName}] Pull ${project.name} from Vercel (${project.id})`, "pnpx", args, { - env: this.env(project.id), - cwd: this.cwd, - }); + if (this.environment === "production") { + args.push("--prod", "--skip-domain"); } - - private async build(project: { id: string; name: string }): Promise { - // let command = `pnpx vercel build --yes --token=${this.token} --debug`; - const args = ["vercel", "build", "--yes", `--token=${this.token}`, "--debug"]; - if (this.environment === "production") { - args.push("--prod"); - } - await loggingExeca(`[${this.environmentName}] Build bundle for ${project.name}`, "pnpx", args, { - env: this.env(project.id), - cwd: this.cwd, - }); + const result = await loggingExeca( + `[${this.environmentName}] Deploy bundle for ${project.name} to Vercel`, + "pnpx", + args, + { + env: this.env(project.id), + cwd: this.cwd, + } + ); + + const deploymentUrl = String(result.stdout).trim(); + + if (!deploymentUrl) { + throw new Error("Deployment failed: no deployment URL returned"); } - private async deploy( - project: { id: string; name: string }, - opts?: { prebuilt?: boolean }, - ): Promise { - // let command = `pnpx vercel deploy --yes --token=${this.token} --archive=tgz`; - const args = ["vercel", "deploy", "--yes", `--token=${this.token}`, "--archive=tgz"]; - - if (opts?.prebuilt) { - args.push("--prebuilt"); - } - - if (this.environment === "production") { - args.push("--prod", "--skip-domain"); - } - const result = await loggingExeca( - `[${this.environmentName}] Deploy bundle for ${project.name} to Vercel`, - "pnpx", - args, - { - env: this.env(project.id), - cwd: this.cwd, - }, - ); - - const deploymentUrl = String(result.stdout).trim(); - - if (!deploymentUrl) { - throw new Error("Deployment failed: no deployment URL returned"); - } + const deployment = await this.vercel.deployments.getDeployment( + cleanDeploymentId(deploymentUrl) + ); - const deployment = await this.vercel.deployments.getDeployment(cleanDeploymentId(deploymentUrl)); + // logCommand(`[${this.environmentName}] Deployment URL: https://${deployment.url}`); - // logCommand(`[${this.environmentName}] Deployment URL: https://${deployment.url}`); + // if ("inspectorUrl" in deployment) { + // logCommand(`[${this.environmentName}] Inspector URL: ${deployment.inspectorUrl}`); + // } - // if ("inspectorUrl" in deployment) { - // logCommand(`[${this.environmentName}] Inspector URL: ${deployment.inspectorUrl}`); - // } + console.log("Deployment Source:", deployment.source); - // eslint-disable-next-line no-console - console.log("Deployment Source:", deployment.source); + return deployment; + } - return deployment; + private async promote( + deployment: Vercel.GetDeploymentResponse + ): Promise { + if (this.environment === "production") { + const isDev2 = this.loadEnvFile().includes( + "registry-dev2.buildwithfern.com" + ); + if (!isDev2) { + return; + } + await requestPromote(this.token, deployment); } - - private async promote(deployment: Vercel.GetDeploymentResponse): Promise { - if (this.environment === "production") { - const isDev2 = this.loadEnvFile().includes("registry-dev2.buildwithfern.com"); - if (!isDev2) { - return; - } - await requestPromote(this.token, deployment); - } + } + + public async buildAndDeployToVercel( + project: string, + { skipDeploy = false }: { skipDeploy?: boolean } = {} + ): Promise { + const prj = await this.vercel.projects.getProject(project, { + teamId: this.teamId, + }); + + await this.pull(prj); + + if (skipDeploy) { + // build-only + await this.build(prj); + return; } - public async buildAndDeployToVercel( - project: string, - { skipDeploy = false }: { skipDeploy?: boolean } = {}, - ): Promise { - const prj = await this.vercel.projects.getProject(project, { teamId: this.teamId }); - - await this.pull(prj); - - if (skipDeploy) { - // build-only - await this.build(prj); - return; - } + const deployment = await this.deploy(prj, { prebuilt: false }); - const deployment = await this.deploy(prj, { prebuilt: false }); + await this.promote(deployment); - await this.promote(deployment); + return deployment; + } - return deployment; - } - - private loadEnvFile(): string { - const dotvercel = join(this.cwd, ".vercel"); - const envfile = join(dotvercel, `.env.${this.environment}.local`); + private loadEnvFile(): string { + const dotvercel = join(this.cwd, ".vercel"); + const envfile = join(dotvercel, `.env.${this.environment}.local`); - // eslint-disable-next-line no-console - console.log("Loading env file:", envfile); + console.log("Loading env file:", envfile); - return readFileSync(envfile, "utf-8"); - } + return readFileSync(envfile, "utf-8"); + } } diff --git a/clis/vercel-scripts/src/utils/global-config.ts b/clis/vercel-scripts/src/utils/global-config.ts index 0368a9858e..10efab0cc9 100644 --- a/clis/vercel-scripts/src/utils/global-config.ts +++ b/clis/vercel-scripts/src/utils/global-config.ts @@ -2,32 +2,33 @@ import { readFileSync } from "fs"; import { homedir } from "os"; import { join } from "path"; -const DARWIN_AUTH_PATH = "~/Library/Application Support/com.vercel.cli/auth.json"; +const DARWIN_AUTH_PATH = + "~/Library/Application Support/com.vercel.cli/auth.json"; const LINUX_AUTH_PATH = "~/.local/share/com.vercel.cli/auth.json"; function getAuthJson(): string | undefined { - try { - if (process.platform === "darwin") { - return String(readFileSync(join(homedir(), DARWIN_AUTH_PATH.slice(1)))); - } else if (process.platform === "linux") { - return String(readFileSync(join(homedir(), LINUX_AUTH_PATH.slice(1)))); - } - } catch (_e) { - // do nothing + try { + if (process.platform === "darwin") { + return String(readFileSync(join(homedir(), DARWIN_AUTH_PATH.slice(1)))); + } else if (process.platform === "linux") { + return String(readFileSync(join(homedir(), LINUX_AUTH_PATH.slice(1)))); } - return undefined; + } catch (_e) { + // do nothing + } + return undefined; } export function getVercelTokenFromGlobalConfig(): string | undefined { - const authJson = getAuthJson(); + const authJson = getAuthJson(); - if (authJson == null) { - return undefined; - } - try { - const auth = JSON.parse(authJson); - return auth.token; - } catch (_e) { - return undefined; - } + if (authJson == null) { + return undefined; + } + try { + const auth = JSON.parse(authJson); + return auth.token; + } catch (_e) { + return undefined; + } } diff --git a/clis/vercel-scripts/src/utils/loggingExeca.ts b/clis/vercel-scripts/src/utils/loggingExeca.ts index 63e396a873..808b4dee66 100644 --- a/clis/vercel-scripts/src/utils/loggingExeca.ts +++ b/clis/vercel-scripts/src/utils/loggingExeca.ts @@ -1,48 +1,55 @@ import { Options as ExecaOptions, Result, execa } from "execa"; export declare namespace loggingExeca { - export interface Options extends ExecaOptions { - doNotPipeOutput?: boolean; - secrets?: string[]; - substitutions?: Record; - } + export interface Options extends ExecaOptions { + doNotPipeOutput?: boolean; + secrets?: string[]; + substitutions?: Record; + } - export type ReturnValue = Result; + export type ReturnValue = Result; } export async function loggingExeca( - title: string, - executable: string, - args: string[] = [], - { doNotPipeOutput = false, secrets = [], substitutions = {}, ...execaOptions }: loggingExeca.Options = {}, + title: string, + executable: string, + args: string[] = [], + { + doNotPipeOutput = false, + secrets = [], + substitutions = {}, + ...execaOptions + }: loggingExeca.Options = {} ): Promise { - const allSubstitutions = secrets.reduce( - (acc, secret) => ({ - ...acc, - [secret]: "", - }), - substitutions, - ); - - let logLine = [executable, ...args].join(" "); - for (const [substitutionKey, substitutionValue] of Object.entries(allSubstitutions)) { - logLine = logLine.replaceAll(substitutionKey, substitutionValue); - } - - logCommand(title, logLine); - const command = execa(executable, args, execaOptions); - if (!doNotPipeOutput) { - command.stdout?.pipe(process.stdout); - command.stderr?.pipe(process.stderr); - } - return command; + const allSubstitutions = secrets.reduce( + (acc, secret) => ({ + ...acc, + [secret]: "", + }), + substitutions + ); + + let logLine = [executable, ...args].join(" "); + for (const [substitutionKey, substitutionValue] of Object.entries( + allSubstitutions + )) { + logLine = logLine.replaceAll(substitutionKey, substitutionValue); + } + + logCommand(title, logLine); + const command = execa(executable, args, execaOptions); + if (!doNotPipeOutput) { + command.stdout?.pipe(process.stdout); + command.stderr?.pipe(process.stderr); + } + return command; } export function prettyCommand(command: string | string[]): string { - if (Array.isArray(command)) { - command = command.join(" "); - } - return command.replace(/ -- .*/, " -- …"); + if (Array.isArray(command)) { + command = command.join(" "); + } + return command.replace(/ -- .*/, " -- …"); } /** @@ -50,12 +57,11 @@ export function prettyCommand(command: string | string[]): string { * @param {string | string[]} [command] */ export function logCommand(title: string, command?: string | string[]): void { - if (command) { - const pretty = prettyCommand(command); - // eslint-disable-next-line no-console - console.log(`\n\x1b[1;4m${title}\x1b[0m\n> \x1b[1m${pretty}\x1b[0m\n`); - } else { - // eslint-disable-next-line no-console - console.log(`\n\x1b[1;4m${title}\x1b[0m\n`); - } + if (command) { + const pretty = prettyCommand(command); + + console.log(`\n\x1b[1;4m${title}\x1b[0m\n> \x1b[1m${pretty}\x1b[0m\n`); + } else { + console.log(`\n\x1b[1;4m${title}\x1b[0m\n`); + } } diff --git a/clis/vercel-scripts/src/utils/promoter.ts b/clis/vercel-scripts/src/utils/promoter.ts index 0d3501200c..25832fc48b 100644 --- a/clis/vercel-scripts/src/utils/promoter.ts +++ b/clis/vercel-scripts/src/utils/promoter.ts @@ -1,47 +1,53 @@ import { Vercel } from "@fern-fern/vercel"; import { logCommand } from "./loggingExeca.js"; -export async function requestPromote(token: string, deployment: Vercel.GetDeploymentResponse): Promise { - logCommand(`[Production] Promote ${deployment.url}`); - - if (deployment.target !== "production") { - throw new Error("Deployment is not a production deployment"); - } - - if (deployment.readyState !== "READY") { - throw new Error("Deployment is not ready"); - } - - if (deployment.readySubstate === "PROMOTED") { - // eslint-disable-next-line no-console - console.log(`Deployment ${deployment.name} is already promoted`); - return; - } - - if (deployment.readySubstate !== "STAGED") { - throw new Error("Deployment is not staged for promotion"); - } - - if (!deployment.project) { - throw new Error("Deployment has no project"); +export async function requestPromote( + token: string, + deployment: Vercel.GetDeploymentResponse +): Promise { + logCommand(`[Production] Promote ${deployment.url}`); + + if (deployment.target !== "production") { + throw new Error("Deployment is not a production deployment"); + } + + if (deployment.readyState !== "READY") { + throw new Error("Deployment is not ready"); + } + + if (deployment.readySubstate === "PROMOTED") { + console.log(`Deployment ${deployment.name} is already promoted`); + return; + } + + if (deployment.readySubstate !== "STAGED") { + throw new Error("Deployment is not staged for promotion"); + } + + if (!deployment.project) { + throw new Error("Deployment has no project"); + } + + /** + * The vercel promote cli command does not accept tokens as an argument, so we have to use the API directly + * + * Note: the fern-generated SDK doesn't work for this, so we have to use fetch directly + */ + // await vercel.projects.requestPromote(deployment.project.id, deployment.id, { teamId }); + await fetch( + `https://api.vercel.com/v10/projects/${deployment.project.id}/promote/${deployment.id}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + // required + body: JSON.stringify({}), } + ); - /** - * The vercel promote cli command does not accept tokens as an argument, so we have to use the API directly - * - * Note: the fern-generated SDK doesn't work for this, so we have to use fetch directly - */ - // await vercel.projects.requestPromote(deployment.project.id, deployment.id, { teamId }); - await fetch(`https://api.vercel.com/v10/projects/${deployment.project.id}/promote/${deployment.id}`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - // required - body: JSON.stringify({}), - }); - - // eslint-disable-next-line no-console - console.log(`Successfully requested promote of ${deployment.name} to ${deployment.project.name}`); + console.log( + `Successfully requested promote of ${deployment.name} to ${deployment.project.name}` + ); } diff --git a/clis/vercel-scripts/src/utils/revalidator.ts b/clis/vercel-scripts/src/utils/revalidator.ts index cc37fc42f8..96cad276b4 100644 --- a/clis/vercel-scripts/src/utils/revalidator.ts +++ b/clis/vercel-scripts/src/utils/revalidator.ts @@ -5,104 +5,121 @@ import { logCommand } from "./loggingExeca.js"; const BANNED_DOMAINS = ["vercel.app", "buildwithfern.com", "ferndocs.com"]; export class FernDocsRevalidator { - private vercel: VercelClient; - private project: string; - private teamId: string; - constructor({ token, project, teamId }: { token: string; project: string; teamId: string }) { - if (!token) { - throw new Error("VERCEL_TOKEN is required"); - } - this.vercel = new VercelClient({ token }); - this.project = project; - this.teamId = teamId; + private vercel: VercelClient; + private project: string; + private teamId: string; + constructor({ + token, + project, + teamId, + }: { + token: string; + project: string; + teamId: string; + }) { + if (!token) { + throw new Error("VERCEL_TOKEN is required"); } - - // TODO: this should not be using vercel as the source of truth for domains becuase vercel's domain list does not include all subpath (proxied) domains. - private async *getProductionDomains(): AsyncGenerator { - let cursor: number | undefined = undefined; - do { - const res = await this.vercel.projects.getProjectDomains(this.project, { - teamId: this.teamId, - production: "true", - verified: "true", - limit: 50, - order: "ASC", - since: cursor ? cursor + 1 : undefined, - }); - - for (const domain of res.domains.filter((domain) => !BANNED_DOMAINS.includes(domain.apexName))) { - yield domain; - } - cursor = res.pagination.next; - } while (cursor); + this.vercel = new VercelClient({ token }); + this.project = project; + this.teamId = teamId; + } + + // TODO: this should not be using vercel as the source of truth for domains becuase vercel's domain list does not include all subpath (proxied) domains. + private async *getProductionDomains(): AsyncGenerator< + Vercel.GetProjectDomainsResponseDomainsItem, + void, + unknown + > { + let cursor: number | undefined = undefined; + do { + const res = await this.vercel.projects.getProjectDomains(this.project, { + teamId: this.teamId, + production: "true", + verified: "true", + limit: 50, + order: "ASC", + since: cursor ? cursor + 1 : undefined, + }); + + for (const domain of res.domains.filter( + (domain) => !BANNED_DOMAINS.includes(domain.apexName) + )) { + yield domain; + } + cursor = res.pagination.next; + } while (cursor); + } + + async getDomains(): Promise { + const domains: string[] = []; + for await (const domain of this.getProductionDomains()) { + domains.push(domain.name); } + return domains.sort(); + } - async getDomains(): Promise { - const domains: string[] = []; - for await (const domain of this.getProductionDomains()) { - domains.push(domain.name); - } - return domains.sort(); + async getPreviewUrls( + deploymentUrl: string + ): Promise<{ url: string; name: string }[]> { + const url = new URL("/api/fern-docs/preview", deploymentUrl); + + const urls: { url: string; name: string }[] = []; + + for await (const domain of this.getProductionDomains()) { + url.searchParams.set("host", domain.name); + urls.push({ url: url.toString(), name: domain.name }); } - async getPreviewUrls(deploymentUrl: string): Promise<{ url: string; name: string }[]> { - const url = new URL("/api/fern-docs/preview", deploymentUrl); + return urls.sort((a, b) => a.name.localeCompare(b.name)); + } - const urls: { url: string; name: string }[] = []; + async revalidateAll(): Promise { + logCommand("Revalidating all docs"); - for await (const domain of this.getProductionDomains()) { - url.searchParams.set("host", domain.name); - urls.push({ url: url.toString(), name: domain.name }); - } + const summary: Record = {}; + const failedDomains: string[] = []; - return urls.sort((a, b) => a.name.localeCompare(b.name)); - } + for await (const domain of this.getProductionDomains()) { + console.log(`Revalidating ${domain.name}...`); - async revalidateAll(): Promise { - logCommand("Revalidating all docs"); - - const summary: Record = {}; - const failedDomains: string[] = []; - - for await (const domain of this.getProductionDomains()) { - // eslint-disable-next-line no-console - console.log(`Revalidating ${domain.name}...`); - - const client = new FernDocsClient({ - // TODO: handle docs with basepath - environment: `https://${domain.name}`, - }); - - try { - const results = await client.revalidation.revalidateAllV4({ limit: 100 }); - - const revalidationSummary = (summary[domain.name] = { success: 0, failed: 0 }); - for await (const result of results) { - if (!result.success) { - // eslint-disable-next-line no-console - console.warn(`[${domain.name}] Failed to revalidate ${result.url}: ${result.error}`); - revalidationSummary.failed++; - } else { - revalidationSummary.success++; - } - } - summary[domain.name] = revalidationSummary; - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Failed to revalidate ${domain.name}: ${error}`); - failedDomains.push(domain.name); - } - } + const client = new FernDocsClient({ + // TODO: handle docs with basepath + environment: `https://${domain.name}`, + }); - // eslint-disable-next-line no-console - logCommand("Revalidation summary"); - Object.entries(summary).forEach(([domain, { success, failed }]) => { - // eslint-disable-next-line no-console - console.log(`- ${domain}: ${success} successful, ${failed} failed`); + try { + const results = await client.revalidation.revalidateAllV4({ + limit: 100, }); - failedDomains.forEach((domain) => { - // eslint-disable-next-line no-console - console.error(`- ${domain}: Failed to revalidate`); + + const revalidationSummary = (summary[domain.name] = { + success: 0, + failed: 0, }); + for await (const result of results) { + if (!result.success) { + console.warn( + `[${domain.name}] Failed to revalidate ${result.url}: ${result.error}` + ); + revalidationSummary.failed++; + } else { + revalidationSummary.success++; + } + } + summary[domain.name] = revalidationSummary; + } catch (error) { + console.error(`Failed to revalidate ${domain.name}: ${error}`); + failedDomains.push(domain.name); + } } + + logCommand("Revalidation summary"); + Object.entries(summary).forEach(([domain, { success, failed }]) => { + console.log(`- ${domain}: ${success} successful, ${failed} failed`); + }); + failedDomains.forEach((domain) => { + console.error(`- ${domain}: Failed to revalidate`); + }); + } } diff --git a/clis/vercel-scripts/src/utils/valid-env.ts b/clis/vercel-scripts/src/utils/valid-env.ts index 7ea07eb23a..2825fd2ac9 100644 --- a/clis/vercel-scripts/src/utils/valid-env.ts +++ b/clis/vercel-scripts/src/utils/valid-env.ts @@ -1,11 +1,13 @@ type Environment = "preview" | "production"; function isValidEnvironment(environment: string): environment is Environment { - return environment === "preview" || environment === "production"; + return environment === "preview" || environment === "production"; } -export function assertValidEnvironment(environment: string): asserts environment is Environment { - if (!isValidEnvironment(environment)) { - throw new Error(`Invalid environment: ${environment}`); - } +export function assertValidEnvironment( + environment: string +): asserts environment is Environment { + if (!isValidEnvironment(environment)) { + throw new Error(`Invalid environment: ${environment}`); + } } diff --git a/clis/vercel-scripts/tsconfig.eslint.json b/clis/vercel-scripts/tsconfig.eslint.json new file mode 100644 index 0000000000..08c2772f9e --- /dev/null +++ b/clis/vercel-scripts/tsconfig.eslint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..84c8416df6 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,197 @@ +// @ts-check + +import { FlatCompat } from "@eslint/eslintrc"; +import eslint from "@eslint/js"; +import vitest from "eslint-plugin-vitest"; +import tseslint from "typescript-eslint"; + +const compat = new FlatCompat({ + baseDirectory: import.meta.dirname, + recommendedConfig: eslint.configs.recommended, + allConfig: eslint.configs.all, +}); + +export default tseslint.config( + { + ignores: [ + "**/generated", + "**/dist", + "**/build", + "**/.next", + "**/storybook-static", + "**/out", + "**/node_modules", + "fern/**", + ], + }, + eslint.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parserOptions: { + project: ["./tsconfig.eslint.json"], + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + + ...compat.config({ + extends: [ + "prettier", + "next/core-web-vitals", + "next/typescript", + "plugin:tailwindcss/recommended", + ], + settings: { + next: { + rootDir: [ + "packages/fern-docs/bundle", + "packages/fern-docs/local-preview-bundle", + "packages/fern-docs/search-ui", + ], + }, + react: { + version: "18", + }, + }, + }), + + { + files: ["**/*.test.ts", "**/*.test.tsx"], + plugins: { + vitest, + }, + rules: { + ...vitest.configs.recommended.rules, + "@typescript-eslint/no-empty-function": "off", + }, + settings: { + vitest: { + typecheck: true, + }, + }, + languageOptions: { + globals: { + ...vitest.environments.env.globals, + }, + }, + }, + + { + rules: { + "no-unused-vars": "off", + "no-undef": "off", + "no-empty": ["error", { allowEmptyCatch: true }], + eqeqeq: ["error", "always", { null: "never" }], + "@typescript-eslint/no-deprecated": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + varsIgnorePattern: "^_", + argsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + "@typescript-eslint/no-empty-function": [ + "error", + { + allow: [ + "private-constructors", + "protected-constructors", + "decoratedFunctions", + ], + }, + ], + "@typescript-eslint/no-namespace": ["error", { allowDeclarations: true }], + "@next/next/no-html-link-for-pages": "off", + "react-hooks/exhaustive-deps": [ + "warn", + { + additionalHooks: "(useMemoOne|useCallbackOne)", + }, + ], + "tailwindcss/no-custom-classname": "off", + "tailwindcss/classnames-order": "off", + }, + }, + + { + rules: { + // TODO: remove these: + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/no-confusing-void-expression": "off", + "@typescript-eslint/no-duplicate-type-constituents": "off", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-redundant-type-constituents": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unnecessary-condition": "off", + "@typescript-eslint/no-unnecessary-type-parameters": "off", + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/prefer-find": "off", + "@typescript-eslint/prefer-nullish-coalescing": "off", + "@typescript-eslint/prefer-regexp-exec": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/require-await": "off", + "import/no-anonymous-default-export": "off", + }, + }, + { + files: [ + "packages/fern-docs/**/*", + "packages/commons/**/*", + "packages/scripts/**/*", + "packages/healthchecks/**/*", + "packages/template-resolver/**/*", + ], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-base-to-string": "off", + }, + }, + { + files: ["packages/parsers/**/*"], + rules: { + "@typescript-eslint/unbound-method": "off", + "@typescript-eslint/no-invalid-void-type": "off", + }, + }, + { + files: ["servers/fern-bot/**/*"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/return-await": "off", + "@typescript-eslint/prefer-promise-reject-errors": "off", + "@typescript-eslint/no-deprecated": "off", + }, + }, + { + files: [ + "packages/fern-docs/search-server/src/algolia/records/archive/**/*", + ], + rules: { + "@typescript-eslint/no-base-to-string": "off", + }, + }, + { + files: ["servers/fdr/src/**/*"], + rules: { + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-base-to-string": "off", + "@typescript-eslint/no-explicit-any": "off", + eqeqeq: "off", + }, + } +); diff --git a/guides/express-openapi-spec.md b/guides/express-openapi-spec.md index bff7f3674f..de8bf42593 100644 --- a/guides/express-openapi-spec.md +++ b/guides/express-openapi-spec.md @@ -12,7 +12,13 @@ Assume you have an Express backend with following set of routes: // src/routes/user.route.js const { Router } = require("express"); -const { createUser, deleteUser, getAllUsers, getUser, updateUser } = require("../controllers/user.controller"); +const { + createUser, + deleteUser, + getAllUsers, + getUser, + updateUser, +} = require("../controllers/user.controller"); const userRoute = () => { const router = Router(); @@ -37,7 +43,13 @@ module.exports = { userRoute }; // src/routes/book.route.js const { Router } = require("express"); -const { createBook, deleteBook, getAllBooks, getBook, updateBook } = require("../controllers/book.controller"); +const { + createBook, + deleteBook, + getAllBooks, + getBook, + updateBook, +} = require("../controllers/book.controller"); const bookRoute = () => { const router = Router(); @@ -101,7 +113,10 @@ In the root of your project, create a file `swagger.js` and add the following co const swaggerAutogen = require("swagger-autogen")(); const outputFile = "./swagger_output.json"; -const endpointsFiles = ["./src/routes/user.route.js", "./src/routes/book.route.js"]; +const endpointsFiles = [ + "./src/routes/user.route.js", + "./src/routes/book.route.js", +]; swaggerAutogen(outputFile, endpointsFiles); ``` diff --git a/lint-staged.config.js b/lint-staged.config.js index 272d2b5087..6bc1b68e34 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,5 +1,5 @@ module.exports = { - "**/*.ts{,x}": ["eslint --fix --max-warnings 0 --no-eslintrc --config .eslintrc.lint-staged.js", "pnpm format"], - "**/*.{js,json,yml,html,css,less,scss,md}": "pnpm format", - "**/package.json": () => "pnpm install --immutable", + "**/*.ts{,x}": ["eslint --fix --max-warnings 0", "pnpm format"], + "**/*.{js,json,yml,html,css,less,scss,md}": "pnpm format", + "**/package.json": () => "pnpm install --immutable", }; diff --git a/package.json b/package.json index e962b312d6..86e6751fe1 100644 --- a/package.json +++ b/package.json @@ -1,44 +1,73 @@ { "name": "fern-platform-monorepo", + "private": true, "description": "Fern Platform Monorepo", "repository": "https://github.com/fern-api/fern-platform", "author": "Birch Solutions, Inc.", - "private": true, "scripts": { - "preinstall": "npx only-allow pnpm", - "test": "CI=true turbo test", - "test:update": "CI=true turbo test -- -u", - "playwright:test": "pnpm exec playwright test", - "playwright:build": "turbo --filter='playwright-*' --filter='@fern-ui/local-preview-bundle' build", + "build": "turbo build", + "check-docs-release-blockers": "pnpm tsx packages/scripts/src/cli.ts -- fern-scripts check-docs-release-blockers", "clean": "turbo clean", "codegen": "turbo codegen", "compile": "turbo compile", - "build": "turbo build", "depcheck": "turbo depcheck", + "docs:build": "turbo docs:build", + "docs:dev": "turbo docs:dev", + "docs:start": "turbo docs:start", + "fdr:generate": "pnpm fern generate --api fdr --local && pnpm turbo --filter=@fern-api/fdr-sdk compile", + "format": "prettier --write --ignore-unknown \"**\"", + "format:check": "prettier --check --ignore-unknown \"**\"", + "preinstall": "npx only-allow pnpm", "lint": "pnpm lint:eslint && pnpm lint:style && pnpm format:check", - "lint:fix": "pnpm root-package:fix && pnpm lint:eslint:fix && pnpm lint:style:fix && pnpm format", "lint:eslint": "turbo lint:eslint", "lint:eslint:fix": "turbo lint:eslint:fix", + "lint:fix": "pnpm root-package:fix && pnpm lint:eslint:fix && pnpm lint:style:fix && pnpm format", "lint:style": "turbo lint:style", "lint:style:fix": "turbo lint:style:fix", - "format": "prettier --write --ignore-unknown --ignore-path ./shared/.prettierignore --ignore-path .gitignore \"**\"", - "format:check": "prettier --check --ignore-unknown --ignore-path ./shared/.prettierignore --ignore-path .gitignore \"**\"", + "playwright:build": "turbo --filter='playwright-*' --filter='@fern-docs/local-preview-bundle' build", + "playwright:test": "pnpm exec playwright test", "publish": "pnpm -r --filter=!private --parallel exec -- npm publish --access public", - "vercel-scripts": "pnpm --filter=@fern-platform/vercel-scripts vercel-scripts", - "check-docs-release-blockers": "pnpm tsx packages/scripts/src/cli.ts -- fern-scripts check-docs-release-blockers", "root-package:check": "pnpm tsx packages/scripts/src/cli.ts -- fern-scripts check-root-package", "root-package:fix": "pnpm root-package:check --fix", - "docs:dev": "turbo docs:dev", - "docs:build": "turbo docs:build", - "docs:start": "turbo docs:start", - "fdr:generate": "pnpm fern generate --api fdr --local && pnpm turbo --filter=@fern-api/fdr-sdk compile" + "test": "CI=true turbo test", + "test:update": "CI=true turbo test -- -u", + "vercel-scripts": "pnpm --filter=@fern-platform/vercel-scripts vercel-scripts" + }, + "resolutions": { + "@babel/core": "7.26.0", + "clipboardy": "4.0.0", + "colorjs.io": "^0.5.2", + "cookie": "0.7.0", + "cross-spawn": "7.0.5", + "elliptic": "6.6.0", + "esbuild": "0.20.2", + "eslint": "9.17.0", + "eslint-config-next": "15.1.2", + "instantsearch.js": "4.75.4", + "jsonpath-plus": "10.0.7", + "markdown-to-jsx": "7.4.0", + "micromatch": "4.0.8", + "postcss": "8.4.31", + "react": "18.3.1", + "react-dom": "18.3.1", + "tailwindcss": "3.4.17", + "typescript": "5.7.2", + "webpack": "5.94.0" + }, + "dependencies": { + "@radix-ui/colors": "^3.0.0", + "fern-api": "0.41.16", + "get-port": "^7.1.0", + "prettier-plugin-packagejson": "^2.5.6" }, "devDependencies": { "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/preset-react": "^7.25.9", "@babel/preset-typescript": "^7.26.0", - "@next/eslint-plugin-next": "14.2.9", + "@eslint/compat": "^1.2.4", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.17.0", "@playwright/test": "^1.47.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", @@ -47,23 +76,24 @@ "@types/is-ci": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/node": "^18.7.18", - "@typescript-eslint/eslint-plugin": "7.3.1", - "@typescript-eslint/parser": "7.3.1", + "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.18.1", "@yarnpkg/sdks": "^3.1.0", "chalk": "^5.3.0", "depcheck": "^1.4.3", "dotenv": "^16.4.5", - "eslint": "^8.56.0", - "eslint-config-next": "14.2.15", + "eslint": "^9", + "eslint-config-next": "15.1.2", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-deprecation": "^2.0.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-tailwindcss": "^3.13.1", - "eslint-plugin-vitest": "^0.3.26", + "eslint-plugin-deprecation": "^3.0.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-tailwindcss": "^3.17.5", + "eslint-plugin-vitest": "^0.5.4", "execa": "^5.1.1", "express": "^4.20.0", + "globals": "^15.14.0", "http-proxy-middleware": "^3.0.3", "husky": "^8.0.1", "immer": "^9.0.15", @@ -73,7 +103,8 @@ "lint-staged": "^13.0.3", "organize-imports-cli": "^0.10.0", "playwright": "^1.47.1", - "prettier": "^3.3.2", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.9", "react": "^18", "rollup": "^4.22.4", "styled-jsx": "^5.1.2", @@ -82,48 +113,27 @@ "stylelint-config-standard-scss": "^13.0.0", "stylelint-config-tailwindcss": "^0.0.7", "stylelint-scss": "^6.0.0", - "tailwindcss": "^3.4.3", + "tailwindcss": "^3", "ts-node": "^10.9.2", - "tsx": "^4.7.1", - "turbo": "^2.1.2", - "typescript": "5.4.3", + "tsx": "^4.19.2", + "turbo": "^2.3.3", + "typescript": "^5", + "typescript-eslint": "^8.18.1", "typescript-plugin-css-modules": "^5.1.0", "vitest": "^2.1.4" }, - "resolutions": { - "cookie": "0.7.0", - "cross-spawn": "7.0.5", - "micromatch": "4.0.8", - "postcss": "8.4.31", - "esbuild": "0.20.2", - "instantsearch.js": "4.75.4", - "next": "npm:@fern-api/next@14.2.9-fork.2", - "jsonpath-plus": "10.0.7", - "markdown-to-jsx": "7.4.0", - "webpack": "5.94.0", - "clipboardy": "4.0.0", - "elliptic": "6.6.0", - "react": "18.3.1", - "react-dom": "18.3.1", - "@babel/core": "7.26.0" - }, "dependenciesMeta": { "jsonc-parser@2.2.1": { "unplugged": true } }, - "dependencies": { - "@radix-ui/colors": "^3.0.0", - "fern-api": "0.41.16", - "get-port": "^7.1.0" - }, "packageManager": "pnpm@9.4.0", "engines": { "node": ">=18.18.0", "pnpm": "^9.4.0" }, "nextBundleAnalysis": { - "buildOutputDirectory": "packages/ui/docs-bundle/.next", + "buildOutputDirectory": "packages/fern-docs/bundle/.next", "budget": 358400, "budgetPercentIncreaseRed": 20, "minimumChangeThreshold": 500, diff --git a/packages/cdk/.prettierrc.cjs b/packages/cdk/.prettierrc.cjs deleted file mode 100644 index 2b5cf5b0c0..0000000000 --- a/packages/cdk/.prettierrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("../../.prettierrc.json"); diff --git a/packages/cdk/package.json b/packages/cdk/package.json index cbb173b4bf..cff2d0a7ff 100644 --- a/packages/cdk/package.json +++ b/packages/cdk/package.json @@ -1,20 +1,20 @@ { - "name": "@fern-ui/cdk", + "name": "@fern-platform/cdk", "version": "0.0.0", + "private": true, "repository": { "type": "git", "url": "https://github.com/fern-api/fern-platform.git", "directory": "packages/cdk" }, - "private": true, - "files": [ - "dist" - ], + "sideEffects": false, "type": "commonjs", - "source": "src/cdk.ts", "main": "dist/cdk.js", + "source": "src/cdk.ts", "types": "dist/cdk.d.ts", - "sideEffects": false, + "files": [ + "dist" + ], "scripts": { "cdk": "cdk", "deploy:dev2": "cdk deploy local-preview-bundle2-dev2 --require-approval never --progress events", @@ -33,7 +33,7 @@ "@types/archiver": "^6.0.2", "@types/node": "^18.7.18", "aws-cdk": "^2.118.0", - "prettier": "^3.3.2", - "typescript": "5.4.3" + "prettier": "^3.4.2", + "typescript": "^5" } } diff --git a/packages/cdk/src/cdk.ts b/packages/cdk/src/cdk.ts index 94a5f016e8..f003cc639d 100644 --- a/packages/cdk/src/cdk.ts +++ b/packages/cdk/src/cdk.ts @@ -1,39 +1,49 @@ #!/usr/bin/env node -import { Environments, EnvironmentType } from "@fern-fern/fern-cloud-sdk/api/index"; +import { + Environments, + EnvironmentType, +} from "@fern-fern/fern-cloud-sdk/api/index"; import * as cdk from "aws-cdk-lib"; import { DocsFeStack } from "./docs-fe-stack"; void main(); async function main() { - const environments = await getEnvironments(); - const app = new cdk.App(); - for (const [environmentType, environmentInfo] of Object.entries(environments)) { - if (environmentInfo == null) { - throw new Error(`No info for environment ${environmentType}`); - } - switch (environmentType) { - case EnvironmentType.Dev2: - case EnvironmentType.Prod: - new DocsFeStack(app, `local-preview-bundle2-${environmentType.toLowerCase()}`, environmentType, { - env: { account: "985111089818", region: "us-east-1" }, - }); - break; - default: - continue; - } + const environments = await getEnvironments(); + const app = new cdk.App(); + for (const [environmentType, environmentInfo] of Object.entries( + environments + )) { + if (environmentInfo == null) { + throw new Error(`No info for environment ${environmentType}`); } + switch (environmentType) { + case EnvironmentType.Dev2: + case EnvironmentType.Prod: + new DocsFeStack( + app, + `local-preview-bundle2-${environmentType.toLowerCase()}`, + environmentType, + { + env: { account: "985111089818", region: "us-east-1" }, + } + ); + break; + default: + continue; + } + } } async function getEnvironments(): Promise { - const response = await fetch( - "https://raw.githubusercontent.com/fern-api/fern-cloud/main/env-scoped-resources/environments.json", - { - method: "GET", - headers: { - Authorization: "Bearer " + process.env["GITHUB_TOKEN"], - }, - }, - ); - return (await response.json()) as any as Environments; + const response = await fetch( + "https://raw.githubusercontent.com/fern-api/fern-cloud/main/env-scoped-resources/environments.json", + { + method: "GET", + headers: { + Authorization: "Bearer " + process.env["GITHUB_TOKEN"], + }, + } + ); + return (await response.json()) as any as Environments; } diff --git a/packages/cdk/src/docs-fe-stack.ts b/packages/cdk/src/docs-fe-stack.ts index 575c8186f8..0ee16ea5c1 100644 --- a/packages/cdk/src/docs-fe-stack.ts +++ b/packages/cdk/src/docs-fe-stack.ts @@ -8,77 +8,96 @@ import { Construct } from "constructs"; import * as fs from "fs"; import path from "path"; -const LOCAL_PREVIEW_BUNDLE_OUT_DIR = path.resolve(__dirname, "../../ui/local-preview-bundle/out"); +const LOCAL_PREVIEW_BUNDLE_OUT_DIR = path.resolve( + __dirname, + "../../fern-docs/local-preview-bundle/out" +); export class DocsFeStack extends Stack { - constructor(scope: Construct, id: string, environmentType: EnvironmentType, props?: StackProps) { - super(scope, id, props); - const bucket = new Bucket(this, "local-preview-bundle2", { - bucketName: `${environmentType.toLowerCase()}-local-preview-bundle2`, - removalPolicy: RemovalPolicy.RETAIN, - cors: [ - { - allowedMethods: [HttpMethods.GET, HttpMethods.POST, HttpMethods.PUT], - allowedOrigins: ["*"], - allowedHeaders: ["*"], - }, - ], - versioned: true, - publicReadAccess: true, - blockPublicAccess: { - blockPublicAcls: false, - blockPublicPolicy: false, - ignorePublicAcls: false, - restrictPublicBuckets: false, - }, - }); - bucket.addToResourcePolicy( - new PolicyStatement({ - resources: [bucket.arnForObjects("*"), bucket.bucketArn], - actions: ["s3:List*", "s3:Get*"], - principals: [new AnyPrincipal()], - }), - ); - - const local_preview_bundle_dist_zip = path.resolve(__dirname, "../../ui/local-preview-bundle/dist/out.zip"); - if (!fs.existsSync(LOCAL_PREVIEW_BUNDLE_OUT_DIR) || !fs.lstatSync(LOCAL_PREVIEW_BUNDLE_OUT_DIR).isDirectory()) { - throw new Error(`Local preview bundle not found at ${LOCAL_PREVIEW_BUNDLE_OUT_DIR}`); - } + constructor( + scope: Construct, + id: string, + environmentType: EnvironmentType, + props?: StackProps + ) { + super(scope, id, props); + const bucket = new Bucket(this, "local-preview-bundle2", { + bucketName: `${environmentType.toLowerCase()}-local-preview-bundle2`, + removalPolicy: RemovalPolicy.RETAIN, + cors: [ + { + allowedMethods: [HttpMethods.GET, HttpMethods.POST, HttpMethods.PUT], + allowedOrigins: ["*"], + allowedHeaders: ["*"], + }, + ], + versioned: true, + publicReadAccess: true, + blockPublicAccess: { + blockPublicAcls: false, + blockPublicPolicy: false, + ignorePublicAcls: false, + restrictPublicBuckets: false, + }, + }); + bucket.addToResourcePolicy( + new PolicyStatement({ + resources: [bucket.arnForObjects("*"), bucket.bucketArn], + actions: ["s3:List*", "s3:Get*"], + principals: [new AnyPrincipal()], + }) + ); - void zipFolder(LOCAL_PREVIEW_BUNDLE_OUT_DIR, local_preview_bundle_dist_zip).then(() => { - new BucketDeployment(this, "deploy-local-preview-bundle2", { - sources: [Source.asset(local_preview_bundle_dist_zip)], - destinationBucket: bucket, - extract: false, - memoryLimit: 1024, - }); - }); + const local_preview_bundle_dist_zip = path.resolve( + __dirname, + "../../fern-docs/local-preview-bundle/dist/out.zip" + ); + if ( + !fs.existsSync(LOCAL_PREVIEW_BUNDLE_OUT_DIR) || + !fs.lstatSync(LOCAL_PREVIEW_BUNDLE_OUT_DIR).isDirectory() + ) { + throw new Error( + `Local preview bundle not found at ${LOCAL_PREVIEW_BUNDLE_OUT_DIR}` + ); } + + void zipFolder( + LOCAL_PREVIEW_BUNDLE_OUT_DIR, + local_preview_bundle_dist_zip + ).then(() => { + new BucketDeployment(this, "deploy-local-preview-bundle2", { + sources: [Source.asset(local_preview_bundle_dist_zip)], + destinationBucket: bucket, + extract: false, + memoryLimit: 1024, + }); + }); + } } function mkdir(dir: string) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } } async function zipFolder(sourceFolder: string, zipFilePath: string) { - mkdir(path.dirname(zipFilePath)); - - return new Promise((resolve, reject) => { - const output = fs.createWriteStream(zipFilePath); - const archive = archiver("zip"); + mkdir(path.dirname(zipFilePath)); - archive.on("error", (err: unknown) => { - reject(err); - }); + return new Promise((resolve, reject) => { + const output = fs.createWriteStream(zipFilePath); + const archive = archiver("zip"); - output.on("close", function () { - resolve(); - }); + archive.on("error", (err: unknown) => { + reject(err); + }); - archive.pipe(output); - archive.directory(sourceFolder, false); - void archive.finalize(); + output.on("close", function () { + resolve(); }); + + archive.pipe(output); + archive.directory(sourceFolder, false); + void archive.finalize(); + }); } diff --git a/packages/commons/core-utils/.depcheckrc.json b/packages/commons/core-utils/.depcheckrc.json index cf8a6564f2..9335a9197e 100644 --- a/packages/commons/core-utils/.depcheckrc.json +++ b/packages/commons/core-utils/.depcheckrc.json @@ -1 +1,4 @@ -{ "ignores": ["@fern-platform/configs", "@types/node", "vite", "@types/react"], "ignore-patterns": ["dist"] } +{ + "ignores": ["@fern-platform/configs", "@types/node", "vite", "@types/react"], + "ignore-patterns": ["dist"] +} diff --git a/packages/commons/core-utils/.prettierrc.cjs b/packages/commons/core-utils/.prettierrc.cjs deleted file mode 100644 index 39cf0d0b8c..0000000000 --- a/packages/commons/core-utils/.prettierrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("../../../.prettierrc.json"); diff --git a/packages/commons/core-utils/package.json b/packages/commons/core-utils/package.json index c68ae3841c..21e461044c 100644 --- a/packages/commons/core-utils/package.json +++ b/packages/commons/core-utils/package.json @@ -6,12 +6,8 @@ "url": "https://github.com/fern-api/fern-platform.git", "directory": "packages/commons/core-utils" }, - "files": [ - "dist" - ], + "sideEffects": false, "type": "module", - "source": "src/index.ts", - "main": "dist/index.js", "exports": { "./assertNever": { "types": "./src/assertNever.ts", @@ -42,19 +38,23 @@ "default": "./dist/index.js" } }, - "sideEffects": false, + "main": "dist/index.js", + "source": "src/index.ts", + "files": [ + "dist" + ], "scripts": { "clean": "rm -rf ./lib && tsc --build --clean", "compile": "tsc --build", - "test": "vitest --run --passWithNoTests --globals", - "lint:eslint": "eslint --max-warnings 0 . --ignore-path=../../../.eslintignore", + "depcheck": "depcheck", + "format": "prettier --write --ignore-unknown \"**\"", + "format:check": "prettier --check --ignore-unknown \"**\"", + "lint:eslint": "eslint --max-warnings 0 .", "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:style": "stylelint 'src/**/*.scss' --allow-empty-input --max-warnings 0", "lint:style:fix": "pnpm lint:style --fix", - "format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", - "format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", "organize-imports": "organize-imports-cli tsconfig.json", - "depcheck": "depcheck" + "test": "vitest --run --passWithNoTests --globals" }, "dependencies": { "date-fns": "^4.1.0", @@ -69,11 +69,11 @@ "@types/title": "^3.4.3", "@types/ua-parser-js": "^0.7.39", "depcheck": "^1.4.3", - "eslint": "^8.56.0", + "eslint": "^9", "organize-imports-cli": "^0.10.0", - "prettier": "^3.3.2", + "prettier": "^3.4.2", "stylelint": "^16.1.0", - "typescript": "5.4.3", + "typescript": "^5", "vitest": "^2.1.4" } } diff --git a/packages/commons/core-utils/src/ObjectPropertiesVisitor.ts b/packages/commons/core-utils/src/ObjectPropertiesVisitor.ts index 91a43c74b1..331bb11bb7 100644 --- a/packages/commons/core-utils/src/ObjectPropertiesVisitor.ts +++ b/packages/commons/core-utils/src/ObjectPropertiesVisitor.ts @@ -1,14 +1,14 @@ import { keys } from "./objects/keys"; export type ObjectPropertiesVisitor = { - [K in keyof T]-?: (value: T[K]) => R; + [K in keyof T]-?: (value: T[K]) => R; }; export async function visitObject>( - object: T, - visitor: ObjectPropertiesVisitor>, + object: T, + visitor: ObjectPropertiesVisitor> ): Promise { - for (const key of keys(visitor)) { - await visitor[key](object[key]); - } + for (const key of keys(visitor)) { + await visitor[key](object[key]); + } } diff --git a/packages/commons/core-utils/src/__test__/combineURLs.test.ts b/packages/commons/core-utils/src/__test__/combineURLs.test.ts index 9b9e05f3b9..f3410218fa 100644 --- a/packages/commons/core-utils/src/__test__/combineURLs.test.ts +++ b/packages/commons/core-utils/src/__test__/combineURLs.test.ts @@ -4,23 +4,33 @@ import { combineURLs } from "../combineURLs"; // Test cases from [Axios](https://github.com/axios/axios/blob/fe7d09bb08fa1c0e414956b7fc760c80459b0a43/test/specs/helpers/combineURLs.spec.js) describe("helpers::combineURLs", function () { - it("should combine URLs", function () { - expect(combineURLs("https://api.github.com", "/users")).toBe("https://api.github.com/users"); - }); + it("should combine URLs", function () { + expect(combineURLs("https://api.github.com", "/users")).toBe( + "https://api.github.com/users" + ); + }); - it("should remove duplicate slashes", function () { - expect(combineURLs("https://api.github.com/", "/users")).toBe("https://api.github.com/users"); - }); + it("should remove duplicate slashes", function () { + expect(combineURLs("https://api.github.com/", "/users")).toBe( + "https://api.github.com/users" + ); + }); - it("should insert missing slash", function () { - expect(combineURLs("https://api.github.com", "users")).toBe("https://api.github.com/users"); - }); + it("should insert missing slash", function () { + expect(combineURLs("https://api.github.com", "users")).toBe( + "https://api.github.com/users" + ); + }); - it("should not insert slash when relative url missing/empty", function () { - expect(combineURLs("https://api.github.com/users", "")).toBe("https://api.github.com/users"); - }); + it("should not insert slash when relative url missing/empty", function () { + expect(combineURLs("https://api.github.com/users", "")).toBe( + "https://api.github.com/users" + ); + }); - it("should allow a single slash for relative url", function () { - expect(combineURLs("https://api.github.com/users", "/")).toBe("https://api.github.com/users/"); - }); + it("should allow a single slash for relative url", function () { + expect(combineURLs("https://api.github.com/users", "/")).toBe( + "https://api.github.com/users/" + ); + }); }); diff --git a/packages/commons/core-utils/src/__test__/formatUtc.test.ts b/packages/commons/core-utils/src/__test__/formatUtc.test.ts index 48b7406ec6..cb4615ab37 100644 --- a/packages/commons/core-utils/src/__test__/formatUtc.test.ts +++ b/packages/commons/core-utils/src/__test__/formatUtc.test.ts @@ -1,8 +1,8 @@ import { formatUtc } from "../formatUtc"; describe("formatUtc", () => { - it("formats a date in UTC", () => { - const formatted = formatUtc(1680480000000, "yyyy-MM-dd"); - expect(formatted).toBe("2023-04-03"); - }); + it("formats a date in UTC", () => { + const formatted = formatUtc(1680480000000, "yyyy-MM-dd"); + expect(formatted).toBe("2023-04-03"); + }); }); diff --git a/packages/commons/core-utils/src/__test__/unknownToString.test.ts b/packages/commons/core-utils/src/__test__/unknownToString.test.ts index 148895ce62..e1326301d5 100644 --- a/packages/commons/core-utils/src/__test__/unknownToString.test.ts +++ b/packages/commons/core-utils/src/__test__/unknownToString.test.ts @@ -1,38 +1,40 @@ import { unknownToString } from "../unknownToString"; describe("unknownToString", () => { - it("should preserve strings", () => { - expect(unknownToString("foo")).toBe("foo"); - }); + it("should preserve strings", () => { + expect(unknownToString("foo")).toBe("foo"); + }); - it("should convert booleans to strings", () => { - expect(unknownToString(true)).toBe("true"); - expect(unknownToString(false)).toBe("false"); - }); + it("should convert booleans to strings", () => { + expect(unknownToString(true)).toBe("true"); + expect(unknownToString(false)).toBe("false"); + }); - it("should convert numbers to strings", () => { - expect(unknownToString(42)).toBe("42"); - expect(unknownToString(3.14)).toBe("3.14"); - expect(unknownToString(1_000_000_000_000_000_000)).toBe("1000000000000000000"); - }); + it("should convert numbers to strings", () => { + expect(unknownToString(42)).toBe("42"); + expect(unknownToString(3.14)).toBe("3.14"); + expect(unknownToString(1_000_000_000_000_000_000)).toBe( + "1000000000000000000" + ); + }); - it("should convert nulls", () => { - expect(unknownToString(null)).toBe(""); - expect(unknownToString(undefined)).toBe(""); - expect(unknownToString(null, { renderNull: true })).toBe("null"); - expect(unknownToString(undefined, { renderNull: true })).toBe("null"); - }); + it("should convert nulls", () => { + expect(unknownToString(null)).toBe(""); + expect(unknownToString(undefined)).toBe(""); + expect(unknownToString(null, { renderNull: true })).toBe("null"); + expect(unknownToString(undefined, { renderNull: true })).toBe("null"); + }); - it("should convert objects to JSON strings", () => { - expect(unknownToString({ foo: "bar" })).toBe('{"foo":"bar"}'); - }); + it("should convert objects to JSON strings", () => { + expect(unknownToString({ foo: "bar" })).toBe('{"foo":"bar"}'); + }); - it("should convert arrays to JSON strings", () => { - expect(unknownToString([1, 2, 3])).toBe("[1,2,3]"); - }); + it("should convert arrays to JSON strings", () => { + expect(unknownToString([1, 2, 3])).toBe("[1,2,3]"); + }); - it("should not render functions", () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - expect(unknownToString(() => {})).toBe(""); - }); + it("should not render functions", () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + expect(unknownToString(() => {})).toBe(""); + }); }); diff --git a/packages/commons/core-utils/src/__test__/withDefaultProtocol.test.ts b/packages/commons/core-utils/src/__test__/withDefaultProtocol.test.ts index 334463e824..7cfcc8fde3 100644 --- a/packages/commons/core-utils/src/__test__/withDefaultProtocol.test.ts +++ b/packages/commons/core-utils/src/__test__/withDefaultProtocol.test.ts @@ -2,89 +2,109 @@ import { describe, expect, it } from "vitest"; import { withDefaultProtocol } from "../withDefaultProtocol"; describe("withDefaultProtocol", () => { - it("adds https:// to a domain without protocol", () => { - expect(withDefaultProtocol("example.com")).toBe("https://example.com"); - }); - - it("does not modify a URL that already has https://", () => { - expect(withDefaultProtocol("https://example.com")).toBe("https://example.com"); - }); - - it("does not modify a URL that already has http://", () => { - expect(withDefaultProtocol("http://example.com")).toBe("http://example.com"); - }); - - it("does not modify URLs with other protocols", () => { - expect(withDefaultProtocol("ftp://example.com")).toBe("ftp://example.com"); - }); - - it("handles mixed case protocols", () => { - expect(withDefaultProtocol("HtTp://example.com")).toBe("HtTp://example.com"); - }); - - it("handles an empty string", () => { - expect(withDefaultProtocol("")).toBe("https://"); - }); - - it("handles IP addresses", () => { - expect(withDefaultProtocol("192.168.1.1")).toBe("https://192.168.1.1"); - }); - - it("handles URLs with ports", () => { - expect(withDefaultProtocol("example.com:8080")).toBe("https://example.com:8080"); - }); - - it("handles URLs with authentication", () => { - expect(withDefaultProtocol("user:pass@example.com")).toBe("https://user:pass@example.com"); - }); - - it("handles URLs with www", () => { - expect(withDefaultProtocol("www.example.com")).toBe("https://www.example.com"); - }); - - it("handles URLs with subdomains", () => { - expect(withDefaultProtocol("subdomain.example.com")).toBe("https://subdomain.example.com"); - }); - - it("handles URLs with paths", () => { - expect(withDefaultProtocol("example.com/path/to/resource")).toBe("https://example.com/path/to/resource"); - }); - - it("handles URLs with query parameters", () => { - expect(withDefaultProtocol("example.com?param1=value1¶m2=value2")).toBe( - "https://example.com?param1=value1¶m2=value2", - ); - }); - - it("handles double slashes in the path", () => { - expect(withDefaultProtocol("example.com//path//to//resource")).toBe("https://example.com//path//to//resource"); - }); - - it("handles URLs with :// in the path", () => { - expect(withDefaultProtocol("example.com/path/to/resource/with/colon://in/path")).toBe( - "https://example.com/path/to/resource/with/colon://in/path", - ); - }); - - it("handles URLs with :// in query parameters", () => { - expect(withDefaultProtocol("example.com?param1=value1¶m2=colon://in/value")).toBe( - "https://example.com?param1=value1¶m2=colon://in/value", - ); - }); - - it("handles URLs with :// in fragment", () => { - expect(withDefaultProtocol("example.com/path#fragment://with/colon")).toBe( - "https://example.com/path#fragment://with/colon", - ); - }); - - it("handles URLs with :// as part of a file name", () => { - expect(withDefaultProtocol("example.com/file:with/colon.txt")).toBe("https://example.com/file:with/colon.txt"); - }); - - it("handles URLs with :// in a path segment", () => { - expect(withDefaultProtocol("example.com/path/segment:with/colon")).toBe( - "https://example.com/path/segment:with/colon", - ); - }); + it("adds https:// to a domain without protocol", () => { + expect(withDefaultProtocol("example.com")).toBe("https://example.com"); + }); + + it("does not modify a URL that already has https://", () => { + expect(withDefaultProtocol("https://example.com")).toBe( + "https://example.com" + ); + }); + + it("does not modify a URL that already has http://", () => { + expect(withDefaultProtocol("http://example.com")).toBe( + "http://example.com" + ); + }); + + it("does not modify URLs with other protocols", () => { + expect(withDefaultProtocol("ftp://example.com")).toBe("ftp://example.com"); + }); + + it("handles mixed case protocols", () => { + expect(withDefaultProtocol("HtTp://example.com")).toBe( + "HtTp://example.com" + ); + }); + + it("handles an empty string", () => { + expect(withDefaultProtocol("")).toBe("https://"); + }); + + it("handles IP addresses", () => { + expect(withDefaultProtocol("192.168.1.1")).toBe("https://192.168.1.1"); + }); + + it("handles URLs with ports", () => { + expect(withDefaultProtocol("example.com:8080")).toBe( + "https://example.com:8080" + ); + }); + + it("handles URLs with authentication", () => { + expect(withDefaultProtocol("user:pass@example.com")).toBe( + "https://user:pass@example.com" + ); + }); + + it("handles URLs with www", () => { + expect(withDefaultProtocol("www.example.com")).toBe( + "https://www.example.com" + ); + }); + + it("handles URLs with subdomains", () => { + expect(withDefaultProtocol("subdomain.example.com")).toBe( + "https://subdomain.example.com" + ); + }); + + it("handles URLs with paths", () => { + expect(withDefaultProtocol("example.com/path/to/resource")).toBe( + "https://example.com/path/to/resource" + ); + }); + + it("handles URLs with query parameters", () => { + expect(withDefaultProtocol("example.com?param1=value1¶m2=value2")).toBe( + "https://example.com?param1=value1¶m2=value2" + ); + }); + + it("handles double slashes in the path", () => { + expect(withDefaultProtocol("example.com//path//to//resource")).toBe( + "https://example.com//path//to//resource" + ); + }); + + it("handles URLs with :// in the path", () => { + expect( + withDefaultProtocol("example.com/path/to/resource/with/colon://in/path") + ).toBe("https://example.com/path/to/resource/with/colon://in/path"); + }); + + it("handles URLs with :// in query parameters", () => { + expect( + withDefaultProtocol("example.com?param1=value1¶m2=colon://in/value") + ).toBe("https://example.com?param1=value1¶m2=colon://in/value"); + }); + + it("handles URLs with :// in fragment", () => { + expect(withDefaultProtocol("example.com/path#fragment://with/colon")).toBe( + "https://example.com/path#fragment://with/colon" + ); + }); + + it("handles URLs with :// as part of a file name", () => { + expect(withDefaultProtocol("example.com/file:with/colon.txt")).toBe( + "https://example.com/file:with/colon.txt" + ); + }); + + it("handles URLs with :// in a path segment", () => { + expect(withDefaultProtocol("example.com/path/segment:with/colon")).toBe( + "https://example.com/path/segment:with/colon" + ); + }); }); diff --git a/packages/commons/core-utils/src/addPrefixToString.ts b/packages/commons/core-utils/src/addPrefixToString.ts index 6e359d186c..3f0c78dbbc 100644 --- a/packages/commons/core-utils/src/addPrefixToString.ts +++ b/packages/commons/core-utils/src/addPrefixToString.ts @@ -1,18 +1,18 @@ import stripAnsi from "strip-ansi"; export function addPrefixToString({ - prefix, - content, - includePrefixOnAllLines = false, + prefix, + content, + includePrefixOnAllLines = false, }: { - prefix: string; - content: string; - /** - * if true, the prefix is included on all lines. - * if false, all lines after the first are indented by the length of the prefix - */ - includePrefixOnAllLines?: boolean; + prefix: string; + content: string; + /** + * if true, the prefix is included on all lines. + * if false, all lines after the first are indented by the length of the prefix + */ + includePrefixOnAllLines?: boolean; }): string { - const prefixLength = stripAnsi(prefix).length; - return `${prefix}${content.replaceAll("\n", `\n${includePrefixOnAllLines ? prefix : " ".repeat(prefixLength)}`)}`; + const prefixLength = stripAnsi(prefix).length; + return `${prefix}${content.replaceAll("\n", `\n${includePrefixOnAllLines ? prefix : " ".repeat(prefixLength)}`)}`; } diff --git a/packages/commons/core-utils/src/assertNever.ts b/packages/commons/core-utils/src/assertNever.ts index b9d0abd3c5..0c2f31e63e 100644 --- a/packages/commons/core-utils/src/assertNever.ts +++ b/packages/commons/core-utils/src/assertNever.ts @@ -1,5 +1,5 @@ export function assertNever(x: never): never { - throw new Error("Unexpected value: " + JSON.stringify(x)); + throw new Error("Unexpected value: " + JSON.stringify(x)); } // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/packages/commons/core-utils/src/assertVoidNoThrow.ts b/packages/commons/core-utils/src/assertVoidNoThrow.ts index 23afd90111..a04bb959ff 100644 --- a/packages/commons/core-utils/src/assertVoidNoThrow.ts +++ b/packages/commons/core-utils/src/assertVoidNoThrow.ts @@ -1,2 +1,2 @@ -// eslint-disable-next-line @typescript-eslint/no-empty-function +// eslint-disable-next-line @typescript-eslint/no-invalid-void-type, @typescript-eslint/no-empty-function export function assertVoidNoThrow(_x: void): void {} diff --git a/packages/commons/core-utils/src/bytes.ts b/packages/commons/core-utils/src/bytes.ts index b8f6ab10bd..c64db5e2bf 100644 --- a/packages/commons/core-utils/src/bytes.ts +++ b/packages/commons/core-utils/src/bytes.ts @@ -1,5 +1,5 @@ export function measureBytes(str: string): number { - return new TextEncoder().encode(str).length; + return new TextEncoder().encode(str).length; } /** @@ -9,18 +9,18 @@ export function measureBytes(str: string): number { * @returns An array of strings, each of the specified byte size. */ export function chunkToBytes(str: string, byteSize: number): string[] { - const encoder = new TextEncoder(); + const encoder = new TextEncoder(); - // TODO: what if the string isn't utf8? - const utf8Bytes = encoder.encode(str); - const numChunks = Math.ceil(utf8Bytes.length / byteSize); - const chunks = new Array(numChunks); + // TODO: what if the string isn't utf8? + const utf8Bytes = encoder.encode(str); + const numChunks = Math.ceil(utf8Bytes.length / byteSize); + const chunks = new Array(numChunks); - for (let i = 0, o = 0; i < numChunks; ++i, o += byteSize) { - chunks[i] = new TextDecoder().decode(utf8Bytes.slice(o, o + byteSize)); - } + for (let i = 0, o = 0; i < numChunks; ++i, o += byteSize) { + chunks[i] = new TextDecoder().decode(utf8Bytes.slice(o, o + byteSize)); + } - return chunks; + return chunks; } /** @@ -30,8 +30,8 @@ export function chunkToBytes(str: string, byteSize: number): string[] { * @returns The truncated string. */ export function truncateToBytes(str: string, byteSize: number): string { - const encoder = new TextEncoder(); - const utf8Bytes = encoder.encode(str); - const truncatedBytes = utf8Bytes.slice(0, byteSize); - return new TextDecoder().decode(truncatedBytes); + const encoder = new TextEncoder(); + const utf8Bytes = encoder.encode(str); + const truncatedBytes = utf8Bytes.slice(0, byteSize); + return new TextDecoder().decode(truncatedBytes); } diff --git a/packages/commons/core-utils/src/combineURLs.ts b/packages/commons/core-utils/src/combineURLs.ts index 587bfe2349..147f0b7b47 100644 --- a/packages/commons/core-utils/src/combineURLs.ts +++ b/packages/commons/core-utils/src/combineURLs.ts @@ -9,5 +9,7 @@ * @returns The combined URL */ export function combineURLs(baseURL: string, relativeURL: string): string { - return relativeURL ? baseURL.replace(/\/?\/$/, "") + "/" + relativeURL.replace(/^\/+/, "") : baseURL; + return relativeURL + ? baseURL.replace(/\/?\/$/, "") + "/" + relativeURL.replace(/^\/+/, "") + : baseURL; } diff --git a/packages/commons/core-utils/src/declarations.d.ts b/packages/commons/core-utils/src/declarations.d.ts index 56c5f853b0..6189f628fc 100644 --- a/packages/commons/core-utils/src/declarations.d.ts +++ b/packages/commons/core-utils/src/declarations.d.ts @@ -2,8 +2,8 @@ type CSSModuleClasses = Readonly>; declare module "*.module.scss" { - const classes: CSSModuleClasses; - export default classes; + const classes: CSSModuleClasses; + export default classes; } // CSS @@ -12,26 +12,26 @@ declare module "*.scss" {} // images declare module "*.png" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.jpg" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.jpeg" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.gif" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.svg" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.ico" { - const src: string; - export default { src }; + const src: string; + export default { src }; } diff --git a/packages/commons/core-utils/src/delay/delay.ts b/packages/commons/core-utils/src/delay/delay.ts index 6413174204..cea624496e 100644 --- a/packages/commons/core-utils/src/delay/delay.ts +++ b/packages/commons/core-utils/src/delay/delay.ts @@ -1,3 +1,3 @@ export function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(() => resolve(), ms)); + return new Promise((resolve) => setTimeout(() => resolve(), ms)); } diff --git a/packages/commons/core-utils/src/delay/withMinimumTime.ts b/packages/commons/core-utils/src/delay/withMinimumTime.ts index 9fe4e3746d..b3825e8f2d 100644 --- a/packages/commons/core-utils/src/delay/withMinimumTime.ts +++ b/packages/commons/core-utils/src/delay/withMinimumTime.ts @@ -8,20 +8,20 @@ import { delay } from "./delay"; * (timer starts when withMinimumTime() is invoked) */ export async function withMinimumTime( - promise: Promise, - ms: number, - { alwaysDelay = false }: { alwaysDelay?: boolean } = {}, + promise: Promise, + ms: number, + { alwaysDelay = false }: { alwaysDelay?: boolean } = {} ): Promise { - const delayPromise = delay(ms); + const delayPromise = delay(ms); - try { - const result = await promise; - if (alwaysDelay) { - await delayPromise; - } - return result; - } catch (error) { - await delayPromise; - throw error; + try { + const result = await promise; + if (alwaysDelay) { + await delayPromise; } + return result; + } catch (error) { + await delayPromise; + throw error; + } } diff --git a/packages/commons/core-utils/src/formatUtc.ts b/packages/commons/core-utils/src/formatUtc.ts index 8d87888e90..8a0f686f6a 100644 --- a/packages/commons/core-utils/src/formatUtc.ts +++ b/packages/commons/core-utils/src/formatUtc.ts @@ -1,5 +1,8 @@ import { formatInTimeZone } from "date-fns-tz"; -export function formatUtc(date: Date | number | string, format: string): string { - return formatInTimeZone(date, "UTC", format); +export function formatUtc( + date: Date | number | string, + format: string +): string { + return formatInTimeZone(date, "UTC", format); } diff --git a/packages/commons/core-utils/src/identity.ts b/packages/commons/core-utils/src/identity.ts index b4134ab8c1..39b6e5be3b 100644 --- a/packages/commons/core-utils/src/identity.ts +++ b/packages/commons/core-utils/src/identity.ts @@ -1,5 +1,5 @@ export function identity(value: T): T { - return value; + return value; } export default identity; diff --git a/packages/commons/core-utils/src/index.ts b/packages/commons/core-utils/src/index.ts index a3cd4cda41..3c6dddc898 100644 --- a/packages/commons/core-utils/src/index.ts +++ b/packages/commons/core-utils/src/index.ts @@ -1,4 +1,7 @@ -export { visitObject, type ObjectPropertiesVisitor } from "./ObjectPropertiesVisitor"; +export { + visitObject, + type ObjectPropertiesVisitor, +} from "./ObjectPropertiesVisitor"; export { addPrefixToString } from "./addPrefixToString"; export { assertNever, assertNeverNoThrow } from "./assertNever"; export { assertVoidNoThrow } from "./assertVoidNoThrow"; diff --git a/packages/commons/core-utils/src/isNonNullish.ts b/packages/commons/core-utils/src/isNonNullish.ts index adbf6c0755..fa221ff4eb 100644 --- a/packages/commons/core-utils/src/isNonNullish.ts +++ b/packages/commons/core-utils/src/isNonNullish.ts @@ -1,9 +1,12 @@ export function isNonNullish(x: T | null | undefined): x is T { - return x != null; + return x != null; } -export function assertNonNullish(x: T | null | undefined, message?: string): asserts x is T { - if (x == null) { - throw new Error(message ?? "Value is null or undefined"); - } +export function assertNonNullish( + x: T | null | undefined, + message?: string +): asserts x is T { + if (x == null) { + throw new Error(message ?? "Value is null or undefined"); + } } diff --git a/packages/commons/core-utils/src/objects/entries.ts b/packages/commons/core-utils/src/objects/entries.ts index dcb080d084..73b032e50f 100644 --- a/packages/commons/core-utils/src/objects/entries.ts +++ b/packages/commons/core-utils/src/objects/entries.ts @@ -1,5 +1,7 @@ export type Entries = [keyof T, T[keyof T]][]; -export function entries>(object: T): Entries { - return Object.entries(object) as Entries; +export function entries>( + object: T +): Entries { + return Object.entries(object) as Entries; } diff --git a/packages/commons/core-utils/src/objects/isPlainObject.ts b/packages/commons/core-utils/src/objects/isPlainObject.ts index b538f2d083..12c1f03426 100644 --- a/packages/commons/core-utils/src/objects/isPlainObject.ts +++ b/packages/commons/core-utils/src/objects/isPlainObject.ts @@ -1,20 +1,22 @@ // https://github.com/lodash/lodash/blob/master/isPlainObject.js -export function isPlainObject(value: unknown): value is Record { - if (!isObjectLike(value) || String(value) !== "[object Object]") { - return false; - } - if (Object.getPrototypeOf(value) == null) { - return true; - } - let proto = value; - while (Object.getPrototypeOf(proto) != null) { - proto = Object.getPrototypeOf(proto); - } - return Object.getPrototypeOf(value) === proto; +export function isPlainObject( + value: unknown +): value is Record { + if (!isObjectLike(value) || String(value) !== "[object Object]") { + return false; + } + if (Object.getPrototypeOf(value) == null) { + return true; + } + let proto = value; + while (Object.getPrototypeOf(proto) != null) { + proto = Object.getPrototypeOf(proto); + } + return Object.getPrototypeOf(value) === proto; } function isObjectLike(value: unknown) { - return typeof value === "object" && value != null; + return typeof value === "object" && value != null; } export default isPlainObject; diff --git a/packages/commons/core-utils/src/objects/keys.ts b/packages/commons/core-utils/src/objects/keys.ts index 5a8dca73c2..7e20369905 100644 --- a/packages/commons/core-utils/src/objects/keys.ts +++ b/packages/commons/core-utils/src/objects/keys.ts @@ -1,3 +1,5 @@ -export function keys>(object: T): (keyof T)[] { - return Object.keys(object) as (keyof T)[]; +export function keys>( + object: T +): (keyof T)[] { + return Object.keys(object) as (keyof T)[]; } diff --git a/packages/commons/core-utils/src/objects/values.ts b/packages/commons/core-utils/src/objects/values.ts index c7f00415b0..49f330c8fd 100644 --- a/packages/commons/core-utils/src/objects/values.ts +++ b/packages/commons/core-utils/src/objects/values.ts @@ -1,5 +1,7 @@ export type Values = T[keyof T]; -export function values>(object: T): Values[] { - return Object.values(object) as Values[]; +export function values>( + object: T +): Values[] { + return Object.values(object) as Values[]; } diff --git a/packages/commons/core-utils/src/platform.ts b/packages/commons/core-utils/src/platform.ts index 7f19d4917b..23189fe136 100644 --- a/packages/commons/core-utils/src/platform.ts +++ b/packages/commons/core-utils/src/platform.ts @@ -6,26 +6,26 @@ type Platform = "mac" | "windows" | "other"; type Device = "desktop" | "mobile" | "tablet"; const getPlatform = (): Platform => { - const { name } = uaParser.getOS(); - if (typeof name === "string") { - if (name.startsWith("Mac") || name.startsWith("iOS")) { - // NOTE: iOS shares the same key bindings as Mac, i.e. the Magic Keyboard on iPad - return "mac"; - } else if (name.startsWith("Windows")) { - return "windows"; - } + const { name } = uaParser.getOS(); + if (typeof name === "string") { + if (name.startsWith("Mac") || name.startsWith("iOS")) { + // NOTE: iOS shares the same key bindings as Mac, i.e. the Magic Keyboard on iPad + return "mac"; + } else if (name.startsWith("Windows")) { + return "windows"; } - return "other"; + } + return "other"; }; const getDevice = (): Device => { - const { type } = uaParser.getDevice(); - if (type === "mobile") { - return "mobile"; - } else if (type === "tablet") { - return "tablet"; - } - return "desktop"; + const { type } = uaParser.getDevice(); + if (type === "mobile") { + return "mobile"; + } else if (type === "tablet") { + return "tablet"; + } + return "desktop"; }; export { getDevice, getPlatform, type Device, type Platform }; diff --git a/packages/commons/core-utils/src/specialTokens.ts b/packages/commons/core-utils/src/specialTokens.ts index e5f69d47da..a157e1c243 100644 --- a/packages/commons/core-utils/src/specialTokens.ts +++ b/packages/commons/core-utils/src/specialTokens.ts @@ -1,361 +1,361 @@ export const SPECIAL_TOKENS = [ - // privacy - "PII", - "PHI", - "PCI", - "GDPR", - "CCPA", - "HIPAA", - "COPPA", - "FERPA", - "GLBA", - "SOX", - "FISMA", - "NIST", - "CIS", - "ISO", - "IEC", - "ITAR", - "EAR", - "CMMC", - "CUI", - "CDI", - "FTC", - "FCC", - "SEC", - "FINRA", + // privacy + "PII", + "PHI", + "PCI", + "GDPR", + "CCPA", + "HIPAA", + "COPPA", + "FERPA", + "GLBA", + "SOX", + "FISMA", + "NIST", + "CIS", + "ISO", + "IEC", + "ITAR", + "EAR", + "CMMC", + "CUI", + "CDI", + "FTC", + "FCC", + "SEC", + "FINRA", - // security - "XSS", - "CSRF", - "SSRF", - "XSRF", - "TLS", - "SSL", - "SSH", - "API", - "OAuth", - "OAuth1", - "OAuth1.0", - "OAuth2", - "OAuth2.0", - "SAML", - "OpenID", - "OpenID Connect", - "CAPTCHA", - "reCAPTCHA", - "2FA", - "MFA", - "OTP", - "TOTP", - "HOTP", - "U2F", - "FIDO", - "FIDO2", - "PKI", - "HMAC", - "AES", - "RSA", - "SHA", - "MD5", - "BCrypt", - "PBKDF2", - "Argon2", - "SCrypt", - "JWT", - "JWE", - "JWS", - "JWK", - "JWA", - "JOSE", + // security + "XSS", + "CSRF", + "SSRF", + "XSRF", + "TLS", + "SSL", + "SSH", + "API", + "OAuth", + "OAuth1", + "OAuth1.0", + "OAuth2", + "OAuth2.0", + "SAML", + "OpenID", + "OpenID Connect", + "CAPTCHA", + "reCAPTCHA", + "2FA", + "MFA", + "OTP", + "TOTP", + "HOTP", + "U2F", + "FIDO", + "FIDO2", + "PKI", + "HMAC", + "AES", + "RSA", + "SHA", + "MD5", + "BCrypt", + "PBKDF2", + "Argon2", + "SCrypt", + "JWT", + "JWE", + "JWS", + "JWK", + "JWA", + "JOSE", - // shopping - "SKU", - "SKUs", - "UPC", - "EAN", - "ISBN", - "ASIN", - "MPN", - "MSRP", - "MAP", - "RRP", - "MSRP", + // shopping + "SKU", + "SKUs", + "UPC", + "EAN", + "ISBN", + "ASIN", + "MPN", + "MSRP", + "MAP", + "RRP", + "MSRP", - // time - "AM", - "PM", - "UTC", - "GMT", - "PST", - "PDT", - "EST", - "EDT", - "CST", - "CDT", - "MST", - "MDT", + // time + "AM", + "PM", + "UTC", + "GMT", + "PST", + "PDT", + "EST", + "EDT", + "CST", + "CDT", + "MST", + "MDT", - // geography - "USA", - "UK", - "EU", - "UAE", - "APAC", - "EMEA", - "LATAM", - "ANZ", - "SEA", - "MEA", - "MENA", - "NATO", - "NA", - "SA", - "CA", - "EU", - "AU", - "NZ", - "JP", - "KR", - "CN", - "HK", - "TW", - "SG", - "MY", - "TH", - "ID", - "PH", - "VN", - "IN", - "PK", - "BD", - "LK", - "NP", - "MM", - "KH", - "LA", - "MM", - "BT", - "MV", + // geography + "USA", + "UK", + "EU", + "UAE", + "APAC", + "EMEA", + "LATAM", + "ANZ", + "SEA", + "MEA", + "MENA", + "NATO", + "NA", + "SA", + "CA", + "EU", + "AU", + "NZ", + "JP", + "KR", + "CN", + "HK", + "TW", + "SG", + "MY", + "TH", + "ID", + "PH", + "VN", + "IN", + "PK", + "BD", + "LK", + "NP", + "MM", + "KH", + "LA", + "MM", + "BT", + "MV", - // finance - "USD", - "EUR", - "GBP", - "JPY", - "CNY", - "RUB", - "INR", - "AUD", - "CAD", - "CHF", - "SGD", - "MYR", - "THB", - "IDR", - "KRW", - "PHP", - "VND", - "HKD", - "TWD", - "MXN", - "BRL", - "ARS", - "CLP", - "COP", - "PEN", - "ZAR", - "NGN", - "EGP", - "AED", - "SAR", - "ILS", - "TRY", - "SEK", - "NOK", - "DKK", - "ISK", - "HUF", - "PLN", - "CZK", - "RON", - "BGN", + // finance + "USD", + "EUR", + "GBP", + "JPY", + "CNY", + "RUB", + "INR", + "AUD", + "CAD", + "CHF", + "SGD", + "MYR", + "THB", + "IDR", + "KRW", + "PHP", + "VND", + "HKD", + "TWD", + "MXN", + "BRL", + "ARS", + "CLP", + "COP", + "PEN", + "ZAR", + "NGN", + "EGP", + "AED", + "SAR", + "ILS", + "TRY", + "SEK", + "NOK", + "DKK", + "ISK", + "HUF", + "PLN", + "CZK", + "RON", + "BGN", - // programming - "API", - "APIs", - "SDK", - "SDKs", - "AI", - "OCR", - "REST", - "SOAP", - "JSON", - "XML", - "HTTP", - "HTTPS", - "URI", - "URL", - "CRUD", - "RESTful", - "KYB", - "KYC", - "AML", - "HTML", - "CSS", - "JS", - "SQL", - "DB", - "UI", - "UX", - "SaaS", - "PaaS", - "IaaS", - "IP", - "TCP", - "UDP", - "DNS", - "FTP", - "SMTP", - "IMAP", - "POP3", - "CSV", - "MVC", - "MVP", - "MVVM", - "DOM", - "SPA", - "SSR", - "CSR", - "DDoS", - "CDN", - "IoT", - "ML", - "DL", - "NLP", - "CLI", - "GUI", - "BI", - "ETL", - "RDBMS", - "NoSQL", - "IDE", - "CMS", - "CCPA", - "POSIX", - "ABI", - "API", - "AST", - "COBOL", - "DDL", - "DML", + // programming + "API", + "APIs", + "SDK", + "SDKs", + "AI", + "OCR", + "REST", + "SOAP", + "JSON", + "XML", + "HTTP", + "HTTPS", + "URI", + "URL", + "CRUD", + "RESTful", + "KYB", + "KYC", + "AML", + "HTML", + "CSS", + "JS", + "SQL", + "DB", + "UI", + "UX", + "SaaS", + "PaaS", + "IaaS", + "IP", + "TCP", + "UDP", + "DNS", + "FTP", + "SMTP", + "IMAP", + "POP3", + "CSV", + "MVC", + "MVP", + "MVVM", + "DOM", + "SPA", + "SSR", + "CSR", + "DDoS", + "CDN", + "IoT", + "ML", + "DL", + "NLP", + "CLI", + "GUI", + "BI", + "ETL", + "RDBMS", + "NoSQL", + "IDE", + "CMS", + "CCPA", + "POSIX", + "ABI", + "API", + "AST", + "COBOL", + "DDL", + "DML", - // AI-related - "NN", // Neural Network - "CNN", // Convolutional Neural Network - "RNN", // Recurrent Neural Network - "LSTM", // Long Short Term Memory - "GRU", // Gated Recurrent Unit - "ANN", // Artificial Neural Network - "GAN", // Generative Adversarial Network - "RL", // Reinforcement Learning - "DL", // Deep Learning - "ML", // Machine Learning - "NLP", // Natural Language Processing - "NLG", // Natural Language Generation - "NLU", // Natural Language Understanding - "BERT", // Bidirectional Encoder Representations from Transformers - "GPT", // Generative Pre-training Transformer - "SVM", // Support Vector Machine - "PCA", // Principal Component Analysis - "AI", // Artificial Intelligence - "CV", // Computer Vision - "TF", // TensorFlow - "TTS", // Text-to-Speech - "ASR", // Automatic Speech Recognition - "HMM", // Hidden Markov Model - "DNN", // Deep Neural Network - "MLP", // Multi-Layer Perceptron - "RBM", // Restricted Boltzmann Machine - "CRF", // Conditional Random Field + // AI-related + "NN", // Neural Network + "CNN", // Convolutional Neural Network + "RNN", // Recurrent Neural Network + "LSTM", // Long Short Term Memory + "GRU", // Gated Recurrent Unit + "ANN", // Artificial Neural Network + "GAN", // Generative Adversarial Network + "RL", // Reinforcement Learning + "DL", // Deep Learning + "ML", // Machine Learning + "NLP", // Natural Language Processing + "NLG", // Natural Language Generation + "NLU", // Natural Language Understanding + "BERT", // Bidirectional Encoder Representations from Transformers + "GPT", // Generative Pre-training Transformer + "SVM", // Support Vector Machine + "PCA", // Principal Component Analysis + "AI", // Artificial Intelligence + "CV", // Computer Vision + "TF", // TensorFlow + "TTS", // Text-to-Speech + "ASR", // Automatic Speech Recognition + "HMM", // Hidden Markov Model + "DNN", // Deep Neural Network + "MLP", // Multi-Layer Perceptron + "RBM", // Restricted Boltzmann Machine + "CRF", // Conditional Random Field - // Media - "PDF", - "PDFs", - "RTF", - "TXT", - "XLS", - "XLSX", - "PPT", + // Media + "PDF", + "PDFs", + "RTF", + "TXT", + "XLS", + "XLSX", + "PPT", - // Image - "JPG", - "JPEG", - "PNG", - "GIF", - "GIFs", - "SVG", - "TIFF", - "BMP", - "ICO", - "PSD", - "WebP", - "AVIF", - "HEIF", - "HEIC", - "EPS", + // Image + "JPG", + "JPEG", + "PNG", + "GIF", + "GIFs", + "SVG", + "TIFF", + "BMP", + "ICO", + "PSD", + "WebP", + "AVIF", + "HEIF", + "HEIC", + "EPS", - // Audio - "MP3", - "WAV", - "AIFF", - "FLAC", - "WMA", - "AAC", - "OGG", + // Audio + "MP3", + "WAV", + "AIFF", + "FLAC", + "WMA", + "AAC", + "OGG", - // Video - "AVI", - "WMV", - "MOV", - "M4V", - "MP4", - "MPG", - "MPEG", - "FLV", - "SWF", - "MKV", - "WebM", + // Video + "AVI", + "WMV", + "MOV", + "M4V", + "MP4", + "MPG", + "MPEG", + "FLV", + "SWF", + "MKV", + "WebM", - // Cloud Computing - "GCP", // Google Cloud Platform - "AWS", // Amazon Web Services - "VM", // Virtual Machines - "VPC", // Virtual Private Cloud - "S3", // AWS Simple Storage Service - "EC2", // AWS Elastic Compute Cloud + // Cloud Computing + "GCP", // Google Cloud Platform + "AWS", // Amazon Web Services + "VM", // Virtual Machines + "VPC", // Virtual Private Cloud + "S3", // AWS Simple Storage Service + "EC2", // AWS Elastic Compute Cloud - // Data Storage and Databases - "DynamoDB", - "CosmosDB", - "BigQuery", - "CI/CD", + // Data Storage and Databases + "DynamoDB", + "CosmosDB", + "BigQuery", + "CI/CD", - // Security and Compliance - "SOC1", // Service Organization Control 1 - "SOC2", // Service Organization Control 2 - "SOC3", // Service Organization Control 3 - "PCI DSS", // Payment Card Industry Data Security Standard - "WAF", // Web Application Firewall - "IAM", // Identity and Access Management + // Security and Compliance + "SOC1", // Service Organization Control 1 + "SOC2", // Service Organization Control 2 + "SOC3", // Service Organization Control 3 + "PCI DSS", // Payment Card Industry Data Security Standard + "WAF", // Web Application Firewall + "IAM", // Identity and Access Management - // Networking - "SDN", // Software-Defined Networking - "MPLS", // Multi-Protocol Label Switching - "BGP", // Border Gateway Protocol + // Networking + "SDN", // Software-Defined Networking + "MPLS", // Multi-Protocol Label Switching + "BGP", // Border Gateway Protocol - // Frameworks and Libraries - "Vue.js", - "Node.js", - ".NET", + // Frameworks and Libraries + "Vue.js", + "Node.js", + ".NET", ]; diff --git a/packages/commons/core-utils/src/titleCase.ts b/packages/commons/core-utils/src/titleCase.ts index d131d79a3f..d070917713 100644 --- a/packages/commons/core-utils/src/titleCase.ts +++ b/packages/commons/core-utils/src/titleCase.ts @@ -2,20 +2,20 @@ import title from "title"; import { SPECIAL_TOKENS } from "./specialTokens"; export function titleCase(name: string): string { - // regex match pascalCase or CamelCase and add spaces between words - name = name.replace(/([a-z])([A-Z])/g, "$1 $2"); + // regex match pascalCase or CamelCase and add spaces between words + name = name.replace(/([a-z])([A-Z])/g, "$1 $2"); - // regex match snake_case and replace "_" with " " - name = name.replace(/_/g, " "); + // regex match snake_case and replace "_" with " " + name = name.replace(/_/g, " "); - // regex match kebab-case and replace "-" with " " - name = name.replace(/-/g, " "); + // regex match kebab-case and replace "-" with " " + name = name.replace(/-/g, " "); - const titleCased = title(name, { special: SPECIAL_TOKENS }); + const titleCased = title(name, { special: SPECIAL_TOKENS }); - // regex match "V 2", "V 4", etc. and replace it with "V2", "V4", etc. - const versionedTitle = titleCased.replace(/V\s(\d)/g, "V$1"); - return versionedTitle; + // regex match "V 2", "V 4", etc. and replace it with "V2", "V4", etc. + const versionedTitle = titleCased.replace(/V\s(\d)/g, "V$1"); + return versionedTitle; } export default titleCase; diff --git a/packages/commons/core-utils/src/types.ts b/packages/commons/core-utils/src/types.ts index 54e81eebb2..cb7b8dd4e3 100644 --- a/packages/commons/core-utils/src/types.ts +++ b/packages/commons/core-utils/src/types.ts @@ -1,30 +1,30 @@ export type LowercaseLetter = - | "a" - | "b" - | "c" - | "d" - | "e" - | "f" - | "g" - | "h" - | "i" - | "j" - | "k" - | "l" - | "m" - | "n" - | "o" - | "p" - | "q" - | "r" - | "s" - | "t" - | "u" - | "v" - | "w" - | "x" - | "y" - | "z"; + | "a" + | "b" + | "c" + | "d" + | "e" + | "f" + | "g" + | "h" + | "i" + | "j" + | "k" + | "l" + | "m" + | "n" + | "o" + | "p" + | "q" + | "r" + | "s" + | "t" + | "u" + | "v" + | "w" + | "x" + | "y" + | "z"; export type UppercaseLetter = Uppercase; diff --git a/packages/commons/core-utils/src/unknownToString.ts b/packages/commons/core-utils/src/unknownToString.ts index 4d7050dacb..8d0f49a1c9 100644 --- a/packages/commons/core-utils/src/unknownToString.ts +++ b/packages/commons/core-utils/src/unknownToString.ts @@ -1,14 +1,17 @@ interface Opts { - renderNull?: boolean; + renderNull?: boolean; } -export function unknownToString(value: unknown, { renderNull = false }: Opts = {}): string { - if (value == null || typeof value === "function") { - return renderNull ? "null" : ""; - } else if (typeof value === "string") { - return value; - } - return JSON.stringify(value); +export function unknownToString( + value: unknown, + { renderNull = false }: Opts = {} +): string { + if (value == null || typeof value === "function") { + return renderNull ? "null" : ""; + } else if (typeof value === "string") { + return value; + } + return JSON.stringify(value); } export default unknownToString; diff --git a/packages/commons/core-utils/src/visitDiscriminatedUnion.ts b/packages/commons/core-utils/src/visitDiscriminatedUnion.ts index 3f406d134c..9b9727b47b 100644 --- a/packages/commons/core-utils/src/visitDiscriminatedUnion.ts +++ b/packages/commons/core-utils/src/visitDiscriminatedUnion.ts @@ -1,35 +1,47 @@ import { assertNever } from "./assertNever"; -export type DiscriminatedUnionVisitor, U, Discriminant extends string> = { - [D in T[Discriminant]]: (value: Extract>) => U; +export type DiscriminatedUnionVisitor< + T extends Record, + U, + Discriminant extends string, +> = { + [D in T[Discriminant]]: (value: Extract>) => U; } & { - _other?: (value: Record) => U; + _other?: (value: Record) => U; }; export function visitDiscriminatedUnion>( - item: T, + item: T ): { _visit: (visitor: DiscriminatedUnionVisitor) => U }; -export function visitDiscriminatedUnion, Discriminant extends string>( - item: T, - discriminant: Discriminant, +export function visitDiscriminatedUnion< + T extends Record, + Discriminant extends string, +>( + item: T, + discriminant: Discriminant ): { _visit: (visitor: DiscriminatedUnionVisitor) => U }; -export function visitDiscriminatedUnion, Discriminant extends string>( - item: T, - discriminant: Discriminant = "type" as Discriminant, -): { _visit: (visitor: DiscriminatedUnionVisitor) => U } { - return { - _visit: (visitor) => { - const visit = visitor[item[discriminant]]; - if (visit != null) { - return visit(item as Extract>); - } else { - if (visitor._other == null) { - assertNever(item as never); - } - return visitor._other(item); - } - }, - }; +export function visitDiscriminatedUnion< + T extends Record, + Discriminant extends string, +>( + item: T, + discriminant: Discriminant = "type" as Discriminant +): { + _visit: (visitor: DiscriminatedUnionVisitor) => U; +} { + return { + _visit: (visitor) => { + const visit = visitor[item[discriminant]]; + if (visit != null) { + return visit(item as Extract>); + } else { + if (visitor._other == null) { + assertNever(item as never); + } + return visitor._other(item); + } + }, + }; } export default visitDiscriminatedUnion; diff --git a/packages/commons/core-utils/src/withDefaultProtocol.ts b/packages/commons/core-utils/src/withDefaultProtocol.ts index a864743087..901fd18ebb 100644 --- a/packages/commons/core-utils/src/withDefaultProtocol.ts +++ b/packages/commons/core-utils/src/withDefaultProtocol.ts @@ -5,23 +5,32 @@ * @param defaultProtocol defaults to "https://" * @returns the endpoint, which will always have a protocol */ -export function withDefaultProtocol(endpoint: string, defaultProtocol?: string): string; -export function withDefaultProtocol(endpoint: string | undefined, defaultProtocol?: string): string | undefined; -export function withDefaultProtocol(endpoint: string | undefined, defaultProtocol = "https://"): string | undefined { - if (endpoint == null) { - return undefined; - } - - // matches any protocol scheme at the beginning of the string (e.g., "http://", "https://", "ftp://") - const protocolRegex = /^[a-z]+:\/\//i; +export function withDefaultProtocol( + endpoint: string, + defaultProtocol?: string +): string; +export function withDefaultProtocol( + endpoint: string | undefined, + defaultProtocol?: string +): string | undefined; +export function withDefaultProtocol( + endpoint: string | undefined, + defaultProtocol = "https://" +): string | undefined { + if (endpoint == null) { + return undefined; + } - if (!protocolRegex.test(endpoint)) { - if (endpoint === "localhost" || endpoint.startsWith("localhost:")) { - return `http://${endpoint}`; - } + // matches any protocol scheme at the beginning of the string (e.g., "http://", "https://", "ftp://") + const protocolRegex = /^[a-z]+:\/\//i; - return `${defaultProtocol}${endpoint}`; + if (!protocolRegex.test(endpoint)) { + if (endpoint === "localhost" || endpoint.startsWith("localhost:")) { + return `http://${endpoint}`; } - return endpoint; + return `${defaultProtocol}${endpoint}`; + } + + return endpoint; } diff --git a/packages/commons/core-utils/tsconfig.eslint.json b/packages/commons/core-utils/tsconfig.eslint.json new file mode 100644 index 0000000000..08c2772f9e --- /dev/null +++ b/packages/commons/core-utils/tsconfig.eslint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + } +} diff --git a/packages/commons/fdr-utils/.depcheckrc.json b/packages/commons/fdr-utils/.depcheckrc.json index cf8a6564f2..9335a9197e 100644 --- a/packages/commons/fdr-utils/.depcheckrc.json +++ b/packages/commons/fdr-utils/.depcheckrc.json @@ -1 +1,4 @@ -{ "ignores": ["@fern-platform/configs", "@types/node", "vite", "@types/react"], "ignore-patterns": ["dist"] } +{ + "ignores": ["@fern-platform/configs", "@types/node", "vite", "@types/react"], + "ignore-patterns": ["dist"] +} diff --git a/packages/commons/fdr-utils/.prettierrc.cjs b/packages/commons/fdr-utils/.prettierrc.cjs deleted file mode 100644 index 39cf0d0b8c..0000000000 --- a/packages/commons/fdr-utils/.prettierrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("../../../.prettierrc.json"); diff --git a/packages/commons/fdr-utils/package.json b/packages/commons/fdr-utils/package.json index d6a8b6b034..f9ab799208 100644 --- a/packages/commons/fdr-utils/package.json +++ b/packages/commons/fdr-utils/package.json @@ -1,35 +1,35 @@ { - "name": "@fern-ui/fdr-utils", + "name": "@fern-platform/fdr-utils", "version": "0.0.0", + "private": true, "repository": { "type": "git", "url": "https://github.com/fern-api/fern-platform.git", "directory": "packages/commons/fdr-utils" }, - "private": true, - "files": [ - "dist" - ], + "sideEffects": false, "type": "module", - "source": "src/index.ts", - "module": "src/index.ts", "main": "dist/index.js", + "module": "src/index.ts", + "source": "src/index.ts", "types": "dist/index.d.ts", - "sideEffects": false, + "files": [ + "dist" + ], "scripts": { "clean": "rm -rf ./lib && tsc --build --clean", "compile": "tsc --build", - "test": "vitest --run --passWithNoTests --globals", - "lint:eslint": "eslint --max-warnings 0 . --ignore-path=../../../.eslintignore", + "depcheck": "depcheck", + "dev": "tsc --watch", + "docs:dev": "pnpm dev", + "format": "prettier --write --ignore-unknown \"**\"", + "format:check": "prettier --check --ignore-unknown \"**\"", + "lint:eslint": "eslint --max-warnings 0 .", "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:style": "stylelint 'src/**/*.scss' --allow-empty-input --max-warnings 0", "lint:style:fix": "pnpm lint:style --fix", - "format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", - "format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", "organize-imports": "organize-imports-cli tsconfig.json", - "depcheck": "depcheck", - "dev": "tsc --watch", - "docs:dev": "pnpm dev" + "test": "vitest --run --passWithNoTests --globals" }, "dependencies": { "@fern-api/fdr-sdk": "workspace:*", @@ -39,11 +39,11 @@ "@fern-platform/configs": "workspace:*", "@types/node": "^18.7.18", "depcheck": "^1.4.3", - "eslint": "^8.56.0", + "eslint": "^9", "organize-imports-cli": "^0.10.0", - "prettier": "^3.3.2", + "prettier": "^3.4.2", "stylelint": "^16.1.0", - "typescript": "5.4.3", + "typescript": "^5", "vitest": "^2.1.4" } } diff --git a/packages/commons/fdr-utils/src/definition-object-factory.ts b/packages/commons/fdr-utils/src/definition-object-factory.ts index 2d9a86ba4a..ce483172e6 100644 --- a/packages/commons/fdr-utils/src/definition-object-factory.ts +++ b/packages/commons/fdr-utils/src/definition-object-factory.ts @@ -1,57 +1,57 @@ import { FdrAPI } from "@fern-api/fdr-sdk/client/types"; export class DefinitionObjectFactory { - public static createDocsDefinition(): FdrAPI.docs.v1.read.DocsDefinition { - return { - pages: {}, - apis: {}, - apisV2: {}, - files: {}, - filesV2: {}, - config: { - colorsV3: { - type: "dark", - accentPrimary: { r: 0, g: 0, b: 0, a: 1 }, - background: { type: "solid", r: 0, g: 0, b: 0, a: 1 }, - logo: undefined, - backgroundImage: undefined, - border: undefined, - sidebarBackground: undefined, - headerBackground: undefined, - cardBackground: undefined, - }, - navbarLinks: [], - navigation: { items: [], landingPage: undefined }, - root: undefined, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - }, - search: { - type: "singleAlgoliaIndex", - value: { - type: "unversioned", - indexSegment: { - id: FdrAPI.IndexSegmentId(""), - searchApiKey: "", - }, - }, - }, - algoliaSearchIndex: undefined, - jsFiles: undefined, - id: undefined, - }; - } + public static createDocsDefinition(): FdrAPI.docs.v1.read.DocsDefinition { + return { + pages: {}, + apis: {}, + apisV2: {}, + files: {}, + filesV2: {}, + config: { + colorsV3: { + type: "dark", + accentPrimary: { r: 0, g: 0, b: 0, a: 1 }, + background: { type: "solid", r: 0, g: 0, b: 0, a: 1 }, + logo: undefined, + backgroundImage: undefined, + border: undefined, + sidebarBackground: undefined, + headerBackground: undefined, + cardBackground: undefined, + }, + navbarLinks: [], + navigation: { items: [], landingPage: undefined }, + root: undefined, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + }, + search: { + type: "singleAlgoliaIndex", + value: { + type: "unversioned", + indexSegment: { + id: FdrAPI.IndexSegmentId(""), + searchApiKey: "", + }, + }, + }, + algoliaSearchIndex: undefined, + jsFiles: undefined, + id: undefined, + }; + } } diff --git a/packages/commons/fdr-utils/src/docs.ts b/packages/commons/fdr-utils/src/docs.ts index 7fd7ba229f..52111af5ac 100644 --- a/packages/commons/fdr-utils/src/docs.ts +++ b/packages/commons/fdr-utils/src/docs.ts @@ -3,81 +3,92 @@ import { Availability } from "@fern-api/fdr-sdk/navigation"; import { UnreachableCaseError } from "ts-essentials"; export function isVersionedNavigationConfig( - navigationConfig: DocsV1Read.NavigationConfig, + navigationConfig: DocsV1Read.NavigationConfig ): navigationConfig is DocsV1Read.VersionedNavigationConfig { - return Array.isArray((navigationConfig as DocsV1Read.VersionedNavigationConfig).versions); + return Array.isArray( + (navigationConfig as DocsV1Read.VersionedNavigationConfig).versions + ); } export function isUnversionedNavigationConfig( - navigationConfig: DocsV1Read.NavigationConfig, + navigationConfig: DocsV1Read.NavigationConfig ): navigationConfig is DocsV1Read.UnversionedNavigationConfig { - return ( - isUnversionedTabbedNavigationConfig(navigationConfig) || isUnversionedUntabbedNavigationConfig(navigationConfig) - ); + return ( + isUnversionedTabbedNavigationConfig(navigationConfig) || + isUnversionedUntabbedNavigationConfig(navigationConfig) + ); } export function isUnversionedTabbedNavigationConfig( - navigationConfig: DocsV1Read.NavigationConfig, + navigationConfig: DocsV1Read.NavigationConfig ): navigationConfig is DocsV1Read.UnversionedTabbedNavigationConfig { - return Array.isArray((navigationConfig as DocsV1Read.UnversionedTabbedNavigationConfig).tabs); + return Array.isArray( + (navigationConfig as DocsV1Read.UnversionedTabbedNavigationConfig).tabs + ); } export function isUnversionedUntabbedNavigationConfig( - navigationConfig: DocsV1Read.NavigationConfig, + navigationConfig: DocsV1Read.NavigationConfig ): navigationConfig is DocsV1Read.UnversionedUntabbedNavigationConfig { - return Array.isArray((navigationConfig as DocsV1Read.UnversionedUntabbedNavigationConfig).items); + return Array.isArray( + (navigationConfig as DocsV1Read.UnversionedUntabbedNavigationConfig).items + ); } export function assertIsVersionedNavigationConfig( - config: DocsV1Read.NavigationConfig, + config: DocsV1Read.NavigationConfig ): asserts config is DocsV1Read.VersionedNavigationConfig { - if (!isVersionedNavigationConfig(config)) { - throw new Error("Invalid navigation config. Expected versioned."); - } + if (!isVersionedNavigationConfig(config)) { + throw new Error("Invalid navigation config. Expected versioned."); + } } export function assertIsUnversionedNavigationConfig( - config: DocsV1Read.NavigationConfig, + config: DocsV1Read.NavigationConfig ): asserts config is DocsV1Read.UnversionedNavigationConfig { - if (!isUnversionedNavigationConfig(config)) { - throw new Error("Invalid navigation config. Expected unversioned."); - } + if (!isUnversionedNavigationConfig(config)) { + throw new Error("Invalid navigation config. Expected unversioned."); + } } -export function getVersionAvailabilityLabel(availability: FdrAPI.Availability): string { - switch (availability) { - case Availability.Beta: - return "beta"; - case Availability.Deprecated: - return "deprecated"; - case Availability.GenerallyAvailable: - return "generally available"; - case Availability.Stable: - return "stable"; - case Availability.InDevelopment: - return "in development"; - case Availability.PreRelease: - return "pre-release"; - default: - throw new UnreachableCaseError(availability); - } +export function getVersionAvailabilityLabel( + availability: FdrAPI.Availability +): string { + switch (availability) { + case Availability.Beta: + return "beta"; + case Availability.Deprecated: + return "deprecated"; + case Availability.GenerallyAvailable: + return "generally available"; + case Availability.Stable: + return "stable"; + case Availability.InDevelopment: + return "in development"; + case Availability.PreRelease: + return "pre-release"; + default: + throw new UnreachableCaseError(availability); + } } -export function getEndpointAvailabilityLabel(availability: FdrAPI.Availability): string { - switch (availability) { - case "Beta": - return "Beta"; - case "Deprecated": - return "Deprecated"; - case "GenerallyAvailable": - return "GA"; - case "Stable": - return "Stable"; - case "InDevelopment": - return "Developing"; - case "PreRelease": - return "RC"; - default: - throw new UnreachableCaseError(availability); - } +export function getEndpointAvailabilityLabel( + availability: FdrAPI.Availability +): string { + switch (availability) { + case "Beta": + return "Beta"; + case "Deprecated": + return "Deprecated"; + case "GenerallyAvailable": + return "GA"; + case "Stable": + return "Stable"; + case "InDevelopment": + return "Developing"; + case "PreRelease": + return "RC"; + default: + throw new UnreachableCaseError(availability); + } } diff --git a/packages/commons/fdr-utils/src/stringifyEndpointPathParts.ts b/packages/commons/fdr-utils/src/stringifyEndpointPathParts.ts index 8a833b2d8e..956633dd40 100644 --- a/packages/commons/fdr-utils/src/stringifyEndpointPathParts.ts +++ b/packages/commons/fdr-utils/src/stringifyEndpointPathParts.ts @@ -1,5 +1,12 @@ import { APIV1Read } from "@fern-api/fdr-sdk/client/types"; -export function stringifyEndpointPathParts(path: APIV1Read.EndpointPathPart[]): string { - return "/" + path.map((part) => (part.type === "literal" ? part.value : `${part.value}`)).join("/"); +export function stringifyEndpointPathParts( + path: APIV1Read.EndpointPathPart[] +): string { + return ( + "/" + + path + .map((part) => (part.type === "literal" ? part.value : `{${part.value}}`)) + .join("/") + ); } diff --git a/packages/commons/fdr-utils/src/types.ts b/packages/commons/fdr-utils/src/types.ts index 6fd309517b..79092d3849 100644 --- a/packages/commons/fdr-utils/src/types.ts +++ b/packages/commons/fdr-utils/src/types.ts @@ -2,48 +2,48 @@ import type { DocsV1Read } from "@fern-api/fdr-sdk/client/types"; import type * as FernNavigation from "@fern-api/fdr-sdk/navigation"; export interface ColorsConfig { - light: DocsV1Read.ThemeConfig | undefined; - dark: DocsV1Read.ThemeConfig | undefined; + light: DocsV1Read.ThemeConfig | undefined; + dark: DocsV1Read.ThemeConfig | undefined; } export interface VersionSwitcherInfo { - id: FernNavigation.VersionId; - title: string; - slug: FernNavigation.Slug; - index: number; - availability: FernNavigation.Availability | undefined; - pointsTo: FernNavigation.Slug | undefined; - hidden: boolean | undefined; - authed: boolean | undefined; + id: FernNavigation.VersionId; + title: string; + slug: FernNavigation.Slug; + index: number; + availability: FernNavigation.Availability | undefined; + pointsTo: FernNavigation.Slug | undefined; + hidden: boolean | undefined; + authed: boolean | undefined; } interface SidebarTabGroup { - type: "tabGroup"; - title: string; - icon: string | undefined; - index: number; - slug: FernNavigation.Slug; - pointsTo: FernNavigation.Slug | undefined; - hidden: boolean | undefined; - authed: boolean | undefined; + type: "tabGroup"; + title: string; + icon: string | undefined; + index: number; + slug: FernNavigation.Slug; + pointsTo: FernNavigation.Slug | undefined; + hidden: boolean | undefined; + authed: boolean | undefined; } interface SidebarTabLink { - type: "tabLink"; - title: string; - icon: string | undefined; - index: number; - url: string; + type: "tabLink"; + title: string; + icon: string | undefined; + index: number; + url: string; } interface SidebarTabChangelog { - type: "tabChangelog"; - title: string; - icon: string | undefined; - index: number; - slug: FernNavigation.Slug; - hidden: boolean | undefined; - authed: boolean | undefined; + type: "tabChangelog"; + title: string; + icon: string | undefined; + index: number; + slug: FernNavigation.Slug; + hidden: boolean | undefined; + authed: boolean | undefined; } export type SidebarTab = SidebarTabGroup | SidebarTabLink | SidebarTabChangelog; diff --git a/packages/commons/fdr-utils/tsconfig.eslint.json b/packages/commons/fdr-utils/tsconfig.eslint.json new file mode 100644 index 0000000000..38efe9d214 --- /dev/null +++ b/packages/commons/fdr-utils/tsconfig.eslint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, + "include": ["src/**/*", "vitest.config.ts"] +} diff --git a/packages/commons/fdr-utils/vitest.config.ts b/packages/commons/fdr-utils/vitest.config.ts index 4f772037cd..e2ec332940 100644 --- a/packages/commons/fdr-utils/vitest.config.ts +++ b/packages/commons/fdr-utils/vitest.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - test: { - globals: true, - }, + test: { + globals: true, + }, }); diff --git a/packages/commons/github/.prettierrc.cjs b/packages/commons/github/.prettierrc.cjs deleted file mode 100644 index 39cf0d0b8c..0000000000 --- a/packages/commons/github/.prettierrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("../../../.prettierrc.json"); diff --git a/packages/commons/github/package.json b/packages/commons/github/package.json index 0a1e06ceac..3bcfc938d1 100644 --- a/packages/commons/github/package.json +++ b/packages/commons/github/package.json @@ -1,39 +1,39 @@ { "name": "@fern-api/github", "version": "0.0.0", + "private": true, "repository": { "type": "git", "url": "https://github.com/fern-api/fern-platform.git", "directory": "packages/commons/github" }, - "private": true, - "files": [ - "dist" - ], + "sideEffects": false, "type": "module", - "source": "src/index.ts", - "module": "src/index.ts", - "main": "dist/index.js", - "types": "dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, - "sideEffects": false, + "main": "dist/index.js", + "module": "src/index.ts", + "source": "src/index.ts", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], "scripts": { "clean": "rm -rf ./lib && tsc --build --clean", "compile": "tsc --build", - "test": "vitest --run --passWithNoTests --globals", - "lint:eslint": "eslint --max-warnings 0 . --ignore-path=../../../.eslintignore", + "depcheck": "depcheck", + "format": "prettier --write --ignore-unknown \"**\"", + "format:check": "prettier --check --ignore-unknown \"**\"", + "lint:eslint": "eslint --max-warnings 0 .", "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:style": "stylelint 'src/**/*.scss' --allow-empty-input --max-warnings 0", "lint:style:fix": "pnpm lint:style --fix", - "format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", - "format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", "organize-imports": "organize-imports-cli tsconfig.json", - "depcheck": "depcheck" + "test": "vitest --run --passWithNoTests --globals" }, "dependencies": { "octokit": "^3.2.0", @@ -43,12 +43,12 @@ "@fern-platform/configs": "workspace:*", "@types/node": "^18.7.18", "depcheck": "^1.4.3", - "eslint": "^8.56.0", - "simple-git": "^3.24.0", - "vitest": "^2.1.4", + "eslint": "^9", "organize-imports-cli": "^0.10.0", - "prettier": "^3.3.2", + "prettier": "^3.4.2", + "simple-git": "^3.24.0", "stylelint": "^16.1.0", - "typescript": "5.4.3" + "typescript": "^5", + "vitest": "^2.1.4" } } diff --git a/packages/commons/github/src/ClonedRepository.ts b/packages/commons/github/src/ClonedRepository.ts index cc8e7da84a..6549db61c5 100644 --- a/packages/commons/github/src/ClonedRepository.ts +++ b/packages/commons/github/src/ClonedRepository.ts @@ -5,30 +5,34 @@ import { README_FILEPATH } from "./constants"; // ClonedRepository is a repository that has been successfully cloned to the local file system // and is ready to be used. export class ClonedRepository { - private clonePath: string; + private clonePath: string; - constructor({ clonePath }: { clonePath: string }) { - this.clonePath = clonePath; - } + constructor({ clonePath }: { clonePath: string }) { + this.clonePath = clonePath; + } - public async getReadme(): Promise { - return await this.readFile({ relativeFilePath: README_FILEPATH }); - } + public async getReadme(): Promise { + return await this.readFile({ relativeFilePath: README_FILEPATH }); + } - private async readFile({ relativeFilePath }: { relativeFilePath: string }): Promise { - const absoluteFilePath = path.join(this.clonePath, relativeFilePath); - if (!(await doesPathExist(absoluteFilePath))) { - return undefined; - } - return await readFile(absoluteFilePath, "utf-8"); + private async readFile({ + relativeFilePath, + }: { + relativeFilePath: string; + }): Promise { + const absoluteFilePath = path.join(this.clonePath, relativeFilePath); + if (!(await doesPathExist(absoluteFilePath))) { + return undefined; } + return await readFile(absoluteFilePath, "utf-8"); + } } async function doesPathExist(filepath: string): Promise { - try { - await lstat(filepath); - return true; - } catch { - return false; - } + try { + await lstat(filepath); + return true; + } catch { + return false; + } } diff --git a/packages/commons/github/src/RepositoryReference.ts b/packages/commons/github/src/RepositoryReference.ts index b47a6ca28d..1632af9c22 100644 --- a/packages/commons/github/src/RepositoryReference.ts +++ b/packages/commons/github/src/RepositoryReference.ts @@ -1,11 +1,11 @@ // RepositoryReference is a parsed GitHub repository reference, which // contains a variety of formats. export interface RepositoryReference { - remote: string; // e.g. github.com - owner: string; // e.g. fern-api - repo: string; // e.g. fern - repoUrl: string; // e.g. https://github.com/fern-api/fern - cloneUrl: string; // e.g. https://github.ccom/fern-api/fern.git + remote: string; // e.g. github.com + owner: string; // e.g. fern-api + repo: string; // e.g. fern + repoUrl: string; // e.g. https://github.com/fern-api/fern + cloneUrl: string; // e.g. https://github.ccom/fern-api/fern.git - getAuthedCloneUrl: (installationToken: string) => string; + getAuthedCloneUrl: (installationToken: string) => string; } diff --git a/packages/commons/github/src/__test__/cloneRepository.test.ts b/packages/commons/github/src/__test__/cloneRepository.test.ts index 00787efa98..3cf9f859f6 100644 --- a/packages/commons/github/src/__test__/cloneRepository.test.ts +++ b/packages/commons/github/src/__test__/cloneRepository.test.ts @@ -1,28 +1,28 @@ import { cloneRepository } from "../cloneRepository"; describe("cloneRepository", () => { - it("fern-api/docs-starter-openapi", async () => { - const repository = await cloneRepository({ - githubRepository: "github.com/fern-api/docs-starter-openapi", - installationToken: undefined, - }); - const readme = await repository.getReadme(); - expect(readme).contains("Fern"); - }); - it("invalid installation token", async () => { - await expect(async () => { - await cloneRepository({ - githubRepository: "https://github.com/fern-api/github-app-test", - installationToken: "ghp_xyz", - }); - }).rejects.toThrow(); - }); - it("repository does not exist", async () => { - await expect(async () => { - await cloneRepository({ - githubRepository: "https://github.com/fern-api/does-not-exist", - installationToken: undefined, - }); - }).rejects.toThrow(); + it("fern-api/docs-starter-openapi", async () => { + const repository = await cloneRepository({ + githubRepository: "github.com/fern-api/docs-starter-openapi", + installationToken: undefined, }); + const readme = await repository.getReadme(); + expect(readme).contains("Fern"); + }); + it("invalid installation token", async () => { + await expect(async () => { + await cloneRepository({ + githubRepository: "https://github.com/fern-api/github-app-test", + installationToken: "ghp_xyz", + }); + }).rejects.toThrow(); + }); + it("repository does not exist", async () => { + await expect(async () => { + await cloneRepository({ + githubRepository: "https://github.com/fern-api/does-not-exist", + installationToken: undefined, + }); + }).rejects.toThrow(); + }); }); diff --git a/packages/commons/github/src/__test__/getLatestTag.test.ts b/packages/commons/github/src/__test__/getLatestTag.test.ts index c6bbb40115..898517a7a7 100644 --- a/packages/commons/github/src/__test__/getLatestTag.test.ts +++ b/packages/commons/github/src/__test__/getLatestTag.test.ts @@ -1,8 +1,8 @@ import { getLatestTag } from "../getLatestTag"; describe("getLatestTag", () => { - it("tag", async () => { - const version = await getLatestTag("lodash/lodash"); - expect(version).toEqual("4.17.21"); - }); + it("tag", async () => { + const version = await getLatestTag("lodash/lodash"); + expect(version).toEqual("4.17.21"); + }); }); diff --git a/packages/commons/github/src/__test__/parseRepository.test.ts b/packages/commons/github/src/__test__/parseRepository.test.ts index f6beb3caff..cdadec0adf 100644 --- a/packages/commons/github/src/__test__/parseRepository.test.ts +++ b/packages/commons/github/src/__test__/parseRepository.test.ts @@ -1,36 +1,38 @@ import { parseRepository } from "../parseRepository"; describe("getLatestTag", () => { - it("fern-api/fern", async () => { - const tests = [ - "fern-api/fern", - "github.com/fern-api/fern", - "https://github.com/fern-api/fern", - "https://github.com/fern-api/fern.git", - ]; - for (const test of tests) { - const reference = parseRepository(test); - expect(reference.remote).toBe("github.com"); - expect(reference.owner).toBe("fern-api"); - expect(reference.repo).toBe("fern"); - expect(reference.repoUrl).toBe("https://github.com/fern-api/fern"); - expect(reference.cloneUrl).toBe("https://github.com/fern-api/fern.git"); - expect(reference.getAuthedCloneUrl("xyz")).toBe("https://x-access-token:xyz@github.com/fern-api/fern.git"); - } - }); - it("invalid remote", async () => { - expect(() => { - parseRepository("https://gitlab.com/inkscape/inkscape"); - }).toThrow(Error); - }); - it("invalid structure", async () => { - expect(() => { - parseRepository("fern"); - }).toThrow(Error); - }); - it("too many slashes", async () => { - expect(() => { - parseRepository("github.com/fern-api//fern"); - }).toThrow(Error); - }); + it("fern-api/fern", async () => { + const tests = [ + "fern-api/fern", + "github.com/fern-api/fern", + "https://github.com/fern-api/fern", + "https://github.com/fern-api/fern.git", + ]; + for (const test of tests) { + const reference = parseRepository(test); + expect(reference.remote).toBe("github.com"); + expect(reference.owner).toBe("fern-api"); + expect(reference.repo).toBe("fern"); + expect(reference.repoUrl).toBe("https://github.com/fern-api/fern"); + expect(reference.cloneUrl).toBe("https://github.com/fern-api/fern.git"); + expect(reference.getAuthedCloneUrl("xyz")).toBe( + "https://x-access-token:xyz@github.com/fern-api/fern.git" + ); + } + }); + it("invalid remote", async () => { + expect(() => { + parseRepository("https://gitlab.com/inkscape/inkscape"); + }).toThrow(Error); + }); + it("invalid structure", async () => { + expect(() => { + parseRepository("fern"); + }).toThrow(Error); + }); + it("too many slashes", async () => { + expect(() => { + parseRepository("github.com/fern-api//fern"); + }).toThrow(Error); + }); }); diff --git a/packages/commons/github/src/cloneRepository.ts b/packages/commons/github/src/cloneRepository.ts index 33b18fe9e7..2f32cb137a 100644 --- a/packages/commons/github/src/cloneRepository.ts +++ b/packages/commons/github/src/cloneRepository.ts @@ -8,23 +8,23 @@ import { parseRepository } from "./parseRepository"; * @param githubRepository a string that can be parsed into a RepositoryReference (e.g. 'owner/repo') */ export async function cloneRepository({ - githubRepository, - installationToken, + githubRepository, + installationToken, }: { - githubRepository: string; - installationToken: string | undefined; + githubRepository: string; + installationToken: string | undefined; }): Promise { - const repositoryReference = parseRepository(githubRepository); - const cloneUrl = - installationToken != null - ? repositoryReference.getAuthedCloneUrl(installationToken) - : repositoryReference.cloneUrl; - const dir = await tmp.dir(); - const clonePath = dir.path; - const git = simpleGit(clonePath); - await git.clone(cloneUrl, "."); + const repositoryReference = parseRepository(githubRepository); + const cloneUrl = + installationToken != null + ? repositoryReference.getAuthedCloneUrl(installationToken) + : repositoryReference.cloneUrl; + const dir = await tmp.dir(); + const clonePath = dir.path; + const git = simpleGit(clonePath); + await git.clone(cloneUrl, "."); - return new ClonedRepository({ - clonePath, - }); + return new ClonedRepository({ + clonePath, + }); } diff --git a/packages/commons/github/src/createOrUpdatePullRequest.ts b/packages/commons/github/src/createOrUpdatePullRequest.ts index be210aba0b..091297ddeb 100644 --- a/packages/commons/github/src/createOrUpdatePullRequest.ts +++ b/packages/commons/github/src/createOrUpdatePullRequest.ts @@ -1,103 +1,110 @@ -/* eslint-disable no-console */ import { Octokit } from "octokit"; interface CreatePRRequest { - title: string; - body: string; - base: string; - draft?: boolean; + title: string; + body: string; + base: string; + draft?: boolean; } interface RepoMetadata { - owner: string; - repo: string; + owner: string; + repo: string; } function parseRepository(repository: string): RepoMetadata { - const [owner, repo] = repository.split("/"); - if (owner == null || repo == null) { - throw new Error(`Failed to parse repository into owner and repo: ${repository}`); - } - return { - owner, - repo, - }; + const [owner, repo] = repository.split("/"); + if (owner == null || repo == null) { + throw new Error( + `Failed to parse repository into owner and repo: ${repository}` + ); + } + return { + owner, + repo, + }; } function getErrorMessage(error: unknown) { - if (error instanceof Error) { - return error.message; - } - return String(error); + if (error instanceof Error) { + return error.message; + } + return String(error); } export async function createOrUpdatePullRequest( - octokit: Octokit, - inputs: CreatePRRequest, - baseRepository: string, - headRepository: string, - branchName: string, + octokit: Octokit, + inputs: CreatePRRequest, + baseRepository: string, + headRepository: string, + branchName: string ): Promise { - const [headOwner] = headRepository.split("/"); - const headBranch = `${headOwner}:${branchName}`; - - // Try to create the pull request - try { - console.log("Attempting creation of pull request"); - const { data: pull } = await octokit.rest.pulls.create({ - ...parseRepository(baseRepository), - title: inputs.title, - head: headBranch, - head_repo: headRepository, - base: inputs.base, - body: inputs.body, - draft: inputs.draft, - }); - console.log( - `Created pull request #${pull.number} (${headBranch} => ${inputs.base}), with info ${JSON.stringify({ - number: pull.number, - html_url: pull.html_url, - created: true, - })}`, - ); + const [headOwner] = headRepository.split("/"); + const headBranch = `${headOwner}:${branchName}`; - return pull.html_url; - } catch (e) { - if (getErrorMessage(e).includes("A pull request already exists for")) { - console.error(`A pull request already exists for ${headBranch}`); - } else { - throw e; - } - } - - // Update the pull request that exists for this branch and base - console.log("Fetching existing pull request"); - const { data: pulls } = await octokit.rest.pulls.list({ - ...parseRepository(baseRepository), - state: "open", - head: headBranch, - base: inputs.base, + // Try to create the pull request + try { + console.log("Attempting creation of pull request"); + const { data: pull } = await octokit.rest.pulls.create({ + ...parseRepository(baseRepository), + title: inputs.title, + head: headBranch, + head_repo: headRepository, + base: inputs.base, + body: inputs.body, + draft: inputs.draft, }); - console.log("Attempting update of pull request"); + console.log( + `Created pull request #${pull.number} (${headBranch} => ${inputs.base}), with info ${JSON.stringify( + { + number: pull.number, + html_url: pull.html_url, + created: true, + } + )}` + ); - const pullNumber = pulls[0]?.number; - if (pullNumber == null) { - throw new Error(`Failed to retrieve pull request number: ${JSON.stringify(pulls)}`); + return pull.html_url; + } catch (e) { + if (getErrorMessage(e).includes("A pull request already exists for")) { + console.error(`A pull request already exists for ${headBranch}`); + } else { + throw e; } + } - const { data: pull } = await octokit.rest.pulls.update({ - ...parseRepository(baseRepository), - pull_number: pullNumber, - title: inputs.title, - body: inputs.body, - }); - console.log( - `Updated pull request #${pull.number} (${headBranch} => ${inputs.base}) with information ${JSON.stringify({ - number: pull.number, - html_url: pull.html_url, - created: false, - })}`, + // Update the pull request that exists for this branch and base + console.log("Fetching existing pull request"); + const { data: pulls } = await octokit.rest.pulls.list({ + ...parseRepository(baseRepository), + state: "open", + head: headBranch, + base: inputs.base, + }); + console.log("Attempting update of pull request"); + + const pullNumber = pulls[0]?.number; + if (pullNumber == null) { + throw new Error( + `Failed to retrieve pull request number: ${JSON.stringify(pulls)}` ); + } - return pull.html_url; + const { data: pull } = await octokit.rest.pulls.update({ + ...parseRepository(baseRepository), + pull_number: pullNumber, + title: inputs.title, + body: inputs.body, + }); + console.log( + `Updated pull request #${pull.number} (${headBranch} => ${inputs.base}) with information ${JSON.stringify( + { + number: pull.number, + html_url: pull.html_url, + created: false, + } + )}` + ); + + return pull.html_url; } diff --git a/packages/commons/github/src/deleteBranch.ts b/packages/commons/github/src/deleteBranch.ts index ee2cc1f4ff..24148e6024 100644 --- a/packages/commons/github/src/deleteBranch.ts +++ b/packages/commons/github/src/deleteBranch.ts @@ -1,20 +1,28 @@ -/* eslint-disable no-console */ import { SimpleGit } from "simple-git"; import { DEFAULT_REMOTE_NAME } from "./constants"; -export async function deleteBranch(git: SimpleGit, branchToDeleteName: string): Promise { - await git.fetch(DEFAULT_REMOTE_NAME, branchToDeleteName); - const deleteResult = await git.branch(["-D", branchToDeleteName]); - // For some reason the API is not typed to return this, but the documentation - // says that it can return a BranchSingleDeleteResult, which is an interface, so we - // cannot do an instance check: - // https://www.npmjs.com/package/simple-git#git-branch - if ("success" in deleteResult) { - console.log(`Deleted branch ${branchToDeleteName}`); - if (deleteResult.success !== true) { - throw new Error("Failed to delete branch, received a failure response: " + JSON.stringify(deleteResult)); - } - } else { - throw new Error("Failed to delete branch, received an unexpected response: " + JSON.stringify(deleteResult)); +export async function deleteBranch( + git: SimpleGit, + branchToDeleteName: string +): Promise { + await git.fetch(DEFAULT_REMOTE_NAME, branchToDeleteName); + const deleteResult = await git.branch(["-D", branchToDeleteName]); + // For some reason the API is not typed to return this, but the documentation + // says that it can return a BranchSingleDeleteResult, which is an interface, so we + // cannot do an instance check: + // https://www.npmjs.com/package/simple-git#git-branch + if ("success" in deleteResult) { + console.log(`Deleted branch ${branchToDeleteName}`); + if (deleteResult.success !== true) { + throw new Error( + "Failed to delete branch, received a failure response: " + + JSON.stringify(deleteResult) + ); } + } else { + throw new Error( + "Failed to delete branch, received an unexpected response: " + + JSON.stringify(deleteResult) + ); + } } diff --git a/packages/commons/github/src/getLatestTag.ts b/packages/commons/github/src/getLatestTag.ts index 030c4dc0e9..87b25ba4e1 100644 --- a/packages/commons/github/src/getLatestTag.ts +++ b/packages/commons/github/src/getLatestTag.ts @@ -5,15 +5,17 @@ import { parseRepository } from "./parseRepository"; * Returns the latest tag on a github repository * @param githubRepository a string with the format `owner/repo` */ -export async function getLatestTag(githubRepository: string): Promise { - const { owner, repo } = parseRepository(githubRepository); +export async function getLatestTag( + githubRepository: string +): Promise { + const { owner, repo } = parseRepository(githubRepository); - const octokit = new Octokit(); - const response = await octokit.rest.repos.listTags({ - owner, - repo, - per_page: 1, // Fetch only the latest tag - }); + const octokit = new Octokit(); + const response = await octokit.rest.repos.listTags({ + owner, + repo, + per_page: 1, // Fetch only the latest tag + }); - return response.data?.[0]?.name; + return response.data?.[0]?.name; } diff --git a/packages/commons/github/src/getOrUpdateBranch.ts b/packages/commons/github/src/getOrUpdateBranch.ts index f981cea9f5..e68ca367c4 100644 --- a/packages/commons/github/src/getOrUpdateBranch.ts +++ b/packages/commons/github/src/getOrUpdateBranch.ts @@ -1,26 +1,28 @@ -/* eslint-disable no-console */ import { SimpleGit } from "simple-git"; import { DEFAULT_REMOTE_NAME } from "./constants"; export async function getOrUpdateBranch( - git: SimpleGit, - defaultBranchName: string, - branchToCheckoutName: string, + git: SimpleGit, + defaultBranchName: string, + branchToCheckoutName: string ): Promise { - try { - // If you can fetch the branch, checkout the branch - await git.fetch(DEFAULT_REMOTE_NAME, branchToCheckoutName); - console.log(`Branch (${branchToCheckoutName}) exists, checking out`); - await git.checkout(branchToCheckoutName); - // Merge the default branch into this branch to update it - // prefer the default branch changes - // - // TODO: we could honestly probably just delete the branch and recreate it - // my concern with that is if there are more changes we decide to make in other actions - // to the same branch that are not OpenAPI related, that we'd lose if we deleted and reupdated the spec. - await git.merge(["-X", "theirs", defaultBranchName]); - } catch (e) { - console.log(`Branch (${branchToCheckoutName}) does not exist, create and checkout`); - await git.checkoutBranch(branchToCheckoutName, defaultBranchName); - } + try { + // If you can fetch the branch, checkout the branch + await git.fetch(DEFAULT_REMOTE_NAME, branchToCheckoutName); + console.log(`Branch (${branchToCheckoutName}) exists, checking out`); + await git.checkout(branchToCheckoutName); + // Merge the default branch into this branch to update it + // prefer the default branch changes + // + // TODO: we could honestly probably just delete the branch and recreate it + // my concern with that is if there are more changes we decide to make in other actions + // to the same branch that are not OpenAPI related, that we'd lose if we deleted and reupdated the spec. + await git.merge(["-X", "theirs", defaultBranchName]); + } catch (e) { + console.log( + `Branch (${branchToCheckoutName}) does not exist, create and checkout`, + e + ); + await git.checkoutBranch(branchToCheckoutName, defaultBranchName); + } } diff --git a/packages/commons/github/src/parseRepository.ts b/packages/commons/github/src/parseRepository.ts index 70dddd1fd3..c167eaf9da 100644 --- a/packages/commons/github/src/parseRepository.ts +++ b/packages/commons/github/src/parseRepository.ts @@ -6,59 +6,83 @@ import { DEFAULT_REMOTE } from "./constants"; * @param githubRepository a string in a variety of formats (e.g. `owner/repo`, `github.com/owner/repo`) */ export function parseRepository(githubRepository: string): RepositoryReference { - const remote = DEFAULT_REMOTE; - let owner: string; - let repo: string; + const remote = DEFAULT_REMOTE; + let owner: string; + let repo: string; - // Remove the prefix and suffix (if any). - if (githubRepository.startsWith("https://")) { - githubRepository = githubRepository.replace("https://", ""); - } - if (githubRepository.endsWith(".git")) { - githubRepository = githubRepository.slice(0, -4); - } + // Remove the prefix and suffix (if any). + if (githubRepository.startsWith("https://")) { + githubRepository = githubRepository.replace("https://", ""); + } + if (githubRepository.endsWith(".git")) { + githubRepository = githubRepository.slice(0, -4); + } - const parts = githubRepository.split("/"); - if (parts.length === 2 && parts[0] != null && parts[1] != null) { - // Format: owner/repo - [owner, repo] = parts; - } else if (parts.length === 3 && parts[0] === DEFAULT_REMOTE && parts[1] != null && parts[2] != null) { - // Format: github.com/owner/repo - [, owner, repo] = parts; - } else { - throw new Error(`Failed to parse GitHub repostiory ${githubRepository}`); - } + const parts = githubRepository.split("/"); + if (parts.length === 2 && parts[0] != null && parts[1] != null) { + // Format: owner/repo + [owner, repo] = parts; + } else if ( + parts.length === 3 && + parts[0] === DEFAULT_REMOTE && + parts[1] != null && + parts[2] != null + ) { + // Format: github.com/owner/repo + [, owner, repo] = parts; + } else { + throw new Error(`Failed to parse GitHub repostiory ${githubRepository}`); + } - return newRepositoryReference({ remote, owner, repo }); + return newRepositoryReference({ remote, owner, repo }); } function newRepositoryReference({ + remote, + owner, + repo, +}: { + remote: string; + owner: string; + repo: string; +}): RepositoryReference { + const repoUrl = getRepoUrl({ remote, owner, repo }); + const cloneUrl = getCloneUrl({ remote, owner, repo }); + return { remote, owner, repo, -}: { - remote: string; - owner: string; - repo: string; -}): RepositoryReference { - const repoUrl = getRepoUrl({ remote, owner, repo }); - const cloneUrl = getCloneUrl({ remote, owner, repo }); - return { - remote, - owner, - repo, - repoUrl, - cloneUrl, - getAuthedCloneUrl: (installationToken: string) => { - return cloneUrl.replace("https://", `https://x-access-token:${installationToken}@`); - }, - }; + repoUrl, + cloneUrl, + getAuthedCloneUrl: (installationToken: string) => { + return cloneUrl.replace( + "https://", + `https://x-access-token:${installationToken}@` + ); + }, + }; } -function getRepoUrl({ remote, owner, repo }: { remote: string; owner: string; repo: string }): string { - return `https://${remote}/${owner}/${repo}`; +function getRepoUrl({ + remote, + owner, + repo, +}: { + remote: string; + owner: string; + repo: string; +}): string { + return `https://${remote}/${owner}/${repo}`; } -function getCloneUrl({ remote, owner, repo }: { remote: string; owner: string; repo: string }): string { - return `https://${remote}/${owner}/${repo}.git`; +function getCloneUrl({ + remote, + owner, + repo, +}: { + remote: string; + owner: string; + repo: string; +}): string { + return `https://${remote}/${owner}/${repo}.git`; } diff --git a/packages/commons/github/tsconfig.eslint.json b/packages/commons/github/tsconfig.eslint.json new file mode 100644 index 0000000000..08c2772f9e --- /dev/null +++ b/packages/commons/github/tsconfig.eslint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + } +} diff --git a/packages/commons/loadable/.prettierrc.cjs b/packages/commons/loadable/.prettierrc.cjs deleted file mode 100644 index 39cf0d0b8c..0000000000 --- a/packages/commons/loadable/.prettierrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("../../../.prettierrc.json"); diff --git a/packages/commons/loadable/package.json b/packages/commons/loadable/package.json index 53d254e88e..78b4e42a67 100644 --- a/packages/commons/loadable/package.json +++ b/packages/commons/loadable/package.json @@ -1,33 +1,33 @@ { "name": "@fern-ui/loadable", "version": "0.0.0", + "private": true, "repository": { "type": "git", "url": "https://github.com/fern-api/fern-platform.git", "directory": "packages/commons/loadable" }, - "private": true, - "files": [ - "dist" - ], + "sideEffects": false, "type": "module", - "source": "src/index.ts", - "module": "src/index.ts", "main": "src/index.ts", + "module": "src/index.ts", + "source": "src/index.ts", "types": "src/index.ts", - "sideEffects": false, + "files": [ + "dist" + ], "scripts": { "clean": "rm -rf ./lib && tsc --build --clean", "compile": "tsc --build", - "test": "vitest --run --passWithNoTests --globals", - "lint:eslint": "eslint --max-warnings 0 . --ignore-path=../../../.eslintignore", + "depcheck": "depcheck", + "format": "prettier --write --ignore-unknown \"**\"", + "format:check": "prettier --check --ignore-unknown \"**\"", + "lint:eslint": "eslint --max-warnings 0 .", "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:style": "stylelint 'src/**/*.scss' --allow-empty-input --max-warnings 0", "lint:style:fix": "pnpm lint:style --fix", - "format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", - "format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", "organize-imports": "organize-imports-cli tsconfig.json", - "depcheck": "depcheck" + "test": "vitest --run --passWithNoTests --globals" }, "dependencies": { "@fern-api/ui-core-utils": "workspace:*" @@ -36,11 +36,11 @@ "@fern-platform/configs": "workspace:*", "@types/node": "^18.7.18", "depcheck": "^1.4.3", - "eslint": "^8.56.0", - "vitest": "^2.1.4", + "eslint": "^9", "organize-imports-cli": "^0.10.0", - "prettier": "^3.3.2", + "prettier": "^3.4.2", "stylelint": "^16.1.0", - "typescript": "5.4.3" + "typescript": "^5", + "vitest": "^2.1.4" } } diff --git a/packages/commons/loadable/src/Loadable.ts b/packages/commons/loadable/src/Loadable.ts index aea9f1d043..c214a67d1b 100644 --- a/packages/commons/loadable/src/Loadable.ts +++ b/packages/commons/loadable/src/Loadable.ts @@ -1,54 +1,72 @@ -export type Loadable = NotStartedLoading | Loading | Loaded | Failed; +export type Loadable = + | NotStartedLoading + | Loading + | Loaded + | Failed; export type UnwrapLoadable = L extends Loadable ? V : never; export interface NotStartedLoading<_V> { - type: "notStartedLoading"; + type: "notStartedLoading"; } export interface Loading<_V> { - type: "loading"; + type: "loading"; } export interface Loaded { - type: "loaded"; - value: V; + type: "loaded"; + value: V; } export interface Failed { - type: "failed"; - error: E; + type: "failed"; + error: E; } export type NotFailed = Exclude, Failed>; -const NOT_STARTED_LOADING: NotStartedLoading = Object.freeze({ type: "notStartedLoading" }); +const NOT_STARTED_LOADING: NotStartedLoading = Object.freeze({ + type: "notStartedLoading", +}); export function notStartedLoading(): NotStartedLoading { - return NOT_STARTED_LOADING; + return NOT_STARTED_LOADING; } -export function isNotStartedLoading(loadable: Loadable | undefined): loadable is NotStartedLoading | undefined; -export function isNotStartedLoading(loadable: Loadable): loadable is NotStartedLoading; export function isNotStartedLoading( - loadable: Loadable | undefined, + loadable: Loadable | undefined +): loadable is NotStartedLoading | undefined; +export function isNotStartedLoading( + loadable: Loadable +): loadable is NotStartedLoading; +export function isNotStartedLoading( + loadable: Loadable | undefined ): loadable is NotStartedLoading | undefined { - return loadable == null || loadable.type === "notStartedLoading"; + return loadable == null || loadable.type === "notStartedLoading"; } const LOADING: Loading = Object.freeze({ type: "loading" }); export function loading(): Loading { - return LOADING; + return LOADING; } -export function isLoading(loadable: Loadable | undefined): loadable is Loading | undefined; +export function isLoading( + loadable: Loadable | undefined +): loadable is Loading | undefined; export function isLoading(loadable: Loadable): loadable is Loading; -export function isLoading(loadable: Loadable | undefined): loadable is Loading | undefined { - return loadable == null || loadable.type === "loading"; +export function isLoading( + loadable: Loadable | undefined +): loadable is Loading | undefined { + return loadable == null || loadable.type === "loading"; } export function loaded(value: V): Loaded { - return { type: "loaded", value }; + return { type: "loaded", value }; } -export function isLoaded(loadable: Loadable | undefined): loadable is Loaded { - return loadable != null && loadable.type === "loaded"; +export function isLoaded( + loadable: Loadable | undefined +): loadable is Loaded { + return loadable != null && loadable.type === "loaded"; } export function failed(error: E): Failed { - return { type: "failed", error }; + return { type: "failed", error }; } -export function isFailed(loadable: Loadable | undefined): loadable is Failed { - return loadable != null && loadable.type === "failed"; +export function isFailed( + loadable: Loadable | undefined +): loadable is Failed { + return loadable != null && loadable.type === "failed"; } diff --git a/packages/commons/loadable/src/declarations.d.ts b/packages/commons/loadable/src/declarations.d.ts index 56c5f853b0..6189f628fc 100644 --- a/packages/commons/loadable/src/declarations.d.ts +++ b/packages/commons/loadable/src/declarations.d.ts @@ -2,8 +2,8 @@ type CSSModuleClasses = Readonly>; declare module "*.module.scss" { - const classes: CSSModuleClasses; - export default classes; + const classes: CSSModuleClasses; + export default classes; } // CSS @@ -12,26 +12,26 @@ declare module "*.scss" {} // images declare module "*.png" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.jpg" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.jpeg" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.gif" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.svg" { - const src: string; - export default { src }; + const src: string; + export default { src }; } declare module "*.ico" { - const src: string; - export default { src }; + const src: string; + export default { src }; } diff --git a/packages/commons/loadable/src/index.ts b/packages/commons/loadable/src/index.ts index e8040bc616..1390549207 100644 --- a/packages/commons/loadable/src/index.ts +++ b/packages/commons/loadable/src/index.ts @@ -1,21 +1,28 @@ export { - failed, - isFailed, - isLoaded, - isLoading, - isNotStartedLoading, - loaded, - loading, - notStartedLoading, + failed, + isFailed, + isLoaded, + isLoading, + isNotStartedLoading, + loaded, + loading, + notStartedLoading, +} from "./Loadable"; +export type { + Failed, + Loadable, + Loaded, + Loading, + NotFailed, + NotStartedLoading, } from "./Loadable"; -export type { Failed, Loadable, Loaded, Loading, NotFailed, NotStartedLoading } from "./Loadable"; export { - flatMapLoadable, - getLoadableValue, - mapLoadable, - mapLoadableArray, - mapLoadables, - mapNotFailedLoadableArray, - visitLoadableArray, + flatMapLoadable, + getLoadableValue, + mapLoadable, + mapLoadableArray, + mapLoadables, + mapNotFailedLoadableArray, + visitLoadableArray, } from "./utils"; export { visitLoadable, type LoadableVisitor } from "./visitor"; diff --git a/packages/commons/loadable/src/utils.ts b/packages/commons/loadable/src/utils.ts index 50784bc644..ae9b99099c 100644 --- a/packages/commons/loadable/src/utils.ts +++ b/packages/commons/loadable/src/utils.ts @@ -1,122 +1,151 @@ import { keys } from "@fern-api/ui-core-utils"; -import { failed, isFailed, isLoaded, Loadable, loaded, Loading, loading, NotFailed } from "./Loadable"; +import { + failed, + isFailed, + isLoaded, + Loadable, + loaded, + Loading, + loading, + NotFailed, +} from "./Loadable"; import { visitLoadable } from "./visitor"; -export function getLoadableValue(loadable: Loadable | undefined): V | undefined { - if (loadable != null && loadable.type === "loaded") { - return loadable.value; - } - return undefined; +export function getLoadableValue( + loadable: Loadable | undefined +): V | undefined { + if (loadable != null && loadable.type === "loaded") { + return loadable.value; + } + return undefined; } -export function mapLoadable(loadable: NotFailed | undefined, map: (value: V) => U): NotFailed; -export function mapLoadable(loadable: Loadable | undefined, map: (value: V) => U): Loadable; -export function mapLoadable(loadable: Loadable | undefined, map: (value: V) => U): Loadable { - return flatMapLoadable(loadable, (value) => loaded(map(value))); +export function mapLoadable( + loadable: NotFailed | undefined, + map: (value: V) => U +): NotFailed; +export function mapLoadable( + loadable: Loadable | undefined, + map: (value: V) => U +): Loadable; +export function mapLoadable( + loadable: Loadable | undefined, + map: (value: V) => U +): Loadable { + return flatMapLoadable(loadable, (value) => loaded(map(value))); } -export function mapLoadables(loadables: { [K in keyof T]: NotFailed }, map: (values: T) => R): NotFailed; -export function mapLoadables(loadables: { [K in keyof T]: Loadable }, map: (values: T) => R): Loadable; -export function mapLoadables(loadables: { [K in keyof T]: Loadable }, map: (values: T) => R): Loadable { - return flatMapLoadables(loadables, (values) => loaded(map(values))); +export function mapLoadables( + loadables: { [K in keyof T]: NotFailed }, + map: (values: T) => R +): NotFailed; +export function mapLoadables( + loadables: { [K in keyof T]: Loadable }, + map: (values: T) => R +): Loadable; +export function mapLoadables( + loadables: { [K in keyof T]: Loadable }, + map: (values: T) => R +): Loadable { + return flatMapLoadables(loadables, (values) => loaded(map(values))); } export function flatMapLoadables( - loadables: { [K in keyof T]: NotFailed }, - map: (values: T) => NotFailed, + loadables: { [K in keyof T]: NotFailed }, + map: (values: T) => NotFailed ): NotFailed; export function flatMapLoadables( - loadables: { [K in keyof T]: Loadable }, - map: (values: T) => Loadable, + loadables: { [K in keyof T]: Loadable }, + map: (values: T) => Loadable ): Loadable; export function flatMapLoadables( - loadables: { [K in keyof T]: Loadable }, - map: (values: T) => Loadable, + loadables: { [K in keyof T]: Loadable }, + map: (values: T) => Loadable ): Loadable { - // prioritize the failed loadables, so that if any of the loadables are - // failed, we return a failed loadable - const sortedKeys = keys(loadables).sort((aKey, bKey) => { - const aLoadable = loadables[aKey]; - const bLoadable = loadables[bKey]; - if (isFailed(aLoadable)) { - return -1; - } - if (isFailed(bLoadable)) { - return 1; - } - return 0; - }); + // prioritize the failed loadables, so that if any of the loadables are + // failed, we return a failed loadable + const sortedKeys = keys(loadables).sort((aKey, bKey) => { + const aLoadable = loadables[aKey]; + const bLoadable = loadables[bKey]; + if (isFailed(aLoadable)) { + return -1; + } + if (isFailed(bLoadable)) { + return 1; + } + return 0; + }); - const values: T = {} as T; + const values: T = {} as T; - for (const key of sortedKeys) { - const loadable = loadables[key]; - if (!isLoaded(loadable)) { - return loadable; - } - values[key] = loadable.value; + for (const key of sortedKeys) { + const loadable = loadables[key]; + if (!isLoaded(loadable)) { + return loadable; } + values[key] = loadable.value; + } - return map(values); + return map(values); } export function flatMapLoadable( - loadable: Loadable | undefined, - map: (value: V) => Loadable, + loadable: Loadable | undefined, + map: (value: V) => Loadable ): Loadable { - return visitLoadable, E>(loadable, { - loading, - loaded: (value) => map(value), - failed, - }); + return visitLoadable, E>(loadable, { + loading, + loaded: (value) => map(value), + failed, + }); } export function mapLoadableArray( - loadable: NotFailed | undefined, - map: (value: V) => U[], - options?: { numLoading?: number }, + loadable: NotFailed | undefined, + map: (value: V) => U[], + options?: { numLoading?: number } ): NotFailed[]; export function mapLoadableArray( - loadable: Loadable | undefined, - map: (value: V) => U[], - options?: { numLoading?: number }, + loadable: Loadable | undefined, + map: (value: V) => U[], + options?: { numLoading?: number } ): Loadable[]; export function mapLoadableArray( - loadable: Loadable | undefined, - map: (value: V) => U[], - { numLoading = 3 }: { numLoading?: number } = {}, + loadable: Loadable | undefined, + map: (value: V) => U[], + { numLoading = 3 }: { numLoading?: number } = {} ): Loadable[] { - return visitLoadable[]>(loadable, { - loading: () => Array>(numLoading).fill(loading()), - loaded: (value) => map(value).map(loaded), - failed: (error) => Array>(numLoading).fill(failed(error)), - }); + return visitLoadable[]>(loadable, { + loading: () => Array>(numLoading).fill(loading()), + loaded: (value) => map(value).map(loaded), + failed: (error) => Array>(numLoading).fill(failed(error)), + }); } export function mapNotFailedLoadableArray( - loadable: Loadable | undefined, - map: (items: NotFailed[]) => W, - { numLoading = 3 }: { numLoading?: number } = {}, + loadable: Loadable | undefined, + map: (items: NotFailed[]) => W, + { numLoading = 3 }: { numLoading?: number } = {} ): W { - if (isLoaded(loadable)) { - return map(loadable.value.map(loaded)); - } - return map(Array>(numLoading).fill(loading())); + if (isLoaded(loadable)) { + return map(loadable.value.map(loaded)); + } + return map(Array>(numLoading).fill(loading())); } export function visitLoadableArray( - loadable: Loadable | undefined, - visitor: { - notFailed: (items: NotFailed[]) => W; - failed: (error: unknown) => W; - }, - { numLoading = 3 }: { numLoading?: number } = {}, + loadable: Loadable | undefined, + visitor: { + notFailed: (items: NotFailed[]) => W; + failed: (error: unknown) => W; + }, + { numLoading = 3 }: { numLoading?: number } = {} ): W { - if (isFailed(loadable)) { - return visitor.failed(loadable.error); - } - if (isLoaded(loadable)) { - return visitor.notFailed(loadable.value.map(loaded)); - } - return visitor.notFailed(Array>(numLoading).fill(loading())); + if (isFailed(loadable)) { + return visitor.failed(loadable.error); + } + if (isLoaded(loadable)) { + return visitor.notFailed(loadable.value.map(loaded)); + } + return visitor.notFailed(Array>(numLoading).fill(loading())); } diff --git a/packages/commons/loadable/src/visitor.ts b/packages/commons/loadable/src/visitor.ts index 69fd2154fd..16a83acb8b 100644 --- a/packages/commons/loadable/src/visitor.ts +++ b/packages/commons/loadable/src/visitor.ts @@ -1,24 +1,34 @@ import { assertNever } from "@fern-api/ui-core-utils"; -import { isFailed, isLoaded, isLoading, isNotStartedLoading, Loadable } from "./Loadable"; +import { + isFailed, + isLoaded, + isLoading, + isNotStartedLoading, + Loadable, +} from "./Loadable"; export function visitLoadable( - loadable: Loadable | undefined, - visitor: LoadableVisitor, + loadable: Loadable | undefined, + visitor: LoadableVisitor ): U { - if (loadable == null || isNotStartedLoading(loadable) || isLoading(loadable)) { - return visitor.loading(); - } - if (isLoaded(loadable)) { - return visitor.loaded(loadable.value); - } - if (isFailed(loadable)) { - return visitor.failed(loadable.error); - } - assertNever(loadable); + if ( + loadable == null || + isNotStartedLoading(loadable) || + isLoading(loadable) + ) { + return visitor.loading(); + } + if (isLoaded(loadable)) { + return visitor.loaded(loadable.value); + } + if (isFailed(loadable)) { + return visitor.failed(loadable.error); + } + assertNever(loadable); } export interface LoadableVisitor { - loading: () => U; - loaded: (value: V) => U; - failed: (error: E) => U; + loading: () => U; + loaded: (value: V) => U; + failed: (error: E) => U; } diff --git a/packages/commons/loadable/tsconfig.eslint.json b/packages/commons/loadable/tsconfig.eslint.json new file mode 100644 index 0000000000..08c2772f9e --- /dev/null +++ b/packages/commons/loadable/tsconfig.eslint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + } +} diff --git a/packages/commons/next-seo/.prettierrc.cjs b/packages/commons/next-seo/.prettierrc.cjs deleted file mode 100644 index 39cf0d0b8c..0000000000 --- a/packages/commons/next-seo/.prettierrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("../../../.prettierrc.json"); diff --git a/packages/commons/next-seo/package.json b/packages/commons/next-seo/package.json deleted file mode 100644 index bff3eaf2ea..0000000000 --- a/packages/commons/next-seo/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@fern-ui/next-seo", - "version": "0.0.0", - "repository": { - "type": "git", - "url": "https://github.com/fern-api/fern-platform.git", - "directory": "packages/commons/core-utils" - }, - "private": true, - "files": [ - "dist" - ], - "type": "module", - "source": "src/index.ts", - "module": "src/index.ts", - "main": "src/index.ts", - "types": "src/index.ts", - "sideEffects": false, - "scripts": { - "clean": "rm -rf ./lib && tsc --build --clean", - "compile": "tsc --build", - "test": "vitest --run --passWithNoTests --globals", - "lint:eslint": "eslint --max-warnings 0 . --ignore-path=../../../.eslintignore", - "lint:eslint:fix": "pnpm lint:eslint --fix", - "lint:style": "stylelint 'src/**/*.scss' --allow-empty-input --max-warnings 0", - "lint:style:fix": "pnpm lint:style --fix", - "format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", - "format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"", - "organize-imports": "organize-imports-cli tsconfig.json", - "depcheck": "depcheck" - }, - "dependencies": { - "@fern-api/fdr-sdk": "workspace:*", - "next": "^14", - "react": "^18" - }, - "devDependencies": { - "@fern-platform/configs": "workspace:*", - "@types/node": "^18.7.18", - "@types/react": "^18", - "depcheck": "^1.4.3", - "eslint": "^8.56.0", - "organize-imports-cli": "^0.10.0", - "prettier": "^3.3.2", - "stylelint": "^16.1.0", - "typescript": "5.4.3", - "vitest": "^2.1.4" - } -} diff --git a/packages/commons/next-seo/src/jsonld/components/Breadcrumb.tsx b/packages/commons/next-seo/src/jsonld/components/Breadcrumb.tsx deleted file mode 100644 index 3eb680a5ff..0000000000 --- a/packages/commons/next-seo/src/jsonld/components/Breadcrumb.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type * as FernDocs from "@fern-api/fdr-sdk/docs"; -import Head from "next/head"; -import { ReactElement, memo } from "react"; - -export const Breadcrumb = memo( - ({ breadcrumbList }: { breadcrumbList: FernDocs.JsonLdBreadcrumbList }): ReactElement => { - return ( - - + ))} + {js?.files.map((file) => ( + - ))} - {js?.files.map((file) => ( -