Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .dev/browser/blocked-icon-sets.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Whole packages that are never sampled or counted, by composer name, with the reason. */
export const BLOCKED_PACKAGES: Readonly<Record<string, string>>;
/** Why an icon (or a whole pack) may not be sampled, or `undefined` when it may. */
export function blockedReason(pkg: string): string | undefined;
34 changes: 34 additions & 0 deletions .dev/browser/blocked-icon-sets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Icon packs the dev fixture must never sample, count or list. The one list:
// `scripts/generate-fixture.mjs` skips each pack whole, and
// `src/core/blockedIconSets.test.ts` proves the generator drops every entry.
//
// It mirrors ichava/react-landingpage's `scripts/check-landing-data.mjs` (INTERNAL and
// check 8, react-landingpage#40). That package carries a copy of this fixture's
// `icons.json`, so the two lists must move together: a pack allowed here and blocked
// there fails the landing check on the next copy.
//
// - Metronic is licensed to Simtabi LLC for internal and client work only and may never
// appear on a public surface (decided 2026-10-26).
// - icon-sets-bundled is private to Simtabi LLC, for internal and client use only and not
// redistributable (decided 2026-10-26). Blocking the whole pack replaced the per-set
// list that blocked its GPL (icomoon), unverified and mislabelled sets.
//
// A blocked pack is excluded entirely: no sample, no asset, no entry in `packages`, and
// no share of `total_ecosystem`. The landing page counts the same way, so the two totals
// agree.

/** Whole packages that are never sampled or counted, by composer name, with the reason. */
export const BLOCKED_PACKAGES = Object.freeze({
'ichava/icon-sets-metronic': 'internal and client use only',
'ichava/icon-sets-bundled': 'private, internal and client use only, not redistributable',
});

/**
* Why an icon (or a whole pack) may not be sampled, or `undefined` when it may.
*
* @param {string} pkg the composer name, e.g. `ichava/icon-sets-bundled`
* @returns {string | undefined}
*/
export function blockedReason(pkg) {
return Object.hasOwn(BLOCKED_PACKAGES, pkg) ? BLOCKED_PACKAGES[pkg] : undefined;
}
135 changes: 135 additions & 0 deletions .dev/browser/brand-bridge.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env node
// Regenerates the brand bridge block in src/styles/theme.css (see the comment above it).
//
// node scripts/brand-bridge.mjs rewrite the block between the markers
// node scripts/brand-bridge.mjs --check exit 1 if the block is out of date
//
// The block re-declares, on the component root, every `:root` token whose value depends
// on `--color-brand-*`, with the same expression `:root` uses. A custom property's var()
// references resolve where it is DECLARED, so a token declared only at `:root` has the
// shipped purple substituted there, and the ramp the component root writes never reaches
// it. `src/styles/brandBridge.test.ts` runs the same derivation, so a resync of
// uui-theme.css that adds a brand token fails `npm test` until this is re-run.

import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { stripCssComments } from '@ichava/js-ui-kernel/build';
import { fileURLToPath } from 'node:url';

export const BEGIN = '/* brand-bridge:begin (generated by scripts/brand-bridge.mjs; do not edit by hand) */';
export const END = '/* brand-bridge:end */';

/** Variables the component root writes inline; inline wins, so re-declaring them is noise. */
export const INLINE = new Set(['--accent', '--accent-text', '--accent-soft', '--ring', '--radius']);

const WORD = /[\w-]/;
const SPACE = /\s/;

/**
* `name: value` declarations in a CSS fragment, comments stripped: exactly the matches of
* `/(--[\w-]+)\s*:\s*([^;{}]+);/g`, with the value trimmed, found in one pass.
*
* That regex is polynomial: `\s*` and `[^;{}]+` both take spaces, and `[\w-]+` retries
* from every dash, so a long unterminated value or a run of dashes took seconds. Here
* each position is visited a fixed number of times. Comments go through the kernel's
* linear `stripCssComments` for the same reason.
*/
export function declarations(css) {
const text = stripCssComments(css);
const n = text.length;
// wordEnd[i]: the first index >= i outside [\w-]; spaceEnd[i]: outside \s;
// stop[i]: the first index >= i holding one of ; { } (n when none).
const wordEnd = new Int32Array(n + 1);
const spaceEnd = new Int32Array(n + 1);
const stop = new Int32Array(n + 1);
wordEnd[n] = spaceEnd[n] = stop[n] = n;
for (let i = n - 1; i >= 0; i--) {
const c = text[i];
wordEnd[i] = WORD.test(c) ? wordEnd[i + 1] : i;
spaceEnd[i] = SPACE.test(c) ? spaceEnd[i + 1] : i;
stop[i] = c === ';' || c === '{' || c === '}' ? i : stop[i + 1];
}
const out = [];
for (let p = 0; p + 1 < n; ) {
if (text[p] === '-' && text[p + 1] === '-') {
const nameEnd = wordEnd[p + 2];
const colon = spaceEnd[nameEnd];
if (nameEnd > p + 2 && text[colon] === ':') {
const end = stop[colon + 1];
if (end < n && text[end] === ';' && end > colon + 1) {
out.push([text.slice(p, nameEnd), text.slice(colon + 1, end).trim()]);
p = end + 1;
continue;
}
}
}
p += 1;
}
return out;
}

/** The `:root`-level declarations: Untitled UI's `@theme` block plus theme.css's `:root` block. */
export function rootDeclarations(uuiCss, themeCss) {
const theme = uuiCss.slice(uuiCss.indexOf('@theme'), uuiCss.indexOf('@layer base'));
const open = themeCss.indexOf(':root {');
const app = open < 0 ? '' : themeCss.slice(open, themeCss.indexOf('\n }\n', open));
return [...declarations(theme), ...declarations(app)];
}

/** Every declaration that depends, directly or transitively, on `--color-brand-<n>`. */
export function brandDependents(decls) {
const map = new Map(decls);
const dep = new Set([...map.keys()].filter((k) => /^--color-brand-\d+$/.test(k)));
for (let changed = true; changed; ) {
changed = false;
for (const [k, v] of map) {
if (dep.has(k)) continue;
if ([...v.matchAll(/var\(\s*(--[\w-]+)/g)].some((m) => dep.has(m[1]))) {
dep.add(k);
changed = true;
}
}
}
return [...map].filter(([k]) => dep.has(k) && !/^--color-brand-\d+$/.test(k) && !INLINE.has(k));
}

export function renderBlock(entries) {
const body = entries.map(([k, v]) => ` ${k}: ${v};`).join('\n');
return `${BEGIN}\n@layer base {\n :where([data-ichava-theme]) {\n${body}\n }\n}\n${END}`;
}

export function currentBlock(themeCss) {
const a = themeCss.indexOf(BEGIN);
const b = themeCss.indexOf(END);
return a < 0 || b < 0 ? null : themeCss.slice(a, b + END.length);
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const themePath = join(root, 'resources/js/styles/theme.css');
// Untitled UI's theme is @ichava/js-ui-kernel/react's since react consolidation 3d (2026-11-12);
// resources/js/styles/uui-theme.css only imports it.
const uui = readFileSync(join(root, 'node_modules/@ichava/js-ui-kernel/dist/react/styles/uui-theme.css'), 'utf8');
const themeCss = readFileSync(themePath, 'utf8');
const entries = brandDependents(rootDeclarations(uui, themeCss));
if (entries.length < 50) {
console.error(`::error::only ${entries.length} brand-dependent tokens found; the parse is probably broken`);
process.exit(1);
}
const want = renderBlock(entries);
const have = currentBlock(themeCss);
if (have === null) {
console.error('::error::brand-bridge markers not found in src/styles/theme.css');
process.exit(1);
}
if (process.argv.includes('--check')) {
if (have !== want) {
console.error('::error::the brand bridge in theme.css is stale; run node scripts/brand-bridge.mjs');
process.exit(1);
}
console.log(`brand-bridge: ok -- ${entries.length} tokens`);
} else {
writeFileSync(themePath, themeCss.replace(have, want));
console.log(`brand-bridge: wrote ${entries.length} tokens`);
}
}
97 changes: 97 additions & 0 deletions .dev/browser/check-boundary-rules.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node
// Proves the module boundary rules in .dependency-cruiser.cjs can fail.
//
// The rules for src/modules/ and src/state/ land before the code they govern
// (architecture.md 4.1), so on a tree with no modules they match nothing, and "no
// violations" would say nothing about whether they work. `core-no-react` once shipped
// matching nothing for exactly that reason.
//
// This plants one violation of each rule in throwaway directories, runs dependency-cruiser
// over them, and fails unless every rule fires on its plant and the allowed import (one
// module reaching another through its index.ts) fires nothing. The plants are removed
// whatever happens.

import { execFileSync } from 'node:child_process';
import { mkdirSync, rmdirSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const A = 'src/modules/__probe_a__';
const B = 'src/modules/__probe_b__';
const STATE = 'src/state/__probe__.ts';
const PLANTED = [A, B, STATE];

/** file -> source. Each import names an existing module or another plant. */
const FILES = {
[`${A}/index.ts`]: `export const probe = 1;\n`,
[`${A}/domain/react.ts`]: `import type { ReactNode } from 'react';\nexport type Node = ReactNode;\n`,
[`${A}/domain/outward.ts`]: `import { service } from '../application/service';\nexport const d = service;\n`,
[`${A}/application/service.ts`]: `import { adapter } from '../infrastructure/adapter';\nexport const service = adapter;\n`,
[`${A}/infrastructure/adapter.ts`]: `import { useT } from '@/hooks/useT';\nexport const adapter = useT;\n`,
[`${B}/domain/deep.ts`]: `import { d } from '../../__probe_a__/domain/outward';\nexport const deep = d;\n`,
[`${B}/domain/through-index.ts`]: `import { probe } from '../../__probe_a__/index';\nexport const ok = probe;\n`,
[STATE]: `import { useT } from '@/hooks/useT';\nexport const s = useT;\n`,
};

/** rule -> the plant it must fire on. */
const EXPECT = {
'module-domain-is-plain-ts': `${A}/domain/react.ts`,
'module-domain-is-innermost': `${A}/domain/outward.ts`,
'module-application-not-outward': `${A}/application/service.ts`,
'module-infrastructure-not-into-ui': `${A}/infrastructure/adapter.ts`,
'modules-meet-through-index': `${B}/domain/deep.ts`,
'state-not-into-ui': STATE,
};
const CONTROL = `${B}/domain/through-index.ts`;

function cleanup() {
for (const p of PLANTED) rmSync(join(root, p), { recursive: true, force: true });
// Remove the parent directories too when the plants were all they held.
for (const dir of ['src/modules', 'src/state']) {
try {
rmdirSync(join(root, dir));
} catch {
/* not empty, or not there */
}
}
}

let violations;
try {
for (const [file, source] of Object.entries(FILES)) {
mkdirSync(dirname(join(root, file)), { recursive: true });
writeFileSync(join(root, file), source);
}
let out;
try {
out = execFileSync(
'npx',
['depcruise', ...PLANTED, '--config', '.dependency-cruiser.cjs', '--output-type', 'json'],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 },
);
} catch (error) {
// depcruise exits non-zero when it finds errors, which is the point here.
out = error.stdout;
}
violations = JSON.parse(out).summary.violations;
} finally {
cleanup();
}

const problems = [];
for (const [rule, file] of Object.entries(EXPECT)) {
if (!violations.some((v) => v.rule.name === rule && v.from === file)) {
problems.push(`${rule} did not fire on ${file}`);
}
}
const control = violations.filter((v) => v.from === CONTROL);
if (control.length) {
problems.push(`the allowed import through index.ts fired ${control.map((v) => v.rule.name).join(', ')}`);
}

if (problems.length) {
for (const p of problems) console.error(`check-boundary-rules: ${p}`);
process.exit(1);
}
console.log(`check-boundary-rules: ok -- ${Object.keys(EXPECT).length} rules fire on their plants, the index.ts import passes`);
112 changes: 112 additions & 0 deletions .dev/browser/check-config-drift.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env node
// Canonical: ichava/js-ui-kernel config/check-config-drift.mjs. Copies must be
// byte-identical, and this script checks its own copy too: list it as one of the pairs.
//
// Usage: node scripts/check-config-drift.mjs <local>=<canonical> [<local>=<canonical> ...]
//
// <local> path in this repository, relative to the working directory
// <canonical> path inside ichava/js-ui-kernel, e.g. config/tsconfig.base.json
//
// Each canonical file is fetched from raw.githubusercontent.com and compared byte for byte.
// It fails on a mismatch, on a non-200 response, on an empty body and on an empty pair
// list, so it cannot pass by comparing nothing.
//
// ICHAVA_KERNEL_REF git ref to compare against (default: main)
// ICHAVA_KERNEL_RAW raw base URL (default: https://raw.githubusercontent.com/ichava/js-ui-kernel)
import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';

export const DEFAULT_RAW = 'https://raw.githubusercontent.com/ichava/js-ui-kernel';

/** Parses `local=canonical` arguments. Throws on a malformed one. */
export function parsePairs(args) {
return args.map((arg) => {
const eq = arg.indexOf('=');
if (eq <= 0 || eq === arg.length - 1) {
throw new Error(`expected <local>=<canonical>, got "${arg}"`);
}
return { local: arg.slice(0, eq), canonical: arg.slice(eq + 1) };
});
}

/**
* Compares each pair. Returns { checked, failures }, where failures is a list of
* human-readable reasons. `fetchImpl` and `readImpl` are injectable for tests.
*/
export async function checkDrift({
pairs,
ref = 'main',
raw = DEFAULT_RAW,
fetchImpl = globalThis.fetch,
readImpl = (path) => readFile(path),
}) {
const failures = [];
if (pairs.length === 0) {
failures.push('no files to compare: pass at least one <local>=<canonical> pair');
return { checked: 0, failures };
}

let checked = 0;
for (const { local, canonical } of pairs) {
const url = `${raw}/${ref}/${canonical}`;
let localBytes;
try {
localBytes = Buffer.from(await readImpl(local));
} catch (error) {
failures.push(`${local}: cannot read the local copy (${error.message})`);
continue;
}

let response;
try {
response = await fetchImpl(url);
} catch (error) {
failures.push(`${local}: cannot fetch ${url} (${error.message})`);
continue;
}
if (response.status !== 200) {
failures.push(`${local}: ${url} answered ${response.status}, expected 200`);
continue;
}
const remoteBytes = Buffer.from(await response.arrayBuffer());
if (remoteBytes.length === 0) {
failures.push(`${local}: ${url} returned an empty body`);
continue;
}

checked += 1;
if (!localBytes.equals(remoteBytes)) {
failures.push(
`${local}: differs from ${canonical} at ${ref} ` +
`(${localBytes.length} bytes here, ${remoteBytes.length} canonical). ` +
`Copy the canonical file over it; change the canonical in ichava/js-ui-kernel first.`,
);
}
}
return { checked, failures };
}

async function main() {
let pairs;
try {
pairs = parsePairs(process.argv.slice(2));
} catch (error) {
console.error(`check-config-drift: ${error.message}`);
return 2;
}
const ref = process.env.ICHAVA_KERNEL_REF || 'main';
const raw = process.env.ICHAVA_KERNEL_RAW || DEFAULT_RAW;
const { checked, failures } = await checkDrift({ pairs, ref, raw });

for (const failure of failures) console.error(`check-config-drift: ${failure}`);
if (failures.length > 0 || checked !== pairs.length) {
console.error(`check-config-drift: FAILED, ${checked} of ${pairs.length} file(s) compared`);
return 1;
}
console.log(`check-config-drift: ${checked} file(s) identical to ichava/js-ui-kernel@${ref}`);
return 0;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exitCode = await main();
}
Loading
Loading