diff --git a/.dev/imported/react-icon-browser/.dependency-cruiser.cjs b/.dependency-cruiser.cjs similarity index 73% rename from .dev/imported/react-icon-browser/.dependency-cruiser.cjs rename to .dependency-cruiser.cjs index 0fe4990..26cc908 100644 --- a/.dev/imported/react-icon-browser/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -27,9 +27,9 @@ * hand-written exception list did not know about the new file. */ // src/app/ is the composition root architecture.md 3.2 names (the browser() builder first). -const APP = '^src/(App|main|mount|embedded|IchavaBrowser)\\.tsx$|^src/app/'; +const APP = '^resources/js/(StandaloneApp|app|main|mount|embedded|IchavaBrowser)\\.tsx$|^resources/js/(app|pages)/'; /** The package barrel re-exports the public surface, entry points included. */ -const BARREL = '^src/index\\.ts$'; +const BARREL = '^resources/js/index\\.ts$'; module.exports = { forbidden: [ @@ -40,7 +40,7 @@ module.exports = { 'return type puts React into the public surface a Vue or Blade consumer inherits. ' + 'Use the plain StyleObject from core/types instead, and put the hook in hooks/.', severity: 'error', - from: { path: '^src/core/' }, + from: { path: '^resources/js/core/' }, // NOTE: `to.path` matches the RESOLVED path, not the import specifier, so this must // be written against node_modules. Written as '^react$' the rule silently never // fires -- which is how it shipped the first time, and why every rule here has a @@ -53,8 +53,12 @@ module.exports = { 'core/ must not depend on any other layer. Test fixtures are the one exception: ' + 'src/test/ is shared scaffolding, not a layer.', severity: 'error', - from: { path: '^src/core/', pathNot: '\\.test\\.tsx?$' }, - to: { path: `^src/(components|hooks|store\\.ts)|${APP}` }, + // Two exemptions, measured when these rules came over from react-icon-browser (react + // consolidation 3.3, 2026-11-13): the Inertia adapter's InertiaCatalog.ts and browserMount.ts + // predate the merge and import the query helpers in hooks/useFilterSync.ts. A third file + // that does the same fails. + from: { path: '^resources/js/core/', pathNot: ['\\.test\\.tsx?$', '^resources/js/core/(InertiaCatalog|browserMount)\\.ts$'] }, + to: { path: `^resources/js/(components|hooks|store\\.ts)|${APP}` }, }, { name: 'core-tests-stay-in-core', @@ -62,14 +66,14 @@ module.exports = { 'A test under core/ that imports a component is testing the wrong layer -- move the ' + 'test next to what it covers. src/test/ fixtures are allowed.', severity: 'error', - from: { path: '^src/core/.*\\.test\\.tsx?$' }, - to: { path: '^src/(components|hooks)/' }, + from: { path: '^resources/js/core/.*\\.test\\.tsx?$' }, + to: { path: '^resources/js/(components|hooks)/' }, }, { name: 'data-no-react', comment: 'The store and data layer must stay portable: no React imports.', severity: 'error', - from: { path: '^src/(store\\.ts|data/)' }, + from: { path: '^resources/js/(store\\.ts|data/)' }, // NOTE: `to.path` matches the RESOLVED path, not the import specifier, so this must // be written against node_modules. Written as '^react$' the rule silently never // fires -- which is how it shipped the first time, and why every rule here has a @@ -80,7 +84,7 @@ module.exports = { name: 'ui-not-into-app', comment: 'Components take props; the app wires them. ui/ must not reach into app/.', severity: 'error', - from: { path: '^src/(components|hooks)/' }, + from: { path: '^resources/js/(components|hooks)/' }, to: { path: APP }, }, { @@ -111,31 +115,31 @@ module.exports = { 'A module domain imports no framework: no React, zustand, react-aria or Untitled UI, ' + 'type-only imports included. The domain tsconfig drops DOM from lib for the same reason.', severity: 'error', - from: { path: '^src/modules/[^/]+/domain/' }, + from: { path: '^resources/js/modules/[^/]+/domain/' }, to: { path: '^node_modules/(react|react-dom|zustand|react-aria|react-aria-components|@react-aria|@react-stately|@react-types|@untitledui)(/|$)' }, }, { name: 'module-domain-is-innermost', comment: 'A domain depends on nothing outward: not its own application or infrastructure, not ui, state or the app.', severity: 'error', - from: { path: '^src/modules/[^/]+/domain/' }, - to: { path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|^src/modules/[^/]+/(application|infrastructure)/|${APP}` }, + from: { path: '^resources/js/modules/[^/]+/domain/' }, + to: { path: `^resources/js/(components|hooks|state|ui|app)/|^resources/js/store\\.ts$|^resources/js/modules/[^/]+/(application|infrastructure)/|${APP}` }, }, { name: 'module-application-not-outward', comment: 'Application services use the domain and ports; adapters, ui, state and React stay outside.', severity: 'error', - from: { path: '^src/modules/[^/]+/application/' }, + from: { path: '^resources/js/modules/[^/]+/application/' }, to: { - path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|^src/modules/[^/]+/infrastructure/|${APP}|^node_modules/(react|react-dom|zustand)(/|$)`, + path: `^resources/js/(components|hooks|state|ui|app)/|^resources/js/store\\.ts$|^resources/js/modules/[^/]+/infrastructure/|${APP}|^node_modules/(react|react-dom|zustand)(/|$)`, }, }, { name: 'module-infrastructure-not-into-ui', comment: 'Adapters implement ports; they never reach ui, state, the app or React.', severity: 'error', - from: { path: '^src/modules/[^/]+/infrastructure/' }, - to: { path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|${APP}|^node_modules/(react|react-dom)(/|$)` }, + from: { path: '^resources/js/modules/[^/]+/infrastructure/' }, + to: { path: `^resources/js/(components|hooks|state|ui|app)/|^resources/js/store\\.ts$|${APP}|^node_modules/(react|react-dom)(/|$)` }, }, { name: 'modules-meet-through-index', @@ -143,15 +147,15 @@ module.exports = { "One module reaches another only through that module's index.ts, so a module's " + 'internals can move without breaking its neighbours.', severity: 'error', - from: { path: '^src/modules/([^/]+)/' }, - to: { path: '^src/modules/[^/]+/', pathNot: ['^src/modules/$1/', '^src/modules/[^/]+/index\\.ts$'] }, + from: { path: '^resources/js/modules/([^/]+)/' }, + to: { path: '^resources/js/modules/[^/]+/', pathNot: ['^resources/js/modules/$1/', '^resources/js/modules/[^/]+/index\\.ts$'] }, }, { name: 'state-not-into-ui', comment: 'The store slices hold state and call domain and application code; they never import ui.', severity: 'error', - from: { path: '^src/state/' }, - to: { path: `^src/(components|hooks|ui)/|${APP}|^node_modules/(react|react-dom)(/|$)` }, + from: { path: '^resources/js/state/' }, + to: { path: `^resources/js/(components|hooks|ui)/|${APP}|^node_modules/(react|react-dom)(/|$)` }, }, { name: 'no-circular', @@ -172,7 +176,7 @@ module.exports = { '(^|/)\\.[^/]+\\.(cjs|mjs|js|ts)$', '\\.d\\.ts$', '(^|/)tsconfig\\.[^/]+\\.json$', - '^src/(main|mount|embedded)\\.tsx$', + '^resources/js/(main|mount|embedded|app)\\.tsx$', ], }, to: {}, @@ -184,10 +188,12 @@ module.exports = { 'no-orphans only sees modules with no edges at all, so a dead file that imported ' + 'clsx (src/lib/utils.ts) passed it. Tests, declaration files and configs are exempt.', severity: 'error', - from: { path: '^src/(main|mount|embedded)\\.tsx$|^src/index\\.ts$' }, + // app.tsx is the Inertia entry; it loads pages/ through import.meta.glob, which the + // cruiser cannot follow, so each page is an entry point too. + from: { path: '^resources/js/(main|mount|embedded|app)\\.tsx$|^resources/js/index\\.ts$|^resources/js/pages/' }, to: { - path: '^src/', - pathNot: ['\\.test\\.tsx?$', '^src/test/', '\\.d\\.ts$', '^src/(main|mount|embedded)\\.tsx$', '^src/index\\.ts$'], + path: '^resources/js/', + pathNot: ['\\.test\\.tsx?$', '^resources/js/test/', '\\.d\\.ts$', '^resources/js/(main|mount|embedded|app)\\.tsx$', '^resources/js/index\\.ts$', '^resources/js/pages/'], reachable: false, }, }, @@ -196,7 +202,7 @@ module.exports = { options: { doNotFollow: { path: 'node_modules' }, tsPreCompilationDeps: true, // so type-only imports are seen -- the whole point of core-no-react - tsConfig: { fileName: 'tsconfig.app.json' }, + tsConfig: { fileName: 'tsconfig.inertia.json' }, enhancedResolveOptions: { exportsFields: ['exports'], conditionNames: ['import', 'require', 'types'] }, reporterOptions: { text: { highlightFocused: true } }, }, diff --git a/.dev/browser/check-boundary-rules.mjs b/.dev/browser/check-boundary-rules.mjs index 2e76097..7ac9e4a 100644 --- a/.dev/browser/check-boundary-rules.mjs +++ b/.dev/browser/check-boundary-rules.mjs @@ -1,7 +1,7 @@ #!/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 +// The rules for resources/js/modules/ and resources/js/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. @@ -16,10 +16,11 @@ 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'; +// .dev/browser/ -> the repository root (react consolidation 3.3, 2026-11-13). +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const A = 'resources/js/modules/__probe_a__'; +const B = 'resources/js/modules/__probe_b__'; +const STATE = 'resources/js/state/__probe__.ts'; const PLANTED = [A, B, STATE]; /** file -> source. Each import names an existing module or another plant. */ @@ -48,7 +49,7 @@ 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']) { + for (const dir of ['resources/js/modules', 'resources/js/state']) { try { rmdirSync(join(root, dir)); } catch { diff --git a/.dev/browser/check-dead-classes.mjs b/.dev/browser/check-dead-classes.mjs index 20df0ef..91a1125 100644 --- a/.dev/browser/check-dead-classes.mjs +++ b/.dev/browser/check-dead-classes.mjs @@ -15,7 +15,10 @@ * source that was meant to produce it. Grepping the Tailwind config or the component * proves how the class was written, never what the compiler kept. * - * Run after `npm run build`, against `dist/`. + * Run after `npm run build:prod`, against the committed bundle's stylesheet in public/assets/css/, + * which is what a host receives. Moved from react-icon-browser's scripts/ (react consolidation 3.3, + * 2026-11-13): it read that repository's dist/assets and src/; the standalone harness pages it + * also scanned now live in e2e/browser/. * * WHAT IT DOES NOT COVER, so nobody reads a pass as more than it is: * @@ -32,9 +35,10 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; import { join, extname, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; -const ROOT = fileURLToPath(new URL('..', import.meta.url)); -const CSS_DIR = join(ROOT, 'dist/assets'); -const SRC_DIR = join(ROOT, 'src'); +const ROOT = fileURLToPath(new URL('../..', import.meta.url)); +const CSS_DIR = join(ROOT, 'public/assets/css'); +const SRC_DIR = join(ROOT, 'resources/js'); +const HTML_DIR = join(ROOT, 'e2e/browser'); /** * Class names that are expected to emit nothing, each with the reason it is not a defect. @@ -84,9 +88,9 @@ function collectTokens() { }; walk(SRC_DIR).forEach(scan); - readdirSync(ROOT) + readdirSync(HTML_DIR) .filter((f) => f.endsWith('.html')) - .forEach((f) => scan(join(ROOT, f))); + .forEach((f) => scan(join(HTML_DIR, f))); return tokens; } @@ -98,11 +102,11 @@ function collectTokens() { */ function loadCss() { if (!existsSync(CSS_DIR)) { - fail(`No ${relative(ROOT, CSS_DIR)} directory. Run \`npm run build\` first.`); + fail(`No ${relative(ROOT, CSS_DIR)} directory. Run \`npm run build:prod\` first.`); } const files = readdirSync(CSS_DIR).filter((f) => f.endsWith('.css')); if (files.length === 0) { - fail(`No CSS in ${relative(ROOT, CSS_DIR)}. Run \`npm run build\` first.`); + fail(`No CSS in ${relative(ROOT, CSS_DIR)}. Run \`npm run build:prod\` first.`); } const css = files.map((f) => readFileSync(join(CSS_DIR, f), 'utf8')).join('\n'); return { css: css.replace(/\\/g, ''), files }; diff --git a/.dev/browser/check-playwright-image.mjs b/.dev/browser/check-playwright-image.mjs index 8bb027d..69967a0 100644 --- a/.dev/browser/check-playwright-image.mjs +++ b/.dev/browser/check-playwright-image.mjs @@ -22,13 +22,14 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; -const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +// .dev/browser/ -> the repository root (react consolidation 3.3, 2026-11-13). +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const IMAGE = /mcr\.microsoft\.com\/playwright:(v[\d.]+-\w+)/g; const sources = [ - ['.github/workflows/ci.yml', 'CI visual job container'], + ['.github/workflows/browser.yml', 'CI visual job container'], ['.github/workflows/visual-baselines.yml', 'seed workflow container'], - ['scripts/linux.sh', 'npm run visual:linux, visual:linux:check and e2e:linux'], + ['.dev/browser/linux.sh', 'npm run visual:linux, visual:linux:check and e2e:linux'], ]; const found = []; diff --git a/.dev/browser/linux.sh b/.dev/browser/linux.sh index 919fa63..82ea425 100644 --- a/.dev/browser/linux.sh +++ b/.dev/browser/linux.sh @@ -7,17 +7,17 @@ # npm run e2e:linux run the behavioural E2E suite # # Needs Docker. Arguments after the mode go to `playwright test` (a spec file, --grep ...). -# The image is pinned here once; check:playwright-image fails if it drifts from ci.yml's +# The image is pinned here once; check:playwright-image fails if it drifts from browser.yml's # visual job, the seed workflow or @playwright/test. set -eu IMAGE=mcr.microsoft.com/playwright:v1.65.0-noble -mode=${1:?usage: scripts/linux.sh visual-update|visual-check|e2e [playwright args]} +mode=${1:?usage: .dev/browser/linux.sh visual-update|visual-check|e2e [playwright args]} shift if ! command -v docker >/dev/null 2>&1; then - echo "scripts/linux.sh: Docker is not installed or not on PATH; CI's image needs it." >&2 + echo ".dev/browser/linux.sh: Docker is not installed or not on PATH; CI's image needs it." >&2 exit 127 fi -exec docker run --rm --ipc=host -v "$PWD":/src "$IMAGE" sh /src/scripts/visual-linux.sh "$mode" "$@" +exec docker run --rm --ipc=host -v "$PWD":/src "$IMAGE" sh /src/.dev/browser/visual-linux.sh "$mode" "$@" diff --git a/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__ax.svg b/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__ax.svg new file mode 100644 index 0000000..cd4e76d --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__ax.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__ba.svg b/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__ba.svg new file mode 100644 index 0000000..1f84409 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__ba.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__bi.svg b/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__bi.svg new file mode 100644 index 0000000..96fac20 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__clip_or_mask__bi.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__defs_ids__ag.svg b/.dev/browser/public/baseline/icon-sets-flag__defs_ids__ag.svg new file mode 100644 index 0000000..f25ccea --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__defs_ids__ag.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__defs_ids__ai.svg b/.dev/browser/public/baseline/icon-sets-flag__defs_ids__ai.svg new file mode 100644 index 0000000..6684777 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__defs_ids__ai.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__defs_ids__aw.svg b/.dev/browser/public/baseline/icon-sets-flag__defs_ids__aw.svg new file mode 100644 index 0000000..a87801e --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__defs_ids__aw.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__gradient__bz.svg b/.dev/browser/public/baseline/icon-sets-flag__gradient__bz.svg new file mode 100644 index 0000000..dbaf398 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__gradient__bz.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__gradient__fk.svg b/.dev/browser/public/baseline/icon-sets-flag__gradient__fk.svg new file mode 100644 index 0000000..1ab36f9 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__gradient__fk.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__gradient__gs.svg b/.dev/browser/public/baseline/icon-sets-flag__gradient__gs.svg new file mode 100644 index 0000000..2453be3 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__gradient__gs.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__own_colour__ae.svg b/.dev/browser/public/baseline/icon-sets-flag__own_colour__ae.svg new file mode 100644 index 0000000..a6e369b --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__own_colour__ae.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__own_colour__am.svg b/.dev/browser/public/baseline/icon-sets-flag__own_colour__am.svg new file mode 100644 index 0000000..676d2ff --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__own_colour__am.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__own_colour__ao.svg b/.dev/browser/public/baseline/icon-sets-flag__own_colour__ao.svg new file mode 100644 index 0000000..d91a3eb --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__own_colour__ao.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__use_ref__ad.svg b/.dev/browser/public/baseline/icon-sets-flag__use_ref__ad.svg new file mode 100644 index 0000000..dde82d2 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__use_ref__ad.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__use_ref__af.svg b/.dev/browser/public/baseline/icon-sets-flag__use_ref__af.svg new file mode 100644 index 0000000..e1614f3 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__use_ref__af.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-flag__use_ref__al.svg b/.dev/browser/public/baseline/icon-sets-flag__use_ref__al.svg new file mode 100644 index 0000000..4abb979 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-flag__use_ref__al.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-tabler__current_color__accessible.svg b/.dev/browser/public/baseline/icon-sets-tabler__current_color__accessible.svg new file mode 100644 index 0000000..4b919b3 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-tabler__current_color__accessible.svg @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-tabler__current_color__ad-circle.svg b/.dev/browser/public/baseline/icon-sets-tabler__current_color__ad-circle.svg new file mode 100644 index 0000000..e328abf --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-tabler__current_color__ad-circle.svg @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/icon-sets-tabler__current_color__ad.svg b/.dev/browser/public/baseline/icon-sets-tabler__current_color__ad.svg new file mode 100644 index 0000000..18c9b65 --- /dev/null +++ b/.dev/browser/public/baseline/icon-sets-tabler__current_color__ad.svg @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/baseline/manifest.json b/.dev/browser/public/baseline/manifest.json new file mode 100644 index 0000000..67fb8bc --- /dev/null +++ b/.dev/browser/public/baseline/manifest.json @@ -0,0 +1,110 @@ +[ + { + "pack": "icon-sets-tabler", + "feature": "current_color", + "file": "icon-sets-tabler__current_color__accessible.svg", + "bytes": 746 + }, + { + "pack": "icon-sets-tabler", + "feature": "current_color", + "file": "icon-sets-tabler__current_color__ad-circle.svg", + "bytes": 794 + }, + { + "pack": "icon-sets-tabler", + "feature": "current_color", + "file": "icon-sets-tabler__current_color__ad.svg", + "bytes": 787 + }, + { + "pack": "icon-sets-flag", + "feature": "defs_ids", + "file": "icon-sets-flag__defs_ids__ag.svg", + "bytes": 750 + }, + { + "pack": "icon-sets-flag", + "feature": "defs_ids", + "file": "icon-sets-flag__defs_ids__ai.svg", + "bytes": 2313 + }, + { + "pack": "icon-sets-flag", + "feature": "defs_ids", + "file": "icon-sets-flag__defs_ids__aw.svg", + "bytes": 10354 + }, + { + "pack": "icon-sets-flag", + "feature": "use_ref", + "file": "icon-sets-flag__use_ref__ad.svg", + "bytes": 29141 + }, + { + "pack": "icon-sets-flag", + "feature": "use_ref", + "file": "icon-sets-flag__use_ref__af.svg", + "bytes": 18983 + }, + { + "pack": "icon-sets-flag", + "feature": "use_ref", + "file": "icon-sets-flag__use_ref__al.svg", + "bytes": 3138 + }, + { + "pack": "icon-sets-flag", + "feature": "gradient", + "file": "icon-sets-flag__gradient__bz.svg", + "bytes": 42676 + }, + { + "pack": "icon-sets-flag", + "feature": "gradient", + "file": "icon-sets-flag__gradient__fk.svg", + "bytes": 28488 + }, + { + "pack": "icon-sets-flag", + "feature": "gradient", + "file": "icon-sets-flag__gradient__gs.svg", + "bytes": 31921 + }, + { + "pack": "icon-sets-flag", + "feature": "clip_or_mask", + "file": "icon-sets-flag__clip_or_mask__ax.svg", + "bytes": 542 + }, + { + "pack": "icon-sets-flag", + "feature": "clip_or_mask", + "file": "icon-sets-flag__clip_or_mask__ba.svg", + "bytes": 1171 + }, + { + "pack": "icon-sets-flag", + "feature": "clip_or_mask", + "file": "icon-sets-flag__clip_or_mask__bi.svg", + "bytes": 1051 + }, + { + "pack": "icon-sets-flag", + "feature": "own_colour", + "file": "icon-sets-flag__own_colour__ae.svg", + "bytes": 273 + }, + { + "pack": "icon-sets-flag", + "feature": "own_colour", + "file": "icon-sets-flag__own_colour__am.svg", + "bytes": 235 + }, + { + "pack": "icon-sets-flag", + "feature": "own_colour", + "file": "icon-sets-flag__own_colour__ao.svg", + "bytes": 1601 + } +] \ No newline at end of file diff --git a/.dev/browser/sample-svg-archetypes.mjs b/.dev/browser/sample-svg-archetypes.mjs index 060f155..336d16a 100644 --- a/.dev/browser/sample-svg-archetypes.mjs +++ b/.dev/browser/sample-svg-archetypes.mjs @@ -9,7 +9,7 @@ * Deterministic: directory entries are sorted before sampling, so re-running picks the * same files and the baseline does not churn. * - * Writes to `public/baseline/` plus a manifest the spec reads. + * Writes to `.dev/browser/public/baseline/` plus a manifest the spec reads. */ import { readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join, basename, dirname } from 'node:path'; @@ -20,10 +20,19 @@ import { fileURLToPath } from 'node:url'; // list of directory names that the 2026-09-21 rebrand renamed -- three ways of // being wrong, none of which anything reported, because the script is run by // hand and is not wired to an npm script. -const APP = dirname(dirname(fileURLToPath(import.meta.url))); +// +// Moved from react-icon-browser's scripts/ to icon-browser's .dev/browser/ (react consolidation +// 3.1, 2026-11-13): the repository root is two levels up, the packs sit beside it in packages/, +// and the sample lands in the e2e harness's public directory. +// +// Public packs only. icon-browser is public, and icon-sets-bundled and icon-sets-metronic are +// private (Metronic "is never shown publicly"), so their samples and contact sheets stay in the +// retired react-icon-browser and are not reproduced here. The style_block and foreign archetypes +// came only from those two packs; e2e/browser/svg-baseline.spec.ts says so where it checks. +const APP = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); const ROOT = dirname(APP); -const OUT = join(APP, 'public', 'baseline'); -const PACKS = ['icon-sets-tabler', 'icon-sets-bundled', 'icon-sets-metronic', 'icon-sets-flag']; +const OUT = join(APP, '.dev', 'browser', 'public', 'baseline'); +const PACKS = ['icon-sets-tabler', 'icon-sets-flag']; const FEATURES = { style_block: (s) => /]/i.test(s), diff --git a/.dev/browser/schema-types.mjs b/.dev/browser/schema-types.mjs index 55b8719..58189fa 100644 --- a/.dev/browser/schema-types.mjs +++ b/.dev/browser/schema-types.mjs @@ -1,6 +1,13 @@ #!/usr/bin/env node // Types for the icon-browser payloads, generated from its JSON Schema. // +// Since react consolidation 3d (2026-11-12) the schema and the browser are in this one +// repository: the generator reads resources/schema/icon-browser.schema.json and writes +// resources/js/core/api/schema.gen.ts, and the record of the schema it was generated from is +// .dev/imported/react-icon-browser/schema/source.json (SOURCE_PATH). Run it with +// `npm run schema:types`, and check with `npm run check:schema-types`. The text below is +// react-icon-browser's, from when the schema was copied across repositories. +// // icon-browser owns the schema (resources/schema/icon-browser.schema.json); this repo // keeps a copy in schema/, with the commit it came from in schema/source.json, because // the two are separate repositories and CI cannot read across them. The generated file @@ -197,7 +204,7 @@ function main() { let current = ''; try { current = readFileSync(OUTPUT_PATH, 'utf8'); } catch { /* missing counts as drift */ } if (current !== text) { - console.error('src/core/api/schema.gen.ts does not match schema/icon-browser.schema.json. Run `npm run schema:types`.'); + console.error('resources/js/core/api/schema.gen.ts does not match resources/schema/icon-browser.schema.json. Run `npm run schema:types`.'); process.exit(1); } console.log(`schema types match (${Object.keys(schema.$defs ?? {}).length} definitions, schema ${schema.version}).`); diff --git a/.dev/browser/visual-linux.sh b/.dev/browser/visual-linux.sh index b940f6d..59f460e 100755 --- a/.dev/browser/visual-linux.sh +++ b/.dev/browser/visual-linux.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Runs inside the Playwright image for scripts/linux.sh, against a copy of the checkout. +# Runs inside the Playwright image for .dev/browser/linux.sh, against a copy of the checkout. # # Mounting the checkout and installing in place failed two ways. In a git worktree `.git` is a # pointer file to a directory outside the mount, so npm's git lookup for a git dependency died @@ -23,7 +23,7 @@ src=${VISUAL_SRC:-/src} work=${VISUAL_WORK:-$(mktemp -d)} tar -C "$src" \ - --exclude=./.git --exclude=./node_modules --exclude=./dist --exclude=./.claude \ + --exclude=./.git --exclude=./node_modules --exclude=./.cache --exclude=./vendor --exclude=./.claude \ --exclude=./test-results --exclude=./playwright-report \ -cf - . | tar -C "$work" -xf - @@ -33,29 +33,29 @@ npm install --no-audit --no-fund status=0 case "$mode" in e2e) - npx playwright test "$@" || status=$? + npx playwright test --config=e2e/browser/playwright.config.ts "$@" || status=$? ;; visual-check) - npx playwright test --config=playwright.visual.config.ts "$@" || status=$? + npx playwright test --config=e2e/browser/playwright.visual.config.ts "$@" || status=$? ;; visual-update) # Measure first, so the summary can say how far each baseline moved. - npx playwright test --config=playwright.visual.config.ts --reporter=json "$@" > "$work/.visual-drift.json" 2>/dev/null || true - npx playwright test --config=playwright.visual.config.ts --update-snapshots "$@" || status=$? + npx playwright test --config=e2e/browser/playwright.visual.config.ts --reporter=json "$@" > "$work/.visual-drift.json" 2>/dev/null || true + npx playwright test --config=e2e/browser/playwright.visual.config.ts --update-snapshots "$@" || status=$? changed="" - for f in e2e/*-snapshots/*.png; do + for f in e2e/browser/*-snapshots/*.png; do [ -f "$f" ] || continue if ! cmp -s "$f" "$src/$f"; then changed="$changed $f"; fi done - for dir in e2e/*-snapshots; do - if [ -d "$dir" ]; then cp -R "$dir" "$src/e2e/"; fi + for dir in e2e/browser/*-snapshots; do + if [ -d "$dir" ]; then cp -R "$dir" "$src/e2e/browser/"; fi done if [ -n "$changed" ]; then echo echo "Changed baselines (review them with git diff before committing):" # shellcheck disable=SC2086 # one path per word, by construction # The table is a courtesy: a missing report must not fail a run that regenerated fine. - node scripts/visual-drift.mjs "$work/.visual-drift.json" $changed || echo "(drift table unavailable)" + node .dev/browser/visual-drift.mjs "$work/.visual-drift.json" $changed || echo "(drift table unavailable)" else echo "Baselines already match; nothing changed." fi diff --git a/.dev/imported/react-icon-browser/tsconfig.domain.json b/.dev/imported/react-icon-browser/tsconfig.domain.json deleted file mode 100644 index 74f5051..0000000 --- a/.dev/imported/react-icon-browser/tsconfig.domain.json +++ /dev/null @@ -1,14 +0,0 @@ -// The stricter overlay for framework-free code (architecture.md 1.4). Typechecked in CI as -// its own step, so a vendored component can never make it fail. `lib` has no DOM, so a -// domain file that reaches for document, window or localStorage does not compile: the -// compiler-level twin of dependency-cruiser's module-domain-is-plain-ts rule. -{ - "extends": "./tsconfig.app.json", - "compilerOptions": { - "exactOptionalPropertyTypes": true, - "lib": ["ES2023", "ESNext.Disposable"], - "types": [] - }, - "include": ["src/modules/*/domain", "src/modules/*/application"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] -} diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 8bdbd32..f1a22cc 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -16,3 +16,9 @@ paths: # The same create-github-app-token v3 false positive as release-please.yml above; delete with it. - 'missing input "app-id" which is required by action "actions/create-github-app-token@v3"' - 'input "client-id" is not defined in action "actions/create-github-app-token@v3"' + .github/workflows/visual-baselines.yml: + ignore: + # The same create-github-app-token v3 false positive as release-please.yml above; delete with it. + # Added with the workflow, ported from react-icon-browser (react consolidation 2.6, 2026-11-13). + - 'missing input "app-id" which is required by action "actions/create-github-app-token@v3"' + - 'input "client-id" is not defined in action "actions/create-github-app-token@v3"' diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml new file mode 100644 index 0000000..e29fb4d --- /dev/null +++ b/.github/workflows/browser.yml @@ -0,0 +1,193 @@ +name: Browser + +# The browser UI's own checks, ported from ichava/react-icon-browser's ci.yml when its source +# moved into resources/js (react consolidation 2.6, 2026-11-13). What that workflow ran and where +# it went: +# +# typecheck, build, lint, Vitest tests.yml's frontend job (typecheck:inertia covers the +# whole of resources/js, tests included; build:prod builds it) +# check:config-drift tests.yml's frontend job +# actionlint, changelog order code-quality.yml, changelog.yml +# build:lib, check:public-api, not ported: the library build is retired (Q2) and nothing +# consumer self-test vendors the browser any more (icon-browser#146) +# everything else below +# +# Pull requests run Linux only, drafts run nothing, superseded runs are cancelled and every job +# has a timeout. The visual suite runs weekly and by dispatch, not on every pull request. +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + guards: + name: Browser guards + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 24 + + # No lock file is committed (org rule), so this resolves fresh. + - name: Install dependencies + run: npm install --no-audit --no-fund + + # The payload types are generated from resources/schema/icon-browser.schema.json. A hand + # edit to the generated file fails here before anything type-checks against the wrong shapes. + - name: Payload types match the schema + run: npm run check:schema-types + + # The stricter overlay for resources/js/modules/*/domain and application: no DOM in lib, + # and exactOptionalPropertyTypes. Its own step, so a vendored component cannot fail it. + - name: Typecheck the domain layer + run: npm run typecheck:domain + + - name: Typecheck the e2e specs + run: npm run typecheck:e2e + + # Reads the stylesheet hosts receive (public/assets/css), which committed-build.yml proves + # equals a fresh build. A class Tailwind could not resolve is dropped with no warning, and + # nothing else notices: tsc and ESLint do not read strings. + - name: Dead CSS classes + run: npm run check:dead-classes + + # The documentation portal has no Ichava pages yet; this fails on a link to them, and on a + # relative Markdown link to a file that does not exist. + - name: Dead documentation links + run: npm run check:dead-links + + # The layering is what makes a later Vue or Blade consumer possible, and it only stays + # true if something checks. core/ leaked React because nothing did. + - name: Layer boundaries + run: npm run boundaries + + # The module rules match little on this tree, so this plants a violation of each and + # fails unless every rule fires. + - name: Boundary rules can fail + run: npm run check:boundary-rules + + # Cheap, and it fails on a config mistake that would otherwise surface as a screenshot + # diff, which reads as a rendering regression that is not one. + - name: Playwright image pinned consistently + run: npm run check:playwright-image + + e2e: + name: Browser E2E (Playwright) + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Install dependencies + run: npm install --no-audit --no-fund + + # The browser comes from Playwright's CDN; its system packages come from Ubuntu's apt + # mirrors, which went unreachable from the runner on 2026-11-11. The runner image already + # ships Chrome's libraries, so the extra packages are best effort: two minutes, then a + # warning instead of a red job. + - name: Install Playwright browsers + run: | + npx playwright install chromium + timeout 120 npx playwright install-deps chromium && exit 0 + echo "::warning title=Playwright system deps::apt mirrors unreachable; running with the runner's own libraries" + + # The config builds the harness (e2e/browser/vite.config.ts) and serves it itself. + - name: Playwright + run: npm run e2e:browser + + - name: Upload report on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: browser-e2e-report + path: test-results/ + retention-days: 7 + + visual: + name: Browser visual regression + runs-on: ubuntu-latest + # Weekly and by dispatch (react consolidation 3.1): the suite compares pixels in a container + # and is the slowest job here. + if: github.event_name != 'pull_request' + timeout-minutes: 20 + + # The gate runs INSIDE Playwright's own image, not bare on the runner. Font stacks differ + # between the two, so a baseline made in the container misses a bare runner by ~1% of + # pixels. Running here makes `npm run visual:linux` byte-identical to CI. + # + # `npm run check:playwright-image` asserts this tag, the seed workflow's and linux.sh's all + # agree, and that they match @playwright/test. Do not edit one alone. + container: + image: mcr.microsoft.com/playwright:v1.65.0-noble + options: --ipc=host + + # A containerised job does NOT inherit the runner's bash default: Actions runs `run:` steps + # under `sh -e`, where the bash arrays below expand to something else entirely. + defaults: + run: + shell: bash + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + # No setup-node and no `playwright install`: the image ships both. + - name: Install dependencies + run: npm install --no-audit --no-fund + + # Without committed Linux baselines Playwright WRITES them and fails, which reads as a + # mysterious diff. Fail here instead, with the fix in the message. + - name: Require Linux baselines + run: | + shopt -s nullglob + baselines=(e2e/browser/*.spec.ts-snapshots/*-linux.png) + if [ ${#baselines[@]} -eq 0 ]; then + echo "::error::No Linux screenshot baselines are committed, so the visual" + echo "::error::suite cannot compare anything." + echo "::error::Seed them with the same image this job runs in:" + echo "::error:: npm run visual:linux # locally, needs Docker" + echo "::error:: gh workflow run visual-baselines.yml --repo ichava/icon-browser --ref $GITHUB_REF_NAME" + echo "::error::Review the PNGs, then commit e2e/browser/*.spec.ts-snapshots/." + exit 1 + fi + echo "Found ${#baselines[@]} Linux baseline(s)." + + # Never --update-snapshots in CI. A self-updating baseline turns every regression into a + # committed one and the gate silently stops existing. + - name: Playwright (visual) + run: npm run visual + + - name: Upload diff report on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: browser-visual-report + path: | + playwright-report/ + test-results/ + retention-days: 7 diff --git a/.github/workflows/visual-baselines.yml b/.github/workflows/visual-baselines.yml new file mode 100644 index 0000000..c86f574 --- /dev/null +++ b/.github/workflows/visual-baselines.yml @@ -0,0 +1,156 @@ +# Seed the visual baselines from the environment that compares them. +# +# Ported from ichava/react-icon-browser with the suite (react consolidation 2.6, 2026-11-13). +# +# That environment is Playwright's own image, because CI's `visual` job runs inside +# it (see browser.yml). Font stacks differ between the image and a bare GitHub runner -- +# about 1% of pixels, too few to see and enough to fail -- so "Linux" alone was +# never specific enough. Pinning both to one image is what removed that gap; the same +# image is also what `npm run visual:linux` uses, so a local seed and this workflow +# produce identical pixels. +# +# Deliberately manual. A workflow that rewrites its own baselines on every run turns +# every regression into a committed one and the gate silently stops existing, so this +# only ever runs when someone dispatches it, and never writes to `main`. +# +# Two ways to run it (Sim, 2026-10-28: "automate" the download and the review): +# - commit: false (default): the new PNGs are uploaded as an artifact, as before. +# - commit: true, dispatched on a pull request's branch: the job first compares the +# current baselines and records how much each one changed, then regenerates them, +# commits only the changed PNGs to that branch with the Refresh Bot token (so CI runs +# again on the new head), and comments on the pull request with a table of what moved +# and by how much. The review is the pull request's own image diff ("Files changed" +# renders old and new side by side); merging stays a human decision. + +name: Seed visual baselines + +on: + workflow_dispatch: + inputs: + commit: + description: Commit the regenerated baselines to this branch and comment on its pull request + type: boolean + default: false + +permissions: + contents: read + +jobs: + seed: + name: Generate baselines in the Playwright image + runs-on: ubuntu-latest + timeout-minutes: 20 + + # Must match browser.yml's `visual` job exactly. If these two images ever differ, this + # workflow starts producing baselines the gate rejects, and the failure reads as a + # rendering regression rather than a configuration one. + container: + image: mcr.microsoft.com/playwright:v1.65.0-noble + options: --ipc=host + + # A containerised job does NOT inherit the runner's bash default -- Actions runs + # `run:` steps under `sh -e` here, and nothing shows the switch until something breaks. + # It broke loudly once (`shopt: not found`, exit 127), but the quiet version is worse: + # `arr=(...)` and `${#arr[@]}` are bash arrays that sh expands to something else + # entirely, so a baseline check could have reported zero and "passed". + defaults: + run: + shell: bash + + steps: + # Refuse before doing any work: committing baselines straight onto the default + # branch would skip the review this workflow exists to keep. + - name: Refuse to commit to the default branch + if: inputs.commit + env: + REF_NAME: ${{ github.ref_name }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + if [ "${REF_NAME}" = "${DEFAULT_BRANCH}" ]; then + echo "::error::commit: true runs on a pull request's branch, never on ${DEFAULT_BRANCH}." + exit 1 + fi + + - name: Mint the Refresh Bot token + if: inputs.commit + id: app + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.REFRESH_APP_CLIENT_ID }} + private-key: ${{ secrets.REFRESH_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - uses: actions/checkout@v7 + with: + token: ${{ inputs.commit && steps.app.outputs.token || github.token }} + + # No setup-node, no `playwright install`: the image ships both. + - name: Install dependencies + run: npm install --no-audit --no-fund + + # Compare against the committed baselines first, so the comment can say how far + # each one moved. Failures are the point here, so this step never fails the job. + - name: Measure the drift + if: inputs.commit + continue-on-error: true + run: npx playwright test --config=e2e/browser/playwright.visual.config.ts --reporter=json > "${RUNNER_TEMP}/visual.json" + + - name: Regenerate baselines + run: npx playwright test --config=e2e/browser/playwright.visual.config.ts --update-snapshots + + - name: Commit the changed baselines and comment on the pull request + if: inputs.commit + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + APP_SLUG: ${{ steps.app.outputs.app-slug }} + BRANCH: ${{ github.ref_name }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + git config --global --add safe.directory "$GITHUB_WORKSPACE" + mapfile -t CHANGED < <(git status --porcelain -- 'e2e/browser/*.spec.ts-snapshots/*.png' | awk '{print $2}') + echo "changed baselines: ${#CHANGED[@]}" + if [ "${#CHANGED[@]}" -eq 0 ]; then + echo "Baselines already match; nothing to commit." + exit 0 + fi + { + echo '### Visual baselines regenerated' + echo + echo "${#CHANGED[@]} baseline(s) changed and were committed to this branch by [the seeding run](${RUN_URL})." + echo 'Review them in **Files changed**, which shows the old and new image side by side. Merge only if every change is intended.' + echo + node .dev/browser/visual-drift.mjs "${RUNNER_TEMP}/visual.json" "${CHANGED[@]}" + } > "${RUNNER_TEMP}/comment.md" + # The Playwright image has no gh CLI, so the API calls go through Node's fetch. + api() { node -e ' + const [path, method = "GET", body] = process.argv.slice(1); + fetch("https://api.github.com" + path, { method, body, headers: { + authorization: "Bearer " + process.env.GH_TOKEN, accept: "application/vnd.github+json", + "content-type": "application/json", "x-github-api-version": "2022-11-28" } }) + .then(async (r) => { const j = await r.json(); if (!r.ok) { console.error(r.status, JSON.stringify(j)); process.exit(1); } console.log(JSON.stringify(j)); }); + ' "$@"; } + BOT_ID="$(api "/users/${APP_SLUG}%5Bbot%5D" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id')" + git config user.name "${APP_SLUG}[bot]" + git config user.email "${BOT_ID}+${APP_SLUG}[bot]@users.noreply.github.com" + git add -- "${CHANGED[@]}" + git commit -m "Regenerate ${#CHANGED[@]} visual baseline(s)" -m "Seeded by ${RUN_URL}." + git push origin "HEAD:refs/heads/${BRANCH}" + PR="$(api "/repos/${GITHUB_REPOSITORY}/pulls?state=open&head=${GITHUB_REPOSITORY_OWNER}:${BRANCH}" | node -pe 'JSON.parse(require("fs").readFileSync(0))[0]?.number ?? ""')" + if [ -n "${PR}" ]; then + api "/repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" POST \ + "$(node -e 'console.log(JSON.stringify({ body: require("fs").readFileSync(process.argv[1], "utf8") }))' "${RUNNER_TEMP}/comment.md")" > /dev/null + echo "commented on #${PR}" + else + echo "::warning::No open pull request for ${BRANCH}; the baselines are committed but nobody was told." + fi + cat "${RUNNER_TEMP}/comment.md" >> "$GITHUB_STEP_SUMMARY" + + - name: Upload for review + uses: actions/upload-artifact@v7 + with: + name: visual-baselines + path: e2e/browser/*.spec.ts-snapshots/*.png + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore index dff012f..34f17df 100644 --- a/.gitignore +++ b/.gitignore @@ -56,8 +56,13 @@ package-lock.json # .claude/worktrees/ and the committed inertia-app.css grows by the classes they use. /.claude/ -# Generated by .dev/browser/sample-svg-archetypes.mjs from the private bundled and Metronic packs -# for the local svg-baseline rendering check. Never tracked: this repository is public. -/.dev/browser/public/baseline/ -/.dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts-snapshots/*bundled* -/.dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts-snapshots/*metronic* +# The svg-baseline rendering check samples the public packs only since react consolidation 3.1 +# (2026-11-13), so its sample (.dev/browser/public/baseline/) is tracked. A contact sheet from the +# private bundled or Metronic packs is never tracked: this repository is public. +/e2e/browser/svg-baseline.spec.ts-snapshots/*bundled* +/e2e/browser/svg-baseline.spec.ts-snapshots/*metronic* + +# The e2e harness build (e2e/browser/vite.config.ts) and Playwright output. +/.cache/ +/playwright-report/ +/test-results/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2100d80..51cdd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to `ichava/icon-browser` follow [Keep a Changelog](https://k ## [Unreleased] +### Added + +- **The browser's Playwright suite and guards run here** (react consolidation 2.6, 3.1 and 3.3). react-icon-browser's 20 e2e specs and their harness pages moved to `e2e/browser/`, built from `resources/js` by `e2e/browser/vite.config.ts` and run by `npm run e2e:browser` (98 tests, as in react-icon-browser); `npm run visual` compares the 12 committed Linux baselines inside the pinned Playwright image. `.github/workflows/browser.yml` runs the guards and the e2e suite on pull requests and the visual suite weekly and by dispatch; `visual-baselines.yml` seeds baselines on a pull request's branch. New npm scripts: `typecheck:domain`, `typecheck:e2e`, `boundaries`, `check:boundary-rules`, `check:schema-types`, `schema:types`, `check:dead-classes`, `check:dead-links`, `check:playwright-image`, `visual`, `visual:update`, `visual:linux`, `visual:linux:check`, `e2e:linux`. +- **The SVG rendering sample is tracked, with the public packs only** (tabler and flag, react-icon-browser's own 18 files). The svg-baseline spec no longer renders the private bundled and Metronic packs, so it covers six archetypes, not eight; `noPrivateContent.test.ts` fails if the sample holds anything else. + +### Changed + +- `core/minifiedBundle.ts`, used only by its test, moved to `resources/js/test/minifiedBundle.ts`; dependency-cruiser's `no-unreachable` rule reported it. + +### Fixed + +- `resources/assets/fonts/README.md` linked `../../src/styles/theme.css`, which does not exist; `check:dead-links` found it. + ## [0.1.0] - 2026-11-13 First release of `ichava/icon-browser`. The repository is published as a single commit; the 0.1.x tags cut between 2026-10-23 and 2026-11-02 (v0.1.0 to v0.1.1, including the release-please v0.1.1 of 2026-11-02) are folded into this release, and their notes are kept below under *Archived history*. diff --git a/.dev/imported/react-icon-browser/e2e/a11y.spec.ts b/e2e/browser/a11y.spec.ts similarity index 98% rename from .dev/imported/react-icon-browser/e2e/a11y.spec.ts rename to e2e/browser/a11y.spec.ts index 5d9e030..37fa282 100644 --- a/.dev/imported/react-icon-browser/e2e/a11y.spec.ts +++ b/e2e/browser/a11y.spec.ts @@ -8,7 +8,7 @@ import { expect, test, type Page } from '@playwright/test'; * * The theme is chosen through `prefers-color-scheme` with no stored preference, so this * also exercises L9: a first load follows the system scheme. The structural rules are - * pinned in jsdom too (src/IchavaBrowser.a11y.test.tsx); colour contrast needs a real + * pinned in jsdom too (resources/js/IchavaBrowser.a11y.test.tsx); colour contrast needs a real * renderer, which is why this suite exists. * * Moderate and minor findings are not gated here. Lower the bar only with a reason. diff --git a/.dev/imported/react-icon-browser/e2e/browser.spec.ts b/e2e/browser/browser.spec.ts similarity index 98% rename from .dev/imported/react-icon-browser/e2e/browser.spec.ts rename to e2e/browser/browser.spec.ts index 6475a5c..77011bd 100644 --- a/.dev/imported/react-icon-browser/e2e/browser.spec.ts +++ b/e2e/browser/browser.spec.ts @@ -25,7 +25,7 @@ async function dismissFirstRun(page: Page) { // assets; the next regeneration made it 200 and every spec in this file failed on // a `beforeEach` that had nothing to do with what they test. const FIXTURE_ICON_COUNT = JSON.parse( - readFileSync(new URL('../public/data/icons.json', import.meta.url), 'utf8'), + readFileSync(new URL('../../.dev/browser/public/data/icons.json', import.meta.url), 'utf8'), ).icons.length; const ALL_FIXTURE_ICONS = new RegExp(`${FIXTURE_ICON_COUNT} icons`); diff --git a/.dev/imported/react-icon-browser/e2e/consent-drag.spec.ts b/e2e/browser/consent-drag.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/consent-drag.spec.ts rename to e2e/browser/consent-drag.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/contrast.spec.ts b/e2e/browser/contrast.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/contrast.spec.ts rename to e2e/browser/contrast.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/credits.spec.ts b/e2e/browser/credits.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/credits.spec.ts rename to e2e/browser/credits.spec.ts diff --git a/.dev/imported/react-icon-browser/embedded.html b/e2e/browser/embedded.html similarity index 90% rename from .dev/imported/react-icon-browser/embedded.html rename to e2e/browser/embedded.html index b1dce6c..9f8c018 100644 --- a/.dev/imported/react-icon-browser/embedded.html +++ b/e2e/browser/embedded.html @@ -10,7 +10,7 @@ browser used to overwrite the html class, the design tokens on , lang, dir, document.title, the description meta and the entire query string. - Not a product page. Referenced only by e2e/embedded.spec.ts. + Not a product page. Referenced only by e2e/browser/embedded.spec.ts. --> Host Application — Dashboard @@ -28,6 +28,6 @@ - + diff --git a/.dev/imported/react-icon-browser/e2e/embedded.spec.ts b/e2e/browser/embedded.spec.ts similarity index 98% rename from .dev/imported/react-icon-browser/e2e/embedded.spec.ts rename to e2e/browser/embedded.spec.ts index 7565023..72dcbcd 100644 --- a/.dev/imported/react-icon-browser/e2e/embedded.spec.ts +++ b/e2e/browser/embedded.spec.ts @@ -11,7 +11,7 @@ import { test, expect, type Page } from '@playwright/test'; * first place, so the kill switch defeated itself. * * `embedded.html` is a host page with its own title, lang, dir, meta and chrome. - * `src/embedded.tsx` mounts through `mountIchavaBrowser` with the defaults. + * `resources/js/embedded.tsx` mounts through `mountIchavaBrowser` with the defaults. */ const HOST = { diff --git a/.dev/imported/react-icon-browser/e2e/exports.spec.ts b/e2e/browser/exports.spec.ts similarity index 95% rename from .dev/imported/react-icon-browser/e2e/exports.spec.ts rename to e2e/browser/exports.spec.ts index 9a74ee3..c161bfd 100644 --- a/.dev/imported/react-icon-browser/e2e/exports.spec.ts +++ b/e2e/browser/exports.spec.ts @@ -12,7 +12,7 @@ async function dismissFirstRun(page: Page) { await page.getByRole('button', { name: 'Skip' }).click({ timeout: 10_000 }).catch(() => {}); } -const FIXTURE_ICON_COUNT = JSON.parse(readFileSync(new URL('../public/data/icons.json', import.meta.url), 'utf8')).icons.length; +const FIXTURE_ICON_COUNT = JSON.parse(readFileSync(new URL('../../.dev/browser/public/data/icons.json', import.meta.url), 'utf8')).icons.length; test.beforeEach(async ({ page }) => { await page.addInitScript(() => localStorage.clear()); diff --git a/.dev/imported/react-icon-browser/e2e/first-run.spec.ts b/e2e/browser/first-run.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/first-run.spec.ts rename to e2e/browser/first-run.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/fixtures/overlap.js b/e2e/browser/fixtures/overlap.js similarity index 100% rename from .dev/imported/react-icon-browser/e2e/fixtures/overlap.js rename to e2e/browser/fixtures/overlap.js diff --git a/.dev/imported/react-icon-browser/e2e/fixtures/react-landingpage-styles.css b/e2e/browser/fixtures/react-landingpage-styles.css similarity index 100% rename from .dev/imported/react-icon-browser/e2e/fixtures/react-landingpage-styles.css rename to e2e/browser/fixtures/react-landingpage-styles.css diff --git a/.dev/imported/react-icon-browser/e2e/focus-return.spec.ts b/e2e/browser/focus-return.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/focus-return.spec.ts rename to e2e/browser/focus-return.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/forms.spec.ts b/e2e/browser/forms.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/forms.spec.ts rename to e2e/browser/forms.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/grid.spec.ts b/e2e/browser/grid.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/grid.spec.ts rename to e2e/browser/grid.spec.ts diff --git a/.dev/imported/react-icon-browser/index.html b/e2e/browser/index.html similarity index 94% rename from .dev/imported/react-icon-browser/index.html rename to e2e/browser/index.html index 67c0683..d68c054 100644 --- a/.dev/imported/react-icon-browser/index.html +++ b/e2e/browser/index.html @@ -25,7 +25,7 @@ [\s\S]*'); const doc = new DOMParser().parseFromString(out, 'text/html'); diff --git a/resources/js/core/minifiedBundle.test.ts b/resources/js/core/minifiedBundle.test.ts index 4d06a21..78904e1 100644 --- a/resources/js/core/minifiedBundle.test.ts +++ b/resources/js/core/minifiedBundle.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest'; import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; -import { minificationProblems } from './minifiedBundle'; +import { minificationProblems } from '@/test/minifiedBundle'; /** * The browser code this public repository ships is fully minified: the page bundle built from - * resources/js (see `minifiedBundle.ts`). The vendored react-icon-browser build was removed on 2026-11-13. + * resources/js (see `test/minifiedBundle.ts`). The vendored react-icon-browser build was removed on 2026-11-13. */ const page = resolve(process.cwd(), 'public/assets/js/inertia-app.js'); diff --git a/resources/js/core/visualLinux.test.ts b/resources/js/core/visualLinux.test.ts index 977464f..5b8ff4b 100644 --- a/resources/js/core/visualLinux.test.ts +++ b/resources/js/core/visualLinux.test.ts @@ -5,7 +5,7 @@ import { join, resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; /** - * The container side of `npm run visual:linux` (`scripts/visual-linux.sh`). + * The container side of `npm run visual:linux` (`.dev/browser/visual-linux.sh`). * * Mounting the checkout and installing in place failed from a git worktree, whose `.git` is a * pointer to a directory outside the mount, and replaced the host's node_modules with Linux @@ -15,26 +15,27 @@ import { describe, expect, it } from 'vitest'; */ const root = process.cwd(); // The standalone app's Playwright tooling moved to .dev/browser/ with react consolidation 3d -// (2026-11-12), and its npm scripts stay in the imported package.json until its e2e suite is -// ported; this checks them where they are. +// (2026-11-12), and its suite to e2e/browser/ with its npm scripts in package.json in 3.1 +// (2026-11-13). const script = resolve(root, '.dev/browser/visual-linux.sh'); -const pkg = JSON.parse(readFileSync(resolve(root, '.dev/imported/react-icon-browser/package.json'), 'utf8')) as { scripts: Record }; +const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { scripts: Record }; +const SNAPSHOTS = ['e2e', 'browser', 'visual.spec.ts-snapshots']; function fakeCheckout(playwrightExit: number, args: string[] = []) { const base = mkdtempSync(join(tmpdir(), 'visual-linux-')); const src = join(base, 'src'); const bin = join(base, 'bin'); - mkdirSync(join(src, 'e2e', 'visual.spec.ts-snapshots'), { recursive: true }); + mkdirSync(join(src, ...SNAPSHOTS), { recursive: true }); mkdirSync(join(src, 'node_modules'), { recursive: true }); mkdirSync(bin); writeFileSync(join(src, '.git'), 'gitdir: /nowhere/.git/worktrees/x\n'); writeFileSync(join(src, 'node_modules', 'host.txt'), 'darwin'); - writeFileSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'), 'old'); + writeFileSync(join(src, ...SNAPSHOTS, 'toolbar.png'), 'old'); writeFileSync(join(src, 'package.json'), '{}'); // npm fails if it can see .git or the host's node_modules. writeFileSync(join(bin, 'npm'), '#!/bin/sh\n[ ! -e .git ] || exit 7\n[ ! -e node_modules/host.txt ] || exit 8\nmkdir -p node_modules && echo linux > node_modules/host.txt\n'); // Records its arguments, so a test can see which suite and options a mode ran. - writeFileSync(join(bin, 'npx'), `#!/bin/sh\necho "$*" >> ${join(base, 'npx.log')}\necho new > e2e/visual.spec.ts-snapshots/toolbar.png\nexit ${playwrightExit}\n`); + writeFileSync(join(bin, 'npx'), `#!/bin/sh\necho "$*" >> ${join(base, 'npx.log')}\necho new > e2e/browser/visual.spec.ts-snapshots/toolbar.png\nexit ${playwrightExit}\n`); chmodSync(join(bin, 'npm'), 0o755); chmodSync(join(bin, 'npx'), 0o755); const run = spawnSync('sh', [script, ...args], { @@ -49,45 +50,45 @@ describe('visual:linux', () => { it('installs and runs in a copy without .git, and hands back only the snapshots', () => { const { src, run } = fakeCheckout(0); expect(run.status, run.stderr).toBe(0); - expect(readFileSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'), 'utf8').trim()).toBe('new'); + expect(readFileSync(join(src, ...SNAPSHOTS, 'toolbar.png'), 'utf8').trim()).toBe('new'); expect(readFileSync(join(src, 'node_modules', 'host.txt'), 'utf8')).toBe('darwin'); }); it('still hands back what a failing run wrote, and fails', () => { const { src, run } = fakeCheckout(1); expect(run.status).toBe(1); - expect(existsSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'))).toBe(true); + expect(existsSync(join(src, ...SNAPSHOTS, 'toolbar.png'))).toBe(true); }); it('checks without touching the committed baselines', () => { const { src, run, calls } = fakeCheckout(0, ['visual-check']); expect(run.status, run.stderr).toBe(0); - expect(readFileSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'), 'utf8')).toBe('old'); - expect(calls()).toEqual(['playwright test --config=playwright.visual.config.ts']); + expect(readFileSync(join(src, ...SNAPSHOTS, 'toolbar.png'), 'utf8')).toBe('old'); + expect(calls()).toEqual(['playwright test --config=e2e/browser/playwright.visual.config.ts']); }); it('runs the E2E suite with the arguments it was given', () => { - const { src, run, calls } = fakeCheckout(0, ['e2e', 'e2e/tooltip.spec.ts']); + const { src, run, calls } = fakeCheckout(0, ['e2e', 'e2e/browser/tooltip.spec.ts']); expect(run.status, run.stderr).toBe(0); - expect(calls()).toEqual(['playwright test e2e/tooltip.spec.ts']); - expect(readFileSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'), 'utf8')).toBe('old'); + expect(calls()).toEqual(['playwright test --config=e2e/browser/playwright.config.ts e2e/browser/tooltip.spec.ts']); + expect(readFileSync(join(src, ...SNAPSHOTS, 'toolbar.png'), 'utf8')).toBe('old'); }); it('measures, then regenerates, when updating', () => { const { run, calls } = fakeCheckout(0, ['visual-update']); expect(run.status, run.stderr).toBe(0); expect(calls()).toEqual([ - 'playwright test --config=playwright.visual.config.ts --reporter=json', - 'playwright test --config=playwright.visual.config.ts --update-snapshots', + 'playwright test --config=e2e/browser/playwright.visual.config.ts --reporter=json', + 'playwright test --config=e2e/browser/playwright.visual.config.ts --update-snapshots', ]); }); it('wires each npm script to its mode through the one pinned image', () => { - expect(pkg.scripts['visual:linux']).toBe('sh scripts/linux.sh visual-update'); - expect(pkg.scripts['visual:linux:check']).toBe('sh scripts/linux.sh visual-check'); - expect(pkg.scripts['e2e:linux']).toBe('sh scripts/linux.sh e2e'); + expect(pkg.scripts['visual:linux']).toBe('sh .dev/browser/linux.sh visual-update'); + expect(pkg.scripts['visual:linux:check']).toBe('sh .dev/browser/linux.sh visual-check'); + expect(pkg.scripts['e2e:linux']).toBe('sh .dev/browser/linux.sh e2e'); const wrapper = readFileSync(resolve(root, '.dev/browser/linux.sh'), 'utf8'); expect(wrapper).toMatch(/-v "\$PWD":\/src /); - expect(wrapper).toMatch(/sh \/src\/scripts\/visual-linux\.sh "\$mode" "\$@"/); + expect(wrapper).toMatch(/sh \/src\/\.dev\/browser\/visual-linux\.sh "\$mode" "\$@"/); }); }); diff --git a/resources/js/noPrivateContent.test.ts b/resources/js/noPrivateContent.test.ts index a135e36..6e7d8ab 100644 --- a/resources/js/noPrivateContent.test.ts +++ b/resources/js/noPrivateContent.test.ts @@ -1,4 +1,6 @@ import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; // @ts-expect-error -- plain .mjs helper without declarations import { privateContent } from '../../.dev/tools/sync-vendored-library.mjs'; @@ -9,19 +11,39 @@ import { privateContent } from '../../.dev/tools/sync-vendored-library.mjs'; * The react-icon-browser import (2026-11-12) brought 34 bundled baseline SVGs and one bundled * snapshot in; they were removed and the folder is ignored. This reads what git tracks, so a file * on disk but ignored is fine, and a tracked one fails. + * + * Since react consolidation 3.1 (2026-11-13) the e2e harness's SVG rendering sample, + * .dev/browser/public/baseline/, is tracked again, holding the public packs only (tabler and flag, + * 18 files and their manifest). `privateContent()` refuses every baseline/ path, and it is a + * byte-identical copy of js-ui-kernel's, so the sample is checked here by name and by manifest + * instead, and every other tracked file still goes through it. */ // Vitest runs from the repository root; under jsdom import.meta.url is not a file: URL. const root = process.cwd(); const tracked = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' }).split('\n').filter(Boolean); const PRIVATE_RENDER = /(icon-sets-bundled|icon-sets-metronic|metronic)[^/]*\.(png|jpe?g|webp)$/i; +/** The public SVG rendering sample: these two packs only, and its manifest. */ +const SAMPLE_DIR = '.dev/browser/public/baseline/'; +const PUBLIC_SAMPLE = /^\.dev\/browser\/public\/baseline\/(icon-sets-(tabler|flag)__[a-z_]+__[a-z0-9-]+\.svg|manifest\.json)$/; +const notPublicSample = (paths: string[]) => paths.filter((f) => f.startsWith(SAMPLE_DIR) && !PUBLIC_SAMPLE.test(f)); +const sample = tracked.filter((f) => f.startsWith(SAMPLE_DIR)); + describe('no private pack content is tracked', () => { it('reads the tracked file list', () => { expect(tracked.length).toBeGreaterThan(500); }); - it('tracks no baseline path and no bundled or Metronic SVG', () => { - expect(privateContent(tracked)).toEqual([]); + it('tracks no baseline path and no bundled or Metronic SVG, apart from the public sample', () => { + expect(privateContent(tracked.filter((f) => !f.startsWith(SAMPLE_DIR)))).toEqual([]); + }); + + it('the rendering sample holds the public packs only', () => { + expect(sample.length, 'the 18 samples and the manifest').toBeGreaterThanOrEqual(19); + expect(notPublicSample(sample)).toEqual([]); + const manifest = JSON.parse(readFileSync(join(root, SAMPLE_DIR, 'manifest.json'), 'utf8')) as { pack: string; file: string }[]; + expect(new Set(manifest.map((m) => m.pack))).toEqual(new Set(['icon-sets-tabler', 'icon-sets-flag'])); + expect(manifest.map((m) => `${SAMPLE_DIR}${m.file}`).sort()).toEqual(sample.filter((f) => !f.endsWith('manifest.json')).sort()); }); it('tracks no rendering of a private pack', () => { @@ -31,5 +53,7 @@ describe('no private pack content is tracked', () => { it('can fail (positive control)', () => { expect(privateContent(['.dev/browser/public/baseline/icon-sets-bundled__x.svg'])).toHaveLength(1); expect(PRIVATE_RENDER.test('e2e/x-snapshots/svg-icon-sets-bundled-chromium-linux.png')).toBe(true); + expect(notPublicSample([`${SAMPLE_DIR}icon-sets-metronic__gradient__forest.svg`, `${SAMPLE_DIR}icon-sets-bundled__foreign__x.svg`])).toHaveLength(2); + expect(notPublicSample([`${SAMPLE_DIR}icon-sets-flag__use_ref__ad.svg`])).toEqual([]); }); }); diff --git a/resources/js/styleRules.test.ts b/resources/js/styleRules.test.ts index f32eb1b..723eeb7 100644 --- a/resources/js/styleRules.test.ts +++ b/resources/js/styleRules.test.ts @@ -19,10 +19,10 @@ import { describe, expect, it } from 'vitest'; */ // The repository root. The source is resources/js since react consolidation 3d (2026-11-12), and -// the standalone app's two pages wait with the other imported root files. +// the standalone app's two pages are the e2e harness since 3.1 (2026-11-13). const ROOT = join(__dirname, '..', '..'); const DIRS = ['resources/js']; -const EXTRA = ['.dev/imported/react-icon-browser/index.html', '.dev/imported/react-icon-browser/embedded.html']; +const EXTRA = ['e2e/browser/index.html', 'e2e/browser/embedded.html']; const EXT = /\.(tsx?|css|html)$/; const MIN_FILES = 150; const MAX_WEIGHT = 600; diff --git a/resources/js/core/minifiedBundle.ts b/resources/js/test/minifiedBundle.ts similarity index 100% rename from resources/js/core/minifiedBundle.ts rename to resources/js/test/minifiedBundle.ts diff --git a/tsconfig.domain.json b/tsconfig.domain.json new file mode 100644 index 0000000..71c05e0 --- /dev/null +++ b/tsconfig.domain.json @@ -0,0 +1,20 @@ +// The stricter overlay for framework-free code (architecture.md 1.4). Typechecked in CI as +// its own step, so a vendored component can never make it fail. `lib` has no DOM, so a +// domain file that reaches for document, window or localStorage does not compile: the +// compiler-level twin of dependency-cruiser's module-domain-is-plain-ts rule. +// +// react-icon-browser's tsconfig.domain.json, its paths moved from src/ to resources/js/ (react +// consolidation 3.3, 2026-11-13). It extended that repository's tsconfig.app.json, which was the +// base config plus JSX and the `@/` path; both are spelled out here. +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "paths": { "@/*": ["./resources/js/*"] }, + "exactOptionalPropertyTypes": true, + "lib": ["ES2023", "ESNext.Disposable"], + "types": [] + }, + "include": ["resources/js/modules/*/domain", "resources/js/modules/*/application"], + "exclude": ["resources/js/**/*.test.ts", "resources/js/**/*.test.tsx"] +}