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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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
Expand All @@ -53,23 +53,27 @@ 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',
comment:
'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
Expand All @@ -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 },
},
{
Expand Down Expand Up @@ -111,47 +115,47 @@ 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',
comment:
"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',
Expand All @@ -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: {},
Expand All @@ -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,
},
},
Expand All @@ -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 } },
},
Expand Down
13 changes: 7 additions & 6 deletions .dev/browser/check-boundary-rules.mjs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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. */
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 12 additions & 8 deletions .dev/browser/check-dead-classes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 };
Expand Down
7 changes: 4 additions & 3 deletions .dev/browser/check-playwright-image.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
8 changes: 4 additions & 4 deletions .dev/browser/linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" "$@"
18 changes: 18 additions & 0 deletions .dev/browser/public/baseline/icon-sets-flag__clip_or_mask__ax.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions .dev/browser/public/baseline/icon-sets-flag__clip_or_mask__ba.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions .dev/browser/public/baseline/icon-sets-flag__clip_or_mask__bi.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions .dev/browser/public/baseline/icon-sets-flag__defs_ids__ag.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading