From 0ae17abc8344bef4442ce59af6ee42ef8ef458a2 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sat, 5 Sep 2026 21:19:15 -0700 Subject: [PATCH 1/3] Fix published declaration compatibility --- index.js | 2 +- lib/build-copy/index.js | 2 +- lib/build-pages/page-builders/html/index.js | 3 +- lib/build-pages/page-builders/md/index.js | 3 +- lib/build-static/index.js | 2 +- lib/builder.js | 25 ++++- package.json | 1 + scripts/test-packed-types.js | 117 ++++++++++++++++++++ types.ts | 2 +- 9 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 scripts/test-packed-types.js diff --git a/index.js b/index.js index 9bae2d6f..59078b25 100644 --- a/index.js +++ b/index.js @@ -137,7 +137,7 @@ export class DomStack { this.#src = src this.#dest = dest - this.#logger = opts.logger ?? createDomStackLogger() + this.#logger = /** @type {PinoLogger} */ (opts.logger ?? createDomStackLogger()) this.opts = normalizeDomStackOpts(opts, dest) const copyDirs = this.opts.copy ?? [] diff --git a/lib/build-copy/index.js b/lib/build-copy/index.js index a213eca0..82b8130a 100644 --- a/lib/build-copy/index.js +++ b/lib/build-copy/index.js @@ -7,7 +7,7 @@ import { join } from 'node:path' import { createCopiedDomstackManifestRecords } from '../helpers/cpx2-report.js' /** - * @typedef {Record>>} CopyBuilderReport + * @typedef {Record} CopyBuilderReport * @typedef {BuildStepResult<'copy', CopyBuilderReport>} CopyBuildStepResult * @typedef {BuildStep<'copy', CopyBuilderReport>} CopyBuildStep */ diff --git a/lib/build-pages/page-builders/html/index.js b/lib/build-pages/page-builders/html/index.js index 22357155..2cc9355e 100644 --- a/lib/build-pages/page-builders/html/index.js +++ b/lib/build-pages/page-builders/html/index.js @@ -8,8 +8,7 @@ import Handlebars from 'handlebars' /** * Build all of the bundles using esbuild. - * @template {Record} T - The type of variables for the page - * @type {PageBuilderType} + * @type {PageBuilderType, string>} */ export async function htmlBuilder ({ pageInfo }) { assert(pageInfo.type === 'html', 'html builder requires a "html" page type') diff --git a/lib/build-pages/page-builders/md/index.js b/lib/build-pages/page-builders/md/index.js index 10ac8981..b5318222 100644 --- a/lib/build-pages/page-builders/md/index.js +++ b/lib/build-pages/page-builders/md/index.js @@ -14,8 +14,7 @@ let md = null /** * Build all of the bundles using esbuild. - * @template {Record} T - The type of variables for the page - * @type {PageBuilderType} + * @type {PageBuilderType, string>} */ export async function mdBuilder ({ pageInfo, options }) { assert(pageInfo.type === 'md', 'md builder requires an "md" page type') diff --git a/lib/build-static/index.js b/lib/build-static/index.js index 21bf15d7..b06821e1 100644 --- a/lib/build-static/index.js +++ b/lib/build-static/index.js @@ -5,7 +5,7 @@ import { copy } from 'cpx2' import { createCopiedDomstackManifestRecords } from '../helpers/cpx2-report.js' /** - * @typedef {Awaited> | Record} StaticBuilderReport + * @typedef {object} StaticBuilderReport */ /** diff --git a/lib/builder.js b/lib/builder.js index a60be66d..882a2364 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -1,6 +1,5 @@ /** * @import {Message as EsbuildMessage} from 'esbuild' - * @import { Logger as PinoLogger } from 'pino' * @import { DomStackWarning } from './helpers/domstack-warning.js' * @import { EsBuildStepResults } from './build-esbuild/index.js' * @import { PageBuildStepResult } from './build-pages/index.js' @@ -9,6 +8,28 @@ * @import { DomstackManifest, DomstackManifestConfig, DomstackManifestRecord } from './domstack-manifest/index.js' */ +/** + * @callback DomStackLogMethod + * @param {...any} args + * @returns {void} + */ + +/** + * Structural logger contract accepted by DOMStack. + * + * Pino loggers satisfy this interface without exposing Pino's implementation + * dependencies through DOMStack's public declarations. + * + * @typedef DomStackLogger + * @property {DomStackLogMethod} trace + * @property {DomStackLogMethod} debug + * @property {DomStackLogMethod} info + * @property {DomStackLogMethod} warn + * @property {DomStackLogMethod} error + * @property {DomStackLogMethod} fatal + * @property {(bindings: Record) => DomStackLogger} child + */ + import { buildPages } from './build-pages/index.js' import { identifyPages } from './identify-pages.js' import { buildStatic } from './build-static/index.js' @@ -68,7 +89,7 @@ import { * @property {string[]|undefined} [target=[]] - Esbuild target values used for JavaScript and CSS bundling. * @property {boolean|undefined} [buildDrafts=false] - Build files marked with the `published: false` variable. * @property {string[]|undefined} [copy=[]] - Paths to copy into the dest directory. Relative paths are resolved to absolute paths from the current working directory by the DomStack constructor, matching the CLI `--copy` behavior. - * @property {PinoLogger|undefined} [logger] - Logger used for watch output and embedded sync output. + * @property {DomStackLogger|undefined} [logger] - Pino-compatible logger used for watch output and embedded sync output. */ /** diff --git a/package.json b/package.json index a3707173..3f24494c 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "test:installed-check": "installed-check --ignore-dev --no-workspaces", "test:neostandard": "eslint . --ignore-pattern 'test-cases/build-errors/src/**/*.js' --ignore-pattern 'test-cases/page-build-errors/src/**/*.js'", "test:node-test": "node --test --experimental-test-coverage --test-reporter=dot --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=lcov.info", + "test:packed-types": "node scripts/test-packed-types.js", "test:playwright": "playwright test", "test:tsc": "tsc", "build-examples": "run-p example:*", diff --git a/scripts/test-packed-types.js b/scripts/test-packed-types.js new file mode 100644 index 00000000..be5997a4 --- /dev/null +++ b/scripts/test-packed-types.js @@ -0,0 +1,117 @@ +import { execFile, spawn } from 'node:child_process' +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { promisify } from 'node:util' + +const execFileAsync = promisify(execFile) +const projectPath = path.resolve(import.meta.dirname, '..') +const temporaryPath = await mkdtemp(path.join(tmpdir(), 'domstack-packed-types-')) +const consumerPath = path.join(temporaryPath, 'consumer') + +try { + await Promise.all([ + run('npm', ['run', 'clean:declarations-top'], projectPath), + run('npm', ['run', 'clean:declarations-lib'], projectPath), + ]) + await run('npm', ['run', 'build:declaration'], projectPath) + const { stdout } = await execFileAsync( + 'npm', + ['pack', '--json', '--ignore-scripts', '--pack-destination', temporaryPath], + { cwd: projectPath, encoding: 'utf8' } + ) + const [{ filename }] = JSON.parse(stdout) + const tarballPath = path.join(temporaryPath, filename) + + await mkdir(consumerPath) + await Promise.all([ + writeFile(path.join(consumerPath, 'package.json'), `${JSON.stringify({ + name: 'domstack-packed-type-consumer', + private: true, + type: 'module', + dependencies: { + '@domstack/static': `file:${tarballPath}`, + '@types/node': '^26.0.1', + 'typescript-5': 'npm:typescript@~5.9.0', + 'typescript-6': 'npm:typescript@~6.0.0', + }, + }, null, 2)}\n`), + writeFile(path.join(consumerPath, 'index.ts'), `import { DomStack, PageData } from '@domstack/static' +import type { + DomStackLogger, + DomStackOpts, + PageFunction, + Results, +} from '@domstack/static/types.js' + +const logger: DomStackLogger = { + trace () {}, + debug () {}, + info () {}, + warn () {}, + error () {}, + fatal () {}, + child () { return logger }, +} +const options: DomStackOpts = { buildDrafts: true, logger } +const render: PageFunction, string> = ({ vars }) => String(vars) +const stack = new DomStack('src', 'public', options) + +void PageData +void render +void stack +void ({} as Results) +`), + writeFile(path.join(consumerPath, 'tsconfig.json'), `${JSON.stringify({ + compilerOptions: { + module: 'NodeNext', + moduleResolution: 'NodeNext', + noEmit: true, + skipLibCheck: false, + strict: true, + target: 'ES2022', + types: ['node'], + }, + include: ['index.ts'], + }, null, 2)}\n`), + ]) + + await run( + 'npm', + ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock'], + consumerPath + ) + for (const version of ['typescript-5', 'typescript-6']) { + await run( + process.execPath, + [path.join(consumerPath, 'node_modules', version, 'bin', 'tsc')], + consumerPath + ) + } +} finally { + await rm(temporaryPath, { recursive: true, force: true }) + await Promise.all([ + run('npm', ['run', 'clean:declarations-top'], projectPath), + run('npm', ['run', 'clean:declarations-lib'], projectPath), + ]) +} + +/** + * @param {string} command + * @param {string[]} args + * @param {string} cwd + * @returns {Promise} + */ +function run (command, args, cwd) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { cwd, stdio: 'inherit' }) + child.once('error', reject) + child.once('exit', (code, signal) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`${command} exited with ${signal ?? code}`)) + } + }) + }) +} diff --git a/types.ts b/types.ts index cbd3c5da..772e6424 100644 --- a/types.ts +++ b/types.ts @@ -6,7 +6,7 @@ import type { Results } from './lib/builder.js' export type { DataDeps } from './lib/build-pages/data-deps.js' export type { BuildOptions } from 'esbuild' -export type { DomStackOpts, Results, SiteData } from './lib/builder.js' +export type { DomStackLogger, DomStackOpts, Results, SiteData } from './lib/builder.js' export type { AsyncGlobalDataFunction, GeneratedPageDefinition, From 06d774a4c89b31dcdefc0610971b8f94be091226 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Mon, 7 Sep 2026 17:05:33 -0700 Subject: [PATCH 2/3] Preserve dependency types with cpx2 fix and thread-stream compatibility --- index.js | 4 +- lib/build-copy/index.js | 2 +- lib/build-static/index.js | 2 +- lib/builder.js | 25 +----------- package.json | 3 +- scripts/test-packed-types.js | 76 ++++++++++++++++++++++++++---------- types.ts | 4 +- types/thread-stream.d.ts | 20 ++++++++++ 8 files changed, 88 insertions(+), 48 deletions(-) create mode 100644 types/thread-stream.d.ts diff --git a/index.js b/index.js index 59078b25..9ae2a2c1 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ +/// + /** * @import { DomStackOpts, Results, SiteData } from './lib/builder.js' * @import { Stats } from 'node:fs' @@ -137,7 +139,7 @@ export class DomStack { this.#src = src this.#dest = dest - this.#logger = /** @type {PinoLogger} */ (opts.logger ?? createDomStackLogger()) + this.#logger = opts.logger ?? createDomStackLogger() this.opts = normalizeDomStackOpts(opts, dest) const copyDirs = this.opts.copy ?? [] diff --git a/lib/build-copy/index.js b/lib/build-copy/index.js index 82b8130a..a213eca0 100644 --- a/lib/build-copy/index.js +++ b/lib/build-copy/index.js @@ -7,7 +7,7 @@ import { join } from 'node:path' import { createCopiedDomstackManifestRecords } from '../helpers/cpx2-report.js' /** - * @typedef {Record} CopyBuilderReport + * @typedef {Record>>} CopyBuilderReport * @typedef {BuildStepResult<'copy', CopyBuilderReport>} CopyBuildStepResult * @typedef {BuildStep<'copy', CopyBuilderReport>} CopyBuildStep */ diff --git a/lib/build-static/index.js b/lib/build-static/index.js index b06821e1..21bf15d7 100644 --- a/lib/build-static/index.js +++ b/lib/build-static/index.js @@ -5,7 +5,7 @@ import { copy } from 'cpx2' import { createCopiedDomstackManifestRecords } from '../helpers/cpx2-report.js' /** - * @typedef {object} StaticBuilderReport + * @typedef {Awaited> | Record} StaticBuilderReport */ /** diff --git a/lib/builder.js b/lib/builder.js index 882a2364..238ba455 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -1,5 +1,6 @@ /** * @import {Message as EsbuildMessage} from 'esbuild' + * @import { Logger as PinoLogger } from 'pino' * @import { DomStackWarning } from './helpers/domstack-warning.js' * @import { EsBuildStepResults } from './build-esbuild/index.js' * @import { PageBuildStepResult } from './build-pages/index.js' @@ -8,28 +9,6 @@ * @import { DomstackManifest, DomstackManifestConfig, DomstackManifestRecord } from './domstack-manifest/index.js' */ -/** - * @callback DomStackLogMethod - * @param {...any} args - * @returns {void} - */ - -/** - * Structural logger contract accepted by DOMStack. - * - * Pino loggers satisfy this interface without exposing Pino's implementation - * dependencies through DOMStack's public declarations. - * - * @typedef DomStackLogger - * @property {DomStackLogMethod} trace - * @property {DomStackLogMethod} debug - * @property {DomStackLogMethod} info - * @property {DomStackLogMethod} warn - * @property {DomStackLogMethod} error - * @property {DomStackLogMethod} fatal - * @property {(bindings: Record) => DomStackLogger} child - */ - import { buildPages } from './build-pages/index.js' import { identifyPages } from './identify-pages.js' import { buildStatic } from './build-static/index.js' @@ -89,7 +68,7 @@ import { * @property {string[]|undefined} [target=[]] - Esbuild target values used for JavaScript and CSS bundling. * @property {boolean|undefined} [buildDrafts=false] - Build files marked with the `published: false` variable. * @property {string[]|undefined} [copy=[]] - Paths to copy into the dest directory. Relative paths are resolved to absolute paths from the current working directory by the DomStack constructor, matching the CLI `--copy` behavior. - * @property {DomStackLogger|undefined} [logger] - Pino-compatible logger used for watch output and embedded sync output. + * @property {PinoLogger|undefined} [logger] - Pino logger instance used for watch output and embedded sync output. */ /** diff --git a/package.json b/package.json index 3f24494c..e7e308bf 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "page.vars.js", "scripts/domstack-manifest-schema.js", "style.css", + "types/thread-stream.d.ts", "types.d.ts", "types.d.ts.map" ], @@ -53,7 +54,7 @@ "async-folder-walker": "^3.0.5", "chokidar": "^5.0.0", "clean-deep": "^3.4.0", - "cpx2": "^9.0.0", + "cpx2": "^9.0.1", "esbuild": "^0.28.1", "fragtml": "^0.0.9", "handlebars": "^4.7.8", diff --git a/scripts/test-packed-types.js b/scripts/test-packed-types.js index be5997a4..84e81314 100644 --- a/scripts/test-packed-types.js +++ b/scripts/test-packed-types.js @@ -1,11 +1,12 @@ import { execFile, spawn } from 'node:child_process' -import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' import { promisify } from 'node:util' const execFileAsync = promisify(execFile) const projectPath = path.resolve(import.meta.dirname, '..') +const { dependencies } = JSON.parse(await readFile(path.join(projectPath, 'package.json'), 'utf8')) const temporaryPath = await mkdtemp(path.join(tmpdir(), 'domstack-packed-types-')) const consumerPath = path.join(temporaryPath, 'consumer') @@ -32,34 +33,51 @@ try { dependencies: { '@domstack/static': `file:${tarballPath}`, '@types/node': '^26.0.1', + pino: dependencies.pino, 'typescript-5': 'npm:typescript@~5.9.0', 'typescript-6': 'npm:typescript@~6.0.0', + 'typescript-7': 'npm:typescript@~7.0.0', }, }, null, 2)}\n`), writeFile(path.join(consumerPath, 'index.ts'), `import { DomStack, PageData } from '@domstack/static' +import pino from 'pino' + +const stack = new DomStack('src', 'public', { logger: pino({ level: 'silent' }) }) +void PageData +void stack +`), + writeFile(path.join(consumerPath, 'types.ts'), `import pino from 'pino' +import type { WorkerOptions } from 'node:worker_threads' import type { - DomStackLogger, DomStackOpts, PageFunction, Results, } from '@domstack/static/types.js' -const logger: DomStackLogger = { - trace () {}, - debug () {}, - info () {}, - warn () {}, - error () {}, - fatal () {}, - child () { return logger }, -} +const logger = pino({ level: 'silent' }) const options: DomStackOpts = { buildDrafts: true, logger } const render: PageFunction, string> = ({ vars }) => String(vars) -const stack = new DomStack('src', 'public', options) -void PageData +// The logger option retains Pino's full contract. +const configuredLogger: pino.Logger | undefined = options.logger +const childOptions: DomStackOpts = { logger: logger.child({ component: 'consumer' }) } +// @ts-expect-error A generic logging object is not a Pino logger. +const invalidOptions: DomStackOpts = { logger: { info () {} } } + +// The compatibility alias preserves the transport's transfer-list type, not any. +type TransferItem = NonNullable['emit']>[2]>[number] +declare const item: TransferItem +const expected: NonNullable[number] = item +const actual: TransferItem = expected +// @ts-expect-error A primitive is not transferable. +const invalidTransfer: TransferItem = 123 + void render -void stack +void configuredLogger +void childOptions +void invalidOptions +void actual +void invalidTransfer void ({} as Results) `), writeFile(path.join(consumerPath, 'tsconfig.json'), `${JSON.stringify({ @@ -74,6 +92,10 @@ void ({} as Results) }, include: ['index.ts'], }, null, 2)}\n`), + writeFile(path.join(consumerPath, 'tsconfig-types.json'), `${JSON.stringify({ + extends: './tsconfig.json', + include: ['types.ts'], + }, null, 2)}\n`), ]) await run( @@ -81,12 +103,26 @@ void ({} as Results) ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock'], consumerPath ) - for (const version of ['typescript-5', 'typescript-6']) { - await run( - process.execPath, - [path.join(consumerPath, 'node_modules', version, 'bin', 'tsc')], - consumerPath - ) + // Node 26 exercises the missing alias; 24 and 22 guard against duplicate + // declarations on supported older releases. Check each entry in isolation. + for (const nodeVersion of [26, 24, 22]) { + if (nodeVersion !== 26) { + await run( + 'npm', + ['install', `@types/node@^${nodeVersion}.0.0`, '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock'], + consumerPath + ) + } + for (const version of ['typescript-5', 'typescript-6', 'typescript-7']) { + for (const config of ['tsconfig.json', 'tsconfig-types.json']) { + console.log(`Checking ${version}, @types/node ${nodeVersion}, ${config}`) + await run( + process.execPath, + [path.join(consumerPath, 'node_modules', version, 'bin', 'tsc'), '--project', config], + consumerPath + ) + } + } } } finally { await rm(temporaryPath, { recursive: true, force: true }) diff --git a/types.ts b/types.ts index 772e6424..da8a48e1 100644 --- a/types.ts +++ b/types.ts @@ -1,3 +1,5 @@ +/// + // Type-only public entry for `import type { ... } from '@domstack/static/types.js'`. // There is intentionally no runtime `types.js` today; this source emits `types.d.ts`, // and `types.js` is reserved for a future runtime/type companion entry if needed. @@ -6,7 +8,7 @@ import type { Results } from './lib/builder.js' export type { DataDeps } from './lib/build-pages/data-deps.js' export type { BuildOptions } from 'esbuild' -export type { DomStackLogger, DomStackOpts, Results, SiteData } from './lib/builder.js' +export type { DomStackOpts, Results, SiteData } from './lib/builder.js' export type { AsyncGlobalDataFunction, GeneratedPageDefinition, diff --git a/types/thread-stream.d.ts b/types/thread-stream.d.ts new file mode 100644 index 00000000..9fd1b2fd --- /dev/null +++ b/types/thread-stream.d.ts @@ -0,0 +1,20 @@ +import type { WorkerOptions } from 'node:worker_threads' + +/** + * Temporary compatibility for thread-stream <=4.2.0, which still references + * worker_threads.TransferListItem after @types/node 26 removed that alias. + * Upstream fix: https://github.com/pinojs/thread-stream/pull/233 + * + * Remove this file, its public entry-point references, and its package files + * entry once the supported Pino dependency tree requires the upstream fix. + */ +declare namespace ThreadStreamCompat { + type TransferListItem = NonNullable[number] +} + +declare module 'worker_threads' { + // An import alias keeps the duplicate-name diagnostic here, rather than in + // Node's declarations. The packed tests check old and new Node declarations. + // @ts-ignore Node <=25 already exports this equivalent alias. + export import TransferListItem = ThreadStreamCompat.TransferListItem +} From 5708bd778fc6d613ceb41ec7fa9b10b2ce8a1caf Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Mon, 7 Sep 2026 17:10:29 -0700 Subject: [PATCH 3/3] Use configured TypeScript version for packed consumer checks --- scripts/test-packed-types.js | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/scripts/test-packed-types.js b/scripts/test-packed-types.js index 84e81314..b157dbc6 100644 --- a/scripts/test-packed-types.js +++ b/scripts/test-packed-types.js @@ -6,7 +6,7 @@ import { promisify } from 'node:util' const execFileAsync = promisify(execFile) const projectPath = path.resolve(import.meta.dirname, '..') -const { dependencies } = JSON.parse(await readFile(path.join(projectPath, 'package.json'), 'utf8')) +const { dependencies, devDependencies } = JSON.parse(await readFile(path.join(projectPath, 'package.json'), 'utf8')) const temporaryPath = await mkdtemp(path.join(tmpdir(), 'domstack-packed-types-')) const consumerPath = path.join(temporaryPath, 'consumer') @@ -34,9 +34,7 @@ try { '@domstack/static': `file:${tarballPath}`, '@types/node': '^26.0.1', pino: dependencies.pino, - 'typescript-5': 'npm:typescript@~5.9.0', - 'typescript-6': 'npm:typescript@~6.0.0', - 'typescript-7': 'npm:typescript@~7.0.0', + typescript: devDependencies.typescript, }, }, null, 2)}\n`), writeFile(path.join(consumerPath, 'index.ts'), `import { DomStack, PageData } from '@domstack/static' @@ -113,15 +111,13 @@ void ({} as Results) consumerPath ) } - for (const version of ['typescript-5', 'typescript-6', 'typescript-7']) { - for (const config of ['tsconfig.json', 'tsconfig-types.json']) { - console.log(`Checking ${version}, @types/node ${nodeVersion}, ${config}`) - await run( - process.execPath, - [path.join(consumerPath, 'node_modules', version, 'bin', 'tsc'), '--project', config], - consumerPath - ) - } + for (const config of ['tsconfig.json', 'tsconfig-types.json']) { + console.log(`Checking TypeScript ${devDependencies.typescript}, @types/node ${nodeVersion}, ${config}`) + await run( + process.execPath, + [path.join(consumerPath, 'node_modules', 'typescript', 'bin', 'tsc'), '--project', config], + consumerPath + ) } } } finally {