package: CommonJS projects get their own declarations (dist/types-cjs; node16 / nodenext refused an ESM-only .d.ts from a CJS file, TS1541); the package-types test covers ES module and CommonJS projects under bundler / node16 / nodenext / node10 and loads the package with import() and require() under Node; pitfall 69

This commit is contained in:
CRTkafa 2026-10-15 06:01:33 +08:00
parent d717ceb4cd
commit 04647c079e
6 changed files with 42 additions and 14 deletions

View file

@ -3,10 +3,10 @@
## Unreleased
- **`import type { ShipConfig } from 'storebound'` type-checks outside Bun.** The package ships declarations
(`dist/types`, the "types" condition) and a JS entry for Node; Bun keeps the .ts source. 0.8.5 named only
for ES module and CommonJS projects (`dist/types`, `dist/types-cjs`) and a JS entry for Node; Bun keeps the .ts source. 0.8.5 named only
`cli/lib/config.ts`, so tsc (`next build`, vue-tsc, ts-loader, `tsc --noEmit`) failed on a scaffolded
storebound.config.ts and a working Next app stopped building. A test packs the entry and type-checks the template
under bundler, node16 and nodenext with no @types/node and skipLibCheck off.
in both project types under bundler, node16, nodenext and node10 with no @types/node and skipLibCheck off, and loads it with import() and require() under Node. Pitfall 69.
- **`check` fails on the scaffolder's own placeholders**: "2026 Example Studio" and example.com / .org / .net URLs and
addresses in `app`, `accounts` and `reviewAccess` would reach the stores as the app's copyright, contact and legal
pages.

View file

@ -9,6 +9,7 @@ fails without the fix.
| # | date | lesson | guard |
|---|---|---|---|
| 69 | 2026-10 | A web app: `bun add -d storebound` plus the scaffolded `storebound.config.ts` broke `next build`: the package exported only its TypeScript source and shipped no declarations; Bun imports .ts, so every storebound test passed, but tsc never takes a package's .ts source as its types. | The package ships declarations for ES module (`dist/types`) and CommonJS projects (`dist/types-cjs`) and a Node entry (`dist/config.js`); Bun keeps the source (`buildEntry`, `pruneDeclarations` in `tools/build-node.ts`, run by prepack). `tests/package-types.test.ts` type-checks the template against the packed entry under bundler / node16 / nodenext / node10 in both project types with no @types/node and skipLibCheck off, and loads the package with `import()` and `require()` under Node. `check` fails on the template's own placeholders (Example Studio, bare example.com). |
| 68 | 2026-10 | A headless Chrome started by storebound's console automation (a test run's `--user-data-dir=$TMPDIR/storebound-profile-apple-…`, `--remote-debugging-port=0 --headless=new`) outlived its parent for ~20 hours as an orphan (parent PID 1). macOS LaunchServices took it for the running "Google Chrome", so the Dock's Chrome only activated the windowless instance and the owner's own Chrome would not open. A Chrome started with a DevTools port does not notice its parent dying. | `cli/console/chrome-guard.ts`: every Chrome storebound starts itself is ended on process exit, closed gracefully on SIGINT / SIGTERM / SIGHUP, and ended by a detached watchdog when the parent dies (SIGKILL included; a reused pid is never killed); every start first ends headless orphans on storebound's profiles and names orphaned windows; headless runs on macOS prefer Chromium / Chrome for Testing over Google Chrome.app; `console fill` closes the window on an error too (`tests/chrome-guard.test.ts`). |
| 67 | 2026-10 | A checkout without `.entitlements` was reported as "builds ship without push / Game Center" while the uploaded build had both (native changes lived on a release branch). | The comparison warns only about this checkout ("built from another branch?") and names the branches / worktrees that have an `.entitlements` file; with a built IPA it reads that build's real entitlements ("uploaded build N"); without one it says the App Store Connect API does not expose them. `checkoutInfo`, `builtIpaEntitlements` (cli/apple/signing.ts), `machoEntitlements`, `compareCapabilities` (cli/lib/signing.ts), tests/signing-capabilities.test.ts. |
| 66 | 2026-10 | `storebound api apple ... --body f.json --yes` only dry-ran from an agent. | By design writes through `storebound api` need an interactive terminal; the message names the dedicated command (`apple signing ensure`, `apple iap`, `apple listing`, ...), which runs anywhere with its own dry run and read-back. `dedicatedCommand` (cli/api/gate.ts), tests/signing-capabilities.test.ts. |

View file

@ -29,15 +29,16 @@
},
"exports": {
".": {
"types": "./dist/types/lib/config.d.ts",
"bun": "./cli/lib/config.ts",
"default": "./dist/config.js"
"import": { "types": "./dist/types/lib/config.d.ts", "default": "./dist/config.js" },
"require": { "types": "./dist/types-cjs/lib/config.d.ts", "default": "./dist/config.js" }
}
},
"files": [
"dist/storebound.mjs",
"dist/config.js",
"dist/types",
"dist/types-cjs",
"cli",
"templates",
"skills",

View file

@ -794,3 +794,15 @@ that fails without the fix (see CONTRIBUTING.md).
SIGTERM / SIGHUP and the parent's death (SIGKILL included) end the Chrome; `console fill` closes it on errors too;
headless runs on macOS prefer Chromium / Chrome for Testing (its own bundle id) over Google Chrome.app
(`persistentExecutable`); `tests/chrome-guard.test.ts`.
## 69. `bun add -d storebound` broke a Next app's build
- **Symptom:** `next build` (or vue-tsc, ts-loader, `tsc --noEmit`) fails on the scaffolded `storebound.config.ts`:
`Cannot find module 'storebound' or its corresponding type declarations`.
- **Cause:** storebound <= 0.8.5 exported only its TypeScript source (`cli/lib/config.ts`) and shipped no `.d.ts`.
Bun imports .ts, so everything storebound tested passed; TypeScript never takes a package's .ts source as its types.
- **Do:** update to 0.8.6+ (declarations for ES module and CommonJS projects). Remove any tsconfig `exclude` of
storebound.config.ts that was added as a workaround.
- **Guard:** `buildEntry` / `pruneDeclarations` (tools/build-node.ts, run by prepack) and `tests/package-types.test.ts`:
the template config type-checks against the packed entry in ES module and CommonJS projects under bundler,
node16, nodenext and node10 with no @types/node and skipLibCheck off; `import()` and `require()` of the package
load under Node.

View file

@ -22,17 +22,25 @@ describe('package types', () => {
mkdirSync(sb, { recursive: true });
writeFileSync(join(sb, 'package.json'), JSON.stringify(pkg));
cpSync(dist, join(sb, 'dist'), { recursive: true });
expect(pkg.files).toEqual(expect.arrayContaining(['dist/config.js', 'dist/types']));
expect(pkg.exports['.'].types).toBe('./dist/types/lib/config.d.ts');
expect(pkg.files).toEqual(expect.arrayContaining(['dist/config.js', 'dist/types', 'dist/types-cjs']));
symlinkSync(join(ROOT, 'node_modules', 'typescript'), join(dir, 'app', 'node_modules', 'typescript'));
cpSync(join(ROOT, 'templates', 'storebound.config.ts'), join(dir, 'app', 'storebound.config.ts'));
// an ES module project (node16 / nodenext read the nearest package.json; Next / Vite use bundler resolution)
writeFileSync(join(dir, 'app', 'package.json'), JSON.stringify({ name: 'app', private: true, type: 'module' }));
for (const [module, moduleResolution] of [['esnext', 'bundler'], ['node16', 'node16'], ['nodenext', 'nodenext']]) {
// no @types/node, skipLibCheck off: the strictest project a config can land in
writeFileSync(join(dir, 'app', 'tsconfig.json'), JSON.stringify({ compilerOptions: { strict: true, noEmit: true, target: 'es2022', module, moduleResolution, skipLibCheck: false, types: [] }, include: ['*.ts'] }));
const r = Bun.spawnSync(['node', TSC, '-p', '.'], { cwd: join(dir, 'app'), stdout: 'pipe', stderr: 'pipe' });
expect(`${moduleResolution}: ${r.stdout.toString()}${r.stderr.toString()}`.trim()).toBe(`${moduleResolution}:`);
// every project a config lands in: ES module and CommonJS (node16 / nodenext read the nearest package.json),
// Next / Vite / Expo (bundler), old tsconfigs (node10 reads "types"); no @types/node, skipLibCheck off
const results: string[] = [];
for (const type of ['module', 'commonjs']) {
writeFileSync(join(dir, 'app', 'package.json'), JSON.stringify({ name: 'app', private: true, type }));
for (const [module, moduleResolution] of [['esnext', 'bundler'], ['preserve', 'bundler'], ['node16', 'node16'], ['nodenext', 'nodenext'], ['commonjs', 'node10']]) {
writeFileSync(join(dir, 'app', 'tsconfig.json'), JSON.stringify({ compilerOptions: { strict: true, noEmit: true, target: 'es2022', module, moduleResolution, skipLibCheck: false, types: [] }, include: ['*.ts'] }));
const r = Bun.spawnSync(['node', TSC, '-p', '.'], { cwd: join(dir, 'app'), stdout: 'pipe', stderr: 'pipe' });
results.push(`${type} ${module}/${moduleResolution}: ${`${r.stdout.toString()}${r.stderr.toString()}`.trim() || 'ok'}`);
}
}
expect(results.filter(r => !r.endsWith(': ok'))).toEqual([]);
// the runtime entry under Node: import() and require() of 'storebound' load (no TypeScript source, no Bun)
for (const code of [`import('storebound').then(m => console.log(typeof m.validateConfig))`, `console.log(typeof require('storebound').validateConfig)`]) {
const r = Bun.spawnSync(['node', '-e', code], { cwd: join(dir, 'app'), stdout: 'pipe', stderr: 'pipe' });
expect(`${r.stdout.toString().trim()}${r.stderr.toString().split('\n')[0]}`).toBe('function');
}
} finally { rmSync(dir, { recursive: true, force: true }); }
}, 120_000);

View file

@ -5,7 +5,7 @@
// bun tools/build-node.ts write dist/storebound.mjs (npm pack / publish run it: prepack)
// bun tools/build-node.ts --out <f> write elsewhere (tests)
// The bundle is checked for this machine's home and checkout paths (like the sidecar): nothing private may ship.
import { chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { chmodSync, cpSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, relative, resolve } from 'node:path';
import { privateStrings, ROOT } from './build-sidecars';
@ -45,6 +45,12 @@ export function buildEntry(dir = join(ROOT, 'dist')): void {
if (tsc.exitCode !== 0) throw new Error(`tsc (declarations) failed\n${tsc.stdout.toString()}${tsc.stderr.toString()}`);
if (!statSync(join(types, 'lib', 'config.d.ts'), { throwIfNoEntry: false })) throw new Error('no dist/types/lib/config.d.ts');
pruneDeclarations(types, 'lib/config.d.ts');
// the same declarations for CommonJS projects (no "type": "module"): node16 / nodenext read a .d.ts as ESM or CJS
// from the nearest package.json, and a CJS file may not type-import an ESM-only declaration (TS1541)
const cjs = join(dir, 'types-cjs');
rmSync(cjs, { recursive: true, force: true });
cpSync(types, cjs, { recursive: true });
writeFileSync(join(cjs, 'package.json'), '{ "type": "commonjs" }\n');
const js = Bun.spawnSync(['bun', 'build', 'cli/lib/config.ts', '--target=node', '--format=esm', ...NODE_EXTERNALS.flatMap(e => ['--external', e]), '--outfile', join(dir, 'config.js')], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
if (js.exitCode !== 0) throw new Error(`bun build (config entry) failed\n${js.stderr.toString()}`);
const leaked = privateStrings().filter(s => readFileSync(join(dir, 'config.js'), 'utf8').includes(s));