diff --git a/index.js b/index.js
index 9bae2d6f..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'
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/builder.js b/lib/builder.js
index a60be66d..238ba455 100644
--- a/lib/builder.js
+++ b/lib/builder.js
@@ -68,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 {PinoLogger|undefined} [logger] - 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 a3707173..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",
@@ -120,6 +121,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..b157dbc6
--- /dev/null
+++ b/scripts/test-packed-types.js
@@ -0,0 +1,149 @@
+import { execFile, spawn } from 'node:child_process'
+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, 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')
+
+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',
+ pino: dependencies.pino,
+ typescript: devDependencies.typescript,
+ },
+ }, 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 {
+ DomStackOpts,
+ PageFunction,
+ Results,
+} from '@domstack/static/types.js'
+
+const logger = pino({ level: 'silent' })
+const options: DomStackOpts = { buildDrafts: true, logger }
+const render: PageFunction, string> = ({ vars }) => String(vars)
+
+// 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 configuredLogger
+void childOptions
+void invalidOptions
+void actual
+void invalidTransfer
+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`),
+ writeFile(path.join(consumerPath, 'tsconfig-types.json'), `${JSON.stringify({
+ extends: './tsconfig.json',
+ include: ['types.ts'],
+ }, null, 2)}\n`),
+ ])
+
+ await run(
+ 'npm',
+ ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock'],
+ 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 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 {
+ 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..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.
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
+}