diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1ba2e1b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +# Gitignored +node_modules +out + +# The template uses intentional syntax (e.g. `{replaced_value}`) +template.html diff --git a/components/Footer/footer.json b/components/Footer/footer.json deleted file mode 100644 index cc1cfa6..0000000 --- a/components/Footer/footer.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "socialLinks": [ - { - "icon": "github", - "link": "https://github.com/nodejs/node", - "alt": "GitHub" - }, - { - "icon": "discord", - "link": "https://discord.gg/nodejs", - "alt": "Discord" - }, - { - "icon": "mastodon", - "link": "https://social.lfx.dev/@nodejs", - "alt": "Mastodon" - }, - { - "icon": "bluesky", - "link": "https://bsky.app/profile/nodejs.org", - "alt": "Bluesky" - }, - { - "icon": "twitter", - "link": "https://twitter.com/nodejs", - "alt": "Twitter" - }, - { - "icon": "slack", - "link": "https://slack-invite.openjsf.org/", - "alt": "Slack" - }, - { - "icon": "linkedin", - "link": "https://www.linkedin.com/company/node-js", - "alt": "LinkedIn" - } - ], - "footerLinks": [ - { "link": "https://openjsf.org/", "text": "OpenJS Foundation" }, - { "link": "https://terms-of-use.openjsf.org/", "text": "Terms of Use" }, - { "link": "https://privacy-policy.openjsf.org/", "text": "Privacy Policy" }, - { "link": "https://bylaws.openjsf.org/", "text": "Bylaws" }, - { - "link": "https://github.com/openjs-foundation/cross-project-council/blob/main/CODE_OF_CONDUCT.md", - "text": "Code of Conduct" - }, - { - "link": "https://trademark-policy.openjsf.org/", - "text": "Trademark Policy" - }, - { "link": "https://trademark-list.openjsf.org/", "text": "Trademark List" }, - { - "link": "https://www.linuxfoundation.org/cookies/", - "text": "Cookie Policy" - }, - { - "link": "https://github.com/nodejs/node/security/policy", - "text": "Security Policy" - } - ] -} diff --git a/components/Footer/index.jsx b/components/Footer/index.jsx index f27d37a..e85ff91 100644 --- a/components/Footer/index.jsx +++ b/components/Footer/index.jsx @@ -1,7 +1,10 @@ import Footer from '@node-core/ui-components/Containers/Footer'; import NavItem from '@node-core/ui-components/Containers/NavBar/NavItem'; -import { socialLinks, footerLinks } from './footer.json'; +import { + socialLinks, + footerLinks, +} from '#site/navigation.json' with { type: 'json' }; // The Node.js Project is legally obligated to include the following text. // It should not be modified unless explicitly requested by OpenJS staff. diff --git a/components/Navigation/index.jsx b/components/Navigation/index.jsx index c2c9903..26a13d2 100644 --- a/components/Navigation/index.jsx +++ b/components/Navigation/index.jsx @@ -5,7 +5,7 @@ import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub'; import SearchBox from '@node-core/doc-kit/src/generators/web/ui/components/SearchBox'; import { useTheme } from '@node-core/doc-kit/src/generators/web/ui/hooks/useTheme.mjs'; - +import { topNavigation } from '#site/navigation.json' with { type: 'json' }; import Logo from '#theme/Logo'; /** @@ -18,23 +18,11 @@ export default ({ metadata }) => { ({ + link, + text: label, + target, + }))} > } */ -const categories = [ - ['getting-started', 'Getting Started'], - ['command-line', 'Command Line'], - ['http', 'HTTP'], - ['manipulating-files', 'Manipulating Files'], - ['asynchronous-work', 'Asynchronous Work'], - ['typescript', 'TypeScript'], - ['modules', 'Modules'], - ['diagnostics', 'Diagnostics'], - ['test-runner', 'Test Runner'], -]; - -/** @type {Map>} */ -const byDir = new Map(); -for (const [heading, path] of pages) { - const dir = path.split('/')[1]; - if (!byDir.has(dir)) byDir.set(dir, []); - byDir.get(dir).push({ heading, path }); -} +import sidebarGroups from '#learn/navigation.json' with { type: 'json' }; /** @param {string} url */ const redirect = url => (window.location.href = url); @@ -31,26 +9,12 @@ const PrefetchLink = props => ; /** * Sidebar component for MDX documentation with page navigation */ -export default ({ metadata }) => { - const { path: currentPath, basename } = metadata; - const pathname = `${basename}.html`; - - const groups = categories.map(([dir, title]) => ({ - groupName: title, - items: byDir.get(dir).map(({ heading, path }) => ({ - label: heading, - link: - currentPath === path ? pathname : `${relative(path, currentPath)}.html`, - })), - })); - - return ( - - ); -}; +export default ({ metadata }) => ( + +); diff --git a/doc-kit.config.mjs b/doc-kit.config.mjs index 7eceff7..3f148a2 100644 --- a/doc-kit.config.mjs +++ b/doc-kit.config.mjs @@ -1,5 +1,7 @@ import web from '@node-core/doc-kit/src/generators/web/index.mjs'; import { join } from 'node:path'; +import { populateI18n, createGroupings } from './utils/index.mjs'; +import sidebarGroups from './navigation.json' with { type: 'json' }; /** @type {import('@node-core/doc-kit/src/utils/configuration/types.d.ts').Configuration} */ export default { @@ -8,12 +10,28 @@ export default { input: ['pages/**/*.md'], }, web: { - title: '', + // Important Configuration + project: 'Node.js', + title: '{project} Learn', pageURL: 'https://nodejs.org/learn{path}.html', editURL: 'https://github.com/nodejs/learn/edit/main/pages{path}.md', + + // Imports imports: { ...web.defaultConfiguration.imports, '#theme/Layout': join(import.meta.dirname, 'components/Layout/index.jsx'), }, + virtualImports: { + '#site/navigation.json': JSON.stringify( + populateI18n( + await fetch( + 'https://raw.githubusercontent.com/nodejs/nodejs.org/refs/heads/main/apps/site/navigation.json' + ).then(r => r.json()) + ) + ), + '#learn/navigation.json': JSON.stringify( + await createGroupings(sidebarGroups) + ), + }, }, }; diff --git a/navigation.json b/navigation.json new file mode 100644 index 0000000..22f866b --- /dev/null +++ b/navigation.json @@ -0,0 +1,104 @@ +[ + [ + "Getting Started", + [ + "/getting-started/introduction-to-nodejs.md", + "/getting-started/how-much-javascript-do-you-need-to-know-to-use-nodejs.md", + "/getting-started/differences-between-nodejs-and-the-browser.md", + "/getting-started/the-v8-javascript-engine.md", + "/getting-started/an-introduction-to-the-npm-package-manager.md", + "/getting-started/ecmascript-2015-es6-and-beyond.md", + "/getting-started/debugging.md", + "/getting-started/fetch.md", + "/getting-started/websocket.md", + "/getting-started/nodejs-the-difference-between-development-and-production.md", + "/getting-started/profiling.md", + "/getting-started/nodejs-with-webassembly.md", + "/getting-started/security-best-practices.md", + "/getting-started/userland-migrations.md" + ] + ], + [ + "Command Line", + [ + "/command-line/run-nodejs-scripts-from-the-command-line.md", + "/command-line/how-to-use-the-nodejs-repl.md", + "/command-line/output-to-the-command-line-using-nodejs.md", + "/command-line/accept-input-from-the-command-line-in-nodejs.md", + "/command-line/how-to-read-environment-variables-from-nodejs.md" + ] + ], + [ + "HTTP", + [ + "/http/anatomy-of-an-http-transaction.md", + "/http/enterprise-network-configuration.md" + ] + ], + [ + "Manipulating Files", + [ + "/manipulating-files/nodejs-file-stats.md", + "/manipulating-files/nodejs-file-paths.md", + "/manipulating-files/reading-files-with-nodejs.md", + "/manipulating-files/writing-files-with-nodejs.md", + "/manipulating-files/working-with-file-descriptors-in-nodejs.md", + "/manipulating-files/working-with-folders-in-nodejs.md", + "/manipulating-files/working-with-different-filesystems.md" + ] + ], + [ + "Asynchronous Work", + [ + "/asynchronous-work/javascript-asynchronous-programming-and-callbacks.md", + "/asynchronous-work/asynchronous-flow-control.md", + "/asynchronous-work/discover-promises-in-nodejs.md", + "/asynchronous-work/discover-javascript-timers.md", + "/asynchronous-work/overview-of-blocking-vs-non-blocking.md", + "/asynchronous-work/event-loop-timers-and-nexttick.md", + "/asynchronous-work/the-nodejs-event-emitter.md", + "/asynchronous-work/understanding-processnexttick.md", + "/asynchronous-work/understanding-setimmediate.md", + "/asynchronous-work/dont-block-the-event-loop.md" + ] + ], + [ + "TypeScript", + [ + "/typescript/introduction.md", + "/typescript/run-natively.md", + "/typescript/transpile.md", + "/typescript/run.md", + "/typescript/publishing-a-ts-package.md" + ] + ], + [ + "Modules", + [ + "/modules/how-to-use-streams.md", + "/modules/backpressuring-in-streams.md", + "/modules/publishing-a-package.md", + "/modules/publishing-node-api-modules.md", + "/modules/abi-stability.md" + ] + ], + [ + "Diagnostics", + [ + "/diagnostics/user-journey.md", + "/diagnostics/memory/index.md", + "/diagnostics/live-debugging/index.md", + "/diagnostics/poor-performance/index.md", + "/diagnostics/flame-graphs.md" + ] + ], + [ + "Test Runner", + [ + "/test-runner/introduction.md", + "/test-runner/using-test-runner.md", + "/test-runner/mocking.md", + "/test-runner/collecting-code-coverage.md" + ] + ] +] diff --git a/package-lock.json b/package-lock.json index 1e503ea..5b81b33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@node-core/doc-kit": "1.3.1", + "@node-core/doc-kit": "1.3.2", "@node-core/remark-lint": "^1.3.0", "eslint": "^10.1.0", "eslint-plugin-mdx": "^3.7.0", @@ -827,9 +827,9 @@ } }, "node_modules/@node-core/doc-kit": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@node-core/doc-kit/-/doc-kit-1.3.1.tgz", - "integrity": "sha512-GNgtXYSmA93Nuh3I9Dn30CsGqpomsfySp3gaqxiQ5Ih2AVH4js9GRxp+WdSJHjRY4dZ+UsQTLcv7ig7m6I+bGA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@node-core/doc-kit/-/doc-kit-1.3.2.tgz", + "integrity": "sha512-TNqNaNOwuynp/T3e0PnxjnthuneD/MVoY4VGnjvzCvsTzvKV4T27PzfL7b8E9Cwn7qltqtY3wHPg+hy1IhDmOw==", "dev": true, "dependencies": { "@actions/core": "^3.0.0", diff --git a/package.json b/package.json index 1256ad4..d9aed25 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@node-core/doc-kit": "1.3.1", + "@node-core/doc-kit": "1.3.2", "@node-core/remark-lint": "^1.3.0", "eslint": "^10.1.0", "eslint-plugin-mdx": "^3.7.0", diff --git a/utils/index.mjs b/utils/index.mjs new file mode 100644 index 0000000..13bb841 --- /dev/null +++ b/utils/index.mjs @@ -0,0 +1,58 @@ +import { createReadStream } from 'node:fs'; +import { join } from 'node:path'; +import { createInterface } from 'node:readline'; + +export const PAGES_DIR = join(import.meta.dirname, '../pages'); + +export const getTitle = async path => { + const rl = createInterface({ + input: createReadStream(join(PAGES_DIR, path)), + crlfDelay: Infinity, + }); + + for await (const line of rl) { + const match = line.match(/^#{1,6}\s+(.+)/); + if (match) { + rl.close(); + return match[1]; + } + } + + return null; +}; + +export const mapValuesAsync = async (obj, fn) => + await Promise.all(obj.map(async ([k, v]) => await fn(v, k))); + +export const createGroupings = groups => + mapValuesAsync(groups, async (paths, label) => ({ + groupName: label, + items: await Promise.all( + paths.map(async path => ({ + link: `/learn${path.replace(/\.md/g, '').replace(/\/index/g, '')}`, + label: await getTitle(path), + })) + ), + })); + +export const createI18nPopulator = async () => { + const i18n = await fetch( + 'https://raw.githubusercontent.com/nodejs/nodejs.org/refs/heads/main/packages/i18n/src/locales/en.json' + ).then(r => r.json()); + + const resolve = key => key.split('.').reduce((acc, k) => acc?.[k], i18n); + + const walk = value => { + if (Array.isArray(value)) return value.map(walk); + if (value && typeof value === 'object') + return Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, walk(v)]) + ); + if (typeof value === 'string') return resolve(value) ?? value; + return value; + }; + + return walk; +}; + +export const populateI18n = await createI18nPopulator(); diff --git a/vercel.json b/vercel.json index 588d5e0..ec083d7 100644 --- a/vercel.json +++ b/vercel.json @@ -14,8 +14,13 @@ "permanent": true }, { - "source": "/learn/(.*)/", - "destination": "/learn/$1", + "source": "/learn/diagnostics/:page(live-debugging|memory|poor-performance)", + "destination": "/learn/diagnostics/:page/", + "permanent": true + }, + { + "source": "/learn/:path((?!diagnostics/live-debugging/?$|diagnostics/memory/?$|diagnostics/poor-performance/?$).+)/", + "destination": "/learn/:path", "permanent": true } ],