diff --git a/app/[locale]/staking/_components/staking.tsx b/app/[locale]/staking/_components/staking.tsx index 8d825d34dee..6196ea39878 100644 --- a/app/[locale]/staking/_components/staking.tsx +++ b/app/[locale]/staking/_components/staking.tsx @@ -1,6 +1,5 @@ -"use client" - import { type HTMLAttributes, ReactNode } from "react" +import { getTranslations } from "next-intl/server" import type { ChildOnlyProp, @@ -32,7 +31,6 @@ import { ListItem, UnorderedList } from "@/components/ui/list" import { cn } from "@/lib/utils/cn" -import useTranslation from "@/hooks/useTranslation" import rhino from "@/public/images/upgrades/upgrade_rhino.png" type BenefitsType = { @@ -109,12 +107,12 @@ type Props = PageWithContributorsProps & { data: StakingStatsData } -const StakingPage = ({ +const StakingPage = async ({ data, contributors, lastEditLocaleTimestamp, }: Props) => { - const { t } = useTranslation("page-staking") + const t = await getTranslations("page-staking") const heroContent = { title: t("page-staking-hero-title"), diff --git a/docs/solutions/build-errors/turbopack-file-tracer-doesnt-fold-cross-module-string-constants.md b/docs/solutions/build-errors/turbopack-file-tracer-doesnt-fold-cross-module-string-constants.md new file mode 100644 index 00000000000..2a781af97af --- /dev/null +++ b/docs/solutions/build-errors/turbopack-file-tracer-doesnt-fold-cross-module-string-constants.md @@ -0,0 +1,110 @@ +--- +title: "Turbopack File Tracer Doesn't Resolve String Constants Imported from Other Modules" +slug: "turbopack-file-tracer-doesnt-fold-cross-module-string-constants" +category: "build-errors" +severity: "low" +symptoms: + - "Turbopack build emits 'Overly broad patterns' warnings pointing at path.join() calls in the MDX pipeline" + - "Warning pattern drops the literal path prefix and widens to mostly when the prefix comes from an imported constant" + - "Pattern match count jumps ~4x (e.g. 28k → 110k files) compared to the same join() with an inlined literal" +components: + - "src/lib/md/compile.ts" + - "src/lib/utils/md.ts" + - "src/lib/i18n/translationRegistry.ts" + - "next.config.js (turbopack.ignoreIssue)" +tags: + - "turbopack" + - "next.js" + - "file-tracing" + - "nft" + - "mdx" + - "output-file-tracing" +resolved_at: "2026-04-01" +pr: "#17906" +--- + +# Turbopack File Tracer Doesn't Resolve String Constants Imported from Other Modules + +## Problem + +Turbopack's file tracer emits "Overly broad patterns can lead to build performance issues and over bundling" warnings on `path.join()` calls that mix a module-level constant with dynamic arguments: + +```ts +// src/lib/md/compile.ts +import { CONTENT_DIR, CONTENT_PATH } from "../constants" +// ... +const mdPath = join(CONTENT_PATH, ...slugArray) +const mdDir = join(CONTENT_DIR, ...slugArray) +``` + +The constants are plain string literals (`CONTENT_DIR = "public/content"`) defined in `src/lib/constants.ts`. Logically the tracer should treat the call as equivalent to `join("public/content", ...slugArray)`, but it does not. + +## Root Cause + +Next's file-tracing pass (the analyzer behind `outputFileTracing`) uses a local per-file evaluator. It can resolve literals that appear **in the same file** as the `join()` call, but it does **not** follow imports to resolve literals declared in another module. When it sees an imported binding, it treats that argument as unknown — a fully dynamic segment. + +Measured in build logs with `ignoreIssue` temporarily disabled: + +| Call site | With literal at call site | With imported constant | +|---|---:|---:| +| `src/lib/utils/md.ts:23` — `join(contentRoot, dir)` | 28,040 files matched | 110,708 files matched | +| `src/lib/i18n/translationRegistry.ts` — `join(TRANSLATIONS_DIR, locale, slug, "index.md")` | 25,044 files matched | 39,272 files matched | + +Same warning **count** in both variants (the tracer still emits one warning per site), but the **scope** — the set of files the tracer considers in-scope for that site — is dramatically larger with imported constants. The literal prefix `"public/content"` dissolves into `` and the scope balloons to "anywhere under project root." + +This is not a bundler-correctness issue. The main Turbopack bundler does fold cross-module literals. It's specifically the file-trace pass (based on Node File Trace) that has a narrower evaluator. + +## Resolution + +Two changes, both in PR #17906, commit `5f1aae57`: + +1. **Inline the literal at the call site** in files that feed `path.join(...)` into `fs`/MDX reads with dynamic suffixes: + - `src/lib/md/compile.ts` — inline `"/content"` and `"public/content"` instead of importing `CONTENT_PATH`/`CONTENT_DIR` + - `src/lib/utils/md.ts` — inline `"public/content"` in `getContentRoot()` and in `getTutorialsData()` + - `src/lib/i18n/translationRegistry.ts` — inline `"public/content/translations"` instead of importing `TRANSLATIONS_DIR` + +2. **Suppress the residual warnings** in `next.config.js`: + + ```js + turbopack: { + ignoreIssue: [ + { path: "**/src/lib/**", description: /Overly broad patterns/ }, + // "Encountered unexpected file in NFT list" attaches to the project + // root (e.g. `./next.config.js`) — not to the src/lib file that + // contains the dynamic fs call — so this rule must match anywhere. + { path: "**", title: /Encountered unexpected file/ }, + ], + } + ``` + + `outputFileTracingExcludes` already prevents these patterns from over-bundling, so the warnings are cosmetic — but narrowing the pattern first (step 1) keeps the tracer's actual walked set small. + +## Why not just use `ignoreIssue` alone? + +Both steps are needed. `ignoreIssue` only silences the log output — it doesn't change what the tracer walks. Inlining narrows the tracer's scope to the correct subtree (`public/content/**`) instead of the whole project tree. If `ignoreIssue` is ever removed or its matcher tightened, the inlined form is what keeps the warnings manageable. + +## Don't revert the inlining + +Reviewers may be tempted to replace the inlined `"public/content"` strings with imports of `CONTENT_DIR` for consistency with `videos.ts`, `editPath.ts`, `contributors.ts`, and `fetchGitHubContributors.ts` (which all import the constant). That works in those files because they combine the constant with a **static suffix** — no dynamic spread — so the tracer never widens anyway: + +```ts +// fine — static suffix, no dynamic spread +const videosDir = join(process.cwd(), CONTENT_DIR, "videos") +``` + +The rule: + +- **Constant + fully-static suffix** → keep the constant. Tracer is happy. +- **Constant + dynamic spread feeding `fs`/MDX reads** → inline the literal. Otherwise the tracer loses the prefix. + +## Related + +- Ineffective `turbopackIgnore` magic comments: the comment only works on `import()` / `require()` calls, not on `path.join()` / `fs.readFile()` / `existsSync()`. Removed from `src/lib/md/rehypeImg.ts` in PR #17906, commit `e074b37c`. +- Dynamic `import()` on templated paths was replaced with `fsp.readFile()` for a different reason — the tracer statically matches the template against all files, causing `pnpm dev` to watch tens of thousands of markdown files. See PR #17906, commit `b8af0517`. + +## References + +- PR #17906 ([refactor: adopt Turbopack as default bundler](https://github.com/ethereum/ethereum-org-website/pull/17906)) +- Commit `5f1aae57` (inline path constants and suppress turbopack file-tracing warnings) +- [Next.js outputFileTracing](https://nextjs.org/docs/app/api-reference/config/next-config-js/output) +- [`@vercel/nft`](https://github.com/vercel/nft) — the underlying file tracer diff --git a/next.config.js b/next.config.js index 52da595ff32..0450d19bee4 100644 --- a/next.config.js +++ b/next.config.js @@ -1,10 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const { PHASE_DEVELOPMENT_SERVER } = require("next/constants") -const withBundleAnalyzer = require("@next/bundle-analyzer")({ - enabled: process.env.ANALYZE === "true", -}) - const createNextIntlPlugin = require("next-intl/plugin") const { withSentryConfig } = require("@sentry/nextjs") @@ -48,12 +44,6 @@ module.exports = (phase) => { "https://ethereum.org", }, webpack: (config) => { - // Parse .all-contributorsrc as JSON (no .json extension) - config.module.rules.push({ - test: /\.all-contributorsrc$/, - type: "json", - }) - config.module.rules.push({ test: /\.ya?ml$/, use: "yaml-loader", @@ -111,6 +101,22 @@ module.exports = (phase) => { "*.md": { loaders: ["raw-loader"], as: "*.js" }, "*.mp3": { as: "*.static" }, }, + // Suppress file-tracing warnings from the MDX pipeline. These files + // use dynamic path.join/readFile to read markdown content at runtime. + // outputFileTracingExcludes already prevents over-bundling. + ignoreIssue: [ + { + path: "**/src/lib/**", + description: /Overly broad patterns/, + }, + // "Encountered unexpected file in NFT list" surfaces on the project + // root (e.g. `./next.config.js`) even though the underlying fs.* + // calls live in src/lib/md/*. Match anywhere so it's suppressed. + { + path: "**", + title: /Encountered unexpected file/, + }, + ], }, // Replaces config.externals.push("pino-pretty", "lokijs", "encoding") serverExternalPackages: ["pino-pretty", "lokijs", "encoding"], @@ -253,7 +259,7 @@ module.exports = (phase) => { } } - return withBundleAnalyzer(withNextIntl(nextConfig)) + return withNextIntl(nextConfig) } module.exports = withSentryConfig(module.exports, { @@ -261,9 +267,4 @@ module.exports = withSentryConfig(module.exports, { project: "ethorg", silent: true, widenClientFileUpload: true, - webpack: { - treeshake: { - removeDebugLogging: true, - }, - }, }) diff --git a/overrides.d.ts b/overrides.d.ts index 0716289ec9d..ec218517d2d 100644 --- a/overrides.d.ts +++ b/overrides.d.ts @@ -8,16 +8,3 @@ declare module "*.mp4" { const src: string export default src } - -declare module "*/.all-contributorsrc" { - const content: { - contributors: Array<{ - login: string - name: string - avatar_url: string - profile?: string - contributions: Array - }> - } - export default content -} diff --git a/package.json b/package.json index 85627480d44..804590a0d63 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "license": "MIT", "private": true, "scripts": { - "dev": "next dev --webpack", - "dev:turbo": "next dev", - "build": "next build --webpack", - "build:turbo": "next build", + "dev": "next dev", + "dev:webpack": "next dev --webpack", + "build": "next build", + "build:webpack": "next build --webpack", "start": "next start", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -111,7 +111,6 @@ "@chromatic-com/storybook": "5.1.1", "@google/genai": "^1.46.0", "@netlify/plugin-nextjs": "^5.15.9", - "@next/bundle-analyzer": "^16.2.1", "@playwright/test": "^1.52.0", "@sentry/nextjs": "^10.46.0", "@storybook/addon-docs": "10.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 188d803b08e..096f2e0930e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -250,9 +250,6 @@ importers: '@netlify/plugin-nextjs': specifier: ^5.15.9 version: 5.15.9 - '@next/bundle-analyzer': - specifier: ^16.2.1 - version: 16.2.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@playwright/test': specifier: ^1.52.0 version: 1.53.1 @@ -1742,10 +1739,6 @@ packages: engines: {node: '>=14'} hasBin: true - '@discoveryjs/json-ext@0.5.7': - resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} - engines: {node: '>=10.0.0'} - '@docsearch/css@3.9.0': resolution: {integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==} @@ -2698,9 +2691,6 @@ packages: resolution: {integrity: sha512-dyJeuggzQM8+Dsi0T8Z9UjfLJ6vCmNC36W6WE2aqzfTdTw4wPkh2xlEu4LoD75+TGuYK7jIhEoU2QcCXOzfyAQ==} engines: {node: ^18.14.0 || >=20} - '@next/bundle-analyzer@16.2.1': - resolution: {integrity: sha512-fbj2WE6dnCyG8CvQnrBfpHyxdOIyZ4aEHJY0bSqAmamRiIXDqunFQPDvuSOPo24mJE9zQHw7TY6d+sGrXO98TQ==} - '@next/env@16.2.3': resolution: {integrity: sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==} @@ -3278,9 +3268,6 @@ packages: '@polka/url@0.5.0': resolution: {integrity: sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==} - '@polka/url@1.0.0-next.29': - resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@prisma/config@6.19.3': resolution: {integrity: sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==} @@ -6769,9 +6756,6 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - debounce@1.2.1: - resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -7026,9 +7010,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} @@ -7863,10 +7844,6 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} - gzip-size@6.0.0: - resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} - engines: {node: '>=10'} - h3@1.15.3: resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} @@ -7950,9 +7927,6 @@ packages: html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - html-minifier-terser@6.1.0: resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} engines: {node: '>=12'} @@ -8289,10 +8263,6 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -9102,10 +9072,6 @@ packages: react-dom: optional: true - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -9344,10 +9310,6 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - opener@1.5.2: - resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} - hasBin: true - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -10434,10 +10396,6 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - sirv@2.0.4: - resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} - engines: {node: '>= 10'} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -10919,10 +10877,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -11428,11 +11382,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-bundle-analyzer@4.10.1: - resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} - engines: {node: '>= 10.13.0'} - hasBin: true - webpack-dev-middleware@6.1.3: resolution: {integrity: sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==} engines: {node: '>= 14.15.0'} @@ -14253,8 +14202,6 @@ snapshots: '@depot/cli-win32-ia32': 0.0.1-cli.2.80.0 '@depot/cli-win32-x64': 0.0.1-cli.2.80.0 - '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/css@3.9.0': {} '@docsearch/react@3.9.0(@algolia/client-search@5.25.0)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)': @@ -15183,13 +15130,6 @@ snapshots: '@netlify/runtime-utils@2.2.1': {} - '@next/bundle-analyzer@16.2.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': - dependencies: - webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@next/env@16.2.3': {} '@next/eslint-plugin-next@15.5.12': @@ -15765,8 +15705,6 @@ snapshots: '@polka/url@0.5.0': {} - '@polka/url@1.0.0-next.29': {} - '@prisma/config@6.19.3(magicast@0.3.5)': dependencies: c12: 3.1.0(magicast@0.3.5) @@ -20590,8 +20528,6 @@ snapshots: dayjs@1.11.13: {} - debounce@1.2.1: {} - debug@3.2.7: dependencies: ms: 2.1.3 @@ -20830,8 +20766,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - duplexer@0.1.2: {} - duplexify@4.1.3: dependencies: end-of-stream: 1.4.4 @@ -21995,10 +21929,6 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 - gzip-size@6.0.0: - dependencies: - duplexer: 0.1.2 - h3@1.15.3: dependencies: cookie-es: 1.2.2 @@ -22128,8 +22058,6 @@ snapshots: html-entities@2.6.0: {} - html-escaper@2.0.2: {} - html-minifier-terser@6.1.0: dependencies: camel-case: 4.1.2 @@ -22456,8 +22384,6 @@ snapshots: is-plain-obj@4.1.0: {} - is-plain-object@5.0.0: {} - is-promise@4.0.0: {} is-reference@1.2.1: @@ -23528,8 +23454,6 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - mrmime@2.0.1: {} - ms@2.1.3: {} multiformats@9.9.0: {} @@ -23800,8 +23724,6 @@ snapshots: is-wsl: 2.2.0 optional: true - opener@1.5.2: {} - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -25085,12 +25007,6 @@ snapshots: dependencies: is-arrayish: 0.3.2 - sirv@2.0.4: - dependencies: - '@polka/url': 1.0.0-next.29 - mrmime: 2.0.1 - totalist: 3.0.1 - sisteransi@1.0.5: {} slash@3.0.0: {} @@ -25622,8 +25538,6 @@ snapshots: toidentifier@1.0.1: {} - totalist@3.0.1: {} - tr46@0.0.3: {} trigger.dev@4.3.3(bufferutil@4.0.9)(react@19.2.4)(superstruct@1.0.4)(typescript@5.8.3)(utf-8-validate@5.0.10): @@ -26273,25 +26187,6 @@ snapshots: webidl-conversions@3.0.1: {} - webpack-bundle-analyzer@4.10.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): - dependencies: - '@discoveryjs/json-ext': 0.5.7 - acorn: 8.16.0 - acorn-walk: 8.3.4 - commander: 7.2.0 - debounce: 1.2.1 - escape-string-regexp: 4.0.0 - gzip-size: 6.0.0 - html-escaper: 2.0.2 - is-plain-object: 5.0.0 - opener: 1.5.2 - picocolors: 1.1.1 - sirv: 2.0.4 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - webpack-dev-middleware@6.1.3(webpack@5.106.2(@swc/core@1.15.24)(esbuild@0.25.12)): dependencies: colorette: 2.0.20 diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts index 1a8425f698a..69e213a69a4 100644 --- a/sentry.edge.config.ts +++ b/sentry.edge.config.ts @@ -5,7 +5,7 @@ const environment = process.env.NEXT_PUBLIC_CONTEXT || "development" Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, tracesSampleRate: 0.01, - debug: environment === "development", + debug: process.env.SENTRY_DEBUG === "true", environment, enabled: environment === "production", initialScope: { tags: { module: "app" } }, diff --git a/sentry.server.config.ts b/sentry.server.config.ts index 1a8425f698a..69e213a69a4 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -5,7 +5,7 @@ const environment = process.env.NEXT_PUBLIC_CONTEXT || "development" Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, tracesSampleRate: 0.01, - debug: environment === "development", + debug: process.env.SENTRY_DEBUG === "true", environment, enabled: environment === "production", initialScope: { tags: { module: "app" } }, diff --git a/src/components/Contributors/Contributors.stories.tsx b/src/components/Contributors/Contributors.stories.tsx index 94ac0d424e2..b5a3f99b4c7 100644 --- a/src/components/Contributors/Contributors.stories.tsx +++ b/src/components/Contributors/Contributors.stories.tsx @@ -1,10 +1,10 @@ import { Meta, StoryObj } from "@storybook/nextjs" -import ContributorsComponent, { type Contributor } from "." +import ContributorsView, { type Contributor } from "./ContributorsView" const meta = { title: "Molecules / Display Content / Contributors", - component: ContributorsComponent, + component: ContributorsView, parameters: { layout: "fullscreen", }, @@ -15,7 +15,7 @@ const meta = { ), ], -} satisfies Meta +} satisfies Meta export default meta @@ -27,70 +27,60 @@ const mockContributors: Contributor[] = [ name: "Carl Fairclough", avatar_url: "https://avatars1.githubusercontent.com/u/4670881?v=4", profile: "http://carlfairclough.me", - contributions: ["design", "code", "bug"], }, { login: "RichardMcSorley", name: "Richard McSorley", avatar_url: "https://avatars2.githubusercontent.com/u/6407008?v=4", profile: "https://github.com/RichardMcSorley", - contributions: ["code"], }, { login: "ajsantander", name: "Alejandro Santander", avatar_url: "https://avatars2.githubusercontent.com/u/550409?v=4", profile: "http://ajsantander.github.io/", - contributions: ["content"], }, { login: "Lililashka", name: "Lililashka", avatar_url: "https://avatars1.githubusercontent.com/u/28689401?v=4", profile: "http://impermanence.co", - contributions: ["design", "bug"], }, { login: "chriseth", name: "chriseth", avatar_url: "https://avatars2.githubusercontent.com/u/9073706?v=4", profile: "https://github.com/chriseth", - contributions: ["content", "review"], }, { login: "fzeoli", name: "Franco Zeoli", avatar_url: "https://avatars2.githubusercontent.com/u/232174?v=4", profile: "https://nomiclabs.io", - contributions: ["content", "review"], }, { login: "P1X3L0V4", name: "Anna Karpińska", avatar_url: "https://avatars2.githubusercontent.com/u/3372341?v=4", profile: "https://github.com/P1X3L0V4", - contributions: ["translation"], }, { login: "vrde", name: "vrde", avatar_url: "https://avatars1.githubusercontent.com/u/134680?v=4", profile: "https://github.com/vrde", - contributions: ["content"], }, { login: "AlexandrouR", name: "Rousos Alexandros", avatar_url: "https://avatars1.githubusercontent.com/u/21177075?v=4", profile: "https://github.com/AlexandrouR", - contributions: ["content"], }, { login: "eswarasai", name: "Eswara Sai", avatar_url: "https://avatars2.githubusercontent.com/u/5172086?v=4", profile: "https://eswarasai.com", - contributions: ["code"], }, ] diff --git a/src/components/Contributors/ContributorsView.tsx b/src/components/Contributors/ContributorsView.tsx new file mode 100644 index 00000000000..4edae548482 --- /dev/null +++ b/src/components/Contributors/ContributorsView.tsx @@ -0,0 +1,73 @@ +import { Flex } from "@/components/ui/flex" + +export interface Contributor { + login: string + name: string + avatar_url: string + profile?: string +} + +interface ContributorsViewProps { + contributors: Contributor[] +} + +const cardClassName = + "hover:bg-background-highlight m-2 block max-w-[132px] shadow transition-transform duration-100 hover:scale-[1.02] hover:rounded focus:scale-[1.02] focus:rounded" + +const ContributorCard = ({ contributor }: { contributor: Contributor }) => { + const body = ( + <> + {/* + * Plain over next/image by design. We render ~1,500 cards; + * expands each avatar into a ~13-variant srcSet (~1.8 KB per + * card → ~3 MB extra HTML). GitHub avatars are served at a fixed + * 132×132 and don't benefit from /_next/image negotiation here. + */} + {/* eslint-disable-next-line @next/next/no-img-element */} + +
+

{contributor.name}

+
+ + ) + + if (contributor.profile) { + // target="_blank" preserves the behavior of the original + // wrapper, which auto-applied it for external hrefs. + return ( + + {body} + + ) + } + + return
{body}
+} + +const ContributorsView = ({ contributors }: ContributorsViewProps) => ( + <> +

+ Thanks to our {contributors.length} Ethereum community members who have + contributed so far! +

+ + + {contributors.map((contributor) => ( + + ))} + + +) + +export default ContributorsView diff --git a/src/components/Contributors/index.tsx b/src/components/Contributors/index.tsx index baf91920465..4d94945de28 100644 --- a/src/components/Contributors/index.tsx +++ b/src/components/Contributors/index.tsx @@ -1,94 +1,38 @@ -"use client" +import { readFileSync } from "fs" +import { join } from "path" -import { useEffect, useState } from "react" import { shuffle } from "lodash" -import { Image } from "@/components/Image" -import { Flex } from "@/components/ui/flex" -import InlineLink from "@/components/ui/Link" -import { LinkBox, LinkOverlay } from "@/components/ui/link-box" +import ContributorsView, { type Contributor } from "./ContributorsView" -import allContributors from "../../../.all-contributorsrc" +import "server-only" -export interface Contributor { - login: string - name: string - avatar_url: string - profile?: string - contributions: Array -} +export type { Contributor } interface ContributorsProps { contributors?: Contributor[] } -const ContributorCard = ({ contributor }: { contributor: Contributor }) => { - const content = ( - <> - -
-

- {contributor.profile ? ( - - - {contributor.name} - - - ) : ( - contributor.name - )} -

-
- - ) - - if (contributor.profile) { - return ( - - {content} - - ) - } - - return
{content}
+type AllContributorsRc = { + contributors: (Contributor & { contributions?: string[] })[] } -const Contributors = ({ contributors }: ContributorsProps) => { - const [contributorsList, setContributorsList] = useState([]) - - useEffect(() => { - if (contributors) { - setContributorsList(contributors) - } else { - setContributorsList(shuffle(allContributors.contributors)) - } - }, [contributors]) - - return ( - <> -

- Thanks to our {contributorsList.length} Ethereum community members who - have contributed so far! -

- - - {contributorsList.map((contributor) => ( - - ))} - - - ) -} +// Read `.all-contributorsrc` (bot-maintained) once at module load. +const raw = readFileSync(join(process.cwd(), ".all-contributorsrc"), "utf-8") +const { contributors: rawContributors } = JSON.parse(raw) as AllContributorsRc + +// Trim to the fields the card actually renders and shuffle once per SSG worker. +const shuffledContributors: Contributor[] = shuffle( + rawContributors.map(({ login, name, avatar_url, profile }) => ({ + login, + name, + avatar_url, + profile, + })) +) + +const Contributors = ({ contributors }: ContributorsProps = {}) => ( + +) export default Contributors diff --git a/src/components/Staking/StakingHierarchy.tsx b/src/components/Staking/StakingHierarchy.tsx index fcd55e0e410..a81cd89afea 100644 --- a/src/components/Staking/StakingHierarchy.tsx +++ b/src/components/Staking/StakingHierarchy.tsx @@ -1,3 +1,5 @@ +"use client" + import React, { HTMLAttributes } from "react" import { ChildOnlyProp } from "@/lib/types" diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 5068bb7a207..27b4c82674a 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -7,7 +7,6 @@ import type { CommunityBlog } from "./types" export const OLD_CONTENT_DIR = "src/content" // For old git commit history -- do not remove export const CONTENT_DIR = "public/content" export const CONTENT_PATH = "/content" -export const TRANSLATIONS_DIR = "public/content/translations" export const TRANSLATED_IMAGES_DIR = "/content/translations" export const PLACEHOLDER_IMAGE_DIR = "src/data/placeholders" export const INTERNAL_TUTORIALS_JSON = "src/data/internalTutorials.json" diff --git a/src/lib/i18n/translationRegistry.ts b/src/lib/i18n/translationRegistry.ts index e64f6be215d..0e2a00daf1d 100644 --- a/src/lib/i18n/translationRegistry.ts +++ b/src/lib/i18n/translationRegistry.ts @@ -4,11 +4,7 @@ import { join } from "path" import { appsCategories } from "@/data/apps/categories" import { DEV_TOOL_CATEGORY_SLUG_LIST } from "@/data/developerTools" -import { - DEFAULT_LOCALE, - LOCALES_CODES, - TRANSLATIONS_DIR, -} from "@/lib/constants" +import { DEFAULT_LOCALE, LOCALES_CODES } from "@/lib/constants" import { getPostSlugs } from "../utils/md" import { getStaticPagePaths } from "../utils/staticPages" @@ -27,7 +23,12 @@ async function isMdPageTranslated( return true } - const translationPath = join(TRANSLATIONS_DIR, locale, slug, "index.md") + const translationPath = join( + "public/content/translations", + locale, + slug, + "index.md" + ) return existsSync(translationPath) } diff --git a/src/lib/md/compile.ts b/src/lib/md/compile.ts index b6f0f578fb4..757de15c6b1 100644 --- a/src/lib/md/compile.ts +++ b/src/lib/md/compile.ts @@ -7,7 +7,6 @@ import remarkSlug from "rehype-slug" import remarkGfm from "remark-gfm" import remarkHeadingId from "remark-heading-id" -import { CONTENT_DIR, CONTENT_PATH } from "../constants" import { Frontmatter, Layout, TocNodeType } from "../types" import { escapeHeadingIds } from "@/lib/md/escapeHeadingIds" @@ -35,8 +34,8 @@ export const compile = async ({ tocNodeItems = "items" in toc ? toc.items : [] } - const mdPath = join(CONTENT_PATH, ...slugArray) - const mdDir = join(CONTENT_DIR, ...slugArray) + const mdPath = join("/content", ...slugArray) + const mdDir = join("public/content", ...slugArray) const mdxOptions = { remarkPlugins: [ diff --git a/src/lib/utils/md.ts b/src/lib/utils/md.ts index 0b86acc1051..1783306da0e 100644 --- a/src/lib/utils/md.ts +++ b/src/lib/utils/md.ts @@ -10,12 +10,12 @@ import { dateToString } from "@/lib/utils/date" import internalTutorialSlugs from "@/data/internalTutorials.json" -import { CONTENT_DIR, DEFAULT_LOCALE } from "@/lib/constants" +import { DEFAULT_LOCALE } from "@/lib/constants" import { toPosixPath } from "./relativePath" function getContentRoot() { - return join(process.cwd(), CONTENT_DIR) + return join(process.cwd(), "public/content") } export const getPostSlugs = async (dir: string, filterRegex?: RegExp) => { @@ -77,35 +77,36 @@ export const getPostSlugs = async (dir: string, filterRegex?: RegExp) => { export const getTutorialsData = async ( locale: string ): Promise => { - // Read tutorials from filesystem in parallel using dynamic imports + const contentRoot = join(process.cwd(), "public/content") + const tutorialPromises = (internalTutorialSlugs as string[]).map( async (slug) => { try { let fileContents: string let isTranslated = true + const enPath = join( + contentRoot, + "developers/tutorials", + slug, + "index.md" + ) + if (locale === DEFAULT_LOCALE) { - // English: read directly from content directory - fileContents = ( - await import( - `../../../public/content/developers/tutorials/${slug}/index.md` - ) - ).default + fileContents = await fsp.readFile(enPath, "utf-8") } else { - // Non-English: try translation first, fallback to English + const translatedPath = join( + contentRoot, + "translations", + locale, + "developers/tutorials", + slug, + "index.md" + ) try { - fileContents = ( - await import( - `../../../public/content/translations/${locale}/developers/tutorials/${slug}/index.md` - ) - ).default + fileContents = await fsp.readFile(translatedPath, "utf-8") } catch { - // Fallback to English content - fileContents = ( - await import( - `../../../public/content/developers/tutorials/${slug}/index.md` - ) - ).default + fileContents = await fsp.readFile(enPath, "utf-8") isTranslated = false } }