Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// <reference path="./types/thread-stream.d.ts" preserve="true" />

/**
* @import { DomStackOpts, Results, SiteData } from './lib/builder.js'
* @import { Stats } from 'node:fs'
Expand Down
3 changes: 1 addition & 2 deletions lib/build-pages/page-builders/html/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import Handlebars from 'handlebars'

/**
* Build all of the bundles using esbuild.
* @template {Record<string, any>} T - The type of variables for the page
* @type {PageBuilderType<T, string>}
* @type {PageBuilderType<Record<string, any>, string>}
*/
export async function htmlBuilder ({ pageInfo }) {
assert(pageInfo.type === 'html', 'html builder requires a "html" page type')
Expand Down
3 changes: 1 addition & 2 deletions lib/build-pages/page-builders/md/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ let md = null

/**
* Build all of the bundles using esbuild.
* @template {Record<string, any>} T - The type of variables for the page
* @type {PageBuilderType<T, string>}
* @type {PageBuilderType<Record<string, any>, string>}
*/
export async function mdBuilder ({ pageInfo, options }) {
assert(pageInfo.type === 'md', 'md builder requires an "md" page type')
Expand Down
2 changes: 1 addition & 1 deletion lib/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

/**
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand All @@ -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",
Expand Down Expand Up @@ -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:*",
Expand Down
149 changes: 149 additions & 0 deletions scripts/test-packed-types.js
Original file line number Diff line number Diff line change
@@ -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<Record<string, unknown>, 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<Parameters<ReturnType<typeof pino.transport>['emit']>[2]>[number]
declare const item: TransferItem
const expected: NonNullable<WorkerOptions['transferList']>[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<void>}
*/
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}`))
}
})
})
}
2 changes: 2 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// <reference path="./types/thread-stream.d.ts" preserve="true" />

// 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.
Expand Down
20 changes: 20 additions & 0 deletions types/thread-stream.d.ts
Original file line number Diff line number Diff line change
@@ -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<WorkerOptions['transferList']>[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
}