diff --git a/eleventy.config.js b/eleventy.config.ts similarity index 82% rename from eleventy.config.js rename to eleventy.config.ts index 3b9ab52635..9f0430dd67 100644 --- a/eleventy.config.js +++ b/eleventy.config.ts @@ -2,10 +2,11 @@ // It configures the core 11ty behavior and registers // plugins and customization that live in `/src/_11ty`. -import { registerFilters } from './src/_11ty/filters.js'; -import { registerShortcodes } from './src/_11ty/shortcodes.js'; -import { markdown } from './src/_11ty/plugins/markdown.js'; -import { configureHighlighting } from './src/_11ty/plugins/highlight.js'; +import {registerFilters} from './src/_11ty/filters.js'; +import {registerShortcodes} from './src/_11ty/shortcodes.js'; +import {markdown} from './src/_11ty/plugins/markdown.js'; +import {configureHighlighting} from './src/_11ty/plugins/highlight.js'; +import {UserConfig} from '@11ty/eleventy'; import minifier from 'html-minifier-terser'; import yaml from 'js-yaml'; @@ -14,13 +15,9 @@ import * as path from 'node:path'; import * as sass from 'sass'; // noinspection JSUnusedGlobalSymbols -/** - * @typedef {import('11ty/eleventy/UserConfig')} EleventyConfig - * @param {EleventyConfig} eleventyConfig - */ -export default function (eleventyConfig) { - const isProduction = process.env.PRODUCTION === 'true'; - const shouldOptimize = process.env.OPTIMIZE === 'true'; +export default function (eleventyConfig: UserConfig) { + const isProduction = process.env['PRODUCTION'] === 'true'; + const shouldOptimize = process.env['OPTIMIZE'] === 'true'; eleventyConfig.on('eleventy.before', async () => { await configureHighlighting(markdown); @@ -30,7 +27,7 @@ export default function (eleventyConfig) { eleventyConfig.setLibrary('md', markdown); - eleventyConfig.addDataExtension('yml,yaml', (contents) => + eleventyConfig.addDataExtension('yml,yaml', (contents: string) => yaml.load(contents), ); @@ -48,7 +45,7 @@ export default function (eleventyConfig) { eleventyConfig.addWatchTarget('src/_sass'); eleventyConfig.addExtension('scss', { outputFileExtension: 'css', - compile: function (inputContent, inputPath) { + compile: function (inputContent: string, inputPath: string) { const parsedPath = path.parse(inputPath); if (parsedPath.name.startsWith('_')) { return; @@ -88,7 +85,7 @@ export default function (eleventyConfig) { if (shouldOptimize) { // If building for production, minify/optimize the HTML output. // Doing so during serving isn't worth the extra build time. - eleventyConfig.addTransform('minify-html', async function (content) { + eleventyConfig.addTransform('minify-html', async function (content: string) { if (this.page.outputPath && this.page.outputPath.endsWith('.html')) { // Minify the page's content if it's an HTML file. // Other options can be enabled, but each should be tested. diff --git a/package.json b/package.json index 1a02630feb..75ca6e40bf 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "url": "https://github.com/dart-lang/site-www.git" }, "scripts": { - "serve": "PRODUCTION=false eleventy --serve", - "build-site-for-staging": "PRODUCTION=false OPTIMIZE=true eleventy", - "build-site-for-production": "PRODUCTION=true OPTIMIZE=true eleventy" + "serve": "PRODUCTION=false tsx node_modules/@11ty/eleventy/cmd.cjs --serve --config=eleventy.config.ts", + "build-site-for-staging": "PRODUCTION=false OPTIMIZE=true tsx node_modules/@11ty/eleventy/cmd.cjs --config=eleventy.config.ts", + "build-site-for-production": "PRODUCTION=true OPTIMIZE=true tsx node_modules/@11ty/eleventy/cmd.cjs --config=eleventy.config.ts" }, "engines": { "node": ">=20.14.0", @@ -23,6 +23,9 @@ }, "devDependencies": { "@11ty/eleventy": "^3.0.0", + "@types/hast": "^3.0.4", + "@types/markdown-it": "^14.1.2", + "@types/node": "^22.10.1", "firebase-tools": "^13.28.0", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.3", @@ -35,6 +38,7 @@ "markdown-it-container": "^4.0.0", "markdown-it-deflist": "^3.0.0", "sass": "^1.82.0", - "shiki": "^1.24.0" + "shiki": "^1.24.0", + "tsx": "^4.19.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0f2dc48dc..b98f54b73c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,15 @@ importers: '@11ty/eleventy': specifier: ^3.0.0 version: 3.0.0 + '@types/hast': + specifier: ^3.0.4 + version: 3.0.4 + '@types/markdown-it': + specifier: ^14.1.2 + version: 14.1.2 + '@types/node': + specifier: ^22.10.1 + version: 22.10.1 firebase-tools: specifier: ^13.28.0 version: 13.28.0(encoding@0.1.13) @@ -38,7 +47,7 @@ importers: version: 14.1.0 markdown-it-anchor: specifier: ^9.2.0 - version: 9.2.0(@types/markdown-it@14.1.1)(markdown-it@14.1.0) + version: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0) markdown-it-attrs: specifier: ^4.3.0 version: 4.3.0(markdown-it@14.1.0) @@ -54,6 +63,9 @@ importers: shiki: specifier: ^1.24.0 version: 1.24.0 + tsx: + specifier: ^4.19.2 + version: 4.19.2 packages: @@ -109,6 +121,150 @@ packages: '@electric-sql/pglite@0.2.12': resolution: {integrity: sha512-J/X42ujcoFEbOkgRyoNqZB5qcqrnJRWVlwpH3fKYoJkTz49N91uAK/rDSSG/85WRas9nC9mdV4FnMTxnQWE/rw==} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@google-cloud/cloud-sql-connector@1.4.0': resolution: {integrity: sha512-OUXs2f91u3afbFjufCJom9lF+GgS9if4F/eKxrLvdkbwkYAQrQUOY6Jw4YfVXUxF3oNDioTgZ4fpwt1MQXwfKg==} engines: {node: '>=14'} @@ -376,8 +532,8 @@ packages: '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} - '@types/markdown-it@14.1.1': - resolution: {integrity: sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==} + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -385,8 +541,8 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/node@22.8.6': - resolution: {integrity: sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==} + '@types/node@22.10.1': + resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} '@types/request@2.48.12': resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} @@ -1087,6 +1243,11 @@ packages: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1297,6 +1458,9 @@ packages: resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} engines: {node: '>=10'} + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + get-uri@6.0.3: resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} engines: {node: '>= 14'} @@ -2454,6 +2618,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -2802,6 +2969,11 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -2820,8 +2992,8 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} @@ -3178,6 +3350,78 @@ snapshots: '@electric-sql/pglite@0.2.12': {} + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + '@google-cloud/cloud-sql-connector@1.4.0(encoding@0.1.13)': dependencies: '@googleapis/sqladmin': 24.0.0(encoding@0.1.13) @@ -3454,7 +3698,7 @@ snapshots: '@types/long@4.0.2': {} - '@types/markdown-it@14.1.1': + '@types/markdown-it@14.1.2': dependencies: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 @@ -3465,14 +3709,14 @@ snapshots: '@types/mdurl@2.0.0': {} - '@types/node@22.8.6': + '@types/node@22.10.1': dependencies: - undici-types: 6.19.8 + undici-types: 6.20.0 '@types/request@2.48.12': dependencies: '@types/caseless': 0.12.5 - '@types/node': 22.8.6 + '@types/node': 22.10.1 '@types/tough-cookie': 4.0.5 form-data: 2.5.2 @@ -4171,6 +4415,33 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.2.0: {} escape-goat@2.1.1: {} @@ -4509,6 +4780,10 @@ snapshots: get-stdin@8.0.0: {} + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + get-uri@6.0.3: dependencies: basic-ftp: 5.0.5 @@ -5131,9 +5406,9 @@ snapshots: - supports-color optional: true - markdown-it-anchor@9.2.0(@types/markdown-it@14.1.1)(markdown-it@14.1.0): + markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0): dependencies: - '@types/markdown-it': 14.1.1 + '@types/markdown-it': 14.1.2 markdown-it: 14.1.0 markdown-it-attrs@4.3.0(markdown-it@14.1.0): @@ -5670,7 +5945,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.8.6 + '@types/node': 22.10.1 long: 5.2.3 proxy-addr@2.0.7: @@ -5808,6 +6083,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -6224,6 +6501,13 @@ snapshots: tsscmp@1.0.6: {} + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + type-fest@0.20.2: {} type-fest@0.21.3: {} @@ -6239,7 +6523,7 @@ snapshots: uc.micro@2.1.0: {} - undici-types@6.19.8: {} + undici-types@6.20.0: {} unicode-emoji-modifier-base@1.0.0: {} diff --git a/src/_11ty/filters.js b/src/_11ty/filters.ts similarity index 73% rename from src/_11ty/filters.js rename to src/_11ty/filters.ts index 00ffe03fcc..d246c0d577 100644 --- a/src/_11ty/filters.js +++ b/src/_11ty/filters.ts @@ -1,44 +1,45 @@ -import { getPageInfo } from './utils/get-page-info.js'; -import { fromHtml } from 'hast-util-from-html'; -import { selectAll } from 'hast-util-select'; -import { toText } from 'hast-util-to-text'; -import { escapeHtml } from 'markdown-it/lib/common/utils.mjs'; - -export function registerFilters(eleventyConfig) { +import {getPageInfo} from './utils/get-page-info.js'; +import {fromHtml} from 'hast-util-from-html'; +import {selectAll} from 'hast-util-select'; +import {toText} from 'hast-util-to-text'; +import {escapeHtml} from 'markdown-it/lib/common/utils.mjs'; +import {UserConfig} from '@11ty/eleventy'; + +export function registerFilters(eleventyConfig: UserConfig): void { eleventyConfig.addFilter('toSimpleDate', toSimpleDate); eleventyConfig.addFilter('regexReplace', regexReplace); eleventyConfig.addFilter('toISOString', toISOString); eleventyConfig.addFilter('activeNavEntryIndexArray', activeNavEntryIndexArray); eleventyConfig.addFilter('arrayToSentenceString', arrayToSentenceString); eleventyConfig.addFilter('underscoreBreaker', underscoreBreaker); - eleventyConfig.addFilter('throwError', function (error) { + eleventyConfig.addFilter('throwError', function (error: any) { throw new Error(error); }); eleventyConfig.addFilter('generateToc', generateToc); eleventyConfig.addFilter('breadcrumbsForPage', breadcrumbsForPage); } -function toSimpleDate(input) { - let dateString; +function toSimpleDate(input: string | Date): string { + let dateString: string; if (input instanceof Date) { dateString = input.toISOString(); } else { // If it's not a Date object, assume it's already in string format. dateString = input; } - return dateString.split('T')[0]; + return dateString.split('T')[0]!; } /** * Replace text in {@link input} that matches the specified {@link regex} * with the specified {@link replacement}. * - * @param {string} input - * @param {RegExp} regex - * @param {string} replacement - * @return {string} The resulting string with the replacement made. + * @param input + * @param regex + * @param replacement + * @return The resulting string with the replacement made. */ -function regexReplace(input, regex, replacement = '') { +function regexReplace(input: string, regex: RegExp, replacement: string = ''): string { return input.toString().replace(new RegExp(regex), replacement); } @@ -47,10 +48,10 @@ function regexReplace(input, regex, replacement = '') { * * Used to add date information to the sitemap. * - * @param {string|Date} input The date to convert - * @return {string} The ISO string + * @param input The date to convert + * @return The ISO string */ -function toISOString(input) { +function toISOString(input: string | Date): string { if (input instanceof Date) { return input.toISOString(); } else { @@ -59,12 +60,12 @@ function toISOString(input) { } } -function activeNavEntryIndexArray(navEntryTree, pageUrlPath = '') { +function activeNavEntryIndexArray(navEntryTree: any, pageUrlPath: string = ''): number[] | null { const activeEntryIndexes = _getActiveNavEntries(navEntryTree, pageUrlPath); return activeEntryIndexes.length === 0 ? null : activeEntryIndexes; } -function _getActiveNavEntries(navEntryTree, pageUrlPath = '') { +function _getActiveNavEntries(navEntryTree: any, pageUrlPath = ''): number[] { // TODO(parlough): Simplify once standardizing with the Flutter site. for (let i = 0; i < navEntryTree.length; i++) { const entry = navEntryTree[i]; @@ -93,13 +94,13 @@ function _getActiveNavEntries(navEntryTree, pageUrlPath = '') { return []; } -function arrayToSentenceString(list, joiner = 'and') { +function arrayToSentenceString(list: string[], joiner: string = 'and'): string { if (!list || list.length === 0) { return ''; } if (list.length === 1) { - return list[0]; + return list[0]!; } let result = ''; @@ -116,7 +117,7 @@ function arrayToSentenceString(list, joiner = 'and') { return result; } -function underscoreBreaker(stringToBreak, inAnchor = false) { +function underscoreBreaker(stringToBreak: string, inAnchor: boolean = false): string { // Only consider text which has underscores in it to keep this simpler. if (!stringToBreak.includes('_')) { return stringToBreak; @@ -127,35 +128,35 @@ function underscoreBreaker(stringToBreak, inAnchor = false) { // we don't want to replace the href, // just the inner text content. return stringToBreak.replace(/>([a-zA-Z_]*?) { - return `>${match[1].replaceAll('_', '_')}<`; + return `>${match[1]!.replaceAll('_', '_')}<`; }); } return stringToBreak.replaceAll('_', '_'); } -function generateToc(contents) { +function generateToc(contents: string) { // TODO(parlough): Speed this up. // Perhaps do the processing before HTML rendering? // Maybe shouldn't be a filter. const dom = fromHtml(contents); const headers = selectAll('h2, h3', dom); - if (headers < 1) { - // If there is only one header, there is no point of a TOC. + if (headers.length < 1) { + // If there's only one header, there's no point of a TOC. return null; } - let currentH2 = null; + let currentH2: {text: string, id: string, children: {text: string, id: string}[]} | null = null; const builtToc = []; let count = 0; for (const header of headers) { const id = header.properties['id']; // Header can't be linked to without an ID. - if (!id || id === '') { + if (!id || typeof id !== 'string' || id === '') { continue; } // Don't include if no_toc is specified as a class on the header. - if (header.properties['className']?.includes('no_toc')) { + if ((header.properties['className'] as string | null)?.includes('no_toc')) { continue; } @@ -185,14 +186,14 @@ function generateToc(contents) { }; } -function breadcrumbsForPage(page) { +function breadcrumbsForPage(page: any): {title: string, url: string}[] { const breadcrumbs = []; // Retrieve the liquid data for this page. let data = this.context.environments; while (page) { - const urlSegments = page.url + const urlSegments = (page.url as string) .split('/') .filter((segment) => segment.length > 0); @@ -202,7 +203,7 @@ function breadcrumbsForPage(page) { }); if (urlSegments.length <= 1) { - // If this only has one segment, it is the root page + // If this only has one segment, it's the root page // and has no more parents, so don't continue on. break; } else { diff --git a/src/_11ty/plugins/highlight.js b/src/_11ty/plugins/highlight.ts similarity index 77% rename from src/_11ty/plugins/highlight.js rename to src/_11ty/plugins/highlight.ts index 6f15bcff4a..b8b3b54e7a 100644 --- a/src/_11ty/plugins/highlight.js +++ b/src/_11ty/plugins/highlight.ts @@ -1,5 +1,7 @@ -import { getSingletonHighlighter } from 'shiki'; +import {getSingletonHighlighter, Highlighter} from 'shiki'; import dashLightTheme from '../syntax/dash-light.js'; +import MarkdownIt from 'markdown-it'; +import * as hast from 'hast'; /** * Replaces the markdown-it code block renderer with our own that: @@ -13,10 +15,10 @@ import dashLightTheme from '../syntax/dash-light.js'; * using the shiki package that uses TextMate grammars * and Code -OSS themes. * - * @param {import('markdown-it/lib').MarkdownIt} markdown The markdown-it instance to + * @param markdown The markdown-it instance to * configure syntax highlighting for. */ -export async function configureHighlighting(markdown) { +export async function configureHighlighting(markdown: MarkdownIt): Promise { const highlighter = await getSingletonHighlighter({ langs: [ 'dart', @@ -42,7 +44,7 @@ export async function configureHighlighting(markdown) { }); markdown.renderer.rules.fence = function (tokens, index) { - const token = tokens[index]; + const token = tokens[index] as MarkdownIt.Token; const splitTokenInfo = token.info.match(/(\S+)\s?(.*?)$/m); @@ -51,8 +53,8 @@ export async function configureHighlighting(markdown) { 'after the opening backticks like: ```dart.'); } - const language = splitTokenInfo.length > 1 ? splitTokenInfo[1] : ''; - const attributes = splitTokenInfo.length > 2 ? splitTokenInfo[2] : ''; + const language = splitTokenInfo.length > 1 ? splitTokenInfo[1]! : ''; + const attributes = splitTokenInfo.length > 2 ? splitTokenInfo[2]! : ''; return _highlight( markdown, @@ -69,22 +71,21 @@ export async function configureHighlighting(markdown) { * and makes modifications to the output structure based on the * passed in {@link attributeString}. * - * @param {import('markdown-it/lib').MarkdownIt} markdown The markdown-it instance. - * @param {import('shiki').Highlighter} highlighter The shiki highlighter - * configured with the correct theme(s) and languages. - * @param {string} content The content to syntax highlight. - * @param {string} language The language of the content. - * @param {string} attributeString The string containing configuration. - * @returns {string} The processed/highlighted content rendered as HTML. - * @private + * @param markdown The markdown-it instance. + * @param highlighter The shiki highlighter + * configured with the correct themes and languages. + * @param content The content to syntax highlight. + * @param language The language of the content. + * @param attributeString The string containing configuration. + * @returns The processed/highlighted content rendered as HTML. */ function _highlight( - markdown, - highlighter, - content, - language, - attributeString, -) { + markdown: MarkdownIt, + highlighter: Highlighter, + content: string, + language: string, + attributeString: string, +): string { const attributes = _parseAttributes(attributeString); // Specially handle DartPad snippets so that inject_embed can convert them. @@ -144,8 +145,8 @@ function _highlight( bodyChildren.unshift(languageText); } - // Create a div container to wrap the pre element. - const blockBody = { + // Create a div container to wrap the `pre` element. + const blockBody: hast.Element = { type: 'element', tagName: 'div', children: bodyChildren, @@ -177,12 +178,12 @@ function _highlight( bodyChildren.unshift(extraTagContent); } - const wrapperChildren = []; + const wrapperChildren: hast.Element[] = []; // Add a title if specified, often used for filenames. const title = attributes['title']; if (title && title !== '') { - const titleElement = { + const titleElement: hast.Element = { type: 'element', tagName: 'div', children: [{ type: 'text', value: title }], @@ -197,7 +198,7 @@ function _highlight( wrapperChildren.push(blockBody); // Create a div to wrap everything including the title/filename bar. - const wrapper = { + const wrapper: hast.Element = { type: 'element', tagName: 'div', children: wrapperChildren, @@ -206,7 +207,7 @@ function _highlight( }, }; - // Replace the pre element with our own wrapper. + // Replace the `pre` element with our own wrapper. return wrapper; }, line(lineElement, line) { @@ -228,7 +229,7 @@ function _highlight( } }, }, - ], + ] }); } @@ -239,17 +240,16 @@ const _attributesPattern = /([^\s=]+)(?:="([^"]*)"|=(\S+))?/g; * Parse a space-separated attribute string, where spaces in a string literal * are ignored. * - * @param {string} attributeString The string containing configuration. - * @return {Object.} The parsed attributes. - * @private + * @param attributeString The string containing configuration. + * @return The parsed attributes. */ -function _parseAttributes(attributeString) { - const attributes = {}; +function _parseAttributes(attributeString: string): {[index: string]: string | null} { + const attributes: {[index: string]: string | null} = {}; if (attributeString === '') return attributes; - let match; + let match: RegExpExecArray | null; while ((match = _attributesPattern.exec(attributeString))) { - const key = match[1]; + const key = match[1]!; attributes[key] = match[2] ?? match[3] ?? null; } @@ -259,14 +259,12 @@ function _parseAttributes(attributeString) { /** * Parses a comma-separated list of numbers and ranges into a set of numbers. * - * @param {string} input A comma-separated list of numbers and ranges. - * @returns {Set} All unique numbers specified in the input. - * @private + * @param input A comma-separated list of numbers and ranges. + * @returns All unique numbers specified in the input. */ -function _parseNumbersAndRanges(input) { +function _parseNumbersAndRanges(input: string): Set { const elements = input.split(','); - /** @type {Set} */ - const combinedNumbers = new Set(); + const combinedNumbers = new Set(); for (const element of elements) { const rangeParts = element.split('-'); @@ -276,7 +274,7 @@ function _parseNumbersAndRanges(input) { // Split by the dash, and turn each string into a number. // Assume the user only included one dash. const [start, end] = rangeParts.map(Number.parseInt); - if (!Number.isNaN(start) && !Number.isNaN(end)) { + if (start && end && !Number.isNaN(start) && !Number.isNaN(end)) { for (let i = start; i <= end; i++) { combinedNumbers.add(i); } @@ -284,7 +282,7 @@ function _parseNumbersAndRanges(input) { } else { // It's (hopefully) just a single number. const number = Number.parseInt(element); - if (!Number.isNaN(number)) { + if (number && !Number.isNaN(number)) { combinedNumbers.add(number); } } @@ -300,41 +298,37 @@ function _parseNumbersAndRanges(input) { * The spans and ranges should be * ordered corresponding to the source line of text. * - * @param {{children: [{type: string, value: string}], type: 'element', tagName: 'span', properties: Object.}[]} spans - * The list of spans to wrap the text of. - * @param {{startIndex: number, endIndex: number}[]} ranges - * The ranges in the text to mark. - * @returns {{children: [{type: string, value: string}], type: 'element', tagName: 'span', properties: Object.}[]} - * A new list of spans with tags added around the specified ranges. + * @param spans The list of spans to wrap the text of. + * @param ranges The ranges in the text to mark. + * @returns A new list of spans with tags added around the specified ranges. */ -function _wrapMarkedText(spans, ranges) { +function _wrapMarkedText(spans: hast.ElementContent[], ranges: {startIndex: number, endIndex: number}[]): hast.Element[] { /** * The current index in the text across all spans. - * @type {number} */ let currentIndexInLine = 0; /** * The index of the current range being marked. - * @type {number} */ let currentRangeIndex = 0; /** * The new collection of spans to replace the original. - * @type {{children: [{type: string, value: string}], type: 'element', tagName: 'span', properties: Object.}[]} - * */ - const updatedSpans = []; + */ + const updatedSpans: hast.Element[] = []; /** - * The mark that will wrap the current range. - * @type {{children: {type: string, value}[], type: string, tagName: string, properties: Object}} + * The mark to wrap the current range with. */ let markElement = _createEmptyMarkElement(); for (const span of spans) { + if (span.type === 'text' || span.type === 'comment') { + throw new Error(`Expected only spans when wrapping, but found: ${span.type}.`); + } const [child, ...otherChildren] = span.children; - if (otherChildren.length > 0 || child.type !== 'text') { + if (!child || otherChildren.length > 0 || child.type !== 'text') { throw new Error('Each span should have exactly one text child.'); } @@ -343,13 +337,11 @@ function _wrapMarkedText(spans, ranges) { /** * The properties that all potentially created children should have too. - * @type {Object.} */ - const spanProperties = span.properties ?? {}; + const spanProperties: hast.Properties = span.properties ?? {}; /** * The current index within the current span. - * @type {number} */ let indexInCurrentSpan = 0; @@ -363,13 +355,12 @@ function _wrapMarkedText(spans, ranges) { indexInCurrentSpan < text.length ) { const { startIndex: rangeStartIndex, endIndex: rangeEndIndex } = - ranges[currentRangeIndex]; + ranges[currentRangeIndex]!; /** * The index in relation to the start of the current span * where the current range starts or the index in the current span if * the range starts before the current index. - * @type {number} */ const relativeRangeStartIndex = Math.max( rangeStartIndex - currentIndexInLine, @@ -380,7 +371,6 @@ function _wrapMarkedText(spans, ranges) { * The index in relation to the start of the current span * where the current range ends or the ending index of the current span if * the range ends after the current index. - * @type {number} */ const relativeEndIndex = Math.min( rangeEndIndex - currentIndexInLine, @@ -388,7 +378,7 @@ function _wrapMarkedText(spans, ranges) { ); // If `indexInCurrentSpan` is less than `relativeRangeStartIndex`, - // all text between the two should not be marked. + // all text between the two shouldn't be marked. if (indexInCurrentSpan < relativeRangeStartIndex) { updatedSpans.push( _createSpanWithText( @@ -413,7 +403,6 @@ function _wrapMarkedText(spans, ranges) { * The index in the whole line of the end of the current range if * it has all been marked, otherwise the index in the whole line * of the end of the current span. - * @type {number} */ const rangeOrSpanEndIndexInLine = currentIndexInLine + relativeEndIndex; @@ -452,11 +441,9 @@ function _wrapMarkedText(spans, ranges) { /** * Creates a new mark element with the `highlight` class and no children. * - * @returns {{children: [{type: string, value}], type: string, tagName: string, properties: Object.}} - * The created hast HTML element. - * @private + * @returns The created hast HTML element. */ -function _createEmptyMarkElement() { +function _createEmptyMarkElement(): hast.Element { return { type: 'element', tagName: 'mark', @@ -471,14 +458,12 @@ function _createEmptyMarkElement() { * Creates a new hast span element with the specified * inline {@link text}, and {@link properties}. * - * @param {string} text The text to include in the HTML element. - * @param {Object.} [properties = {}] The properties to specify - * for the HTML element, such as class. - * @returns {{children: [{type: string, value}], type: string, tagName: string, properties: Object.}} - * The created hast HTML element. - * @private + * @param text The text to include in the HTML element. + * @param properties The properties to specify for the HTML element, + * such as classes to add. + * @returns The created hast HTML element. */ -function _createSpanWithText(text, properties = {}) { +function _createSpanWithText(text: string, properties: hast.Properties = {}): hast.Element { return { type: 'element', tagName: 'span', @@ -493,34 +478,31 @@ function _createSpanWithText(text, properties = {}) { * Returns the start and end indices of each instance of marked text, * as well as the updated text with all the open and close markers removed. * - * @param {string} text The text to search through and potentially update. - * @returns {{updatedText: string, linesToMarkedRanges: Object.}} - * The updated text and the indices of marked text in each line. - * @private + * @param text The text to search through and potentially update. + * @returns The updated text and the indices of marked text in each line. */ -function _findMarkedTextAndUpdate(text) { +function _findMarkedTextAndUpdate(text: string): { + updatedText: string, + linesToMarkedRanges: { [p: number]: { startIndex: number; endIndex: number }[] }; +} { const lines = text.split('\n'); - /** @type {Object.} */ - const linesToMarkedRanges = {}; - /** @type string[] */ - const textWithMarksRemoved = []; + const linesToMarkedRanges: {[index: number]: {startIndex: number, endIndex: number}[]} = {}; + const textWithMarksRemoved: string[] = []; for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - const line = lines[lineIndex]; + const line = lines[lineIndex]!; let currentIndexInLine = 0; /** * The updated line with the marks (`[!` and `!]`) removed. - * @type {string} */ let updatedLine = ''; /** * The ranges of marked text in the current line. - * @type {{startIndex: number, endIndex: number}[]} */ - let markedRanges = []; + let markedRanges: {startIndex: number, endIndex: number}[] = []; while (currentIndexInLine < line.length) { const startIndex = line.indexOf('[!', currentIndexInLine); diff --git a/src/_11ty/plugins/markdown.js b/src/_11ty/plugins/markdown.ts similarity index 78% rename from src/_11ty/plugins/markdown.js rename to src/_11ty/plugins/markdown.ts index 03634859e4..0a9c2c7388 100644 --- a/src/_11ty/plugins/markdown.js +++ b/src/_11ty/plugins/markdown.ts @@ -1,13 +1,12 @@ -import markdownIt from 'markdown-it'; +import MarkdownIt from 'markdown-it'; import markdownItContainer from 'markdown-it-container'; import markdownItDefinitionList from 'markdown-it-deflist'; import markdownItAttrs from 'markdown-it-attrs'; import markdownItAnchor from 'markdown-it-anchor'; -import { slugify } from '../utils/slugify.js'; +import {slugify} from '../utils/slugify.js'; -/** @type {import('markdown-it/lib').MarkdownIt} */ export const markdown = (() => { - const markdown = markdownIt({ html: true }) + const markdown = new MarkdownIt({ html: true }) .use(markdownItDefinitionList) .use(markdownItAttrs, { leftDelimiter: '{:', @@ -35,14 +34,12 @@ export const markdown = (() => { /** * Wrap all tables in a div with `table-wrapper` class. - * - * @param markdown {import('markdown-it/lib').MarkdownIt} markdown */ -function _setUpTables(markdown) { +function _setUpTables(markdown: MarkdownIt): void { markdown.renderer.rules = { ...markdown.renderer.rules, - table_open: function (tokens, idx, options, env, self) { - const token = tokens[idx]; + table_open: function (tokens, idx, _options, _env, self) { + const token = tokens[idx]!; // Render added attributes from `{:.table .table-striped}` syntax. return `
\n\n`; }, @@ -53,20 +50,19 @@ function _setUpTables(markdown) { /** * Register a custom aside/admonition. * - * @param {import('markdown-it/lib').MarkdownIt} markdown + * @param markdown * @param id The name to use in Markdown to create the aside. * @param defaultTitle The title to use if no title is specified in Markdown. * @param icon The material icon to use in the aside. * @param style The classes to add to the aside. - * @private */ -function _registerAside(markdown, id, defaultTitle, icon, style) { +function _registerAside(markdown: MarkdownIt, id: string, defaultTitle: string | null, icon: string | null, style: string): void { markdown.use(markdownItContainer, id, { - render: function (tokens, index) { + render: function (tokens: any[], index: number) { if (tokens[index].nesting === 1) { const parsedArgs = /\s+(.*)/.exec(tokens[index].info); - const title = parsedArgs?.[1] ?? defaultTitle; + const title = parsedArgs?.[1] ?? defaultTitle ?? ''; return `