cursedbelt 4.3.0 β 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react/components/Autocomplete.d.ts.map +1 -1
- package/dist/react/components/Autocomplete.js +1 -1
- package/dist/react/components/Autocomplete.js.map +1 -1
- package/dist/react/components/ComboboxField.d.ts.map +1 -1
- package/dist/react/components/ComboboxField.js +1 -1
- package/dist/react/components/ComboboxField.js.map +1 -1
- package/dist/react/media-gallery/MediaGallery.d.ts +9 -2
- package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
- package/dist/react/media-gallery/MediaGallery.js +12 -4
- package/dist/react/media-gallery/MediaGallery.js.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
- package/dist/styles-areas/analytics.css +1 -1
- package/dist/styles-areas/data-table.css +1 -1
- package/dist/styles-areas/fields.css +1 -1
- package/dist/styles-areas/media-gallery.css +1 -1
- package/dist/styles-areas/wizard.css +1 -1
- package/package.json +50 -6
- package/scripts/demoServer.ts +211 -0
- package/scripts/gate.ts +101 -0
- package/scripts/guardrailsEnforce.spec.ts +69 -0
- package/scripts/guardrailsEnforce.ts +59 -19
- package/scripts/paths.ts +1 -1
- package/src/demoFixture.spec.ts +54 -10
- package/src/demoStaticServer.spec.ts +169 -0
- package/src/publishShape.spec.ts +79 -0
- package/src/react/components/Autocomplete.tsx +11 -1
- package/src/react/components/ComboboxField.spec.tsx +12 -0
- package/src/react/components/ComboboxField.tsx +11 -1
- package/src/react/media-gallery/MediaGallery.spec.tsx +23 -0
- package/src/react/media-gallery/MediaGallery.tsx +26 -9
- package/src/styles-areas/analytics.css +1 -1
- package/src/styles-areas/data-table.css +1 -1
- package/src/styles-areas/fields.css +1 -1
- package/src/styles-areas/media-gallery.css +1 -1
- package/src/styles-areas/wizard.css +1 -1
- package/src/testFilesRunInParallel.spec.ts +3 -3
- package/src/typecheckCachesAreSeparate.spec.ts +2 -2
- package/src/verifyGraph.spec.ts +75 -108
- package/scripts/verify.ts +0 -296
package/src/demoFixture.spec.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import {
|
|
5
5
|
DEMO_COMMAND,
|
|
6
6
|
DEMO_DEV_SCRIPT,
|
|
7
|
+
DEMO_DIST_DIR,
|
|
8
|
+
DEMO_HOST,
|
|
7
9
|
DEMO_PACKAGE_DIR,
|
|
8
10
|
DEMO_PORT,
|
|
9
11
|
DEMO_ROOT,
|
|
@@ -12,7 +14,8 @@ import {
|
|
|
12
14
|
} from '../demo/devServer';
|
|
13
15
|
import { DEMO_TSCONFIG, derivePaths, readDemoTsconfig, renderPathsBlock } from '../demo/generatePaths';
|
|
14
16
|
import pkg from '../package.json';
|
|
15
|
-
|
|
17
|
+
// The declared graph, read as data β there is no scheduler in this repo to import from.
|
|
18
|
+
const STAGES: readonly { script: string }[] = (pkg as { gate?: { stages?: { script: string }[] } }).gate?.stages ?? [];
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* π΄ The e2e suite must never be able to sit green while it drives nothing.
|
|
@@ -56,19 +59,38 @@ describe('the e2e fixture exists', () => {
|
|
|
56
59
|
}
|
|
57
60
|
expect(pkg.scripts.demo, 'the demo script must start vite on the demo config').toInclude('demo/vite.config.ts');
|
|
58
61
|
// Playwright drives the BUILT bundle, not the dev server β see demo/devServer.ts.
|
|
59
|
-
expect(pkg.scripts['demo:serve'], 'demo:serve must build before it
|
|
60
|
-
|
|
62
|
+
expect(pkg.scripts['demo:serve'], 'demo:serve must build before it serves').toInclude('demo:build');
|
|
63
|
+
// π΄ And it serves it from OUR server, never `vite preview` β which under Bun 1.4.2
|
|
64
|
+
// exits 1 with ERR_STREAM_WRITE_AFTER_END when a Playwright worker hangs up
|
|
65
|
+
// mid-response, reddening this gate about one run in four (task 388, 2026-09-18).
|
|
66
|
+
// Going back to it is a one-word edit, so it is a one-line assertion.
|
|
67
|
+
expect(pkg.scripts['demo:serve'], 'demo:serve must run scripts/demoServer.ts').toInclude('scripts/demoServer.ts');
|
|
68
|
+
expect(pkg.scripts['demo:serve'], '`vite preview` dies on a client hang-up under Bun').not.toInclude(
|
|
69
|
+
'vite preview',
|
|
70
|
+
);
|
|
61
71
|
});
|
|
62
72
|
|
|
63
|
-
it('
|
|
73
|
+
it('every server serves the port the specs are pointed at', () => {
|
|
64
74
|
// vite silently walks to the next free port unless `strictPort` is set, which is
|
|
65
75
|
// the same outage wearing a costume: a server that starts perfectly well and a
|
|
66
|
-
// suite that times out against the port nobody is listening on.
|
|
76
|
+
// suite that times out against the port nobody is listening on. `Bun.serve` throws
|
|
77
|
+
// EADDRINUSE rather than walking, and `demoStaticServer.spec.ts` proves it does.
|
|
67
78
|
const viteConfig = readFileSync(resolve(DEMO_ROOT, 'vite.config.ts'), 'utf8');
|
|
68
|
-
expect(DEMO_URL).toBe(`http
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
expect(DEMO_URL).toBe(`http://${DEMO_HOST}:${DEMO_PORT}`);
|
|
80
|
+
// π΄ A literal address, never `localhost`. Measured 2026-09-18: two demo servers
|
|
81
|
+
// bound 4318 at the same time β one on `[::1]`, one on `127.0.0.1` β because
|
|
82
|
+
// `localhost` resolves to both and each process took whichever its resolver offered
|
|
83
|
+
// first. Neither bind collided, and which one a client reached was its own
|
|
84
|
+
// resolution order. A name here is a gate that can silently test a stale bundle.
|
|
85
|
+
expect(DEMO_HOST, 'the fixture must not be reached through a name that resolves twice').toBe('127.0.0.1');
|
|
86
|
+
expect(viteConfig, 'the dev server must bind the same one address').toInclude('host: DEMO_HOST');
|
|
87
|
+
// Once, for `server` β the dev server a human runs. There is no `preview` block any
|
|
88
|
+
// more, and a second match here would mean one grew back unread.
|
|
89
|
+
expect(viteConfig.match(/port: DEMO_PORT/g) ?? [], 'the dev server must pin the port').toHaveLength(1);
|
|
90
|
+
expect(viteConfig.match(/strictPort: true/g) ?? [], 'and must set strictPort').toHaveLength(1);
|
|
91
|
+
expect(viteConfig, 'the preview block is dead config now β e2e is served by scripts/demoServer.ts').not.toInclude(
|
|
92
|
+
'preview:',
|
|
93
|
+
);
|
|
72
94
|
});
|
|
73
95
|
|
|
74
96
|
it('every page the specs navigate to is a real demo entry point', () => {
|
|
@@ -98,13 +120,35 @@ describe('the e2e fixture exists', () => {
|
|
|
98
120
|
expect(unlisted, `demo/vite.config.ts's rollup input omits:\n ${unlisted.join('\n ')}`).toEqual([]);
|
|
99
121
|
});
|
|
100
122
|
|
|
123
|
+
it('π΄ never reuses a server somebody else started', () => {
|
|
124
|
+
// The gate's meaning must not depend on what the previous agent in this checkout
|
|
125
|
+
// left running. `reuseExistingServer: !process.env.CI` meant exactly that: `CI` is
|
|
126
|
+
// unset on this Mac, so a stale listener on 4318 was silently adopted and the demo
|
|
127
|
+
// build was skipped. Two agents measured the same command at 34.8s and 45.0s on
|
|
128
|
+
// 2026-09-18 for that reason β the fast one tested a bundle nobody had rebuilt.
|
|
129
|
+
const config = readFileSync(resolve(e2eDir, 'playwright.config.ts'), 'utf8');
|
|
130
|
+
expect(config, 'the webServer must always be ours').toInclude('reuseExistingServer: false');
|
|
131
|
+
expect(config, 'reuse must not be conditional on an env var this machine never sets').not.toMatch(
|
|
132
|
+
/reuseExistingServer:\s*!?\s*process\.env/,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('serves exactly the directory the demo build writes', () => {
|
|
137
|
+
// `scripts/demoServer.ts` serves DEMO_DIST_DIR; vite writes to `outDir` resolved
|
|
138
|
+
// against its `root`. A custom `outDir` would leave the server pointed at an empty
|
|
139
|
+
// directory and every spec 404ing against a 200-answering process.
|
|
140
|
+
const viteConfig = readFileSync(resolve(DEMO_ROOT, 'vite.config.ts'), 'utf8');
|
|
141
|
+
expect(DEMO_DIST_DIR).toBe(resolve(DEMO_ROOT, 'dist'));
|
|
142
|
+
expect(viteConfig, 'demoServer.ts assumes viteβs default outDir').not.toInclude('outDir');
|
|
143
|
+
});
|
|
144
|
+
|
|
101
145
|
it('verify actually runs the suite', () => {
|
|
102
146
|
// Everything above proves the fixture resolves. Only this proves anybody looks:
|
|
103
147
|
// without it, `e2e` drops back out of the gate and this whole file becomes a
|
|
104
148
|
// comment about a suite nothing runs.
|
|
105
149
|
//
|
|
106
150
|
// π΄ `verify` stopped being an `&&` chain on 2026-09-15 and became a dependency
|
|
107
|
-
// graph (`
|
|
151
|
+
// graph (the `gate` block in `package.json`), so "the script mentions e2e" is no longer where the
|
|
108
152
|
// answer lives. Asking the graph is strictly stronger than asking the string was:
|
|
109
153
|
// `verifyGraph.spec.ts` additionally pins that every OTHER stage of the old chain
|
|
110
154
|
// is still in it, which the `toInclude` never checked for any of them.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π΄ The demo's static server, and specifically the fault that used to end the gate.
|
|
3
|
+
*
|
|
4
|
+
* `bun run verify` went red about one run in four on an unmodified tree (task 388,
|
|
5
|
+
* 2026-09-18): `vite preview` β connect on Bun's `node:http` shim β threw
|
|
6
|
+
* `ERR_STREAM_WRITE_AFTER_END` out of `advanceResponsePipeline` when a Playwright worker
|
|
7
|
+
* hung up on a response that was still being written, and the throw landed after the
|
|
8
|
+
* handler had returned, where no middleware could catch it. The process exited 1, the
|
|
9
|
+
* `webServer` never came ready, and 138 innocent specs went red 120 seconds later.
|
|
10
|
+
*
|
|
11
|
+
* So the first assertion here is the one that matters: abort requests mid-flight and prove
|
|
12
|
+
* the server is still serving afterwards. Everything else in this file is the ordinary
|
|
13
|
+
* contract that makes the fixture pages reachable at all.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it } from 'bun:test';
|
|
16
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join, resolve } from 'node:path';
|
|
19
|
+
import { DEMO_HOST, DEMO_URL } from '../demo/devServer';
|
|
20
|
+
import { isPeerHangup, resolveRequestPath, startDemoServer } from '../scripts/demoServer';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A throwaway `dist` shaped like the one `bun run demo:build` writes. `$TMPDIR` is already
|
|
24
|
+
* redirected into this repo's scratch root by `scripts/testTmpRoot.ts` in the preload.
|
|
25
|
+
*/
|
|
26
|
+
function fixtureRoot(): string {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), 'demo-server-'));
|
|
28
|
+
mkdirSync(resolve(root, 'assets'), { recursive: true });
|
|
29
|
+
writeFileSync(resolve(root, 'index.html'), '<!doctype html><title>kitchen sink</title>');
|
|
30
|
+
writeFileSync(resolve(root, 'table-layout.html'), '<!doctype html><title>table layout</title>');
|
|
31
|
+
// Big enough that an abort lands mid-body rather than after the whole thing arrived.
|
|
32
|
+
writeFileSync(resolve(root, 'assets', 'app.js'), `export const pad = '${'x'.repeat(4_000_000)}';\n`);
|
|
33
|
+
writeFileSync(resolve(root, 'assets', 'app.css'), ':root { color-scheme: dark; }\n');
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Runs `body` against a server on an ephemeral port, and always stops it. */
|
|
38
|
+
async function withServer(body: (base: string) => Promise<void>): Promise<void> {
|
|
39
|
+
const server = startDemoServer({ root: fixtureRoot(), port: 0, hostname: '127.0.0.1' });
|
|
40
|
+
try {
|
|
41
|
+
await body(`http://127.0.0.1:${server.port}`);
|
|
42
|
+
} finally {
|
|
43
|
+
await server.stop(true);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('π΄ a client that hangs up is a non-event', () => {
|
|
48
|
+
it('keeps serving after a burst of aborted requests', async () => {
|
|
49
|
+
await withServer(async (base) => {
|
|
50
|
+
expect((await fetch(`${base}/`)).status, 'the server must be up before we abort at it').toBe(200);
|
|
51
|
+
|
|
52
|
+
// Eight, because that is how many Playwright workers this repo runs, and each one
|
|
53
|
+
// abandons in-flight subresources on every navigation.
|
|
54
|
+
for (let i = 0; i < 8; i++) {
|
|
55
|
+
const control = new AbortController();
|
|
56
|
+
const inFlight = fetch(`${base}/assets/app.js`, { signal: control.signal });
|
|
57
|
+
// Abort on the next turn of the loop: late enough that the response is being
|
|
58
|
+
// written, early enough that the body has not finished.
|
|
59
|
+
queueMicrotask(() => control.abort());
|
|
60
|
+
await inFlight.then(
|
|
61
|
+
async (response) => {
|
|
62
|
+
// If it won the race, drain it and abort during the read instead.
|
|
63
|
+
control.abort();
|
|
64
|
+
await response.text().catch(() => undefined);
|
|
65
|
+
},
|
|
66
|
+
() => undefined,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The whole point. Under `vite preview` the process was gone by now.
|
|
71
|
+
const after = await fetch(`${base}/`);
|
|
72
|
+
expect(after.status, 'the server died on a client hang-up β the exact task-388 bug').toBe(200);
|
|
73
|
+
expect(await after.text()).toInclude('kitchen sink');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('names the peer faults it tolerates, and nothing else', () => {
|
|
78
|
+
// π΄ A blanket handler would keep a broken server alive and let the gate measure a
|
|
79
|
+
// corpse that answers 200. The list is closed; this is the negative control.
|
|
80
|
+
expect(isPeerHangup({ code: 'ERR_STREAM_WRITE_AFTER_END' })).toBe(true);
|
|
81
|
+
expect(isPeerHangup({ code: 'ECONNRESET' })).toBe(true);
|
|
82
|
+
expect(isPeerHangup({ code: 'EPIPE' })).toBe(true);
|
|
83
|
+
expect(isPeerHangup(Object.assign(new Error('aborted'), { name: 'AbortError' }))).toBe(true);
|
|
84
|
+
expect(isPeerHangup(new TypeError('x is not a function'))).toBe(false);
|
|
85
|
+
expect(isPeerHangup({ code: 'ENOSPC' })).toBe(false);
|
|
86
|
+
expect(isPeerHangup(null)).toBe(false);
|
|
87
|
+
expect(isPeerHangup('ECONNRESET')).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('the pages the specs navigate to', () => {
|
|
92
|
+
it('serves the fixture pages, with the right content type and no caching', async () => {
|
|
93
|
+
await withServer(async (base) => {
|
|
94
|
+
const index = await fetch(`${base}/`);
|
|
95
|
+
expect(index.status).toBe(200);
|
|
96
|
+
expect(index.headers.get('content-type')).toInclude('text/html');
|
|
97
|
+
// π΄ A cached bundle is a gate reading pre-checkout code, which is the trap this
|
|
98
|
+
// whole change exists to close.
|
|
99
|
+
expect(index.headers.get('cache-control')).toInclude('no-store');
|
|
100
|
+
|
|
101
|
+
const page = await fetch(`${base}/table-layout.html`);
|
|
102
|
+
expect(page.status).toBe(200);
|
|
103
|
+
expect(await page.text()).toInclude('table layout');
|
|
104
|
+
|
|
105
|
+
const css = await fetch(`${base}/assets/app.css`);
|
|
106
|
+
expect(css.status).toBe(200);
|
|
107
|
+
expect(css.headers.get('content-type')).toInclude('text/css');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('404s what it does not have, rather than answering with index.html', async () => {
|
|
112
|
+
// A SPA fallback here would make `demoFixture.spec.ts`'s "every page the specs
|
|
113
|
+
// navigate to is real" assertion unfalsifiable at runtime: a typo'd fixture URL
|
|
114
|
+
// would load the kitchen sink and fail on a missing selector instead.
|
|
115
|
+
await withServer(async (base) => {
|
|
116
|
+
expect((await fetch(`${base}/no-such-page.html`)).status).toBe(404);
|
|
117
|
+
expect((await fetch(`${base}/assets/`)).status, 'no directory listings').toBe(404);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('what it refuses to serve', () => {
|
|
123
|
+
const root = '/tmp/demo-dist';
|
|
124
|
+
|
|
125
|
+
it('resolves the shapes the fixture actually uses', () => {
|
|
126
|
+
expect(resolveRequestPath(root, '/../../etc/passwd')).toBeNull();
|
|
127
|
+
expect(resolveRequestPath(root, '/%2e%2e/%2e%2e/etc/passwd')).toBeNull();
|
|
128
|
+
expect(resolveRequestPath(root, '/%zz')).toBeNull();
|
|
129
|
+
expect(resolveRequestPath(root, '/nope.html')).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('cannot be walked out of its root even through an encoded traversal', async () => {
|
|
133
|
+
// The pure check above says `null`; this says the socket agrees, because a server
|
|
134
|
+
// that resolved correctly and then read the file anyway would pass the first.
|
|
135
|
+
await withServer(async (base) => {
|
|
136
|
+
for (const path of ['/../package.json', '/%2e%2e/package.json', '/..%2fpackage.json']) {
|
|
137
|
+
const response = await fetch(`${base}${path}`);
|
|
138
|
+
expect(await response.text(), `${path} escaped the demo dist`).not.toInclude('cursedbelt');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('the port', () => {
|
|
145
|
+
it('π΄ binds one literal address, so a second server cannot hide beside it', async () => {
|
|
146
|
+
// Measured 2026-09-18: with `hostname: 'localhost'` two processes bound 4318 at once,
|
|
147
|
+
// one on `[::1]` and one on `127.0.0.1`, and NEITHER raised EADDRINUSE β `localhost`
|
|
148
|
+
// resolves to both, so they were never competing for the same socket. The gate would
|
|
149
|
+
// then test whichever one the client happened to reach.
|
|
150
|
+
const server = startDemoServer({ root: fixtureRoot(), port: 0 });
|
|
151
|
+
try {
|
|
152
|
+
expect(server.hostname, 'a resolvable NAME here is two sockets pretending to be one').toBe(DEMO_HOST);
|
|
153
|
+
expect(DEMO_URL).toInclude(DEMO_HOST);
|
|
154
|
+
} finally {
|
|
155
|
+
await server.stop(true);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('π΄ refuses a taken port instead of walking to the next one', async () => {
|
|
160
|
+
// The same rule `strictPort: true` gave the vite servers: a fixture that quietly
|
|
161
|
+
// moves to 4319 is a suite that times out against the port it was pointed at.
|
|
162
|
+
const first = startDemoServer({ root: fixtureRoot(), port: 0, hostname: '127.0.0.1' });
|
|
163
|
+
try {
|
|
164
|
+
expect(() => startDemoServer({ root: fixtureRoot(), port: first.port, hostname: '127.0.0.1' })).toThrow();
|
|
165
|
+
} finally {
|
|
166
|
+
await first.stop(true);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
package/src/publishShape.spec.ts
CHANGED
|
@@ -52,6 +52,31 @@ const typesThatAreNotDeclarations = (exports: Record<string, unknown>): string[]
|
|
|
52
52
|
.filter(([, target]) => !target.endsWith('.d.ts'))
|
|
53
53
|
.map(([subpath, target]) => `${subpath} β ${target}`);
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Every `[subpath, condition, target]` this manifest resolves OUTSIDE `dist/`.
|
|
57
|
+
*
|
|
58
|
+
* `./dist/` is deliberately excluded: those are `scripts/checkDistExports.ts`' job, and it
|
|
59
|
+
* has to be a build step because `dist/` is the one directory no spec here may open
|
|
60
|
+
* (`src/verifyGraph.spec.ts` asserts it, because `build` runs beside `test`). Everything
|
|
61
|
+
* else is a file that is already on disk when the tests run, so it belongs here.
|
|
62
|
+
*
|
|
63
|
+
* A string-valued export (`'./styles.css': './src/styles.css'`) is one target under the
|
|
64
|
+
* implicit `default` condition β 48 of this manifest's entries are that shape.
|
|
65
|
+
*/
|
|
66
|
+
const targetsOutsideDist = (exports: Record<string, unknown>): [string, string, string][] => {
|
|
67
|
+
const out: [string, string, string][] = [];
|
|
68
|
+
for (const [subpath, entry] of Object.entries(exports)) {
|
|
69
|
+
const conditions: Record<string, unknown> =
|
|
70
|
+
typeof entry === 'string' ? { default: entry } : typeof entry === 'object' && entry !== null ? (entry as Record<string, unknown>) : {};
|
|
71
|
+
for (const [condition, target] of Object.entries(conditions)) {
|
|
72
|
+
if (typeof target === 'string' && target.startsWith('./') && !target.startsWith('./dist/')) {
|
|
73
|
+
out.push([subpath, condition, target]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
};
|
|
79
|
+
|
|
55
80
|
describe('publish shape', () => {
|
|
56
81
|
it('ships every directory its exports resolve into', () => {
|
|
57
82
|
const files = (pkg as { files?: string[] }).files ?? [];
|
|
@@ -147,7 +172,61 @@ describe('publish shape', () => {
|
|
|
147
172
|
expect(typesThatAreNotDeclarations({ './a': { types: './dist/a.js' } })).toHaveLength(1);
|
|
148
173
|
});
|
|
149
174
|
|
|
175
|
+
it('π΄ points every non-dist export condition at a file that EXISTS', async () => {
|
|
176
|
+
// ββ The third of three ways this manifest can lie, and the only one nothing watched ββ
|
|
177
|
+
// `publishShape` above proves an export's ROOT directory is in `files`;
|
|
178
|
+
// `scripts/checkDistExports.ts` proves every `./dist/` target was emitted. Between
|
|
179
|
+
// them sits the condition a consumer actually RUNS β `bun`/`source`/`default`, which
|
|
180
|
+
// for this package is raw `./src/**` and `./scripts/guardrailsEnforce.ts` β and
|
|
181
|
+
// nothing proved those files are there. `files` ships whole directories, so renaming
|
|
182
|
+
// or moving one leaves the root shipped, the tarball valid, and the target dangling.
|
|
183
|
+
//
|
|
184
|
+
// It is not hypothetical and it is not ours to survive: since task 202 folded
|
|
185
|
+
// `--baseline` into the scanner, SIX apps run
|
|
186
|
+
// `bun node_modules/cursedbelt/scripts/guardrailsEnforce.ts β¦` inside their own
|
|
187
|
+
// `verify`. That path and this manifest's `./guardrails` conditions name the same
|
|
188
|
+
// file, so the day it moves, six repos go red in a file none of them wrote β the
|
|
189
|
+
// exact three-repos-away failure that tasks 172 and 219 each cost a session to find.
|
|
190
|
+
//
|
|
191
|
+
// The 48 string-valued CSS entries are the sharper half: they are GENERATED
|
|
192
|
+
// (`bun run styles:areas`) and have no second condition to fall through to, so a
|
|
193
|
+
// generator that stops emitting one is an unresolvable import for every consumer
|
|
194
|
+
// with nothing red on this side at all.
|
|
195
|
+
const repo = `${import.meta.dir}/..`;
|
|
196
|
+
const missing: string[] = [];
|
|
197
|
+
for (const [subpath, condition, target] of targetsOutsideDist(pkg.exports as unknown as Record<string, unknown>)) {
|
|
198
|
+
// π΄ Bun.file, NOT node:fs β the preload virtualizes node:fs and an existsSync
|
|
199
|
+
// here would answer from MOCK data. Same trap as the header of this file.
|
|
200
|
+
if (!(await Bun.file(`${repo}/${target.slice(2)}`).exists())) missing.push(`${subpath} [${condition}] β ${target}`);
|
|
201
|
+
}
|
|
202
|
+
expect(
|
|
203
|
+
missing,
|
|
204
|
+
'these export conditions name a file that is not on disk. A consumer does not get a\n' +
|
|
205
|
+
'helpful error β the resolver falls through to whatever condition is left, or fails\n' +
|
|
206
|
+
'outright for the CSS entries, which have none:\n ' +
|
|
207
|
+
missing.join('\n '),
|
|
208
|
+
).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('catches a dangling non-dist target, and leaves dist to the build step', () => {
|
|
212
|
+
// The negative control for the rule above. Without it, `targetsOutsideDist` could
|
|
213
|
+
// quietly stop matching and the assertion would be green against nothing.
|
|
214
|
+
const found = targetsOutsideDist({
|
|
215
|
+
'./guardrails': { types: './dist/scripts/guardrailsEnforce.d.ts', bun: './scripts/moved.ts' },
|
|
216
|
+
'./styles.css': './src/styles.css',
|
|
217
|
+
'./react': { types: './dist/react/index.d.ts', import: './dist/react/index.js' },
|
|
218
|
+
});
|
|
219
|
+
// The `./dist/` conditions are checkDistExports' to police, so neither appears here.
|
|
220
|
+
expect(found).toEqual([
|
|
221
|
+
['./guardrails', 'bun', './scripts/moved.ts'],
|
|
222
|
+
['./styles.css', 'default', './src/styles.css'],
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
150
226
|
it('reads the real manifest, so the rule cannot pass vacuously', () => {
|
|
227
|
+
// The scan above must see the whole non-dist surface, not a handful of it: 97 targets
|
|
228
|
+
// across the `bun`/`source`/`default` conditions and the 48 bare CSS entries.
|
|
229
|
+
expect(targetsOutsideDist(pkg.exports as unknown as Record<string, unknown>).length).toBeGreaterThan(90);
|
|
151
230
|
// `types` is what every conditional export in this package leads with; if the scan
|
|
152
231
|
// ever returns nothing, both assertions above are green about a manifest they never
|
|
153
232
|
// looked at.
|
|
@@ -138,7 +138,17 @@ export function AutocompleteField<S = string>({
|
|
|
138
138
|
</Autocomplete.InputGroup>
|
|
139
139
|
|
|
140
140
|
<Autocomplete.Portal>
|
|
141
|
-
|
|
141
|
+
{/* π΄ The z-index goes on the POSITIONER, not only the popup. Base UI's positioner is
|
|
142
|
+
the element that is `position: absolute` (with a transform); the popup inside it is
|
|
143
|
+
`position: static`, so the popup's own `z-[var(--z-popover)]` does nothing and the
|
|
144
|
+
list painted UNDER any sibling with a z-index β measured 2026-09-22 in the
|
|
145
|
+
binary-server inspector, a sticky table header (z-index 1) covering the second option
|
|
146
|
+
of an open MultiSelectField. */}
|
|
147
|
+
<Autocomplete.Positioner
|
|
148
|
+
sideOffset={4}
|
|
149
|
+
collisionPadding={FLOATING_COLLISION_PADDING}
|
|
150
|
+
className='z-[var(--z-popover)]'
|
|
151
|
+
>
|
|
142
152
|
<Autocomplete.Popup
|
|
143
153
|
className={cn(
|
|
144
154
|
FLOATING_CONTENT_BASE,
|
|
@@ -53,6 +53,18 @@ describe('ComboboxField (single)', () => {
|
|
|
53
53
|
expect(getByText('Banana')).not.toBeNull();
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test('π΄ the POSITIONER carries the popover z-index β the popup is static inside it', () => {
|
|
57
|
+
const { container, getByRole } = render(<Controlled />);
|
|
58
|
+
const trigger = container.querySelector('[aria-label="Toggle options"]') as HTMLElement;
|
|
59
|
+
act(() => {
|
|
60
|
+
fireEvent.click(trigger);
|
|
61
|
+
});
|
|
62
|
+
const popup = getByRole('listbox').closest('[data-side]') as HTMLElement;
|
|
63
|
+
const positioner = popup.parentElement?.closest('[data-side]') as HTMLElement;
|
|
64
|
+
expect(positioner).not.toBeNull();
|
|
65
|
+
expect(positioner.className).toContain('z-[var(--z-popover)]');
|
|
66
|
+
});
|
|
67
|
+
|
|
56
68
|
test('disabled prevents the trigger from responding', () => {
|
|
57
69
|
const onChange = mock(() => {});
|
|
58
70
|
const { container } = render(
|
|
@@ -291,7 +291,17 @@ export function ComboboxField(props: ComboboxFieldProps) {
|
|
|
291
291
|
</Combobox.InputGroup>
|
|
292
292
|
|
|
293
293
|
<Combobox.Portal>
|
|
294
|
-
|
|
294
|
+
{/* π΄ The z-index goes on the POSITIONER, not only the popup. Base UI's positioner is
|
|
295
|
+
the element that is `position: absolute` (with a transform); the popup inside it is
|
|
296
|
+
`position: static`, so the popup's own `z-[var(--z-popover)]` does nothing and the
|
|
297
|
+
list painted UNDER any sibling with a z-index β measured 2026-09-22 in the
|
|
298
|
+
binary-server inspector, a sticky table header (z-index 1) covering the second option
|
|
299
|
+
of an open MultiSelectField. */}
|
|
300
|
+
<Combobox.Positioner
|
|
301
|
+
sideOffset={4}
|
|
302
|
+
collisionPadding={FLOATING_COLLISION_PADDING}
|
|
303
|
+
className='z-[var(--z-popover)]'
|
|
304
|
+
>
|
|
295
305
|
<Combobox.Popup
|
|
296
306
|
className={cn(
|
|
297
307
|
FLOATING_CONTENT_BASE,
|
|
@@ -1027,6 +1027,14 @@ describe('MediaGallery β size and length', () => {
|
|
|
1027
1027
|
expect(tileLine(container, 'notes.pdf')).toBe('5.0 MB');
|
|
1028
1028
|
});
|
|
1029
1029
|
|
|
1030
|
+
test("'length-and-size' puts both facts on a film's tile, and a document keeps its size", () => {
|
|
1031
|
+
const { container } = render(
|
|
1032
|
+
<MediaGallery items={MEASURED} meta={{ tileMeasure: 'length-and-size' }} />,
|
|
1033
|
+
);
|
|
1034
|
+
expect(tileLine(container, 'holiday.mp4')).toBe('3:07 Β· 1.3 GB');
|
|
1035
|
+
expect(tileLine(container, 'notes.pdf')).toBe('5.0 MB');
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1030
1038
|
test('tags keep their line and the running time joins them on it', () => {
|
|
1031
1039
|
// π΄ Never a fourth line: the caption block is a fixed 65px (`CBGD_META_HEIGHT`), and a
|
|
1032
1040
|
// tile taller than its neighbour is what made rows lap each other across the fleet.
|
|
@@ -1116,6 +1124,21 @@ describe('MediaGallery β size and length', () => {
|
|
|
1116
1124
|
{ measure: true },
|
|
1117
1125
|
),
|
|
1118
1126
|
).toBe('1:05');
|
|
1127
|
+
// `length-and-size` keeps the size beside the running time β binary-server's ask, both
|
|
1128
|
+
// facts on one tile β and a clip nobody measured is just its size, never `β Β· 1.3 GB`.
|
|
1129
|
+
expect(galleryTileLine(film, { measure: 'length-and-size' })).toBe('3:07 Β· 1.3 GB');
|
|
1130
|
+
expect(
|
|
1131
|
+
galleryTileLine({ ...film, tags: ['collections'] }, { measure: 'length-and-size' }),
|
|
1132
|
+
).toBe('collections Β· 3:07 Β· 1.3 GB');
|
|
1133
|
+
expect(
|
|
1134
|
+
galleryTileLine({ ...film, durationSeconds: undefined }, { measure: 'length-and-size' }),
|
|
1135
|
+
).toBe('1.3 GB');
|
|
1136
|
+
expect(
|
|
1137
|
+
galleryTileLine({ id: 'v', name: 'bare.mp4', category: 'video' }, { measure: 'length-and-size' }),
|
|
1138
|
+
).toBe('β');
|
|
1139
|
+
expect(
|
|
1140
|
+
galleryTileLine({ id: 'd', name: 'doc.pdf', sizeBytes: 900 }, { measure: 'length-and-size' }),
|
|
1141
|
+
).toBe('900 B');
|
|
1119
1142
|
// Off, nothing about the line moves β tags win, then the size, then nothing at all.
|
|
1120
1143
|
expect(galleryTileLine({ ...film, tags: ['holiday'] })).toBe('holiday');
|
|
1121
1144
|
expect(galleryTileLine({ id: 'x', name: 'bare.bin' })).toBeNull();
|
|
@@ -445,6 +445,10 @@ function isTurned(item: MediaGalleryItem): boolean {
|
|
|
445
445
|
return item.orientation ? swapsAxes(item.orientation.turns) : false;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
/** What a tile's caption measures β see {@link galleryTileLine}. `true` = the running time on a
|
|
449
|
+
* time-based item (else its size); `'length-and-size'` = both, side by side. */
|
|
450
|
+
export type TileMeasure = boolean | 'length-and-size';
|
|
451
|
+
|
|
448
452
|
type BytesCache = MutableRefObject<Map<string, Promise<ArrayBuffer>>>;
|
|
449
453
|
|
|
450
454
|
/**
|
|
@@ -473,19 +477,28 @@ type BytesCache = MutableRefObject<Map<string, Promise<ArrayBuffer>>>;
|
|
|
473
477
|
*/
|
|
474
478
|
export function galleryTileLine(
|
|
475
479
|
item: MediaGalleryItem,
|
|
476
|
-
options: { measure?:
|
|
480
|
+
options: { measure?: TileMeasure } = {},
|
|
477
481
|
): string | null {
|
|
478
482
|
const tags = item.tags && item.tags.length > 0 ? item.tags.slice(0, 3).join(' Β· ') : null;
|
|
479
483
|
const size = item.sizeBytes != null ? formatBytes(item.sizeBytes) : null;
|
|
480
484
|
if (!options.measure) return tags ?? size;
|
|
481
485
|
const category = galleryItemCategory(item);
|
|
482
486
|
const timeBased = category === 'video' || category === 'audio';
|
|
483
|
-
const
|
|
487
|
+
const length = timeBased ? durationLabel(item.durationSeconds) : null;
|
|
488
|
+
// `length-and-size` keeps the size BESIDE the running time instead of dropping it β the
|
|
489
|
+
// binary-server inspector's ask, which named both facts for the same tile. A clip with no
|
|
490
|
+
// measured length is then just its size, never `β Β· 1.3 GB`.
|
|
491
|
+
const measure =
|
|
492
|
+
options.measure === 'length-and-size'
|
|
493
|
+
? ([length, size].filter(Boolean).join(' Β· ') || (timeBased ? 'β' : null))
|
|
494
|
+
: timeBased
|
|
495
|
+
? (length ?? size ?? 'β')
|
|
496
|
+
: size;
|
|
484
497
|
return tags && measure ? `${tags} Β· ${measure}` : (tags ?? measure);
|
|
485
498
|
}
|
|
486
499
|
|
|
487
|
-
function GalleryMeta({ item, measure }: { item: MediaGalleryItem; measure?:
|
|
488
|
-
const line = galleryTileLine(item, { measure
|
|
500
|
+
function GalleryMeta({ item, measure }: { item: MediaGalleryItem; measure?: TileMeasure }) {
|
|
501
|
+
const line = galleryTileLine(item, { measure });
|
|
489
502
|
// The date is its OWN line rather than appended to the one above, because that line is
|
|
490
503
|
// already whichever of tags/size exists β folding a third fact into it would push the tags
|
|
491
504
|
// out of a tile caption that is two lines tall by design.
|
|
@@ -647,7 +660,7 @@ function GalleryTile({
|
|
|
647
660
|
/** Preview every video tile without hovering it β see `readThumbAutoplay`. */
|
|
648
661
|
autoPreview?: boolean;
|
|
649
662
|
/** Lead the caption with a running time / a size β see {@link galleryTileLine}. */
|
|
650
|
-
measure?:
|
|
663
|
+
measure?: TileMeasure;
|
|
651
664
|
/** See {@link TileShell}'s `stageMark` β the label for the tile that is ON the stage. */
|
|
652
665
|
stageMark?: string;
|
|
653
666
|
}) {
|
|
@@ -741,7 +754,7 @@ function RawGalleryTile({
|
|
|
741
754
|
cache: MediaCache;
|
|
742
755
|
onOpen: () => void;
|
|
743
756
|
autoPreview?: boolean;
|
|
744
|
-
measure?:
|
|
757
|
+
measure?: TileMeasure;
|
|
745
758
|
}) {
|
|
746
759
|
const category = galleryItemCategory(item);
|
|
747
760
|
const { media } = useItemMedia(item, cache);
|
|
@@ -1749,8 +1762,12 @@ export interface MediaGalleryProps {
|
|
|
1749
1762
|
* A tile's caption leads with the measurement that means something for its kind: the
|
|
1750
1763
|
* RUNNING TIME on a film or a track, the size on everything else. Tags keep the line
|
|
1751
1764
|
* they had and the measurement joins them on it β see {@link galleryTileLine}.
|
|
1765
|
+
*
|
|
1766
|
+
* `'length-and-size'` shows BOTH on a film or a track (`3:07 Β· 1.3 GB`) rather than the
|
|
1767
|
+
* running time instead of the size β `ideas/binary-server-09-15.md` asked for the two
|
|
1768
|
+
* facts on the same tile, where `collections` asked for one in place of the other.
|
|
1752
1769
|
*/
|
|
1753
|
-
tileMeasure?:
|
|
1770
|
+
tileMeasure?: TileMeasure;
|
|
1754
1771
|
/**
|
|
1755
1772
|
* The table offers the exact byte count as its own column, beside the rounded Size one
|
|
1756
1773
|
* rather than instead of it β see `exactBytesLabel` in `GalleryTable.tsx`. A rounded size cannot answer
|
|
@@ -3094,7 +3111,7 @@ export function MediaGallery({
|
|
|
3094
3111
|
// β¦and so does the running time on a film's caption: the strip under an open
|
|
3095
3112
|
// file is the same grid, and a tile that told you its length on one screen and
|
|
3096
3113
|
// its size on the other would be two tiles.
|
|
3097
|
-
measure={meta?.tileMeasure
|
|
3114
|
+
measure={meta?.tileMeasure || undefined}
|
|
3098
3115
|
// The one tile the stage is showing. Clicking it re-selects what is already
|
|
3099
3116
|
// open, which `select` makes a no-op change β it stays a live tile rather than
|
|
3100
3117
|
// a dead one, because a tile you cannot press reads as broken.
|
|
@@ -3292,7 +3309,7 @@ export function MediaGallery({
|
|
|
3292
3309
|
selection={tileSelection(item)}
|
|
3293
3310
|
drag={tileDrag(item)}
|
|
3294
3311
|
autoPreview={thumbAutoplay}
|
|
3295
|
-
measure={meta?.tileMeasure
|
|
3312
|
+
measure={meta?.tileMeasure || undefined}
|
|
3296
3313
|
/>
|
|
3297
3314
|
) : null;
|
|
3298
3315
|
}}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits β proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 1312 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits β proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 1347 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits β proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 613 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits β proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 2115 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits β proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 389 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* | **`--parallel=6`** | **14.6s** | 2689 / 0 |
|
|
14
14
|
* | `--parallel` (14) | 11.8s | 2689 / 0 |
|
|
15
15
|
*
|
|
16
|
-
* Six, not fourteen, and the reason is the one
|
|
16
|
+
* Six, not fourteen, and the reason is the one `$FORGE/tools/gate.ts` gives for its own job
|
|
17
17
|
* cap: `test` runs BESIDE `e2e` in the graph, and e2e is seven Playwright workers driving
|
|
18
18
|
* seven browsers. Fourteen more processes on top converts parallelism into thrash, and
|
|
19
19
|
* browser specs degrade worse than anything else under contention. The gain from 6 β 14 is
|
|
@@ -57,7 +57,7 @@ describe('the unit suite runs on more than one core', () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
test('the worker count stays inside the budget the verify graph assumes', () => {
|
|
60
|
-
//
|
|
60
|
+
// `$FORGE/tools/gate.ts` runs up to `jobLimit()` stages at once and `e2e` is seven
|
|
61
61
|
// browsers. A bare `--parallel` means one worker per core, which is that budget
|
|
62
62
|
// ignored β so the number is written down, and it is written down here as well as in
|
|
63
63
|
// package.json because the two have to agree about why.
|
|
@@ -71,7 +71,7 @@ describe('the unit suite runs on more than one core', () => {
|
|
|
71
71
|
expect(workers).toBeGreaterThanOrEqual(2);
|
|
72
72
|
expect(
|
|
73
73
|
workers,
|
|
74
|
-
'more workers than a third of the cores is the over-subscription
|
|
74
|
+
'more workers than a third of the cores is the over-subscription `$FORGE/tools/gate.ts` ' +
|
|
75
75
|
'caps its own stage count to avoid; measured, 6 β 14 workers buys 2.8s and is paid ' +
|
|
76
76
|
'for by the browser stage running beside it.',
|
|
77
77
|
).toBeLessThanOrEqual(8);
|