From df4ffcbcdbe8a971f6581598146eb86e8891b40b Mon Sep 17 00:00:00 2001 From: avivkeller Date: Wed, 12 Aug 2026 10:15:43 -0400 Subject: [PATCH 1/2] chore: pre-render --- .changeset/index-page-from-input.md | 5 ++ .changeset/one-shot-generator-resolution.md | 5 ++ .changeset/remove-reading-time.md | 5 ++ .changeset/silent-log-level.md | 5 ++ README.md | 2 +- docs/cli.md | 4 +- docs/creating-generators.md | 5 ++ .../src/generators/__tests__/loader.test.mjs | 67 +++++++++++++++++ packages/core/src/generators/loader.mjs | 75 ++++++++++++++++++- .../core/src/logger/__tests__/logger.test.mjs | 21 +++++- packages/core/src/logger/constants.mjs | 4 + packages/react/package.json | 5 +- packages/react/src/html/README.md | 1 - .../src/html/ui/components/Layout/index.jsx | 10 +-- .../src/html/ui/components/MetaBar/index.jsx | 5 +- packages/react/src/html/ui/index.css | 1 + packages/react/src/jsx-ast/README.md | 9 ++- .../src/jsx-ast/__tests__/generate.test.mjs | 47 +++++++++++- packages/react/src/jsx-ast/generate.mjs | 11 ++- packages/react/src/jsx-ast/index.mjs | 1 - packages/react/src/jsx-ast/types.d.ts | 1 - .../utils/__tests__/buildBarProps.test.mjs | 31 +------- .../documentationIndex.test.mjs} | 60 +++++++++++---- .../react/src/jsx-ast/utils/buildBarProps.mjs | 16 ---- .../react/src/jsx-ast/utils/buildContent.mjs | 4 +- .../index.mjs => documentationIndex.mjs} | 39 +++++----- pnpm-lock.yaml | 18 ++--- scripts/vercel-build.sh | 1 - scripts/vercel-prepare.sh | 2 + www/doc-kit.config.mjs | 7 -- 30 files changed, 333 insertions(+), 134 deletions(-) create mode 100644 .changeset/index-page-from-input.md create mode 100644 .changeset/one-shot-generator-resolution.md create mode 100644 .changeset/remove-reading-time.md create mode 100644 .changeset/silent-log-level.md create mode 100644 packages/core/src/generators/__tests__/loader.test.mjs rename packages/react/src/jsx-ast/utils/{synthetic/__tests__/index.test.mjs => __tests__/documentationIndex.test.mjs} (67%) rename packages/react/src/jsx-ast/utils/{synthetic/index.mjs => documentationIndex.mjs} (57%) diff --git a/.changeset/index-page-from-input.md b/.changeset/index-page-from-input.md new file mode 100644 index 000000000..4a08607c2 --- /dev/null +++ b/.changeset/index-page-from-input.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/generator-react': patch +--- + +Generate `index.html` from the input `index` document instead of a synthetic page diff --git a/.changeset/one-shot-generator-resolution.md b/.changeset/one-shot-generator-resolution.md new file mode 100644 index 000000000..8182e24a4 --- /dev/null +++ b/.changeset/one-shot-generator-resolution.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/core': patch +--- + +Resolve generator packages from the invoking project's `node_modules` and the npm global root when they are not installed alongside core, so one-shot runs (`npx @doc-kit/cli`) find locally or globally installed generators diff --git a/.changeset/remove-reading-time.md b/.changeset/remove-reading-time.md new file mode 100644 index 000000000..78f7d271d --- /dev/null +++ b/.changeset/remove-reading-time.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/generator-react': patch +--- + +Remove the estimated reading time from the MetaBar; the `Layout` component no longer receives a `readingTime` prop diff --git a/.changeset/silent-log-level.md b/.changeset/silent-log-level.md new file mode 100644 index 000000000..de841ee80 --- /dev/null +++ b/.changeset/silent-log-level.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/core': patch +--- + +Add a `silent` log level, so `--log-level silent` suppresses all output diff --git a/README.md b/README.md index 9e3948492..92e6bf3ca 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ CLI tool to generate the Node.js API documentation Options: --log-level Log level (choices: "debug", "info", "warn", "error", - "fatal", default: "info") + "fatal", "silent", default: "info") -h, --help display help for command Commands: diff --git a/docs/cli.md b/docs/cli.md index e82748e7d..a6ebf2ebe 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -10,8 +10,8 @@ npx @doc-kit/cli [command] [options] One option applies to every command: -- `--log-level ` {string} `debug`, `info`, `warn`, `error`, or - `fatal`. **Default:** `'info'`. +- `--log-level ` {string} `debug`, `info`, `warn`, `error`, `fatal`, + or `silent` (no output at all). **Default:** `'info'`. ## `doc-kit generate` diff --git a/docs/creating-generators.md b/docs/creating-generators.md index 6580fbbc9..470cea4c0 100644 --- a/docs/creating-generators.md +++ b/docs/creating-generators.md @@ -162,6 +162,11 @@ npx @doc-kit/cli generate -t @my-scope/my-package/my-format ... npx @doc-kit/cli generate -t ./generators/my-format/index.mjs ... ``` +Package specifiers are resolved from wherever doc-kit is installed, then from +the invoking project's `node_modules`, then from the npm global root — so +one-shot runs (`npx @doc-kit/cli`) find generator packages installed either +in your project or globally. + Built-in generators additionally get a shorthand alias in `packages/core/src/generators/index.mjs`, which maps the name users type to the import specifier it resolves to: diff --git a/packages/core/src/generators/__tests__/loader.test.mjs b/packages/core/src/generators/__tests__/loader.test.mjs new file mode 100644 index 000000000..fc55de9a2 --- /dev/null +++ b/packages/core/src/generators/__tests__/loader.test.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import process from 'node:process'; +import { describe, it, before, after } from 'node:test'; + +import { loadGenerator } from '../loader.mjs'; + +// A package that only exists in the fake project's node_modules — never in +// the workspace — so a bare import() from core is guaranteed to miss and +// exercise the cwd fallback used by one-shot (`npx @doc-kit/cli`) runs. +const PACKAGE_NAME = '@doc-kit-test/fake-generator'; + +describe('loadGenerator', () => { + let projectDir; + let originalCwd; + + before(async () => { + projectDir = await mkdtemp(join(tmpdir(), 'doc-kit-loader-')); + + const packageDir = join(projectDir, 'node_modules', PACKAGE_NAME); + await mkdir(packageDir, { recursive: true }); + + await writeFile( + join(packageDir, 'package.json'), + JSON.stringify({ + name: PACKAGE_NAME, + version: '1.0.0', + exports: { './gen': './gen.mjs' }, + }) + ); + + await writeFile( + join(packageDir, 'gen.mjs'), + 'export default { name: "fake", generate: () => {} };\n' + ); + + originalCwd = process.cwd(); + process.chdir(projectDir); + }); + + after(async () => { + process.chdir(originalCwd); + await rm(projectDir, { recursive: true, force: true }); + }); + + it('should resolve packages from the invoking project', async () => { + const generator = await loadGenerator(`${PACKAGE_NAME}/gen`); + + assert.equal(generator.name, 'fake'); + }); + + it('should throw a friendly error when a package is not installed anywhere', async () => { + await assert.rejects( + loadGenerator('@doc-kit-test/does-not-exist'), + /Could not load generator "@doc-kit-test\/does-not-exist"/ + ); + }); + + it('should reject modules that are not generators', async () => { + const notAGenerator = join(projectDir, 'not-a-generator.mjs'); + await writeFile(notAGenerator, 'export default { name: "broken" };\n'); + + await assert.rejects(loadGenerator(notAGenerator), /is not a generator/); + }); +}); diff --git a/packages/core/src/generators/loader.mjs b/packages/core/src/generators/loader.mjs index 653d202bc..faee80a1d 100644 --- a/packages/core/src/generators/loader.mjs +++ b/packages/core/src/generators/loader.mjs @@ -1,6 +1,9 @@ 'use strict'; +import { execSync } from 'node:child_process'; +import { createRequire } from 'node:module'; import { isAbsolute } from 'node:path'; +import process from 'node:process'; import { pathToFileURL } from 'node:url'; import { allGenerators } from './index.mjs'; @@ -27,6 +30,65 @@ export const resolveGeneratorSpecifier = target => { return target; }; +let npmGlobalRoot; + +/** + * Asking npm for its global root spawns a process, so only do it when a + * generator package is neither installed alongside core nor in the invoking + * project, and remember the answer (`''` = npm unavailable). + * + * @returns {string} The npm global `node_modules` directory, or `''` + */ +const getNpmGlobalRoot = () => { + if (npmGlobalRoot === undefined) { + try { + npmGlobalRoot = execSync('npm root -g', { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + npmGlobalRoot = ''; + } + } + + return npmGlobalRoot; +}; + +/** + * Resolves a specifier starting from the given directory's `node_modules` + * hierarchy instead of core's own location. + * + * @param {string} specifier - Bare package specifier + * @param {string} base - Directory to resolve from + * @returns {string | undefined} File URL of the resolved module, if found + */ +const tryResolveFrom = (specifier, base) => { + if (!base) { + return undefined; + } + + const require = createRequire(import.meta.url); + + try { + return pathToFileURL(require.resolve(specifier, { paths: [base] })).href; + } catch { + return undefined; + } +}; + +/** + * Resolves a generator package from the invoking project or the npm global + * root. One-shot runs (`npx @doc-kit/cli`) install core into the npx cache, + * where a bare import() cannot see generator packages the user has installed + * locally or globally. + * + * @param {string} specifier - Bare package specifier that failed to import + * @returns {string | undefined} File URL of the resolved module, if found + */ +const resolveInstalledPackage = specifier => + tryResolveFrom(specifier, process.cwd()) ?? + tryResolveFrom(specifier, getNpmGlobalRoot()); + /** * Imports a generator by specifier and returns its default export. * @@ -42,15 +104,22 @@ export const loadGenerator = async specifier => { try { module = await import(resolved); } catch (error) { - if (error.code === 'ERR_MODULE_NOT_FOUND') { + if (error.code !== 'ERR_MODULE_NOT_FOUND') { + throw error; + } + + const installed = resolveInstalledPackage(resolved); + + if (!installed) { throw new Error( `Could not load generator "${specifier}" (resolved to "${resolved}"). ` + - 'If it lives in a separate package, make sure that package is installed.', + 'If it lives in a separate package, make sure that package is ' + + 'installed in your project or globally.', { cause: error } ); } - throw error; + module = await import(installed); } const generator = module.default; diff --git a/packages/core/src/logger/__tests__/logger.test.mjs b/packages/core/src/logger/__tests__/logger.test.mjs index 1673b453e..904e14361 100644 --- a/packages/core/src/logger/__tests__/logger.test.mjs +++ b/packages/core/src/logger/__tests__/logger.test.mjs @@ -173,13 +173,26 @@ describe('createLogger', () => { }); }); - it('should filter all messages when minimum level is set above FATAL', t => { + it('should filter all messages when level is SILENT', t => { const transport = t.mock.fn(); - // silent logs - const logger = createLogger(transport, 100); + const logger = createLogger(transport, LogLevel.silent); - Object.keys(LogLevel).forEach(level => { + ['debug', 'info', 'warn', 'error', 'fatal'].forEach(level => { + logger[level]('Hello, World!'); + }); + + strictEqual(transport.mock.callCount(), 0); + }); + + it('should filter all messages when SILENT is set by name at runtime', t => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, LogLevel.info); + + logger.setLogLevel('silent'); + + ['debug', 'info', 'warn', 'error', 'fatal'].forEach(level => { logger[level]('Hello, World!'); }); diff --git a/packages/core/src/logger/constants.mjs b/packages/core/src/logger/constants.mjs index 07683124b..489556dc8 100644 --- a/packages/core/src/logger/constants.mjs +++ b/packages/core/src/logger/constants.mjs @@ -9,6 +9,10 @@ export const LogLevel = { warn: 30, error: 40, fatal: 50, + // Threshold-only level: no message is ever emitted at `silent` (there is no + // logger method for it), so setting it suppresses all output. It has no + // entry in the tag/color maps below for the same reason. + silent: Infinity, }; /** diff --git a/packages/react/package.json b/packages/react/package.json index a60ec0c5c..45f34601e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -28,12 +28,12 @@ ], "dependencies": { "@11ty/is-land": "^5.0.1", + "@doc-kit/core": "workspace:*", "@fontsource-variable/open-sans": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0", "@heroicons/react": "^2.2.0", - "@doc-kit/core": "workspace:*", "@node-core/rehype-shiki": "^1.4.3", - "@node-core/ui-components": "^1.7.4", + "@node-core/ui-components": "^1.7.5", "@orama/orama": "^3.1.18", "@orama/ui": "^1.5.4", "estree-util-to-js": "^2.0.0", @@ -42,7 +42,6 @@ "mdast-util-slice-markdown": "^2.0.1", "preact": "^10.29.7", "preact-render-to-string": "^6.7.0", - "reading-time": "^1.5.0", "recma-jsx": "^1.0.1", "recma-stringify": "^1.0.0", "rehype-raw": "^7.0.0", diff --git a/packages/react/src/html/README.md b/packages/react/src/html/README.md index 5dddd6529..988bcfa11 100644 --- a/packages/react/src/html/README.md +++ b/packages/react/src/html/README.md @@ -416,7 +416,6 @@ export default ({ metadata }) => ( - `metadata` {Object} Serialized page metadata — all YAML frontmatter properties plus `addedIn`, `basename`, `path`, and any custom user-defined fields. - `headings` {Array} Pre-computed table of contents heading entries. -- `readingTime` {string} Estimated reading time (e.g. `'5 min read'`). - `children` {ComponentChildren} Processed page content. The `Layout` component receives the props above. Custom Layout components can use diff --git a/packages/react/src/html/ui/components/Layout/index.jsx b/packages/react/src/html/ui/components/Layout/index.jsx index 6308aa748..74bd92f0b 100644 --- a/packages/react/src/html/ui/components/Layout/index.jsx +++ b/packages/react/src/html/ui/components/Layout/index.jsx @@ -18,9 +18,9 @@ import SideBar from '#theme/Sidebar'; * main content, meta bar, and footer. Override via `#theme/Layout` in your * configuration's `imports` to customize the entire page structure. * - * @param {{ metadata: import('../../types').SerializedMetadata, headings: Array, readingTime: string, children: import('preact').ComponentChildren }} props + * @param {{ metadata: import('../../types').SerializedMetadata, headings: Array, children: import('preact').ComponentChildren }} props */ -export default ({ metadata, headings, readingTime, children }) => { +export default ({ metadata, headings, children }) => { const crossLinkItems = navigation.showCrossLinks ? (navigation.sidebar?.flatMap(({ items }) => items) ?? []) : []; @@ -67,11 +67,7 @@ export default ({ metadata, headings, readingTime, children }) => { )} - +