Files
zeekay 5095f79659 0.5.17 — a CommonJS consumer can require the package
The package is `type: module` and its exports map offered only `import`, so
every `.js` it ships is ESM and a `require()` reaches a syntax error inside
`dist/index.js` rather than a module. Plain Node hides that now that
`require(esm)` works; jest loads a dependency as CommonJS and does not, so a
jsdom test importing anything downstream of this package failed on our line.

A second `tsc` pass emits CommonJS to `dist-cjs`, `scripts/cjs.mjs` retargets
the sibling specifiers and folds it into `dist` as `.cjs`, and the exports map
names it under `require`. Same two-pass shape @hanzo/ui already builds with.
`main` follows, since that field is read by the resolvers that predate
`exports` and those are the CommonJS ones.

The dist check now loads BOTH artifacts and compares their export names to
each other, so the two formats cannot drift and neither can go missing.
Tokens, CSS and the ESM entry are untouched — all 23 exports are identical to
0.5.16.
2026-08-21 07:58:06 -07:00

102 lines
4.8 KiB
JavaScript

/**
* Import the BUILT package the way a consumer does.
*
* Everything else in this repo checks the sources: the token files, the
* generator's output, the preference contract. All of it passed while
* `dist/index.js` was unimportable — 0.4.10 and 0.4.11 both shipped a barrel
* that re-exported `'./preference'` with no extension, which TypeScript emits
* verbatim and Node ESM refuses to resolve. `pnpm build` was green, `pnpm test`
* was green, and `import '@hanzo/design'` threw ERR_MODULE_NOT_FOUND for every
* consumer.
*
* A test that reads source cannot see that. This one loads the artifact.
*/
import { readFileSync, existsSync } from 'node:fs'
import { createRequire } from 'node:module'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { dirname, join } from 'node:path'
const here = dirname(fileURLToPath(import.meta.url))
const root = join(here, '..')
let failed = 0
const check = async (name, fn) => {
try {
await fn()
console.log(` ok ${name}`)
} catch (e) {
failed++
console.error(` FAIL ${name}\n ${e.message}`)
}
}
console.log('dist:')
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
await check('every export the package advertises actually exists on disk', () => {
const missing = []
for (const [name, entry] of Object.entries(pkg.exports ?? {})) {
for (const target of typeof entry === 'string' ? [entry] : Object.values(entry)) {
if (typeof target !== 'string' || target.includes('*')) continue
if (!existsSync(join(root, target))) missing.push(`${name} -> ${target}`)
}
}
if (missing.length) throw new Error(`exports pointing at nothing:\n ${missing.join('\n ')}`)
})
await check('the barrel IMPORTS — the check that 0.4.10 and 0.4.11 needed', async () => {
const entry = join(root, 'dist/index.js')
if (!existsSync(entry)) throw new Error('dist/index.js is missing — run the build first')
const mod = await import(pathToFileURL(entry).href)
// The preference API is the part that was unreachable; naming it here means a
// future re-export losing its extension fails by name rather than by silence.
for (const name of ['vars', 'css', 'isColor', 'TYPE_MIN', 'TYPE_MAX', 'cssVar', 'tokenValue']) {
if (mod[name] === undefined) throw new Error(`dist/index.js does not export ${name}`)
}
if (typeof mod.vars !== 'function') throw new Error('vars is not callable from the built package')
const v = mod.vars({ type: 1.2, density: 'compact' })
if (v['--type-scale'] !== '1.2') throw new Error(`built vars() returned ${JSON.stringify(v)}`)
})
await check('the barrel REQUIRES, with the same names it imports', async () => {
// `type: module` makes every `.js` here ESM, so a CommonJS consumer cannot read
// the barrel at all — it parses `export` as a syntax error and reports the line
// in OUR file. Plain Node hides this now that `require(esm)` works, but jest
// still loads a dependency as CommonJS, so `require('@hanzo/design')` in a
// jsdom test died at `dist/index.js:12` through 0.5.16.
//
// Same names from both formats, checked against each other rather than a list:
// a list is a third copy that goes stale the next time an export is added.
const esm = await import(pathToFileURL(join(root, 'dist/index.js')).href)
const cjs = createRequire(import.meta.url)(join(root, 'dist/index.cjs'))
const names = (m) => Object.keys(m).filter((k) => k !== 'default' && k !== '__esModule').sort()
const missing = names(esm).filter((n) => !(n in cjs))
if (missing.length) throw new Error(`dist/index.cjs is missing ${missing.join(', ')}`)
if (typeof cjs.vars !== 'function') throw new Error('vars is not callable from the CommonJS build')
if (cjs.vars({ type: 1.2, density: 'compact' })['--type-scale'] !== '1.2')
throw new Error('the CommonJS build computes a different preference than the ESM one')
})
await check('no emitted module uses an extensionless relative specifier', () => {
// The class of bug, not just the one instance: Node ESM resolves relative
// specifiers literally, so a missing `.js` is always a runtime failure.
const bad = []
for (const [, entry] of Object.entries(pkg.exports ?? {})) {
const target = typeof entry === 'string' ? entry : entry.import
if (typeof target !== 'string' || target.includes('*') || !target.endsWith('.js')) continue
const file = join(root, target)
if (!existsSync(file)) continue
for (const m of readFileSync(file, 'utf8').matchAll(/from\s+['"](\.[^'"]*)['"]/g)) {
if (!/\.(js|mjs|cjs|json|css)$/.test(m[1])) bad.push(`${target}: ${m[1]}`)
}
}
if (bad.length) throw new Error(`extensionless relative imports:\n ${bad.join('\n ')}`)
})
if (failed) {
console.error(`\n${failed} dist check(s) failed`)
process.exit(1)
}
console.log('dist: all checks passed')