Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@node-core/doc-kit",
"type": "module",
"version": "1.3.2",
"version": "1.3.3",
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/doc-kit.git"
Expand Down
2 changes: 1 addition & 1 deletion src/generators/orama-db/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function generate(input) {
description: paragraph
? transformNodeToString(paragraph, true)
: undefined,
href: `${entry.path.slice(1)}.html#${entry.heading.data.slug}`,
href: `${entry.path}.html#${entry.heading.data.slug}`,
siteSection: headings[0].heading.data.name,
};
})
Expand Down
1 change: 1 addition & 0 deletions src/generators/web/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default createLazyGenerator({
templatePath: join(import.meta.dirname, 'template.html'),
project: 'Node.js',
title: '{project} v{version} Documentation',
useAbsoluteURLs: false,
editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`,
pageURL: '{baseURL}/latest-{version}/api{path}.html',
imports: {
Expand Down
14 changes: 7 additions & 7 deletions src/generators/web/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://nodejs.org/static/images/favicons/favicon.png"/>
<title>{title}</title>
<title>${title}</title>
<meta name="description" content="Node.js® is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.">
<link rel="stylesheet" href="{root}styles.css" />
<meta property="og:title" content="{title}">
<link rel="stylesheet" href="${root}styles.css" />
<meta property="og:title" content="${title}">
<meta property="og:description" content="Node.js® is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.">
<meta property="og:image" content="https://nodejs.org/en/next-data/og/announcement/Node.js%20%E2%80%94%20Run%20JavaScript%20Everywhere" />
<meta property="og:type" content="website">
Expand All @@ -20,12 +20,12 @@

<!-- Apply theme before paint to avoid Flash of Unstyled Content -->
<script>document.documentElement.setAttribute("data-theme", document.documentElement.style.colorScheme = localStorage.getItem("theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));</script>
<script type="importmap">{importMap}</script>
<script type="speculationrules">{speculationRules}</script>
<script type="importmap">${importMap}</script>
<script type="speculationrules">${speculationRules}</script>
</head>

<body>
<div id="root">{dehydrated}</div>
<script type="module" src="{root}{entrypoint}"></script>
<div id="root">${dehydrated}</div>
<script type="module" src="${root}${entrypoint}"></script>
</body>
</html>
1 change: 1 addition & 0 deletions src/generators/web/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { JSXContent } from '../jsx-ast/utils/buildContent.mjs';
export type Configuration = {
templatePath: string;
title: string;
useAbsoluteURLs: boolean;
Comment thread
avivkeller marked this conversation as resolved.
imports: Record<string, string>;
virtualImports: Record<string, string>;
};
Expand Down
4 changes: 2 additions & 2 deletions src/generators/web/ui/components/SearchBox/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import SearchResults from '@node-core/ui-components/Common/Search/Results';
import SearchHit from '@node-core/ui-components/Common/Search/Results/Hit';

import styles from './index.module.css';
import { relative } from '../../../../../utils/url.mjs';
import useOrama from '../../hooks/useOrama.mjs';
import { relativeOrAbsolute } from '../../utils/relativeOrAbsolute.mjs';

const SearchBox = ({ pathname }) => {
const client = useOrama(pathname);
Expand All @@ -23,7 +23,7 @@ const SearchBox = ({ pathname }) => {
<SearchHit
document={{
...hit.document,
href: relative(hit.document.href, pathname),
href: relativeOrAbsolute(hit.document.href, pathname),
}}
/>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/generators/web/ui/components/SideBar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Select from '@node-core/ui-components/Common/Select';
import SideBar from '@node-core/ui-components/Containers/Sidebar';

import styles from './index.module.css';
import { relative } from '../../../../../utils/url.mjs';
import { relativeOrAbsolute } from '../../utils/relativeOrAbsolute.mjs';

import { project, version, versions, pages } from '#theme/config';

Expand Down Expand Up @@ -41,7 +41,7 @@ export default ({ metadata }) => {
link:
metadata.path === path
? `${metadata.basename}.html`
: `${relative(path, metadata.path)}.html`,
: `${relativeOrAbsolute(path, metadata.path)}.html`,
}));

return (
Expand Down
4 changes: 2 additions & 2 deletions src/generators/web/ui/hooks/useOrama.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { create, search, load } from '@orama/orama';
import { useState, useEffect } from 'react';

import { relative } from '../../../../utils/url.mjs';
import { relativeOrAbsolute } from '../utils/relativeOrAbsolute.mjs';

/**
* Hook for initializing and managing Orama search database
Expand All @@ -26,7 +26,7 @@ export default pathname => {
setClient(db);

// Load the search data
fetch(relative('/orama-db.json', pathname))
fetch(relativeOrAbsolute('/orama-db.json', pathname))
.then(response => response.ok && response.json())
.then(data => load(db, data));
}, []);
Expand Down
14 changes: 14 additions & 0 deletions src/generators/web/ui/utils/relativeOrAbsolute.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { relative } from '../../../../utils/url.mjs';

import { useAbsoluteURLs, baseURL } from '#theme/config';

/**
* Returns an absolute URL (based on baseURL) or a relative URL,
* depending on the useAbsoluteURLs configuration option.
*
* @param {string} to - Target path (e.g., '/fs', '/orama-db.json')
* @param {string} from - Current page path (e.g., '/api/fs')
* @returns {string}
*/
export const relativeOrAbsolute = (to, from) =>
useAbsoluteURLs ? new URL(`.${to}`, baseURL).href : relative(to, from);
63 changes: 63 additions & 0 deletions src/generators/web/utils/__tests__/processing.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { populateWithEvaluation } from '../processing.mjs';

describe('populateWithEvaluation', () => {
it('substitutes simple ${variable} placeholders', () => {
const result = populateWithEvaluation('Hello ${name}!', { name: 'World' });
assert.strictEqual(result, 'Hello World!');
});

it('supports multiple variables', () => {
const result = populateWithEvaluation('${greeting} ${name}!', {
greeting: 'Hi',
name: 'Node',
});
assert.strictEqual(result, 'Hi Node!');
});

it('supports JavaScript expressions', () => {
const result = populateWithEvaluation('${value > 5 ? "big" : "small"}', {
value: 10,
});
assert.strictEqual(result, 'big');
});

it('supports ternary expressions for conditional content', () => {
const result = populateWithEvaluation(
'${showExtra ? "extra content" : ""}',
{ showExtra: false }
);
assert.strictEqual(result, '');
});

it('handles JSON.stringify for objects', () => {
const obj = { key: 'value' };
const result = populateWithEvaluation('${JSON.stringify(data)}', {
data: obj,
});
assert.strictEqual(result, '{"key":"value"}');
});

it('preserves surrounding HTML content', () => {
const result = populateWithEvaluation(
'<title>${title}</title><link href="${root}styles.css" />',
{ title: 'Test Page', root: '../' }
);
assert.strictEqual(
result,
'<title>Test Page</title><link href="../styles.css" />'
);
});

it('handles empty string values', () => {
const result = populateWithEvaluation('[${content}]', { content: '' });
assert.strictEqual(result, '[]');
});

it('handles numeric values', () => {
const result = populateWithEvaluation('count: ${count}', { count: 42 });
assert.strictEqual(result, 'count: 42');
});
});
66 changes: 66 additions & 0 deletions src/generators/web/utils/__tests__/relativeOrAbsolute.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import assert from 'node:assert/strict';
import { beforeEach, describe, it } from 'node:test';

import {
setConfig,
default as getConfig,
} from '../../../../utils/configuration/index.mjs';
import { relativeOrAbsolute } from '../relativeOrAbsolute.mjs';

await setConfig({
version: 'v22.0.0',
changelog: [],
generators: {
web: {
useAbsoluteURLs: false,
baseURL: 'https://nodejs.org/docs',
Comment thread
avivkeller marked this conversation as resolved.
},
},
});

describe('relativeOrAbsolute (relative mode)', () => {
beforeEach(() => {
getConfig('web').useAbsoluteURLs = false;
});

it('returns a relative path from a nested page to root', () => {
const result = relativeOrAbsolute('/', '/api/fs');
assert.strictEqual(result, '..');
});

it('returns a relative path between sibling pages', () => {
const result = relativeOrAbsolute('/http', '/fs');
assert.strictEqual(result, 'http');
});

it('returns a relative path for a deeper target', () => {
const result = relativeOrAbsolute('/orama-db.json', '/api/fs');
assert.strictEqual(result, '../orama-db.json');
});

it('returns "." when source and target resolve to the same path', () => {
const result = relativeOrAbsolute('/', '/');
assert.strictEqual(result, '.');
});
});

describe('relativeOrAbsolute (absolute mode)', () => {
beforeEach(() => {
getConfig('web').useAbsoluteURLs = true;
});

it('returns an absolute URL to root', () => {
const result = relativeOrAbsolute('/', '/api/fs');
assert.strictEqual(result, 'https://nodejs.org/docs');
});

it('returns an absolute URL for a page path', () => {
const result = relativeOrAbsolute('/http', '/fs');
Comment thread
avivkeller marked this conversation as resolved.
assert.strictEqual(result, 'https://nodejs.org/docs/http');
});

it('returns an absolute URL for a resource', () => {
const result = relativeOrAbsolute('/orama-db.json', '/api/fs');
assert.strictEqual(result, 'https://nodejs.org/docs/orama-db.json');
});
});
36 changes: 27 additions & 9 deletions src/generators/web/utils/processing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,29 @@ import bundleCode from './bundle.mjs';
import { createChunkedRequire } from './chunks.mjs';
import createConfigSource from './config.mjs';
import createASTBuilder from './generate.mjs';
import { relativeOrAbsolute } from './relativeOrAbsolute.mjs';
import getConfig from '../../../utils/configuration/index.mjs';
import { populate } from '../../../utils/configuration/templates.mjs';
import { minifyHTML } from '../../../utils/html-minifier.mjs';
import { relative } from '../../../utils/url.mjs';
import { SPECULATION_RULES } from '../constants.mjs';

/**
* Populates a template string by evaluating it as a JavaScript template literal,
* allowing full JS expression syntax (e.g., ${if ...}, ${JSON.stringify(...)}).
Comment thread
avivkeller marked this conversation as resolved.
*
* ONLY used for HTML template population. Do not use elsewhere.
*
* @param {string} template - The template string with ${...} placeholders
* @param {Record<string, unknown>} config - The values available in the template
* @returns {string} The populated template
*/
export const populateWithEvaluation = (template, config) => {
const keys = Object.keys(config);
const values = Object.values(config);
const fn = new Function(...keys, `return \`${template}\`;`);
return fn(...values);
};
Comment thread
avivkeller marked this conversation as resolved.

/**
* Converts JSX AST entries to server and client JavaScript code.
*
Expand Down Expand Up @@ -107,21 +124,22 @@ export async function processJSXEntries(entries, template) {

// Step 3: Render final HTML pages
const results = await Promise.all(
entries.map(async ({ data: { api, path, heading } }) => {
const root = `${relative('/', path)}/`;
entries.map(async ({ data }) => {
const root = `${relativeOrAbsolute('/', data.path)}/`;

// Replace template placeholders with actual content
const renderedHtml = populate(template, {
title: `${heading.data.name} | ${titleSuffix}`,
dehydrated: serverBundle.pages.get(`${api}.js`) ?? '',
const renderedHtml = populateWithEvaluation(template, {
title: `${data.heading.data.name} | ${titleSuffix}`,
dehydrated: serverBundle.pages.get(`${data.api}.js`) ?? '',
Comment thread
avivkeller marked this conversation as resolved.
Outdated
importMap: clientBundle.importMap?.replaceAll('/', root) ?? '',
entrypoint: `${api}.js?${randomUUID()}`,
entrypoint: `${data.api}.js?${randomUUID()}`,
speculationRules: SPECULATION_RULES,
Comment thread
avivkeller marked this conversation as resolved.
root,
path,
metadata: data,
config,
});

return { html: await minifyHTML(renderedHtml), path };
return { html: await minifyHTML(renderedHtml), path: data.path };
})
);

Expand Down
16 changes: 16 additions & 0 deletions src/generators/web/utils/relativeOrAbsolute.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import getConfig from '../../../utils/configuration/index.mjs';
import { relative } from '../../../utils/url.mjs';

/**
* Returns an absolute URL (based on baseURL) or a relative URL,
* depending on the useAbsoluteURLs configuration option.
*
* @param {string} to - Target path (e.g., '/fs', '/orama-db.json')
* @param {string} from - Current page path (e.g., '/api/fs')
* @returns {string}
*/
export const relativeOrAbsolute = (to, from) => {
const { useAbsoluteURLs, baseURL } = getConfig('web');

return useAbsoluteURLs ? new URL(`.${to}`, baseURL).href : relative(to, from);
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
};
Loading