diff --git a/docs/link-checker/package.json b/docs/link-checker/package.json index 6e4ffaf8fc026..d88a14ca2881b 100644 --- a/docs/link-checker/package.json +++ b/docs/link-checker/package.json @@ -6,11 +6,10 @@ "src" ], "scripts": { - "check-links": "cd ../site/content && tsx ../../link-checker/src/validate-docs-links.ts" + "check-links": "cd ../site/content && node --experimental-strip-types ../../link-checker/src/validate-docs-links.ts" }, "devDependencies": { "@types/node": "22.7.8", - "tsx": "4.19.1", "typescript": "5.5.4" }, "dependencies": { diff --git a/docs/link-checker/src/validate-docs-links.ts b/docs/link-checker/src/validate-docs-links.ts index 380dce1bad713..c6faa836ba021 100644 --- a/docs/link-checker/src/validate-docs-links.ts +++ b/docs/link-checker/src/validate-docs-links.ts @@ -1,4 +1,4 @@ -import { collectLinkErrors } from "./markdown"; +import { collectLinkErrors } from "./markdown.ts"; /* This script validates internal links in /docs and /errors including internal, diff --git a/docs/link-checker/tsconfig.json b/docs/link-checker/tsconfig.json index 203631f74470a..07db6cbcc0ebf 100644 --- a/docs/link-checker/tsconfig.json +++ b/docs/link-checker/tsconfig.json @@ -7,7 +7,9 @@ "strict": true, "noImplicitAny": true, "allowSyntheticDefaultImports": true, - "esModuleInterop": true + "esModuleInterop": true, + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true }, "include": ["src", "types.d.ts"] } diff --git a/docs/site/package.json b/docs/site/package.json index f94e85d7f11ac..4ad9e16f98aee 100644 --- a/docs/site/package.json +++ b/docs/site/package.json @@ -14,14 +14,14 @@ "lint": "fumadocs-mdx && next lint", "lint:fix": "fumadocs-mdx && next lint --fix", "lint:prettier": "prettier --write -c . --cache --ignore-path=../../.prettierignore", - "index-docs": "tsx ./scripts/sync-algolia.mjs", - "rss": "node scripts/generate-rss.cjs", - "schema": "node scripts/copy-json-schema.mjs", + "index-docs": "node --experimental-strip-types ./scripts/sync-algolia.ts", + "rss": "node --experimental-strip-types scripts/generate-rss.ts", + "schema": "node --experimental-strip-types scripts/copy-json-schema.ts", "start": "next start", "check-types": "fumadocs-mdx && tsc --noEmit", - "generate-openapi": "node ./scripts/generate-docs.mjs", - "write-private-files": "node ./scripts/write-private-files.mjs", - "collect-examples-data": "node ./scripts/collect-examples-data.mjs" + "generate-openapi": "node --experimental-strip-types ./scripts/generate-docs.ts", + "write-private-files": "node --experimental-strip-types ./scripts/write-private-files.ts", + "collect-examples-data": "node --experimental-strip-types ./scripts/collect-examples-data.ts" }, "dependencies": { "@heroicons/react": "1.0.6", @@ -71,6 +71,7 @@ "@types/node": "20.11.30", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", + "@types/rss": "^0.0.32", "@types/semver": "^7.3.13", "@vercel/analytics": "1.5.0", "@vercel/toolbar": "0.1.30", @@ -81,7 +82,6 @@ "rss": "^1.2.2", "spellchecker-cli": "^6.2.0", "tailwindcss": "^3.4.1", - "tsx": "^4.6.2", "typescript": "5.6.3" } } diff --git a/docs/site/scripts/collect-examples-data.mjs b/docs/site/scripts/collect-examples-data.ts similarity index 83% rename from docs/site/scripts/collect-examples-data.mjs rename to docs/site/scripts/collect-examples-data.ts index fcc0b4b88c407..9476bd6a0423c 100644 --- a/docs/site/scripts/collect-examples-data.mjs +++ b/docs/site/scripts/collect-examples-data.ts @@ -1,9 +1,3 @@ -// @ts-check -// @ts-nocheck -// This script exports examples data to a JSON file -// Use Node.js ESM syntax -// @ts-ignore - import fs from "node:fs"; import path from "node:path"; import { z } from "zod"; @@ -32,7 +26,7 @@ const examples = fs dirent.name !== "node_modules" ) .filter((dirent) => dirent.name !== "with-nextjs") - // @ts-expect-error + // @ts-expect-error -- TODO .sort((a, b) => a.name - b.name) .map((dirent) => dirent.name); @@ -43,11 +37,13 @@ for (const example of examples) { if (fs.existsSync(metaPath)) { try { const metaContent = fs.readFileSync(metaPath, "utf8"); - const metaJson = JSON.parse(metaContent); + const metaJson = JSON.parse(metaContent) as z.infer< + typeof ExampleMetaSchema + >; EXAMPLES.push({ ...metaJson, slug: example }); } catch (error) { - // @ts-expect-error - throw new Error(error); + // Ensure error is converted to string when creating new Error + throw new Error(error instanceof Error ? error.message : String(error)); } } } diff --git a/docs/site/scripts/copy-json-schema.mjs b/docs/site/scripts/copy-json-schema.ts similarity index 100% rename from docs/site/scripts/copy-json-schema.mjs rename to docs/site/scripts/copy-json-schema.ts diff --git a/docs/site/scripts/generate-docs.mjs b/docs/site/scripts/generate-docs.ts similarity index 74% rename from docs/site/scripts/generate-docs.mjs rename to docs/site/scripts/generate-docs.ts index 41bc46478a14f..86b6101a5d146 100644 --- a/docs/site/scripts/generate-docs.mjs +++ b/docs/site/scripts/generate-docs.ts @@ -18,17 +18,58 @@ import { generateFiles } from "fumadocs-openapi"; const out = "./content/openapi"; +interface OpenAPISpec { + paths?: Record< + string, + Record< + string, + { + responses?: Record< + string, + { + description?: string; + headers?: Record< + string, + { + schema: { + type: string; + }; + description: string; + } + >; + } + >; + } + > + >; + servers?: Array<{ + url: string; + description?: string; + }>; +} + +// Define a more specific type for the OpenAPI value structure +type OpenAPIValue = + | string + | number + | boolean + | null + | { [key: string]: OpenAPIValue } + | Array; + /* The Vercel Remote Cache spec has examples that show Vercel values. * Removing them makes the self-hosted spec easier to use. */ -const removeExamples = (obj) => { +const removeExamples = (obj: OpenAPIValue): OpenAPIValue => { if (!obj || typeof obj !== "object") return obj; if (Array.isArray(obj)) { return obj.map((item) => removeExamples(item)); } - const result = {}; - for (const [key, value] of Object.entries(obj)) { + const result: Record = {}; + for (const [key, value] of Object.entries( + obj as Record + )) { if (key !== "example") { result[key] = removeExamples(value); } @@ -39,7 +80,7 @@ const removeExamples = (obj) => { /* The Vercel Remote Cache spec has responses related to billing. * Self-hosted users don't need these. */ -function removeBillingRelated403Responses(spec) { +function removeBillingRelated403Responses(spec: OpenAPISpec): OpenAPISpec { // Define billing-related phrases to filter out const billingPhrases = [ "The customer has reached their spend cap limit and has been paused", @@ -56,11 +97,11 @@ function removeBillingRelated403Responses(spec) { const methodObj = pathObj[method]; // Check if the method has responses - if (methodObj?.responses["403"]) { + if (methodObj.responses?.["403"]) { const description = methodObj.responses["403"].description; // Split the description by newlines - const descriptionLines = description.split("\n"); + const descriptionLines = description?.split("\n") ?? []; // Filter out billing-related lines const filteredLines = descriptionLines.filter((line) => { @@ -83,15 +124,15 @@ function removeBillingRelated403Responses(spec) { } /* Add x-artifact-tag header to artifact download endpoint response */ -function addArtifactTagHeader(spec) { +function addArtifactTagHeader(spec: OpenAPISpec): OpenAPISpec { // Target only the specific /v8/artifacts/{hash} endpoint const artifactEndpoint = "/v8/artifacts/{hash}"; if (spec.paths?.[artifactEndpoint]) { // Get the GET method for this endpoint - const getMethod = spec.paths[artifactEndpoint]?.get; + const getMethod = spec.paths[artifactEndpoint].get; - if (getMethod?.responses?.["200"]) { + if (getMethod.responses?.["200"]) { const response = getMethod.responses["200"]; // Add headers to the response if they don't exist @@ -112,7 +153,7 @@ function addArtifactTagHeader(spec) { return spec; } -const updateServerDescription = (spec) => { +const updateServerDescription = (spec: OpenAPISpec): OpenAPISpec => { if (spec.servers && spec.servers.length > 0) { const serverIndex = spec.servers.findIndex( (server) => server.url === "https://api.vercel.com" @@ -124,11 +165,12 @@ const updateServerDescription = (spec) => { } return spec; } + return spec; }; const thing = await fetch("https://turborepo.com/api/remote-cache-spec") .then((res) => res.json()) - .then((json) => removeExamples(json)) + .then((json: unknown) => removeExamples(json as OpenAPIValue) as OpenAPISpec) .then((json) => removeBillingRelated403Responses(json)) .then((json) => addArtifactTagHeader(json)) .then((json) => updateServerDescription(json)); diff --git a/docs/site/scripts/generate-rss.cjs b/docs/site/scripts/generate-rss.ts similarity index 53% rename from docs/site/scripts/generate-rss.cjs rename to docs/site/scripts/generate-rss.ts index deb21e5386f0c..712c49bce0828 100644 --- a/docs/site/scripts/generate-rss.cjs +++ b/docs/site/scripts/generate-rss.ts @@ -1,9 +1,22 @@ -const { promises: fs, statSync } = require("node:fs"); -const path = require("node:path"); -const RSS = require("rss"); -const matter = require("gray-matter"); +import { promises as fs, statSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import RSS from "rss"; +import matter from "gray-matter"; -function dateSortDesc(a, b) { +interface FrontMatter { + data: { + date: string; + title: string; + description: string; + ogImage: string; + href?: string; + }; + content: string; + slug?: string; +} + +function dateSortDesc(a: FrontMatter, b: FrontMatter): number { const date1 = new Date(a.data.date); const date2 = new Date(b.data.date); if (date1 > date2) return -1; @@ -11,7 +24,7 @@ function dateSortDesc(a, b) { return 0; } -async function generate() { +async function generate(): Promise { const feed = new RSS({ title: "Turborepo Blog", description: "Turborepo news, updates, and announcements.", @@ -20,20 +33,26 @@ async function generate() { image_url: "https://turborepo.com/api/og", }); - const posts = await fs.readdir(path.join(__dirname, "..", "content", "blog")); + const currentDir = path.dirname(fileURLToPath(import.meta.url)); - const promises = posts.map(async (post) => { + const posts = await fs.readdir( + path.join(currentDir, "..", "content", "blog") + ); + + const promises = posts.map(async (post: string) => { if (post.startsWith("index.") || post.startsWith("_meta.json")) return; const file = await fs.readFile( - path.join(__dirname, "..", "content", "blog", post) + path.join(currentDir, "..", "content", "blog", post) ); - const frontmatter = matter(file); - if (frontmatter.data.href) return; - return { ...frontmatter, slug: post.replace(".mdx", "") }; + const { data, content } = matter(file); + if (data.href) return; + return { data, content, slug: post.replace(".mdx", "") } as FrontMatter; }); const results = await Promise.all(promises); - const sortedData = results.filter(Boolean); // Remove null values + const sortedData = results.filter( + (item): item is FrontMatter & { slug: string } => Boolean(item) + ); // sort by date sortedData.sort(dateSortDesc); @@ -41,8 +60,9 @@ async function generate() { for (const frontmatter of sortedData) { // get the og image size const stat = statSync( - path.join(__dirname, "..", "public", frontmatter.data.ogImage) + path.join(currentDir, "..", "public", frontmatter.data.ogImage) ); + feed.item({ title: frontmatter.data.title, url: `https://turborepo.com/blog/${frontmatter.slug}`, // intentionally including slash here @@ -59,4 +79,4 @@ async function generate() { await fs.writeFile("./public/feed.xml", feed.xml({ indent: true })); } -generate(); +void generate(); diff --git a/docs/site/scripts/sync-algolia.mjs b/docs/site/scripts/sync-algolia.ts similarity index 82% rename from docs/site/scripts/sync-algolia.mjs rename to docs/site/scripts/sync-algolia.ts index cb0317552524c..2dd45bb77bb55 100644 --- a/docs/site/scripts/sync-algolia.mjs +++ b/docs/site/scripts/sync-algolia.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs"; import algosearch from "algoliasearch"; import env from "@next/env"; -import { sync } from "fumadocs-core/search/algolia"; +import { sync, type DocumentRecord } from "fumadocs-core/search/algolia"; // We assume you're working in development if this is not provided. if (!process.env.NEXT_PUBLIC_ALGOLIA_INDEX) { @@ -19,12 +19,17 @@ const ALGOLIA_INDEX_NAME = process.env.NEXT_PUBLIC_ALGOLIA_INDEX ?? "_docs_dev"; const content = fs.readFileSync(".next/server/app/static.json.body"); -/** @type {import('fumadocs-core/search/algolia').DocumentRecord[]} **/ -const indexes = JSON.parse(content.toString()).filter( +const indexes = ( + JSON.parse(content.toString()) as Array +).filter( // These path don't have information that we think people want in search. (doc) => !["docs/community", "/docs"].includes(doc.url) ); +if (!process.env.ALGOLIA_APP_ID) { + throw new Error("No ALGOLIA_APP_ID found."); +} + const algoliaClient = algosearch( process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_API_KEY @@ -56,7 +61,7 @@ void sync(algoliaClient, { .then(() => { console.log(`Search index updated for ${ALGOLIA_INDEX_NAME}.`); }) - .catch((err) => { + .catch((err: unknown) => { console.error(err); - throw new Error(err); + throw err instanceof Error ? err : new Error(String(err)); }); diff --git a/docs/site/scripts/write-private-files.mjs b/docs/site/scripts/write-private-files.mjs deleted file mode 100644 index 269b58dd814d6..0000000000000 --- a/docs/site/scripts/write-private-files.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import { writeFile } from "node:fs/promises"; - -// List of files that get overwritten during CI. -// These files have content from our closed source repos -// but we still want the site to run smoothly for -// open source contributors who don't have access. -// For this reasons, these files are stubbed in -// source control and overwritten during the build. -// Files listed below are responsible for analytics -// and cookie consent banners. -const FILES_TO_WRITE = [ - { - path: "./lib/site-analytics/index.ts", - envVarKey: "SITE_ANALYTICS_MODULE_CODE", - }, - { - path: "./lib/site-analytics/index.ts", - envVarKey: "SITE_ANALYTICS_MODULE_CODE", - }, -]; - -async function modifyFiles() { - if (!process.env.CI) { - return; - } - - for (const fileConfig of FILES_TO_WRITE) { - try { - console.log(`Processing file: ${fileConfig.path}`); - - // Step 1: Delete the file's contents by writing an empty string - await writeFile(fileConfig.path, ""); - - const envVarContent = process.env[fileConfig.envVarKey]; - if (!envVarContent) { - throw new Error(`No process.env.${fileConfig.envVarKey} provided.`); - } - - // Step 2: Write new contents to the file - await writeFile(fileConfig.path, envVarContent); - console.log(`New contents written to ${fileConfig.path} successfully.`); - } catch (error) { - console.error(`Error modifying file ${fileConfig.path}:`, error.message); - process.exit(1); - } - } -} - -// Execute the function -modifyFiles(); diff --git a/docs/site/scripts/write-private-files.ts b/docs/site/scripts/write-private-files.ts new file mode 100644 index 0000000000000..7af3a2ba50bed --- /dev/null +++ b/docs/site/scripts/write-private-files.ts @@ -0,0 +1,58 @@ +import { writeFile } from "node:fs/promises"; + +// List of files that get overwritten during CI. +// These files have content from our closed source repos +// but we still want the site to run smoothly for +// open source contributors who don't have access. +// For this reasons, these files are stubbed in +// source control and overwritten during the build. +// Files listed below are responsible for analytics +// and cookie consent banners. +const FILES_TO_WRITE = [ + { + path: "./lib/site-analytics/index.ts", + envVarKey: "SITE_ANALYTICS_MODULE_CODE", + }, + { + path: "./lib/site-analytics/index.ts", + envVarKey: "SITE_ANALYTICS_MODULE_CODE", + }, +]; + +async function modifyFiles(): Promise { + if (!process.env.CI) { + return; + } + + await Promise.all( + FILES_TO_WRITE.map(async (fileConfig) => { + try { + console.log(`Processing file: ${fileConfig.path}`); + + // Step 1: Delete the file's contents by writing an empty string + await writeFile(fileConfig.path, ""); + + const envVarContent = process.env[fileConfig.envVarKey]; + if (!envVarContent) { + throw new Error(`No process.env.${fileConfig.envVarKey} provided.`); + } + + // Step 2: Write new contents to the file + await writeFile(fileConfig.path, envVarContent); + console.log(`New contents written to ${fileConfig.path} successfully.`); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error(`Error modifying file ${fileConfig.path}:`, errorMessage); + process.exit(1); + } + }) + ); +} + +// Execute the function +void modifyFiles().catch((error: unknown) => { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("Failed to modify files:", errorMessage); + process.exit(1); +}); diff --git a/docs/site/tsconfig.json b/docs/site/tsconfig.json index 97cf45bfa53ce..69d9d9e573e33 100644 --- a/docs/site/tsconfig.json +++ b/docs/site/tsconfig.json @@ -15,7 +15,9 @@ "lib": ["dom", "dom.iterable", "esnext"], "resolveJsonModule": true, "noEmit": true, - "incremental": true + "incremental": true, + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true }, "include": [ "next-env.d.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4849154179cc8..97f5dd37bdd91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,9 +88,6 @@ importers: '@types/node': specifier: 22.7.8 version: 22.7.8 - tsx: - specifier: 4.19.1 - version: 4.19.1 typescript: specifier: 5.5.4 version: 5.5.4 @@ -233,6 +230,9 @@ importers: '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + '@types/rss': + specifier: ^0.0.32 + version: 0.0.32 '@types/semver': specifier: ^7.3.13 version: 7.5.8 @@ -263,9 +263,6 @@ importers: tailwindcss: specifier: ^3.4.1 version: 3.4.17 - tsx: - specifier: ^4.6.2 - version: 4.19.1 typescript: specifier: 5.6.3 version: 5.6.3 @@ -2135,8 +2132,8 @@ packages: dev: false optional: true - /@emnapi/runtime@1.4.0: - resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} + /@emnapi/runtime@1.4.3: + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} requiresBuild: true dependencies: tslib: 2.8.1 @@ -3312,7 +3309,7 @@ packages: cpu: [wasm32] requiresBuild: true dependencies: - '@emnapi/runtime': 1.4.0 + '@emnapi/runtime': 1.4.3 optional: true /@img/sharp-win32-ia32@0.33.2: @@ -5823,6 +5820,10 @@ packages: resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} dev: true + /@types/rss@0.0.32: + resolution: {integrity: sha512-2oKNqKyUY4RSdvl5eZR1n2Q9yvw3XTe3mQHsFPn9alaNBxfPnbXBtGP8R0SV8pK1PrVnLul0zx7izbm5/gF5Qw==} + dev: true + /@types/semver@7.3.12: resolution: {integrity: sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==} dev: true @@ -16896,6 +16897,7 @@ packages: /typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} + hasBin: true dev: true /typical@2.6.1: