diff --git a/.gitattributes b/.gitattributes index 513eca2..70a0d19 100644 --- a/.gitattributes +++ b/.gitattributes @@ -32,3 +32,10 @@ /resources/js/**/*.test.ts export-ignore /resources/js/**/*.test.tsx export-ignore /resources/js/test export-ignore + +# The browser's Playwright suite (e2e/browser, with its baselines and harness pages) and the host +# e2e check, and the two guard configs that came with them (react consolidation 3.1/3.3), are +# development-only. +/e2e export-ignore +/.dependency-cruiser.cjs export-ignore +/tsconfig.domain.json export-ignore diff --git a/.github/workflows/committed-build.yml b/.github/workflows/committed-build.yml index 2b51563..afd15f0 100644 --- a/.github/workflows/committed-build.yml +++ b/.github/workflows/committed-build.yml @@ -47,8 +47,10 @@ jobs: - name: Compare with the committed bundle run: | - if ! git diff --exit-code --stat public/assets; then - echo "::error title=Committed bundle is stale::public/assets differs from a fresh build of this tree. Run 'npm install --no-package-lock && npm run build:prod' and commit the changes under public/assets." + # build:prod also writes resources/views/prepaint.html (react consolidation 4.1), which the + # prepaint component inlines, so it is compared with the bundle. + if ! git diff --exit-code --stat public/assets resources/views/prepaint.html; then + echo "::error title=Committed bundle is stale::public/assets or resources/views/prepaint.html differs from a fresh build of this tree. Run 'npm install --no-package-lock && npm run build:prod' and commit the changes." exit 1 fi if [ -n "$(git status --porcelain --untracked-files=all public/assets)" ]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 51cdd5d..b246b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,15 @@ All notable changes to `ichava/icon-browser` follow [Keep a Changelog](https://k ### Added +- **[Work on the browser UI](docs/recipes/frontend-development.md)**, a recipe for the dev server with hot reload, the checks, the Playwright suite and its baselines, and the build. - **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 +- **`npm run dev` serves the browser as it ships** (react consolidation 4.1). The dev server scopes the browser's stylesheet to its root and serves its fonts, as the build always did; before, a page in dev carried the browser's tokens on `:root` and its rules on the whole host page, with fallback fonts. `devStylesheet.test.ts` pins it. +- **One build per UI change.** `npm run build` and `npm run build:prod` write `resources/views/prepaint.html` before the bundle; `npm run build:prepaint` still works alone. `committed-build.yml` compares the prepaint view as well as `public/assets`. +- **The Composer archive leaves out `e2e/`, `.dependency-cruiser.cjs` and `tsconfig.domain.json`**, which the previous change had added (484 files, from 529). - `core/minifiedBundle.ts`, used only by its test, moved to `resources/js/test/minifiedBundle.ts`; dependency-cruiser's `no-unreachable` rule reported it. ### Fixed diff --git a/README.md b/README.md index 8633d6f..4762a7c 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ The docs live in this repository, under [`docs/`](docs/). - [REST mode in a non-Inertia app](docs/recipes/rest-mode.md) — the API without the Inertia pages - [Grant admin access](docs/recipes/grant-admin-access.md) — one Gate for status and the cache - [Turn features off](docs/recipes/turn-features-off.md) — hide what the host does not offer +- [Work on the browser UI](docs/recipes/frontend-development.md) — dev server with hot reload, checks, e2e and the one build ### Ecosystem diff --git a/docs/architecture.md b/docs/architecture.md index 3d32488..7cb4762 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -33,11 +33,18 @@ that repository's built library, vendored under `resources/vendor/react-icon-bro radio buttons, toggle, featured icon and `uui-theme.css`) come from `@ichava/js-ui-kernel/react`, pinned to `#v0.1.0` (the tree of `bc5120c`). Their old paths are deprecated re-exports. The tabs and the dropdown stay local. -- The standalone demo (`main.tsx`, `StandaloneApp.tsx`, `embedded.tsx`, `components/boot/`) came - with the snapshot and is not built here. -- `.dev/browser/` holds the browser's build plugins, generators and the fixture catalogue its tests - read. `.dev/imported/react-icon-browser/` holds the rest of that repository's root files - (configs, docs, e2e, the standalone pages). Both are left out of the Composer archive. +- The standalone demo (`main.tsx`, `StandaloneApp.tsx`, `embedded.tsx`, `components/boot/`) is not + shipped. It is the fixture react-icon-browser's Playwright suite drives: `e2e/browser/` holds the + specs, their baselines and the two harness pages, which `e2e/browser/vite.config.ts` builds from + `resources/js` with a static catalog (`npm run e2e:browser`, `npm run visual`). +- `.dev/browser/` holds the browser's build plugins, generators, guard scripts and the fixture + catalogue its tests read. `.dev/imported/react-icon-browser/` holds the rest of that repository's + root files (its docs, configs and manifest). `.dev/`, `e2e/` and the tests are left out of the + Composer archive. +- The layering rules came across too: `.dependency-cruiser.cjs` (`npm run boundaries`, proven able + to fail by `npm run check:boundary-rules`) and the stricter `tsconfig.domain.json` for + `modules/*/domain` and `application` (`npm run typecheck:domain`). `.github/workflows/browser.yml` + runs them, the other guards and the e2e suite on every pull request, and the visual suite weekly. `resources/js` is also the Inertia adapter around the browser: @@ -87,8 +94,8 @@ baselines of the private Metronic pack) were not imported. `sync:react-icon-browser` and `check:consumers` scripts that had only printed where the work moved. The one file still read from it, `prepaint.html`, now lives at `resources/views/prepaint.html`: the `` component inlines it, -`npm run build:prepaint` writes it from `resources/js/boot/prepaint.html`, and a Vitest test -fails while the two differ. +`npm run build:prod` writes it from `resources/js/boot/prepaint.html` before it builds the bundle +(`npm run build:prepaint` does it alone), and a Vitest test fails while the two differ. The look comes with the library: since the 0.1.0 restyle the browser draws Ichava's warm greys, the teal `#0e7c66` default accent, a 6px radius, Tabler chrome icons, square tags and tabs, flat diff --git a/docs/getting-started.md b/docs/getting-started.md index c47f8cc..4e5dd51 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -54,6 +54,7 @@ Browser adapts to the host application rather than imposing a stack, so the prot ## See also +- [Work on the browser UI](recipes/frontend-development.md), to change the React source in `resources/js` with hot reload - [Configuration](configuration.md) - [Environment variables](environment.md) - [Architecture](architecture.md) diff --git a/docs/recipes/frontend-development.md b/docs/recipes/frontend-development.md new file mode 100644 index 0000000..2293218 --- /dev/null +++ b/docs/recipes/frontend-development.md @@ -0,0 +1,58 @@ +# Work on the browser UI + +Change the browser's React source in `resources/js`, see it live in a host, then check and build it with one command each. + +## Run it with hot reload + +Start the dev server from this checkout, and let a host that has the package installed load from it: + +```bash +npm install +npm run dev # Vite on http://localhost:5174, the Inertia entry resources/js/app.tsx +``` + +```dotenv +# in the host's .env (app.debug must be on) +ICHAVA_VITE_DEV=true +``` + +The layout (`resources/views/app.blade.php`) loads `resources/js/app.tsx` from the dev server, with React Fast Refresh, whenever `ICHAVA_VITE_DEV` is on, `APP_DEBUG` is on and the server answers on `ichava.icon-browser.vite.host` and `.port`. Otherwise it loads the committed bundle. The dev server serves the browser's stylesheet scoped to its root and its fonts from `resources/assets/fonts`, as the build ships them; `resources/js/core/devStylesheet.test.ts` pins that. + +## Check it + +```bash +npm run typecheck:inertia && npm run lint && npm run test:js +npm run typecheck:domain && npm run typecheck:e2e +npm run boundaries && npm run check:boundary-rules +npm run check:schema-types && npm run check:dead-links +``` + +The Playwright suite drives the browser's standalone pages, built from `resources/js` by `e2e/browser/vite.config.ts`, with a static catalog and no Laravel host: + +```bash +npx playwright install chromium +npm run e2e:browser # 98 behavioural tests +npm run visual:linux:check # 12 screenshots, inside CI's Playwright image (needs Docker) +``` + +Screenshots are compared only in `mcr.microsoft.com/playwright:v1.65.0-noble`; `npm run check:playwright-image` fails when that tag drifts between `browser.yml`, `visual-baselines.yml`, `.dev/browser/linux.sh` and `@playwright/test`. To regenerate baselines for a pull request, dispatch the seeding workflow on its branch; it commits the changed PNGs and comments the drift: + +```bash +gh workflow run visual-baselines.yml --repo ichava/icon-browser --ref -f commit=true +``` + +## Build it + +```bash +npm run build:prod # public/assets and resources/views/prepaint.html +npm run check:dead-classes # reads the stylesheet the build wrote +``` + +Commit both outputs with the source change. `committed-build.yml` rebuilds every pull request and fails when either differs. Two traps: + +- `resources/assets/styles/ichava.scss` is a second Tailwind root whose utilities are global. It scans the repository except the paths it lists under `@source not`, so a word in a new top-level folder can become a class on every host page. Rebuild and check the diff of `public/assets/css/inertia-app.css`. +- No lock file is committed, so a rebuild on another day can pick up new dependency releases. + +--- + +[← Docs index](../../README.md#documentation) diff --git a/docs/release.md b/docs/release.md index 0782060..9e3d8b7 100644 --- a/docs/release.md +++ b/docs/release.md @@ -64,11 +64,11 @@ The browser's source lives in `resources/js` since 2026-11-12 (see [Architecture ```bash npm install npm run typecheck:inertia && npm run lint && npm run test:js -npm run build:prod # then commit public/assets -npm run build:prepaint # only when resources/js/boot/prepaint.html changed +npm run e2e:browser # the browser's Playwright suite, on its standalone harness +npm run build:prod # then commit public/assets and resources/views/prepaint.html ``` -`npm run build:prepaint` writes `resources/views/prepaint.html`, which the prepaint component inlines; commit it with the bundle. The vendored `resources/vendor/react-icon-browser` and its `sync:react-icon-browser` and `build:lib` scripts were removed on 2026-11-13. +`npm run build:prod` is the one build: it writes `resources/views/prepaint.html`, which the prepaint component inlines, and then the bundle. Commit both; `committed-build.yml` fails when either differs from a fresh build. [Work on the browser UI](recipes/frontend-development.md) covers the dev server, every check and the visual baselines. The vendored `resources/vendor/react-icon-browser` and its `sync:react-icon-browser` and `build:lib` scripts were removed on 2026-11-13. The source and the committed bundle contain Untitled UI PRO components, used under the Untitled UI licence as part of this product ([LICENSE](../LICENSE), [Architecture](architecture.md#untitled-ui-pro-components)). The repository is public (decided 2026-10-30) and the package stays proprietary. A release tag on the public repository publishes normally: `release.yml` does not gate on repository visibility. Its other safeguards stand, namely the tag-only trigger, the refusal of any ref that is not a `vX.Y.Z` tag, and the changelog section it extracts and fails closed without. diff --git a/package.json b/package.json index f5a97d6..74e3f6c 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "check:config-drift": "node .dev/tools/check-config-drift.mjs eslint.base.js=config/eslint.base.js tsconfig.base.json=config/tsconfig.base.json .dev/tools/sync-vendored-library.mjs=config/tools/sync-vendored-library.mjs .dev/tools/check-config-drift.mjs=config/check-config-drift.mjs", "// clean": "Narrow on purpose. inertia-app.js/.css are CHECKED IN and no npm script or CI job rebuilds them -- only build does. rm -rf public/* destroyed them, and a failing vite build destroyed them for nothing. Delete only what the build re-emits. See issue #26.", "clean": "rm -rf public/assets/js/inertia-app*.js public/assets/css/inertia-app.css public/.vite 2>/dev/null || true", - "build": "npm run clean && echo '' && echo '🔨 Building Ichava (dev)...' && NODE_ENV=development vite build --config vite.inertia.config.ts", - "build:prod": "npm run clean && echo '' && echo '🚀 Building Ichava (production)...' && NODE_ENV=production vite build --config vite.inertia.config.ts", + "build": "npm run clean && npm run -s build:prepaint && echo '' && echo '🔨 Building Ichava (dev)...' && NODE_ENV=development vite build --config vite.inertia.config.ts", + "build:prod": "npm run clean && npm run -s build:prepaint && echo '' && echo '🚀 Building Ichava (production)...' && NODE_ENV=production vite build --config vite.inertia.config.ts", "watch": "echo '' && echo '👀 Watching Ichava for changes...' && vite build --watch --config vite.inertia.config.ts", "dev": "vite --config vite.inertia.config.ts", "test:js": "vitest run", diff --git a/resources/js/core/devStylesheet.test.ts b/resources/js/core/devStylesheet.test.ts new file mode 100644 index 0000000..529dcd2 --- /dev/null +++ b/resources/js/core/devStylesheet.test.ts @@ -0,0 +1,54 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { createServer, type ViteDevServer } from 'vite'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +/** + * `npm run dev` serves the browser's stylesheet as a host receives it (react consolidation 4.1, + * 2026-11-13): scoped to the browser's root, with fonts that load. + * + * vite.inertia.config.ts scoped the stylesheet and rewrote its font URLs in the build only, so + * under the dev server the browser's tokens sat on `:root`, its rules reached the whole host page + * and its fonts asked for `/fonts/...`, which nothing serves. This asks the dev server for the + * stylesheet in middleware mode, which needs no port, and checks what it would send. + */ +const root = process.cwd(); +let server: ViteDevServer; +let css = ''; + +beforeAll(async () => { + server = await createServer({ + configFile: join(root, 'vite.inertia.config.ts'), + server: { middlewareMode: true, hmr: false, ws: false }, + logLevel: 'error', + }); + css = (await server.transformRequest('/resources/js/styles/theme.css?direct'))?.code ?? ''; +}, 60_000); + +afterAll(async () => { + await server?.close(); +}); + +describe('the dev server serves the browser stylesheet as the build ships it', () => { + it('serves a stylesheet at all', () => { + expect(css.length).toBeGreaterThan(50_000); + }); + + it('scopes the utilities to the browser root', () => { + expect(css).toMatch(/@layer utilities\s*\{\s*@scope \(\[data-ichava-theme\]\)/); + }); + + it('declares no browser token on the host document', () => { + const onRoot = [...css.matchAll(/(?:^|[}\s]):root\b[^{]*\{([^}]*)\}/g)].filter((m) => /--(color|ich|bg|fg|accent)/.test(m[1] ?? '')); + expect(onRoot.map((m) => m[0].slice(0, 80))).toEqual([]); + }); + + it('points every font at a file the dev server can serve', () => { + const urls = [...css.matchAll(/url\((['"]?)([^'")]*\.woff2)\1\)/g)].map((m) => m[2] ?? ''); + expect(urls.length).toBeGreaterThanOrEqual(2); + for (const url of urls) { + expect(url, url).toMatch(/^\/resources\/assets\/fonts\//); + expect(existsSync(join(root, url)), url).toBe(true); + } + }); +}); diff --git a/vite.inertia.config.ts b/vite.inertia.config.ts index b34ef89..580630a 100644 --- a/vite.inertia.config.ts +++ b/vite.inertia.config.ts @@ -42,13 +42,15 @@ const BROWSER_SCOPE = '[data-ichava-theme]'; * `scopeThemeTokens` and `scopeUtilityLayer`. Those ran over every emitted stylesheet; here the * browser's CSS shares one bundle with this package's own (ichava.scss, pages.css), whose * `:root` and `body` rules must stay global. So this runs on the browser's module alone, after - * Tailwind has compiled and minified it, and before Vite joins the stylesheets. Build only, like - * the two plugins it replaces. + * Tailwind has compiled it, and before Vite joins the stylesheets. + * + * It runs under `npm run dev` too (react consolidation 4.1, 2026-11-13). It used to be build only, + * so the dev server put the browser's tokens on `:root` and its rules on the whole host page, and + * a page looked different in dev from what a host receives. `DevStylesheetTest` pins both modes. */ function browserStylesheet(): Plugin { return { name: 'ichava:browser-stylesheet', - apply: 'build', transform(code, id) { if (id.split('?')[0] !== BROWSER_STYLESHEET) return null; return { code: scopeSheet(scopeTokens(code, BROWSER_SCOPE), BROWSER_SCOPE), map: null }; @@ -85,13 +87,30 @@ function shipFonts(): Plugin { }; } +/** + * Serve the fonts under `npm run dev`. theme.css asks for `/fonts/...`, which only the build + * rewrites (shipFonts); the dev server serves the repository at its root, so in dev the URLs point + * at the files in resources/assets/fonts instead of at a path nothing serves. + */ +function devFonts(): Plugin { + return { + name: 'ichava:browser-dev-fonts', + apply: 'serve', + enforce: 'post', + transform(code, id) { + if (id.split('?')[0] !== BROWSER_STYLESHEET) return null; + return { code: code.replace(/url\((['"]?)\/fonts\//g, 'url($1/resources/assets/fonts/'), map: null }; + }, + }; +} + /** package.json's version, for the browser's `config.meta.version`. */ const VERSION = (JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as { version: string }).version; export default defineConfig({ base: './', define: { __ICHAVA_BROWSER_VERSION__: JSON.stringify(VERSION) }, - plugins: [react(), tailwindcss(), browserStylesheet(), shipFonts()], + plugins: [react(), tailwindcss(), browserStylesheet(), shipFonts(), devFonts()], resolve: { alias: { '@': fileURLToPath(new URL('./resources/js', import.meta.url)),