New:Socket for Asana Is Now Available.Learn more
Sign In

@ultimat3/testing

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/testing - npm Package Compare versions

Comparing version
11.1.0
to
11.2.0
+146
src/define-island-states.ts
// `defineIslandStates` — an island's photographable states, validated once, frozen, and knowable
// without a browser. The vocabulary it takes and hands back is `island-states.ts`; the rules it
// applies are `island-states-check.ts`. Every default a declaration leaves out is resolved HERE, so
// nothing downstream has to decide what an absent viewport or an absent theme list meant.
import { DEFAULT_NOW } from './determinism';
import {
IslandStateDuplicateError,
IslandStateIdInvalidError,
IslandStateInstantInvalidError,
IslandStateJsonInvalidError,
IslandStateStubInvalidError,
IslandStatesEmptyError,
IslandStateZoneInvalidError,
} from './island-state-errors';
import type {
IslandState,
IslandStateDecl,
IslandStatesDecl,
IslandStatesManifest,
IslandTheme,
IslandViewport,
} from './island-states';
import {
DEFAULT_ISLAND_VIEWPORT,
ISLAND_SHOT_TIME_ZONE,
ISLAND_STATES,
ISLAND_THEMES,
islandStatesName,
} from './island-states';
import {
isPinnedInstant,
isStateId,
isStubMatch,
isTimeZone,
jsonFault,
slugifyStateId,
} from './island-states-check';
/** A dimension a browser can be sized to. Anything else inherits, rather than photographing 0px. */
const usableViewport = (viewport: IslandViewport | undefined): IslandViewport | undefined =>
viewport !== undefined &&
Number.isInteger(viewport.width) &&
Number.isInteger(viewport.height) &&
viewport.width > 0 &&
viewport.height > 0
? { width: viewport.width, height: viewport.height }
: undefined;
/**
* Declared themes, deduplicated, in declaration order — both when the list is absent, and both
* again when nothing in it is a theme this framework knows. Falling back rather than throwing is
* the same rule `parseIslandAddress` follows: an unreadable theme must still show the component.
*/
const usableThemes = (themes: readonly IslandTheme[] | undefined): readonly IslandTheme[] => {
const known = (themes ?? []).filter((theme) => ISLAND_THEMES.includes(theme));
const unique = [...new Set(known)];
return unique.length > 0 ? unique : ISLAND_THEMES;
};
/** Frozen all the way down: a harness that mutated props would poison every later picture. */
function freezeJson<T>(value: T): T {
if (typeof value !== 'object' || value === null) return value;
for (const entry of Object.values(value)) freezeJson(entry);
return Object.freeze(value);
}
function checkClock(decl: IslandStatesDecl): void {
if (decl.timeZone !== undefined && !isTimeZone(decl.timeZone)) {
throw new IslandStateZoneInvalidError({ island: decl.island, value: decl.timeZone });
}
if (decl.now !== undefined && !isPinnedInstant(decl.now)) {
throw new IslandStateInstantInvalidError({ island: decl.island, value: decl.now });
}
}
function normalizeState(
decl: IslandStatesDecl,
state: IslandStateDecl,
seen: Set<string>,
inherited: IslandViewport,
): IslandState {
if (!isStateId(state.id)) {
throw new IslandStateIdInvalidError({
island: decl.island,
id: state.id,
slug: slugifyStateId(state.id),
});
}
if (seen.has(state.id))
throw new IslandStateDuplicateError({ island: decl.island, id: state.id });
seen.add(state.id);
const propsFault = jsonFault(state.props, `state "${state.id}" props`);
if (propsFault !== undefined) {
throw new IslandStateJsonInvalidError({ island: decl.island, ...propsFault });
}
const routes = state.routes ?? [];
for (const [index, stub] of routes.entries()) {
if (!isStubMatch(stub.match)) {
throw new IslandStateStubInvalidError({ island: decl.island, match: stub.match });
}
if (stub.respond.kind !== 'json') continue;
const bodyFault = jsonFault(
stub.respond.body,
`state "${state.id}" routes[${index}].respond.body`,
);
if (bodyFault !== undefined) {
throw new IslandStateJsonInvalidError({ island: decl.island, ...bodyFault });
}
}
return {
id: state.id,
title: state.title,
...(state.note === undefined ? {} : { note: state.note }),
props: state.props,
routes,
viewport: usableViewport(state.viewport) ?? inherited,
themes: usableThemes(state.themes),
};
}
/**
* Declare the states one island can be photographed in. Validated here rather than by the command
* that takes the pictures, because a manifest that is only checked at capture time is a manifest
* whose defects are found by a browser — long after the file that has to change was in view.
*/
export function defineIslandStates(decl: IslandStatesDecl): IslandStatesManifest {
if (decl.states.length === 0) throw new IslandStatesEmptyError({ island: decl.island });
checkClock(decl);
const viewport = usableViewport(decl.viewport) ?? DEFAULT_ISLAND_VIEWPORT;
const seen = new Set<string>();
const states = decl.states.map((state) => normalizeState(decl, state, seen, viewport));
return freezeJson({
[ISLAND_STATES]: true,
name: islandStatesName(decl.island),
island: decl.island,
states,
viewport,
...(decl.target === undefined ? {} : { target: decl.target }),
timeZone: decl.timeZone ?? ISLAND_SHOT_TIME_ZONE,
now: decl.now ?? DEFAULT_NOW,
});
}
// The expansion: one manifest in, one record per PICTURE out. Pure, and deliberately total — the
// command that photographs them knows the complete expected file list before a browser exists, so
// "produced nothing and exited 0" is a state it can refuse rather than a state it can report.
import type { IslandStatesManifest, IslandTheme, IslandViewport } from './island-states';
import { DEFAULT_ISLAND_THEME, ISLAND_THEMES } from './island-states';
export interface IslandShotTarget {
/** App-root-relative path of the `.island.tsx`, as the manifest declared it. */
readonly island: string;
/** The manifest's own name — the shot directory, and what a reader types to ask for it. */
readonly name: string;
readonly state: string;
readonly theme: IslandTheme;
readonly viewport: IslandViewport;
/** CSS selector to crop to. Absent means the island's host element. */
readonly target?: string;
readonly timeZone: string;
readonly now: string;
/** `<name>/<state>-<theme>.png` — flat and mechanical, because the reader GUESSES this path. */
readonly file: string;
/** The harness address that renders exactly this picture. `parseIslandAddress` is its inverse. */
readonly query: string;
}
export const islandShotFile = (name: string, state: string, theme: IslandTheme): string =>
`${name}/${state}-${theme}.png`;
export interface IslandAddress {
readonly island: string;
readonly state: string;
readonly theme: IslandTheme;
}
/**
* The address as a query string, `?` included, so it appends to any harness URL. Ordered
* island → state → theme and never sorted: an address is read by people as often as by code.
*/
export function islandAddress(address: IslandAddress): string {
const params = new URLSearchParams([
['island', address.island],
['state', address.state],
['theme', address.theme],
]);
return `?${params.toString()}`;
}
/**
* The inverse, made TOTAL. Every failure falls back rather than throwing: a mistyped theme, a
* missing key or an empty string all still answer an address, because the harness that reads this
* is a page — and a page that renders an error instead of the component turns a typo into a
* screenshot of the framework. The one refusal in this design is `findIslandStates`, which names
* every valid island when it cannot resolve one.
*/
export function parseIslandAddress(query: string): IslandAddress {
const params = new URLSearchParams(query.startsWith('?') ? query.slice(1) : query);
const theme = params.get('theme');
return {
island: params.get('island') ?? '',
state: params.get('state') ?? '',
theme: isIslandTheme(theme) ? theme : DEFAULT_ISLAND_THEME,
};
}
export function isIslandTheme(value: unknown): value is IslandTheme {
return typeof value === 'string' && ISLAND_THEMES.includes(value as IslandTheme);
}
/**
* Every picture this manifest asks for, state by state and theme by theme in declaration order.
* `selector` is the caller's override for the manifest's own `target` — a flag beats a file — and
* absent means "whatever the manifest said", never "crop nothing".
*/
export function islandShotTargets(
manifest: IslandStatesManifest,
selector?: string,
): readonly IslandShotTarget[] {
const target = selector ?? manifest.target;
return manifest.states.flatMap((state) =>
state.themes.map((theme) => ({
island: manifest.island,
name: manifest.name,
state: state.id,
theme,
viewport: state.viewport,
...(target === undefined ? {} : { target }),
timeZone: manifest.timeZone,
now: manifest.now,
file: islandShotFile(manifest.name, state.id, theme),
query: islandAddress({ island: manifest.island, state: state.id, theme }),
})),
);
}
/** The same expansion over a whole set, in the order the manifests arrived. */
export function islandShotPlan(
manifests: readonly IslandStatesManifest[],
selector?: string,
): readonly IslandShotTarget[] {
return manifests.flatMap((manifest) => islandShotTargets(manifest, selector));
}
// The ten X_TEST_ISLAND_STATE* codes, apart from ./errors only because one file has one job and the
// catalogue is already at its ceiling. The codes themselves, their titles and the single
// `registerErrorCodes` call stay in ./errors — one owner, one registration, one place a duplicate
// can surface.
import { renderCauseValue, renderFixLiteral, UltimateError } from '@ultimat3/core';
// No `docs:` on the subclasses below. `UltimateError` fills it from `describeErrorCode(code).docs`.
/** Neither path is controlled by the framework: both arrive from an app's own declaration. */
const ISLAND_PLACEHOLDER = '<the island path the cause names>';
/** The same, for the three refusals whose subject is the states file rather than the island. */
const STATES_PLACEHOLDER = '<the states file the cause names>';
/**
* The file a `defineIslandStates` call for this island lives in, by convention: the island's own
* name with `.states.ts` where `.tsx` was. Spelled here, in the leaf module, because every error
* below has to name the file the reader must edit and an error may never import the module that
* throws it. Only `.tsx` is read — the island EXTENSION is `@ultimat3/render`'s constant and is
* deliberately not restated in this package.
*/
export function islandStatesFile(island: string): string {
return island.endsWith('.tsx')
? `${island.slice(0, -'.tsx'.length)}.states.ts`
: `${island}.states.ts`;
}
const at = (island: string): string =>
renderFixLiteral(islandStatesFile(island), ISLAND_PLACEHOLDER);
/**
* A manifest that declares no states. It parses, it registers and it expands to nothing — so the
* command that photographs it produces no file and exits 0, which is the one outcome a reader
* cannot tell from success. Refused at the declaration, where the file to edit is known.
*/
export class IslandStatesEmptyError extends UltimateError {
constructor(input: { readonly island: string }) {
super({
code: 'X_TEST_ISLAND_STATES_EMPTY',
cause: `defineIslandStates(${renderCauseValue(input.island)}) declares no states, so it can never produce a picture`,
fix: `in ${at(input.island)} add: states: [{ id: 'empty', title: 'no rows yet', props: {} }]`,
});
}
}
/**
* A state id that is not a slug. The id becomes a FILENAME stem, so a space, a slash or a capital
* is either an unguessable path or a path outside the shot directory. The suggestion is the id
* slugified, so the edit is a paste rather than a decision.
*/
export class IslandStateIdInvalidError extends UltimateError {
constructor(input: { readonly island: string; readonly id: string; readonly slug: string }) {
super({
code: 'X_TEST_ISLAND_STATE_ID_INVALID',
cause: `island state id ${renderCauseValue(input.id)} is not a slug — it becomes the screenshot filename stem`,
fix:
input.slug.length === 0
? `in ${at(input.island)} give the state an id of lowercase letters, digits and single dashes: id: 'over-quota'`
: `in ${at(input.island)} write: id: '${input.slug}'`,
});
}
}
/**
* Two states with one id. The second picture overwrites the first at the same path, so the run
* reports two states and leaves one file — a loss with nothing in the output pointing at it.
*/
export class IslandStateDuplicateError extends UltimateError {
constructor(input: { readonly island: string; readonly id: string }) {
super({
code: 'X_TEST_ISLAND_STATE_DUPLICATE',
cause: `two island states share the id ${renderCauseValue(input.id)}; the second picture would overwrite the first`,
fix: `in ${at(input.island)} rename one of them — id: '${input.id}-2', or the state it really is: id: 'over-quota'`,
});
}
}
/**
* A value that `JSON.stringify` does not carry. Island props ride the `data-x-props` script tag,
* which is JSON — so a `Date`, a function, a `Map` or an `undefined` is not "slightly wrong" in the
* picture, it is a prop the component never receives. Refused where the path is still known.
*/
export class IslandStateJsonInvalidError extends UltimateError {
constructor(input: { readonly island: string; readonly path: string; readonly reason: string }) {
super({
code: 'X_TEST_ISLAND_STATE_JSON_INVALID',
cause: `${input.path} is ${input.reason}, and island props travel as JSON in data-x-props`,
fix: `in ${at(input.island)} write ${input.path} as JSON — a string, a number, a boolean, null, an array or a plain object`,
});
}
}
/**
* The zone a picture is rendered in. A harness that freezes the INSTANT and leaves the zone ambient
* renders every date in the host machine's, so the same state photographs differently on two
* machines and the review diff reports a component change that never happened.
*
* Its own class beside the one below, sharing one code: two conditions, two instructions, and a
* `fix:` assembled by a ternary is a `fix:` the gate's own scanner reads only half of.
*/
export class IslandStateZoneInvalidError extends UltimateError {
constructor(input: { readonly island: string; readonly value: unknown }) {
super({
code: 'X_TEST_ISLAND_STATE_CLOCK_INVALID',
cause: `timeZone ${renderCauseValue(input.value)} is not an IANA zone, so a rendered date would fall back to the host's`,
fix: `in ${at(input.island)} write: timeZone: 'UTC' — or one of Intl.supportedValuesOf('timeZone')`,
});
}
}
/** The frozen instant, with no offset on it — a different moment on every machine that reads it. */
export class IslandStateInstantInvalidError extends UltimateError {
constructor(input: { readonly island: string; readonly value: unknown }) {
super({
code: 'X_TEST_ISLAND_STATE_CLOCK_INVALID',
cause: `now ${renderCauseValue(input.value)} carries no explicit offset, so it means a different moment on every machine`,
fix: `in ${at(input.island)} write: now: '2026-01-01T00:00:00.000Z' — an ISO instant ending in Z or an offset`,
});
}
}
/**
* A route stub whose `match` cannot match. It is a `"<METHOD> <pathname>"` prefix, so a bare path
* or a lowercase verb silently stubs nothing: the component fetches for real, the request is
* refused by the sealed network, and the picture shows an error state nobody declared.
*/
export class IslandStateStubInvalidError extends UltimateError {
constructor(input: { readonly island: string; readonly match: string }) {
super({
code: 'X_TEST_ISLAND_STATE_STUB_INVALID',
cause: `route stub ${renderCauseValue(input.match)} is not "<METHOD> <pathname>", so it would match no request`,
fix: `in ${at(input.island)} write: { match: 'GET /api/settings', respond: { kind: 'json', body: {} } }`,
});
}
}
/**
* A states file that imports the component it describes. The whole design rests on this file being
* readable from Bun with no browser and no bundle — the command must know the complete expected
* screenshot list BEFORE a browser exists, or "produced nothing and exited 0" is indistinguishable
* from success. One JSX import makes the file unreadable by every consumer but the browser.
*/
export class IslandStatesNotPureError extends UltimateError {
constructor(input: { readonly file: string; readonly specifier: string }) {
super({
code: 'X_TEST_ISLAND_STATES_NOT_PURE',
cause: `${renderCauseValue(input.file)} imports ${renderCauseValue(input.specifier)} — a states file is pure data and is read without a browser`,
fix: `in ${renderFixLiteral(input.file, STATES_PLACEHOLDER)} delete the import of ${renderCauseValue(input.specifier)} and declare the props as JSON instead`,
});
}
}
/**
* A states file that value-imports a SIBLING module. Its own class beside the one above, sharing
* one code, because the edit is a different one: `./settings.island` resolves to
* `./settings.island.tsx` under Bun, so the import the reader must repair is usually a missing
* `type` keyword rather than an import to delete — and `./helpers` may reach the component one hop
* further on, which a rule reading ONE file's text can never follow.
*/
export class IslandStatesSiblingImportError extends UltimateError {
constructor(input: { readonly file: string; readonly specifier: string }) {
super({
code: 'X_TEST_ISLAND_STATES_NOT_PURE',
cause: `${renderCauseValue(input.file)} imports ${renderCauseValue(input.specifier)} at runtime, and a sibling module can reach the component the states file may not`,
fix: `in ${renderFixLiteral(input.file, STATES_PLACEHOLDER)} write: import type { Props } from ${renderFixLiteral(input.specifier, '<the specifier the cause names>')} — a type-only import is erased, and a value one must be inlined as JSON`,
});
}
}
/**
* A specifier this scanner cannot read: `import(`./${name}.island`)`, `require(SPEC)`. Its own
* class for the same reason as the one above — the edit is to write the specifier as a literal, so
* a static reader can judge it. Answering PURE over an unreadable import is the optimism the
* extensionless case already shipped once.
*/
export class IslandStatesOpaqueImportError extends UltimateError {
constructor(input: { readonly file: string; readonly expression: string }) {
super({
code: 'X_TEST_ISLAND_STATES_NOT_PURE',
cause: `${renderCauseValue(input.file)} computes the import ${renderCauseValue(input.expression)}, so no reader can say where it goes without running it`,
fix: `in ${renderFixLiteral(input.file, STATES_PLACEHOLDER)} write the specifier as a string literal, or delete the import and inline what it exports as JSON`,
});
}
}
/**
* A declared island that is not on disk. Always a real defect and never a warning: the state list
* is the expected screenshot set, so a manifest pointing at a moved file expands to pictures that
* can never be taken.
*/
export class IslandStatesMissingFileError extends UltimateError {
constructor(input: { readonly island: string; readonly root: string }) {
super({
code: 'X_TEST_ISLAND_STATES_MISSING_FILE',
cause: `no file at ${renderCauseValue(input.island)} under ${renderCauseValue(input.root)}, so its declared states can never be photographed`,
fix: `in ${at(input.island)} set island to the path that exists — it is relative to the app root, not to the states file`,
});
}
}
/**
* A name nothing answers to. Listing every valid name is the whole value: a typo and an island
* whose states were never declared are one symptom and two different edits, and only the list tells
* them apart without opening a directory.
*/
export class IslandStatesUnknownError extends UltimateError {
constructor(input: { readonly name: string; readonly known: readonly string[] }) {
super({
code: 'X_TEST_ISLAND_STATES_UNKNOWN',
cause:
input.known.length === 0
? `no island states are declared in this process, so ${renderCauseValue(input.name)} resolves to nothing`
: `no island states answer to ${renderCauseValue(input.name)}; declared: ${input.known.join(', ')}`,
fix:
input.known.length === 0
? "declare one beside the island: export const states = defineIslandStates({ island: 'apps/web/app/settings/settings.island.tsx', states: [...] })"
: `name one of them instead: ${input.known[0] ?? ''}`,
});
}
}
/**
* Two manifests answering to one name. Resolution is loose on purpose — `Settings`, `settings` and
* `settings.island.tsx` are one name — and that looseness is exactly what makes two islands with
* the same basename ambiguous. Refused when the set is loaded, not when a picture is missing.
*/
export class IslandStatesAmbiguousError extends UltimateError {
constructor(input: { readonly name: string; readonly islands: readonly string[] }) {
super({
code: 'X_TEST_ISLAND_STATES_AMBIGUOUS',
cause: `${input.islands.length} islands answer to ${renderCauseValue(input.name)}: ${input.islands.join(', ')}`,
fix: `rename one island file so the two basenames differ — the basename is the shot directory, so today they would share one`,
});
}
}
// The rules a declared island state must satisfy, as pure functions over values. Separate from
// `island-states.ts` so the vocabulary can be read without the rules and the rules can be tested
// without building a manifest; each answers a FAULT rather than throwing, so the caller is the one
// place that decides which code the failure carries.
/** A slug: lowercase letters and digits, single dashes between them. It becomes a filename stem. */
const STATE_ID = /^[a-z\d]+(?:-[a-z\d]+)*$/;
export const isStateId = (id: string): boolean => STATE_ID.test(id);
/** The id the author probably meant — `''` when nothing survives, which the error reads as "no suggestion". */
export function slugifyStateId(id: string): string {
return id
.toLowerCase()
.replace(/[^a-z\d]+/g, '-')
.replace(/^-+|-+$/g, '');
}
/**
* An instant with an EXPLICIT offset. `2026-01-01T00:00` parses on every runtime and means a
* different moment in every zone, which is the defect this vocabulary exists to close: a harness
* that pins the instant and leaves the zone ambient photographs two different pictures on two
* machines and neither is wrong.
*/
const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
export function isPinnedInstant(value: string): boolean {
return INSTANT.test(value) && Number.isFinite(Date.parse(value));
}
/**
* An IANA zone, asked of the runtime rather than of a list: `Intl` is the thing that will format
* the date, so its answer is the only one that matters. It throws a `RangeError` on a name it does
* not know — the one case where a `catch` is the check.
*/
export function isTimeZone(zone: string): boolean {
try {
new Intl.DateTimeFormat('en-US', { timeZone: zone });
return true;
} catch {
return false;
}
}
/** `"<METHOD> <pathname>"`, matched as a prefix by the harness. A lowercase verb matches nothing. */
const STUB_MATCH = /^[A-Z]+ \/\S*$/;
export const isStubMatch = (match: string): boolean => STUB_MATCH.test(match);
export interface JsonFault {
/** Where in the declaration the value sits — `props.user.createdAt`, `routes[0].respond.body`. */
readonly path: string;
/** What it is, phrased to finish "…is <reason>". */
readonly reason: string;
}
/** `[object Date]` → `Date`. Read this way because a `constructor.name` getter can throw. */
const tagOf = (value: object): string =>
Object.prototype.toString.call(value).slice('[object '.length, -1);
/**
* The first value in `value` that `JSON.stringify` would not carry, or `undefined` when the whole
* structure survives the trip. Island props ride `data-x-props`, which is JSON by construction
* (`@ultimat3/render`'s `emitIslandProps`), so anything else is not "approximately right" in the
* picture — it is a prop the component never receives, silently.
*
* Stricter than `JSON.stringify` on purpose, in the three places it degrades instead of failing:
* `undefined` disappears, a non-finite number becomes `null`, and a `Date` becomes a string that no
* longer answers `.getTime()`. Each is a state that photographs as a crash with nothing pointing
* back at the declaration.
*/
export function jsonFault(
value: unknown,
path: string,
seen: readonly object[] = [],
): JsonFault | undefined {
if (value === null) return undefined;
switch (typeof value) {
case 'string':
case 'boolean':
return undefined;
case 'number':
return Number.isFinite(value) ? undefined : { path, reason: 'not a finite number' };
case 'undefined':
return { path, reason: 'undefined, which JSON drops without a trace' };
case 'bigint':
return { path, reason: 'a bigint, which JSON cannot carry' };
case 'symbol':
return { path, reason: 'a symbol' };
case 'function':
return { path, reason: 'a function — a picture takes no callbacks' };
default:
break;
}
const object = value as object;
if (seen.includes(object)) return { path, reason: 'a cycle' };
const next = [...seen, object];
if (Array.isArray(object)) {
for (const [index, item] of object.entries()) {
const fault = jsonFault(item, `${path}[${index}]`, next);
if (fault !== undefined) return fault;
}
return undefined;
}
const proto: unknown = Object.getPrototypeOf(object);
if (proto !== Object.prototype && proto !== null) {
return { path, reason: `a ${tagOf(object)}, and only a plain object survives JSON` };
}
for (const [key, entry] of Object.entries(object)) {
const fault = jsonFault(entry, `${path}.${key}`, next);
if (fault !== undefined) return fault;
}
return undefined;
}
// The guard the whole design rests on: a `*.island.states.ts` file is PURE DATA, so the command
// that photographs the states, the harness page and a test can all read it — and one import of a
// sibling module leaves it readable by a bundler alone. Static, because a module importing Solid
// evaluates fine under Bun, so no amount of loading it can notice.
import {
IslandStatesNotPureError,
IslandStatesOpaqueImportError,
IslandStatesSiblingImportError,
} from './island-state-errors';
/** Line and block comments blanked, so a specifier written in prose is never read as an import. */
const stripComments = (source: string): string =>
source.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1');
/**
* One edge of the file's module graph, carrying the single distinction that decides whether the
* edge EXISTS at runtime: `verbatimModuleSyntax` erases a statement that begins `import type` /
* `export type` and keeps every other one — including `import { type X } from './y'`, which it
* emits as `import {} from './y'` and which therefore evaluates `./y`.
*/
export interface ModuleEdge {
readonly specifier: string;
readonly typeOnly: boolean;
}
/**
* `from '<spec>'`, `import '<spec>'`, `import('<spec>')`, `require('<spec>')` — every static and
* dynamic edge a Bun module graph can have, read off the text rather than by loading it. The
* leading `type` is captured with the statement rather than guessed at afterwards; the lookahead
* is what keeps `import type from './y'` (a default import NAMED type) a value import.
*/
const EDGE =
/\b(?:import|export)\s+(?<only>type\s+(?!from\s*['"]))?[^'"();]*?\bfrom\s*['"](?<from>[^'"]+)['"]|\bimport\s*\(\s*['"](?<dynamic>[^'"]+)['"]|\brequire\s*\(\s*['"](?<required>[^'"]+)['"]|\bimport\s+['"](?<bare>[^'"]+)['"]/g;
/** Every module edge the text declares, in source order, type-only ones included. */
export function moduleEdges(source: string): readonly ModuleEdge[] {
const edges: ModuleEdge[] = [];
for (const match of stripComments(source).matchAll(EDGE)) {
const groups = match.groups;
if (groups === undefined) continue;
const from = groups['from'];
const specifier = from ?? groups['dynamic'] ?? groups['required'] ?? groups['bare'];
if (specifier === undefined) continue;
edges.push({ specifier, typeOnly: from !== undefined && groups['only'] !== undefined });
}
return edges;
}
/** Backwards-compatible view: the specifiers alone, which cannot say whether one is erased. */
export function importSpecifiers(source: string): readonly string[] {
return moduleEdges(source).map((edge) => edge.specifier);
}
/**
* An `import(…)` / `require(…)` whose argument is not a string literal. Captured up to the closing
* paren or the end of the line, so the refusal can quote the expression the reader must rewrite.
*/
const COMPUTED = /\b(?:import|require)\s*\(\s*(?!['")])(?<expression>[^)\n]{1,60})/g;
/** A specifier resolved against this file's own directory, `.json` excluded — see below. */
const RELATIVE = /^\.\.?\//;
/**
* A specifier that is PROVABLY a browser module: JSX, or the renderer one import earlier. Its own
* predicate because it is what the older refusal — delete this import — is the right edit for.
*/
export function browserSpecifier(specifier: string): boolean {
return (
specifier === 'solid-js' ||
specifier.startsWith('solid-js/') ||
specifier.endsWith('.tsx') ||
specifier.endsWith('.jsx')
);
}
/**
* What a states file may not reach for at RUNTIME, which is a wider set than the one above.
*
* Any RELATIVE specifier counts, and that is the rule with teeth: it names a module whose own
* imports this scanner never sees. `./settings.island` resolves to `./settings.island.tsx` under
* Bun and read as pure until 2026-08-23 — an extension test answers about the spelling and not
* about the graph — and `./helpers` is the same edge one hop longer. The relativeness is the rule
* rather than the `.island` stem, which is also why nothing here restates `@ultimat3/render`'s
* `ISLAND_EXTENSION`.
*
* `.json` is the one exemption, and it is the only one that can be safe: a JSON module has no
* imports, so it is the single relative target whose graph needs no following.
*
* A BARE specifier other than `solid-js` is not followed either and is deliberately not refused —
* a package list here would be the drift a list always is. `@ultimat3/ui` in a states file is
* therefore an open hole, named in this package's CLAUDE.md rather than left silent.
*/
export function impureSpecifier(specifier: string): boolean {
return browserSpecifier(specifier) || (RELATIVE.test(specifier) && !specifier.endsWith('.json'));
}
/** Which refusal the fault carries — the reader's edit differs by kind, so the class does too. */
export type IslandFaultKind = 'browser' | 'sibling' | 'opaque';
export interface IslandStatesFault {
readonly kind: IslandFaultKind;
/** The specifier, or — for `opaque` — the expression text that stands where one should be. */
readonly specifier: string;
}
/**
* The first thing that breaks the rule, or `undefined`. NOT total: a specifier this cannot read is
* reported as `opaque` rather than passed, because "unreadable, therefore pure" is exactly the
* optimism that let an extensionless import of the component through.
*/
export function islandStatesFault(source: string): IslandStatesFault | undefined {
for (const edge of moduleEdges(source)) {
// A type-only edge is erased before the module is ever evaluated — proved against Bun in
// `island-states-pure.test.ts`, because the whole exemption rests on it. It is also the one
// way a states file may name its component, and the reference app types its props that way.
if (edge.typeOnly) continue;
if (!impureSpecifier(edge.specifier)) continue;
// A specifier that is PROVABLY a browser module keeps the older refusal and its older edit —
// deleting `import './x.island.tsx'` is the instruction, not writing it as a type import.
return {
kind: browserSpecifier(edge.specifier) ? 'browser' : 'sibling',
specifier: edge.specifier,
};
}
const computed = stripComments(source).match(COMPUTED);
const expression = computed?.[0]?.replace(/^\s*(?:import|require)\s*\(\s*/, '').trim();
return expression === undefined ? undefined : { kind: 'opaque', specifier: expression };
}
/** The first specifier that breaks the rule, or `undefined`. */
export function islandStatesImportFault(source: string): string | undefined {
return islandStatesFault(source)?.specifier;
}
/** The same rule, as a refusal. `file` is only ever named back to the reader, never resolved here. */
export function assertIslandStatesPure(file: string, source: string): void {
const fault = islandStatesFault(source);
if (fault === undefined) return;
if (fault.kind === 'opaque') {
throw new IslandStatesOpaqueImportError({ file, expression: fault.specifier });
}
const input = { file, specifier: fault.specifier };
throw fault.kind === 'sibling'
? new IslandStatesSiblingImportError(input)
: new IslandStatesNotPureError(input);
}
// Resolution, in the two directions a caller needs it: a NAME a reader typed → the manifest it
// meant, and a manifest → the island file it claims on disk. Loose on the way in and strict on the
// way out — a typo must never be a silent miss, so an unresolved name lists every valid one.
import { join } from 'node:path'; // why: no Bun native joins a path; `Bun.file` takes one already joined.
import {
IslandStatesAmbiguousError,
IslandStatesMissingFileError,
IslandStatesUnknownError,
} from './island-state-errors';
import type { IslandStatesManifest } from './island-states';
/**
* `Settings`, `settings`, `settings.island.tsx` and `apps/web/app/settings/settings.island.tsx` are
* one name. Case and separators are dropped because a reader types the component the way it is
* spelled in JSX and the file the way it is spelled on disk, and neither is wrong.
*/
export function normalizeIslandName(value: string): string {
const base = value.split('/').pop() ?? value;
const stem = base.split('.')[0] ?? base;
return stem.toLowerCase().replace(/[^a-z\d]+/g, '');
}
/** Every name a reader may type, in manifest order — what an unresolved lookup reports back. */
export const islandStatesNames = (all: readonly IslandStatesManifest[]): readonly string[] =>
all.map((manifest) => manifest.name);
/** Manifests answering to `name`. Zero and two are both failures, and different ones. */
export function islandStatesMatching(
all: readonly IslandStatesManifest[],
name: string,
): readonly IslandStatesManifest[] {
const wanted = normalizeIslandName(name);
return all.filter((manifest) => normalizeIslandName(manifest.name) === wanted);
}
/**
* The one refusal in this vocabulary. Everything downstream falls back — a mistyped THEME still
* shows the component — but a name nothing answers to has no defensible fallback: photographing
* some other island would be a picture that reads as an answer.
*/
export function findIslandStates(
all: readonly IslandStatesManifest[],
name: string,
): IslandStatesManifest {
const matches = islandStatesMatching(all, name);
const first = matches[0];
if (first === undefined) {
throw new IslandStatesUnknownError({ name, known: islandStatesNames(all) });
}
if (matches.length > 1) {
throw new IslandStatesAmbiguousError({
name,
islands: matches.map((manifest) => manifest.island),
});
}
return first;
}
/**
* Two manifests one name would resolve to. Asked of the whole SET, once, rather than discovered by
* a lookup that happens to be made: two islands sharing a basename also share a shot directory, so
* the collision loses pictures whether or not anyone ever types the ambiguous name.
*/
export function assertUniqueIslandStates(all: readonly IslandStatesManifest[]): void {
const byName = new Map<string, IslandStatesManifest[]>();
for (const manifest of all) {
const key = normalizeIslandName(manifest.name);
byName.set(key, [...(byName.get(key) ?? []), manifest]);
}
for (const [name, group] of byName) {
if (group.length > 1) {
throw new IslandStatesAmbiguousError({
name,
islands: group.map((manifest) => manifest.island),
});
}
}
}
/**
* Declared islands with no file under `root`. Not part of `defineIslandStates`: a declaration is
* evaluated wherever the module is imported from, and a rule that reads the filesystem at import
* time fails on the cwd rather than on the path. The check belongs where a root is known — the
* guard test, and the command that takes the pictures.
*/
export async function missingIslandFiles(
all: readonly IslandStatesManifest[],
root: string,
): Promise<readonly string[]> {
const missing: string[] = [];
for (const manifest of all) {
if (!(await Bun.file(join(root, manifest.island)).exists())) missing.push(manifest.island);
}
return missing;
}
/** The same check as a refusal, naming the first island that is not there. */
export async function assertIslandFiles(
all: readonly IslandStatesManifest[],
root: string,
): Promise<void> {
const missing = await missingIslandFiles(all, root);
const first = missing[0];
if (first !== undefined) throw new IslandStatesMissingFileError({ island: first, root });
}
// The vocabulary for declaring the STATES one island can be photographed in — error, empty,
// over-quota, read-only: the ones a reviewer cannot reach by clicking. Types and constants only, so
// a `settings.island.states.ts` beside the island is readable without a browser or a bundle;
// `define-island-states.ts` is what validates one, and nothing here imports anything at all.
export const ISLAND_THEMES = ['light', 'dark'] as const;
export type IslandTheme = (typeof ISLAND_THEMES)[number];
/** What an address falls back to. A mistyped theme shows the component, never an error page. */
export const DEFAULT_ISLAND_THEME: IslandTheme = 'light';
export interface IslandViewport {
readonly width: number;
readonly height: number;
}
/** A laptop, not a phone: the state under review is the component's, not the breakpoint's. */
export const DEFAULT_ISLAND_VIEWPORT: IslandViewport = { width: 1280, height: 800 };
/**
* The zone every picture is rendered in unless a manifest says otherwise. Pinned in the VOCABULARY
* rather than left to the harness: a run that freezes the instant and leaves the zone ambient
* renders `12:00` on one machine and `14:00` on the next, and the diff between two reviews then
* says the component changed when only the reviewer moved.
*/
export const ISLAND_SHOT_TIME_ZONE = 'UTC';
export type IslandStubResponse =
| { readonly kind: 'json'; readonly status?: number; readonly body: unknown }
| { readonly kind: 'pending' }
| { readonly kind: 'offline' };
export interface IslandRouteStub {
/** `"<METHOD> <pathname>"`, matched as a PREFIX: `'GET /api/quota'` catches its query string. */
readonly match: string;
readonly respond: IslandStubResponse;
}
export interface IslandStateDecl {
/** A slug. It becomes the screenshot filename stem, so it is guessable or it is nothing. */
readonly id: string;
/** One line: what this state IS. */
readonly title: string;
/** WHY it deserves a picture — usually "you cannot reach this by clicking, because …". */
readonly note?: string;
/** JSON, by construction: these ride the same `data-x-props` seam hydration already uses. */
readonly props: Readonly<Record<string, unknown>>;
/** Fixtures for whatever the component fetches on its own. */
readonly routes?: readonly IslandRouteStub[];
readonly viewport?: IslandViewport;
/** Both, unless the state is only meaningful in one of them. */
readonly themes?: readonly IslandTheme[];
}
export interface IslandStatesDecl {
/** App-root-relative path of the `.island.tsx` these states belong to. */
readonly island: string;
readonly states: readonly IslandStateDecl[];
readonly viewport?: IslandViewport;
/** CSS selector to crop to. The island's host element when absent. */
readonly target?: string;
/** IANA zone. `UTC` unless the state under review is about a zone. */
readonly timeZone?: string;
/** The frozen instant, with an explicit offset. The suite's own `DEFAULT_NOW` when absent. */
readonly now?: string;
}
/** A declared state with every default resolved — what a target is expanded from. */
export interface IslandState {
readonly id: string;
readonly title: string;
readonly note?: string;
readonly props: Readonly<Record<string, unknown>>;
readonly routes: readonly IslandRouteStub[];
readonly viewport: IslandViewport;
readonly themes: readonly IslandTheme[];
}
/**
* Registered globally so two copies of this module agree on what a manifest is — the same reason
* `@ultimat3/render`'s island node carries one. It is what lets a loader read an app's states file
* and tell a manifest from every other export in it without trusting a name.
*/
export const ISLAND_STATES: unique symbol = Symbol.for('ultimate.testing.island-states') as never;
export interface IslandStatesManifest {
readonly [ISLAND_STATES]: true;
/** Derived from the island's own filename: the shot directory, and the name a reader types. */
readonly name: string;
readonly island: string;
readonly states: readonly IslandState[];
readonly viewport: IslandViewport;
readonly target?: string;
readonly timeZone: string;
readonly now: string;
}
export function isIslandStatesManifest(value: unknown): value is IslandStatesManifest {
return typeof value === 'object' && value !== null && ISLAND_STATES in value;
}
/**
* `apps/web/app/settings/settings.island.tsx` → `settings`. The basename up to its FIRST dot, so
* the island extension is never restated in this package — it is `@ultimat3/render`'s constant and
* a second copy of it here is a second thing to keep in step.
*/
export function islandStatesName(island: string): string {
const base = island.split('/').pop() ?? island;
return (base.split('.')[0] ?? base).toLowerCase();
}
+11
-1

@@ -63,3 +63,3 @@ # @ultimat3/testing — boundary

| An island needs a BUILDER, not an import | `buildIslands` is `@ultimat3/cli`'s and both packages are tier 5; the one declared edge is `cli → testing`, so the reverse is a `bun run boundaries` failure. `mountIsland({ build, root, file })` takes the function as a parameter and declares only the two fields it reads — `IslandChunkLike` is `{ file, code }`, so a CSS artifact, a source map or a dev/production flag the bundler grows is invisible here. Moving the bundler down a tier was the alternative and it drags `@babel/core` and `babel-preset-solid` with it, into a package whose whole point is being importable from tier 0 |
| The micro-DOM is the fixture's, once | `island-dom.ts`. `bun test` has no DOM and no DOM library may be added; `generate: 'dom'` builds every element from `_$template("<label …>")`, so a stub without a parsed `<template>` cannot run one line of a compiled island. It lived twice, ~200 lines each, in `packages/cli/src/island-bundle.test.ts` and the reference app's island test |
| The micro-DOM is the fixture's, once **for islands** | `island-dom.ts`. **"Once" is scoped to this job, and said so only from 2026-08-23**: `packages/ui/src/fake-dom.ts` is a second micro-DOM (201 lines, test-only, off that package's barrel) for a different one — focus, `activeElement`, `contains` and a `:not()` selector grammar for keyboard code, none of which parses a `<template>`. It is not a copy to collapse: `ui` is tier 4 and this package is tier 5, so `ui -> testing` is an upward import the boundary check refuses, and the merge would need the shared half moved down to a tier neither grammar belongs in. `bun test` has no DOM and no DOM library may be added; `generate: 'dom'` builds every element from `_$template("<label …>")`, so a stub without a parsed `<template>` cannot run one line of a compiled island. It lived twice, ~200 lines each, in `packages/cli/src/island-bundle.test.ts` and the reference app's island test |
| `style` and `classList` RECORD, `As of 2026-08` | `FakeStyle` is one declaration map behind all four spellings compiled Solid uses on one element: a STATIC entry baked into the template's `style=` attribute, a dynamic one through `setStyleProperty` → `style.setProperty`, a whole-object or string prop through `style()` → `cssText`, and a cleared one through `removeAttribute`. It was `style = {}` until 2026-08-21 — `<Form>`, `<Stack>`, `<Grid>` and `<Container>` each set a CSS custom property, so every one of them died inside `mount` with `e.style.setProperty is not a function`, and `x g resource` emitted a plain `<form>` rather than the design system's. A design-system component kept out of generated code by the limits of a TEST DOUBLE. A no-op `setProperty` would have stopped the crash and left "the component set `--form-gap`" unassertable, which is the same hole one layer down |

@@ -71,2 +71,12 @@ | `classList` is the class attribute | not a list of its own, so `classList.toggle` — what the compiler emits INLINE for `classList={{ … }}`, with no runtime helper in front of it — and `className` can never answer one element two ways. `add` was the only method the stand-in had and is the one Solid never calls |

| `fire` answers whether a handler ran | a selector matching nothing and an island that attached no handler are the same silence otherwise — the second is a bug, the first a typo. It reads Solid's delegated `$$click` property or an `addEventListener` listener; a compiled island uses one or the other |
| A states file is PURE DATA, and it is enforced | `defineIslandStates` declares the states an island can be photographed in — error, empty, over-quota, read-only — in a sibling file (`settings.island.states.ts`) that may not import the component, JSX or `solid-js`. Three consumers read that file and only one of them has a browser: the command that takes the pictures (which must know the complete expected list BEFORE a browser exists, or "produced nothing and exited 0" reads as success), the harness page, and `island-states-guard.test.ts`. `assertIslandStatesPure` is the static rule — `X_TEST_ISLAND_STATES_NOT_PURE` — because a module that imports Solid still evaluates perfectly well under Bun, so nothing dynamic can catch it |
| The rule is the RELATIVENESS, not the extension, `As of 2026-08-23` | the scan refused `solid-js` and a specifier ENDING in `.tsx`/`.jsx`, and `import { X } from './settings.island'` resolves to `./settings.island.tsx` under Bun — so the guard answered PURE for a file that drags Solid into a browser-free process, which is worse than no guard. Every RELATIVE runtime specifier is refused now (`IslandStatesSiblingImportError`), because `./helpers` reaches the component one hop further on and this scanner reads ONE file's text. `.json` is the one exemption: a JSON module has no imports, so it is the single relative target whose graph needs no following. That rule needs no copy of `ISLAND_EXTENSION` either, which is the row below still holding |
| `import type` is not an import, and it is the one way to name the component | `verbatimModuleSyntax` erases a statement that BEGINS `import type` / `export type` — proved against Bun in `island-states-pure.test.ts`, which writes the pair to disk and asserts the sibling never evaluated, because the whole exemption rests on it. `examples/dummy`'s states file types its props that way and the rule may never refuse it. The other direction is the one that would leave the hole open: `import { type X } from './y'` is emitted as `import {} from './y'` and DOES evaluate `./y`, so an inline modifier is a runtime edge and is refused. Both directions are pinned |
| Unreadable is not pure | a computed specifier — ``import(`./${name}.island`)``, `require(SPEC)` — is refused as `IslandStatesOpaqueImportError` rather than passed. "A file it cannot read is pure" was this scanner's stated totality and it is the same optimism the extensionless case shipped with |
| What the scan does NOT follow, stated rather than silent | a BARE specifier other than `solid-js` (`@ultimat3/ui` re-exports Solid components and is not refused — a package list here would be the drift a list always is), an ABSOLUTE path specifier (`packages/cli`'s own test tree and this package's guard test both import the barrel by absolute path, which is the only way to reach it from a scratch directory with no `node_modules`), and a specifier inside a string LITERAL, which is read as an import. The first two are holes; the third is a false refusal, and the safe direction of the two |
| Props are JSON or they are refused | they ride `data-x-props`, which `@ultimat3/render`'s `emitIslandProps` `JSON.stringify`s, so anything else is a prop the component never receives. `jsonFault` is stricter than `JSON.stringify` in exactly the three places that DEGRADE rather than throw: `undefined` disappears, a non-finite number becomes `null`, and a `Date` becomes a string that no longer answers `.getTime()` |
| The clock is pinned in the vocabulary, zone included | `timeZone` defaults to `ISLAND_SHOT_TIME_ZONE` (`UTC`) and `now` to this package's own `DEFAULT_NOW`, and both ride onto every `IslandShotTarget`. A harness that freezes the instant and leaves the ZONE ambient photographs `12:00` on one machine and `14:00` on the next; the review diff then reports a component change that never happened |
| Loose in, strict out | `findIslandStates` resolves `Settings`, `settings`, `settings.island.tsx` and the full path to one manifest, and refuses a name nothing answers to by listing EVERY valid one — a typo and an island whose states were never declared are one symptom and two edits. It is the only refusal in the vocabulary: `parseIslandAddress` falls back on an unknown theme instead, because a page that renders an error over a typo turns a mistyped address into a screenshot of the framework |
| The disk check is not in `defineIslandStates` | a declaration evaluates wherever it is imported from, so a rule that reads the filesystem at import time fails on the cwd rather than on the path. `assertIslandFiles(manifests, root)` is separate and belongs to whoever knows a root — the guard test, and the command |
| The island EXTENSION is not restated here | `.island.tsx` is `@ultimat3/render`'s `ISLAND_EXTENSION` and `render` is not a dependency of this package. A manifest's `name` is therefore the island basename up to its FIRST dot, which needs no copy of that constant — and the shot directory is that name, so two islands sharing a basename are `X_TEST_ISLAND_STATES_AMBIGUOUS` rather than two sets of pictures in one folder |
| The chunk is imported from a temp FILE, `As of 2026-08-21` | `mkdtemp`ed on the FIRST mount and named by the chunk's SHA-256, so an edited island is a different module rather than a cache hit on the same path and no test leaves a `.mjs` behind in the app it just built. It was a `data:` URL until 2026-08-21 and that read better: `bun test --coverage` panics with `range end index N out of range for slice of length 4096` on `import()` of any `data:` module past ~4 kB, and an island chunk is 12-55 kB — so every island test dumped core in the per-package CI job while the root gate stayed green. Measured on Bun 1.4.0; `fixture-island.test.ts` pins the file form as a source rule, because the failure is invisible to a `bun test` without `--coverage` |

@@ -73,0 +83,0 @@ | The scratch directory is lazy and removed, `As of 2026-08-22` | `mkdtempSync` ran at MODULE scope and nothing removed it, so every process importing `@ultimat3/testing` at all — this module is on the `.` barrel, so `expect` alone did it — left one directory in `/tmp` forever. Created on the first `modulePathFor` and `rmSync`ed from `process.on('exit')`: the handler has to be synchronous, and it is per PROCESS while `MountedIsland`'s `Disposable` is per mount and is never reached by a mount that threw. `fixture-island-cleanup.test.ts` asserts both halves from a CHILD process, which is the only place either is observable |

+12
-12
{
"name": "@ultimat3/testing",
"version": "11.1.0",
"version": "11.2.0",
"description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",

@@ -36,14 +36,14 @@ "license": "MIT",

"dependencies": {
"@ultimat3/cache": "11.1.0",
"@ultimat3/core": "11.1.0",
"@ultimat3/db": "11.1.0",
"@ultimat3/entity": "11.1.0",
"@ultimat3/i18n": "11.1.0",
"@ultimat3/jobs": "11.1.0",
"@ultimat3/mail": "11.1.0",
"@ultimat3/policy": "11.1.0",
"@ultimat3/query": "11.1.0",
"@ultimat3/realtime": "11.1.0",
"@ultimat3/time": "11.1.0"
"@ultimat3/cache": "11.2.0",
"@ultimat3/core": "11.2.0",
"@ultimat3/db": "11.2.0",
"@ultimat3/entity": "11.2.0",
"@ultimat3/i18n": "11.2.0",
"@ultimat3/jobs": "11.2.0",
"@ultimat3/mail": "11.2.0",
"@ultimat3/policy": "11.2.0",
"@ultimat3/query": "11.2.0",
"@ultimat3/realtime": "11.2.0",
"@ultimat3/time": "11.2.0"
}
}

@@ -25,2 +25,8 @@ # @ultimat3/testing

| `island-dom.ts` | the micro-DOM `mountIsland` drives: what compiled Solid touches, and nothing else |
| `island-states.ts` | the vocabulary: what a photographable island STATE is. Types and constants, importing nothing |
| `define-island-states.ts` | `defineIslandStates()` — one manifest, validated and frozen, with every default resolved |
| `island-states-check.ts` | the rules a declaration must satisfy, as pure functions answering a fault |
| `island-states-pure.ts` | the guard the design rests on: a `*.island.states.ts` file reaches no browser and no bundler |
| `island-shot-targets.ts` | the expansion — one record per PICTURE — and `islandAddress` / `parseIslandAddress`, inverses |
| `island-states-resolve.ts` | a name a reader typed → the manifest it meant; a manifest → the island file it claims |
| `framework-fixtures.ts` | registers both sets; the app registers only what it owns |

@@ -282,2 +288,74 @@ | `registry-leak-guard.ts` | fails the run naming the FILE that left a process-global registry dirty, and restores the ones that can be restored at the same boundary |

## Declaring the states an island can be photographed in
A reviewer can click their way to most of a component. They cannot click their way to *the account
is read-only*, *the workspace is over quota* or *the request failed* — so those states are declared,
beside the island, in a file that is **pure data**:
```ts
// apps/web/app/settings/settings.island.states.ts
import { defineIslandStates } from '@ultimat3/testing';
export const settingsStates = defineIslandStates({
island: 'apps/web/app/settings/settings.island.tsx',
target: '[data-settings]', // what to crop to; the island's host element otherwise
states: [
{
id: 'over-quota', // a slug: it becomes the screenshot filename stem
title: 'the workspace is over quota',
note: 'you cannot reach this by clicking — billing sets the flag, not the UI',
props: { quota: { used: 120, limit: 100 } },
routes: [{ match: 'GET /api/quota', respond: { kind: 'json', body: { used: 120 } } }],
themes: ['dark'], // both, when the key is absent
},
],
});
```
`islandShotTargets(manifest)` expands that to one record per picture —
`{ island, name, state, theme, viewport, target, timeZone, now, file, query }` — where `file` is
`settings/over-quota-dark.png` and `query` is the harness address that renders exactly it.
`parseIslandAddress` is that address's inverse, and it is **total**: an unknown theme falls back to
`light` rather than photographing an error page.
**Pure data is the constraint, not a preference.** The command that takes the pictures has to know
the complete expected list BEFORE a browser exists, or "produced nothing and exited 0" is
indistinguishable from success — and the harness page and this package's own guard test read the
same file. One `import './settings.island.tsx'` makes it readable by a bundler alone, so
`assertIslandStatesPure` refuses it (`X_TEST_ISLAND_STATES_NOT_PURE`).
**And no RUNTIME import of a sibling, `As of 2026-08-23`.** The rule is the relativeness, not the
extension: `./settings.island` resolves to `./settings.island.tsx` under Bun, and `./helpers` may
reach the component one hop further on — a scanner reading ONE file's text can follow neither. A
computed specifier — ``import(`./${name}.island`)`` — is refused for the same reason, because a
specifier nothing can read is not a specifier anything may call pure.
**`import type` is the one way to reach the component, and it is not an import.**
`verbatimModuleSyntax` erases a statement that BEGINS `import type` / `export type`, so
`import type { SettingsProps } from './settings.island'` costs the file nothing and types its props
against the component. An inline modifier does not: `import { type X } from './y'` is emitted as
`import {} from './y'`, which evaluates `./y`, and is refused.
| A states file writes | Verdict |
|---|---|
| `import { defineIslandStates } from '@ultimat3/testing'` | allowed — a bare specifier |
| `import type { Props } from './x.island'` | allowed — erased before anything evaluates |
| `import props from './props.json' with { type: 'json' }` | allowed — a JSON module imports nothing |
| `import { X } from './x.island'` · `./helpers` · `../shared/props` | refused — a graph this cannot follow |
| `import { type X } from './x.island'` | refused — the statement survives erasure |
| `import './x.island.tsx'` · `solid-js` | refused — JSX and a renderer |
| ``await import(`./${name}.island`)`` | refused — unreadable, and unreadable is not pure |
**Props are JSON or they are refused.** They ride the same `data-x-props` script tag hydration
already uses, so a `Date`, a function or an `undefined` is not "approximately right" in the picture
— it is a prop the component never receives. `X_TEST_ISLAND_STATE_JSON_INVALID` names the path.
**The clock is pinned in the vocabulary, zone included.** `timeZone` defaults to `UTC` and `now` to
this package's own `DEFAULT_NOW`. A harness that freezes the instant and leaves the zone ambient
renders `12:00` on one machine and `14:00` on the next, and the review diff then says the component
changed when only the reviewer moved.
**The command that takes the pictures is not here yet.** `As of 2026-08-23` this package ships the
vocabulary, the expansion and the refusals; the browser half is `x shot`'s.
## Errors

@@ -287,3 +365,7 @@

`X_TEST_FACTORY_TRAIT_UNKNOWN` `X_TEST_FACTORY_NOT_PERSISTED` `X_TEST_REGISTRY_LEAK`
`X_TEST_ISLAND_NOT_BUILT` `X_TEST_ISLAND_NO_MOUNT`
`X_TEST_ISLAND_NOT_BUILT` `X_TEST_ISLAND_NO_MOUNT` `X_TEST_ISLAND_STATES_EMPTY`
`X_TEST_ISLAND_STATES_NOT_PURE` `X_TEST_ISLAND_STATES_MISSING_FILE` `X_TEST_ISLAND_STATES_UNKNOWN`
`X_TEST_ISLAND_STATES_AMBIGUOUS` `X_TEST_ISLAND_STATE_ID_INVALID` `X_TEST_ISLAND_STATE_DUPLICATE`
`X_TEST_ISLAND_STATE_JSON_INVALID` `X_TEST_ISLAND_STATE_CLOCK_INVALID`
`X_TEST_ISLAND_STATE_STUB_INVALID`

@@ -290,0 +372,0 @@ ## One process, one registry

@@ -32,2 +32,14 @@ // The X_* codes owned by @ultimat3/testing. A test failure has to be as actionable as a runtime

'X_TEST_ISLAND_NO_MOUNT',
// Declared here and thrown from `island-state-errors.ts`: one file has one job and this
// catalogue is at its ceiling, so the classes moved and the registration did not.
'X_TEST_ISLAND_STATES_EMPTY',
'X_TEST_ISLAND_STATES_NOT_PURE',
'X_TEST_ISLAND_STATES_MISSING_FILE',
'X_TEST_ISLAND_STATES_UNKNOWN',
'X_TEST_ISLAND_STATES_AMBIGUOUS',
'X_TEST_ISLAND_STATE_ID_INVALID',
'X_TEST_ISLAND_STATE_DUPLICATE',
'X_TEST_ISLAND_STATE_JSON_INVALID',
'X_TEST_ISLAND_STATE_CLOCK_INVALID',
'X_TEST_ISLAND_STATE_STUB_INVALID',
] as const;

@@ -55,2 +67,13 @@

X_TEST_ISLAND_NO_MOUNT: 'an island chunk exports no mount function',
X_TEST_ISLAND_STATES_EMPTY: 'an island state manifest declares no states',
X_TEST_ISLAND_STATES_NOT_PURE:
'an island states file imports the component, a renderer or a sibling module',
X_TEST_ISLAND_STATES_MISSING_FILE: 'an island state manifest names an island that is not on disk',
X_TEST_ISLAND_STATES_UNKNOWN: 'no island state manifest answers to that name',
X_TEST_ISLAND_STATES_AMBIGUOUS: 'two island state manifests answer to one name',
X_TEST_ISLAND_STATE_ID_INVALID: 'an island state id is not slug-shaped',
X_TEST_ISLAND_STATE_DUPLICATE: 'two island states share one id',
X_TEST_ISLAND_STATE_JSON_INVALID: 'an island state carries a value JSON does not',
X_TEST_ISLAND_STATE_CLOCK_INVALID: 'an island state manifest pins a clock that is not pinnable',
X_TEST_ISLAND_STATE_STUB_INVALID: 'an island route stub is not "<METHOD> <pathname>"',
};

@@ -57,0 +80,0 @@

@@ -26,2 +26,6 @@ export type {

export { afterAll, afterEach, beforeAll, beforeEach, describe, expect } from 'bun:test';
// The island-state vocabulary. Pure data by design: a `*.island.states.ts` file is read by the
// command that photographs the states, by the harness page and by a guard test — none of which has
// a bundler, and only one of which has a browser.
export { defineIslandStates } from './define-island-states';
export type { DeterminismOptions, DeterminismSnapshot } from './determinism';

@@ -119,2 +123,64 @@ // `captureDeterminism` + `restoreCapturedDeterminism` are the pair a NESTED install needs;

export type { FakeElement, FakeNode, FakeText } from './island-dom';
export type { IslandAddress, IslandShotTarget } from './island-shot-targets';
export {
isIslandTheme,
islandAddress,
islandShotFile,
islandShotPlan,
islandShotTargets,
parseIslandAddress,
} from './island-shot-targets';
// The file an error tells the reader to edit — the island's own name with `.states.ts` where
// `.tsx` was. Exported so the command that takes the pictures names the same file the refusal does.
export { islandStatesFile } from './island-state-errors';
export type {
IslandRouteStub,
IslandState,
IslandStateDecl,
IslandStatesDecl,
IslandStatesManifest,
IslandStubResponse,
IslandTheme,
IslandViewport,
} from './island-states';
export {
DEFAULT_ISLAND_THEME,
DEFAULT_ISLAND_VIEWPORT,
ISLAND_SHOT_TIME_ZONE,
ISLAND_STATES,
ISLAND_THEMES,
isIslandStatesManifest,
islandStatesName,
} from './island-states';
export type { JsonFault } from './island-states-check';
export {
isPinnedInstant,
isStateId,
isStubMatch,
isTimeZone,
jsonFault,
slugifyStateId,
} from './island-states-check';
export type {
IslandFaultKind,
IslandStatesFault,
ModuleEdge,
} from './island-states-pure';
export {
assertIslandStatesPure,
importSpecifiers,
impureSpecifier,
islandStatesFault,
islandStatesImportFault,
moduleEdges,
} from './island-states-pure';
export {
assertIslandFiles,
assertUniqueIslandStates,
findIslandStates,
islandStatesMatching,
islandStatesNames,
missingIslandFiles,
normalizeIslandName,
} from './island-states-resolve';
export type { LiveConnection, LiveNodeHandle, LiveNodeOptions } from './live-node';

@@ -121,0 +187,0 @@ export { createLiveNode } from './live-node';