Files
Robert Fleischmann 7ddfe1dddb ci: verify the packed artifact from a consumer's point of view
Three defects shipped this cycle that neither `npm run build` nor
`npm run typecheck` could see, because all three only exist from outside the
package: exported types silently degrading to `any` under skipLibCheck when
the declarations carried extensionless relative imports, the ESM default
import resolving to the module namespace instead of the class, and callbacks
being bound at runtime but not typed that way.

scripts/verify-package.sh packs the package, installs the tarball into a
throwaway project, and typechecks a real consumer against it under node16,
nodenext and bundler resolution with skipLibCheck deliberately off, then
smoke-tests require(), import, and callback binding at runtime. The fixtures
in scripts/package-fixtures include the Readme's gradient example verbatim,
so the documented API cannot drift from the shipped types.

Verified against both historical failures: reverting the `this` typing or the
.js import extensions makes it fail.

Runs as its own CI job, once rather than per Node version, since it checks the
artifact and not the runtime.
2026-07-22 12:07:45 -04:00

101 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Verify the published artifact rather than the source tree.
#
# Packs the package, installs the tarball into a throwaway project, then
# typechecks a real consumer against it under every module resolution mode with
# skipLibCheck OFF, and smoke-tests both require() and import at runtime.
#
# This exists because three separate defects — exported types silently
# degrading to `any`, the ESM default import resolving to a namespace, and
# callbacks not being typed as bound — were all invisible to `npm run build`
# and `npm run typecheck`, and only appeared from a consumer's point of view.
#
# Usage: npm run verify:package (assumes `npm run build` has already run)
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
cd "$ROOT"
TARBALL="$(npm pack --pack-destination "$WORK" --silent | tail -n 1)"
echo "packed $TARBALL"
cd "$WORK"
printf '{"name":"consumer","version":"1.0.0","private":true}\n' > package.json
cp "$ROOT"/scripts/package-fixtures/use.cts .
cp "$ROOT"/scripts/package-fixtures/use.mts .
# install the tarball, and typescript from the package's own devDependency
TS_VERSION="$(node -p "require('$ROOT/package.json').devDependencies.typescript")"
npm install --no-audit --no-fund --silent "$WORK/$TARBALL" "typescript@$TS_VERSION"
status=0
# skipLibCheck is deliberately OFF: with it on, unresolvable declarations fail
# silently and every exported type degrades to `any`.
for mode in node16 nodenext; do
echo "--- typecheck: module=$mode moduleResolution=$mode (skipLibCheck off)"
if ! npx tsc --noEmit --strict --module "$mode" --moduleResolution "$mode" \
--lib ES2022,DOM use.cts use.mts; then
status=1
fi
done
echo "--- typecheck: moduleResolution=bundler (skipLibCheck off)"
if ! npx tsc --noEmit --strict --module esnext --moduleResolution bundler \
--lib ES2022,DOM use.mts; then
status=1
fi
echo "--- runtime: require()"
node -e "
const assert = require('assert');
const EPC = require('easy-pie-chart');
assert.strictEqual(typeof EPC, 'function', 'CJS export should be the constructor');
assert.strictEqual(EPC.name, 'EasyPieChart');
for (const k of ['EasyPieChart', 'CanvasRenderer', 'defaultEasing', 'defaultOptions']) {
assert.ok(k in EPC, 'missing static: ' + k);
}
assert.strictEqual(typeof require('easy-pie-chart/jquery'), 'function');
console.log(' ok');
" || status=1
echo "--- runtime: import"
node --input-type=module -e "
import assert from 'assert';
import EPC, { EasyPieChart, defaultOptions } from 'easy-pie-chart';
import { registerJQueryPlugin } from 'easy-pie-chart/jquery';
assert.strictEqual(EPC, EasyPieChart, 'default export should be the class');
assert.strictEqual(typeof registerJQueryPlugin, 'function');
assert.strictEqual(defaultOptions.size, 110);
console.log(' ok');
" || status=1
echo "--- runtime: callbacks are bound to the chart"
node --input-type=module -e "
import assert from 'assert';
import { EasyPieChart } from 'easy-pie-chart';
globalThis.window = { devicePixelRatio: 1 };
const ctx = new Proxy({}, { get: () => () => ({ data: [] }) });
globalThis.document = { createElement: () => ({ getContext: () => ctx, style: {}, parentNode: null }) };
globalThis.requestAnimationFrame = () => 1;
globalThis.cancelAnimationFrame = () => {};
const el = { appendChild() {}, dataset: {}, removeChild() {} };
let seen;
new EasyPieChart(el, { animate: false, barColor() { seen = this.el; return '#000'; } }).update(10);
assert.strictEqual(seen, el, 'this.el inside barColor should be the host element');
console.log(' ok');
" || status=1
if [ "$status" -ne 0 ]; then
echo
echo "package verification FAILED — the published artifact would be broken for consumers"
exit 1
fi
echo
echo "package verification passed"