From 1473a440b1907842b2e34f527344faf3cfbe4bb4 Mon Sep 17 00:00:00 2001 From: Vivek Date: Wed, 5 Aug 2026 22:44:22 +0530 Subject: [PATCH 01/10] docs: correct drifted claims in five posts and the CSP comment Six published statements contradicted the code they described, and the blog is the surface search engines and AI agents read most, so a reader was being sent after rules and fallbacks the framework has never had. Four rule names in the posts were never in RULES, and one of them was promised as a linter that would catch unprefixed light-DOM selectors. The rule list and rule count are gone rather than corrected, because a list copied into a post is what drifted in the first place; the posts now name a few real rules and point at `webjs check --rules`. The esbuild fallback was removed from the strip path long ago, and the post describing it was the last place claiming it still existed. The same post showed a TS_CACHE identifier that does not exist. The CSP comment blamed Tailwind's browser runtime for the style-src inline allowance. A scaffolded app compiles Tailwind to a static file and has no browser runtime at all, so the comment now enumerates the four things that genuinely carry no nonce, and contrasts them with the framework's own head style, which is signed. Verified by booting an app with csp on and no Tailwind in it: four style tags, one nonce. Two router non-goals had since been built, prefetch and the per-nav full refetch, the second by a mechanism the same post already explains. Comment-only in csp.js; every directive string is untouched and packages/server/test/csp/csp.test.js passes unedited. --- blog/ai-first-is-plumbing.md | 11 ++++------- blog/client-router-turbo-drive-style.md | 8 ++++---- blog/light-dom-by-default.md | 2 +- blog/stop-ai-agents-breaking-your-code.md | 2 +- blog/strip-types-not-esbuild.md | 8 +++++--- packages/server/src/csp.js | 18 ++++++++++++------ 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/blog/ai-first-is-plumbing.md b/blog/ai-first-is-plumbing.md index 123dc4ea7..a5482c2b1 100644 --- a/blog/ai-first-is-plumbing.md +++ b/blog/ai-first-is-plumbing.md @@ -48,21 +48,18 @@ What this gets us is that an agent in autonomous mode (running with permission t # The convention validator -`webjs check` runs a set of lint rules over the project: +`webjs check` runs a set of lint rules over the project. A few real ones: -- `tests-exist`: every server action / query has a corresponding test -- `no-server-imports-in-components`: components do not import `node:*` or a DB driver (`pg`) +- `no-server-import-in-browser-module`: a module that genuinely ships to the browser does not import a server-only file, whose client stub throws at load - `use-server-needs-extension`: a `'use server'` directive requires a `.server.{js,ts}` filename - `no-static-properties`: reactive properties are declared via the `extends WebComponent({ … })` factory, not a hand-written `static properties` - `erasable-typescript-only`: `tsconfig.json` has `erasableSyntaxOnly: true` - `shell-in-non-root-layout`: non-root layouts and pages don't write `` / `` / `` / `` -- `no-json-data-files`: app data lives in the database, not in JSON files - `no-server-env-in-components`: `process.env.X` in components only reads `WEBJS_PUBLIC_*` or `NODE_ENV` -- `light-dom-css-prefix`: light-DOM components with custom CSS prefix every selector with the tag name -Each rule lives in `packages/server/src/check.js`. New rules are about 20 lines apiece. The agent runs `webjs check` before committing, sees violations as concrete messages, and fixes them. +Each rule lives in `packages/server/src/check.js`. The agent runs `webjs check` before committing, sees violations as concrete messages, and fixes them. -The lint is intentionally narrow. We have ~10 rules, not 100. The rules cover invariants that crash in production. The framework does not lint style preferences. +The lint is intentionally narrow. Every rule covers an invariant that crashes in production, and the framework does not lint style preferences. `webjs check --rules` prints the current set, which is the only place worth reading it from, because a list copied into a blog post goes stale the next time a rule lands. # Hooks beyond pre-commit diff --git a/blog/client-router-turbo-drive-style.md b/blog/client-router-turbo-drive-style.md index bc74edf1a..69d527831 100644 --- a/blog/client-router-turbo-drive-style.md +++ b/blog/client-router-turbo-drive-style.md @@ -73,13 +73,13 @@ Forms that already call `event.preventDefault()` in their `@submit` handler are # What the router does not do -Three explicit non-goals, called out in the source. +One explicit non-goal, plus two that used to be on this list and have since been built. -No prefetching. The router does not warm `` targets on hover or viewport entry. The platform is getting better at this (Speculation Rules API, Chrome's per-link prefetch hints), and we did not want to ship a heuristic we would have to tune. Apps that need it can layer it on. +No view-transitions API by default. View Transitions are great when supported, but the spec is still evolving, so the default off-state matches what works in every browser. An app that wants them opts in with a `` tag in the root layout, which is the whole configuration. -No view-transitions API by default. View Transitions are great when supported (Chromium-only as of writing), but the spec is still evolving. The default off-state matches what works in every browser. +Prefetching was the first of the two. The router warms link targets on its own now, and it picks the strategy from the device rather than applying one everywhere: hover intent where there is a real pointer, and dwell-gated viewport entry on touch, where hover does not exist to hook. I wrote up how that choice gets made in [Device-adaptive link prefetch](/blog/device-adaptive-link-prefetch). -No nested-route data deduplication. Each navigation re-fetches the page from scratch. We do not try to keep "data we already have" and only refetch the diff. The HTTP cache and the framework's `cache()` query memoization handle this at a different layer. +Per-navigation full refetch was the second. The `X-Webjs-Have` mechanism described above is exactly the deduplication this section used to disclaim, so a navigation that shares a layout chain with the current page pays for the divergent fragment and nothing more. # What happens on a rapid click diff --git a/blog/light-dom-by-default.md b/blog/light-dom-by-default.md index 819ddad51..9375f7b26 100644 --- a/blog/light-dom-by-default.md +++ b/blog/light-dom-by-default.md @@ -110,7 +110,7 @@ The argument for shadow DOM is usually scoping: your component's styles will not For most application code, this is not the problem you have. You control the page. You control the components. Your CSS is intentional. The "leakage" risk is mostly a thought experiment. -Tailwind sidesteps the scoping question entirely: utility classes are atomic, intentional, and unique by construction. There is nothing to leak. If you author components with Tailwind utilities, scoping is a non-issue. The same applies to other naming-discipline approaches like BEM or scoped class prefixes; the framework's `webjs check` ships a `light-dom-css-prefix` rule that flags unprefixed class selectors in vanilla CSS for light-DOM components, so the linter helps you keep selectors uniquely scoped if you choose that route. +Tailwind sidesteps the scoping question entirely: utility classes are atomic, intentional, and unique by construction. There is nothing to leak. If you author components with Tailwind utilities, scoping is a non-issue. The same applies to other naming-discipline approaches like BEM or scoped class prefixes. When a light-DOM component does author vanilla CSS, the convention is to prefix every class selector with the component's own tag name, which makes it as unique as a utility class would be. That one is a discipline you keep rather than something tooling checks for you, which is one more reason Tailwind stays the lower-effort default. WebJs's recommendation: use Tailwind (or your chosen styling story) in light DOM by default. If a specific component needs strict isolation (third-party embed, design-system component meant to drop into hostile pages), opt into shadow DOM for that one component: diff --git a/blog/stop-ai-agents-breaking-your-code.md b/blog/stop-ai-agents-breaking-your-code.md index 8d7112c3e..2e2d27a6c 100644 --- a/blog/stop-ai-agents-breaking-your-code.md +++ b/blog/stop-ai-agents-breaking-your-code.md @@ -84,7 +84,7 @@ The rules are the concrete failure modes an agent trips over. A few real ones: - `no-static-properties`, reactive properties are declared through the `WebComponent({ ... })` factory, not a hand-written `static properties` block that would throw at runtime. - `erasable-typescript-only`, the tsconfig must keep types strippable, because WebJs has no bundler to fall back on. - `shell-in-non-root-layout`, only the root layout may write `` / `` / `` / ``. -- `light-dom-css-prefix`, a light-DOM component with custom CSS prefixes every selector with its tag name. +- `no-browser-globals-in-render`, a component's constructor and `render()` stay off browser globals, because both run on the server too and a `window` read there crashes the SSR pass. An agent runs `webjs check`, reads concrete violation messages, and fixes them before the code is anywhere near a review. The narrowness is deliberate. It is a short list of "this will break", not a hundred style opinions, so a green check actually means something. diff --git a/blog/strip-types-not-esbuild.md b/blog/strip-types-not-esbuild.md index 91d4cf555..8a227bc2e 100644 --- a/blog/strip-types-not-esbuild.md +++ b/blog/strip-types-not-esbuild.md @@ -52,14 +52,16 @@ The work landed as PR #9 (merge `3c29d99`, branch `feat/replace-esbuild-with-str The cache shape is straightforward: ```ts -const TS_CACHE = new Map(); const TS_CACHE_MAX = 500; +// state.tsCache, one Map per request handler // Entry: { mtimeMs, code, map: string | null } ``` -Capped at 500 entries to prevent unbounded memory growth in long-running production servers. Keyed by absolute path, invalidated when the file's mtime changes. First request through is on the order of a hundred microseconds per file. Subsequent requests are Map lookups. +Capped at 500 entries to prevent unbounded memory growth in long-running production servers. Keyed by absolute path, invalidated when the file's mtime changes. First request through is on the order of a hundred microseconds per file. Subsequent requests are Map lookups. The Map hangs off the request handler rather than the module, because the cached bytes bake in that handler's elision verdict, so two handlers for the same app with different elision settings must not share one. -For the rare case where a file uses non-erasable syntax, the server falls back to `esbuild.transform`. The fallback path is triggered specifically when the primary path throws `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`. esbuild emits an inline sourcemap so DevTools can still resolve source positions for the regenerated JS. Mostly fires for third-party `.ts` files; user code is enforced erasable by `webjs check`'s `erasable-typescript-only` rule. +There is no fallback for a file that uses non-erasable syntax. Stripping throws, the server returns a clean 500 naming the file, and that is the entire error path. Nothing regenerates the code through a bundler, which is what keeps the guarantee worth having: the file on disk is the file that runs, with no second code path where that stops being true. Two `webjs check` rules catch the problem at edit time instead, `erasable-typescript-only` on the tsconfig flag and `no-non-erasable-typescript` on the source itself. + +What does vary is the backend doing the stripping. On Node it is the built-in `module.stripTypeScriptTypes`. On Bun, which has no such built-in, it is `amaro`, and since Node's built-in is itself a thin wrapper over `amaro`'s `strip-only` mode, both runtimes produce byte-identical output with the same position preservation. # What this enabled downstream diff --git a/packages/server/src/csp.js b/packages/server/src/csp.js index e3b7a1929..b3daeaa66 100644 --- a/packages/server/src/csp.js +++ b/packages/server/src/csp.js @@ -50,8 +50,8 @@ const CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/; * with webjs's own output, which is: nonce-signed inline `