Skip to content

feat(server): replace esbuild TS stripping with Node 24+ strip-types#9

Merged
vivek7405 merged 7 commits into
mainfrom
feat/replace-esbuild-with-strip-types
May 18, 2026
Merged

feat(server): replace esbuild TS stripping with Node 24+ strip-types#9
vivek7405 merged 7 commits into
mainfrom
feat/replace-esbuild-with-strip-types

Conversation

@vivek7405
Copy link
Copy Markdown
Collaborator

Summary

Replace the esbuild-based TypeScript stripper with Node 24+'s built-in module.stripTypeScriptTypes. The new stripper uses whitespace replacement (byte-exact line + column position preservation), so no sourcemap is shipped to the browser. Result: roughly 65-70% reduction in wire bytes for every user .ts file, plus byte-exact stack traces without sourcemap indirection.

For the rare third-party .ts dependency that uses non-erasable syntax (enum, namespace with values, parameter properties, legacy decorators), the dev server transparently falls back to esbuild.transform with an inline sourcemap. esbuild stays in the dependency tree as a runtime fallback and as the bundler for vendor.js (auto-bundling npm packages for the browser), but its scope shrinks substantially.

User code is held to erasable TypeScript via the new erasableSyntaxOnly: true flag in tsconfig (shipped by the scaffold and the framework's own first-party apps) and a new webjs check rule (erasable-typescript-only) that warns if the flag is missing or false.

What changed

  • engines >=24.0.0 across every published package (root, core, server, cli, ui, ts-plugin) plus the first-party apps (examples/blog, docs, website). Node 24 made strip-types default-on with no experimental warning.
  • packages/server/src/esbuild-loader.js deleted. Server-side .ts imports work natively via Node's default strip-types. No module.register() hook needed.
  • packages/server/src/dev.js: new stripTs(source, abs) helper. Primary path is module.stripTypeScriptTypes. Catches ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX and falls back to esbuild + inline sourcemap. The one-shot ExperimentalWarning is suppressed at startup.
  • packages/server/src/check.js: new erasable-typescript-only rule. Loads the user's tsconfig.json, warns when compilerOptions.erasableSyntaxOnly is missing or false. Lighter than parsing TS ourselves; the actual checking is delegated to tsc.
  • packages/cli/lib/create.js: scaffold writes erasableSyntaxOnly: true into every new app's tsconfig.
  • First-party tsconfigs in examples/blog, docs, website all add the flag. Verified passing.
  • AGENTS.md + agent-docs/typescript.md + docs site (typescript / editor-setup pages) + scaffold AGENTS / CONVENTIONS / .cursorrules / .windsurfrules / copilot-instructions.md: erasable-TS guidance with do/don't examples for each banned pattern.
  • Website marketing copy ("No Build Step" + "TypeScript or JSDoc" cards) reflects the new model.

Wire-byte impact, measured

examples/blog/app/page.ts (4,371 bytes source) round-trip through the dev server:

Bytes Brotli
Before (esbuild + inline sourcemap) 12,657 ~5.2 KB
After (Node strip-types, no sourcemap) 4,382 ~1.6 KB
Reduction -65% -70%

For a typical webjs app with ~20 first-party .ts files, this saves ~70 KB brotli on a cold page load.

Test plan

  • Unit tests: 895/895 pass (no regressions vs main)
  • Browser tests (wtr + Chromium): 74 pass / 27 fail. The 27 failures are the same pre-existing ui-stateful / ui-overlay ones that fail on main. Zero new regressions.
  • E2E tests (WEBJS_E2E=1): 46/48 pass. The 2 failures are pre-existing on main (layout data-layout wrapper, dynamic-route head). Zero new regressions.
  • App-level unit tests (per-file): 43/43 pass.
  • webjs check passes against examples/blog (with the new erasable-typescript-only rule active).
  • Manual verification that the hybrid stripper works end-to-end: erasable TS goes through stripTypeScriptTypes (no sourcemap, byte-exact positions); a temporary enum file routed correctly through the esbuild fallback (with inline sourcemap).
  • Docs site (port 4000) and website (port 5000) boot and serve their pages with no errors.

Breaking changes

  • Node 24+ required. Previously the framework declared engines: ">=20" but used module.register() for the loader hook. Now native strip-types replaces the loader, and Node 24 is the floor where it ships default-on without an experimental warning.
  • erasableSyntaxOnly: true is now the recommended tsconfig setting. Apps that previously used enum, namespace, parameter properties, or legacy decorators will either need to switch to erasable equivalents (recommended) or accept the esbuild fallback path (slower, ~3x wire bytes).

vivek7405 added 7 commits May 19, 2026 01:32
Node 24 made TypeScript type-stripping default-on (no flag required)
and stable (no experimental warning). This is the floor that lets the
framework drop its custom esbuild module loader and rely on Node's
built-in stripper for server-side .ts imports.
Server-side .ts imports now use Node 24+'s default type-stripping
natively. The module.register('./esbuild-loader.js') hook and the
loader file itself are deleted: SSR and the test runner both pick
up .ts files without any framework bootstrap.

Browser-bound .ts files served by the dev server's tsResponse path
go through a new stripTs(source, abs) helper that:

  1. Tries module.stripTypeScriptTypes (Node built-in). The whitespace
     replacement preserves every (line, column) position byte-exactly,
     so no sourcemap is emitted. ~70% wire-byte reduction vs the prior
     esbuild + inline sourcemap pipeline.

  2. Catches ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX and falls back to
     esbuild.transform with sourcemap=inline. This handles the rare
     third-party .ts dependency that uses enum, namespace, parameter
     properties, or legacy decorators. Framework + user code stays on
     erasable TS via a forthcoming webjs check rule.

The ExperimentalWarning that node:module's stripTypeScriptTypes emits
on first call is suppressed by intercepting process.emitWarning for
just that warning (every other warning passes through unchanged).
…in tsconfigs

Adds compilerOptions.erasableSyntaxOnly to: the scaffold tsconfig
generator (packages/cli/lib/create.js), the blog example, the docs
site, the marketing site. TypeScript 5.8+ rejects enum, namespace
with values, constructor parameter properties, legacy decorators
with emitDecoratorMetadata, and import = require at compile time
when this flag is set. Violations surface as red squiggles in the
editor, well before they would hit Node's built-in strip-types and
trigger the (slower, sourcemap-bearing) esbuild fallback path.

Adds a new webjs check rule, erasable-typescript-only, that loads
the project's tsconfig.json and warns when erasableSyntaxOnly is
missing or set to false. Lighter than a custom TS parser because
the actual checking is delegated to tsc; this rule just verifies
the tsconfig opted in. Verified against the blog example: passes
with the flag on, flags a single violation when flipped to false.
…affolds

Update guidance everywhere AI agents and humans read framework rules:
root AGENTS.md (invariant #10), agent-docs/typescript.md (full
rewrite of TypeScript Feature Support), docs site (typescript and
editor-setup pages), scaffold AGENTS.md / CONVENTIONS.md /
.cursorrules / .windsurfrules / copilot-instructions.md.

Each surface explains: (1) Node 24+ requirement, (2) byte-exact
position preservation via module.stripTypeScriptTypes (no sourcemap
shipped to the browser), (3) the erasable-TS constraint with worked
do-and-dont examples for the four banned patterns (enum, namespace
with values, constructor parameter properties, legacy decorators),
(4) the esbuild fallback for the rare third-party non-erasable
dep, (5) the cost (~3x wire bytes + sourcemap overhead) of taking
the fallback path, (6) how erasableSyntaxOnly in tsconfig and the
erasable-typescript-only convention check enforce the constraint.
The 'No Build Step' card now mentions byte-exact whitespace
replacement and no sourcemap overhead. The 'TypeScript or JSDoc'
card calls out the erasable-TS constraint explicitly and notes the
erasableSyntaxOnly tsconfig flag + the erasable-typescript-only
convention check as the editor-time guard.
Add the runtime-backing detail in framework-engineering-facing
surfaces (not user-facing scaffold templates, which don't need it).

If the framework ever runs on a non-Node runtime (Bun, Deno), Node's
`module.stripTypeScriptTypes` won't be available. The replacement
is the `amaro` package (what Node uses internally, wraps SWC's
WASM TypeScript transform in position-preserving strip-only mode).
Other equivalents: Sucrase (lines preserved, columns not), SWC's
strip mode. Documented in:

  - packages/server/src/dev.js (next to the stripTypeScriptTypes
    import, where someone doing runtime work will read it)
  - agent-docs/typescript.md (deep TS reference)
  - root AGENTS.md invariant #10 (the erasable-TS invariant)
  - packages/server/AGENTS.md (the dev.js row in the module map)

Also remove the stale esbuild-loader.js row from packages/server/
AGENTS.md (the file was deleted earlier in this branch).
@vivek7405 vivek7405 merged commit b9bae89 into main May 18, 2026
@vivek7405 vivek7405 deleted the feat/replace-esbuild-with-strip-types branch May 18, 2026 20:37
vivek7405 added a commit that referenced this pull request May 21, 2026
…types

feat(server): replace esbuild TS stripping with Node 24+ strip-types
vivek7405 added a commit that referenced this pull request May 21, 2026
…pes model

Follow-up to PR #9 (the esbuild-to-Node-strip-types refactor). The
prior sweep updated the headline TypeScript and editor-setup docs
but missed six other surfaces that still described the old loader
hook + Node 20.6 minimum.

Updated:
  - docs/app/docs/getting-started/page.ts (prerequisites + How It Works)
  - docs/app/docs/deployment/page.ts (lead paragraph + checklist)
  - docs/app/docs/routing/page.ts (file extension paragraph)
  - docs/app/docs/ssr/page.ts (TypeScript paragraph after the page example)
  - agent-docs/typescript.md (Import convention section)
  - test/dev-handler.test.js (comment on the non-erasable TS test, now
    correctly describes the hybrid stripper instead of "both paths
    use esbuild")

Each surface now uses the strip-types language already established
in docs/app/docs/typescript/page.ts. Sanity-checked: all updated
pages serve 200 from a local docs dev server with the new copy.
vivek7405 added a commit that referenced this pull request May 22, 2026
* feat(website): launch the /blog with 11 grounded long-form posts

Infrastructure:
- `blog/<slug>.md` with frontmatter at the repo root, mirroring the
  changelog/ shape. Hand-rolled frontmatter parse + markdown renderer
  in the page handlers (no markdown library, no client runtime).
- `website/app/blog/page.ts`: index page listing all posts sorted by
  date DESC. Layout boundary matches /changelog (max-w-[840px]).
- `website/app/blog/[slug]/page.ts`: per-post page with full SEO
  metadata (title, description, og:title, og:description, og:type,
  og:url, twitter:card, publishedTime, author, tags). canonical URL
  per post. Custom-positioned bullets that stay inside the layout
  via `before:` pseudo-elements. Code blocks with internal padding so
  long lines do not stick to the left border when they overflow-x.
- Nav: `/blog` link added to both desktop and mobile header.
- Railway watch path: `/blog/**` added to the website service via
  the railway agent, so future blog edits trigger redeploys.

Posts, each anchored in actual git history / PR descriptions /
source-file docstrings (not invented details):

- `why-webjs` (origin/thesis, derived from the author's existing
  post at heyvivek.com; tagline "tiny in size, not in power")
- `betting-on-lits-mental-model` (the API parity rationale; 127
  lit-ported tests from PR #31's title)
- `strip-types-not-esbuild` (the Node 24 stripper migration in
  PR #9; cache details from packages/server/src/dev.js)
- `signals-replaced-setstate` (PR #43, breaking change; TC39
  Stage 1 shape; algorithm description from signal.js docstring)
- `light-dom-slots-with-full-parity` (PR #8 / #44; polyfill
  design from slot.js docstring)
- `the-naming-saga` (the wjs/webjscli/webjsdev/create-webjs arc
  from this PR's own development)
- `ai-first-is-plumbing` (AGENTS.md + the multi-tool config files
  + hooks + lint rules, all verifiable in scaffold templates)
- `file-based-routing` (router.js JSDoc lists the conventions;
  same Next.js shape, with the divergences spelled out)
- `client-router-turbo-drive-style` (router-client.js docstring
  + ssr.js's X-Webjs-Have handling)
- `why-not-lit-as-a-dependency` (SSR + decorators + the
  AI-reads-node_modules angle the user surfaced)
- `built-ins-auth-session-cookies-cache` (the four-method cache
  store interface from cache.js's CacheStore typedef, the Remix-
  shaped Session class, the NextAuth-shaped createAuth())

Typography:
- 17px paragraph at 1.8 leading, my-7 spacing.
- Title at clamp(36px, 6vw, 56px), more presence on the page.
- Description in serif italic at 19px.
- Headings at clamp 21-34px with strong vertical rhythm.
- Code blocks at 13px monospace with px-6 py-5 padding inside
  the code element (not the pre) so overflow-x scrolls cleanly.
- Footer pad-top + mt-28 so the "All posts" link does not collide
  with the last paragraph.

Markdown supported by the renderer:
- # / ## / ### headings (h2 / h3 / h4 in output)
- Paragraphs
- Bulleted lists with custom-positioned markers
- `> ` blockquotes with accent border
- ```fenced``` code blocks
- Inline: **bold**, *italic*, `code`, [link](url)

* fix(blog): switch [slug] page spacing to arbitrary-value classes

Three concrete formatting fixes that all stemmed from one root cause:
Tailwind named-scale utilities (mt-20, my-8, mt-14, my-7) were NOT
landing in the compiled tailwind.css. The dev-server's watcher had
not picked them up from the new blog/[slug]/page.ts. So the page
rendered with classes that resolved to no CSS at all, which is why
the user saw:

  - Headings sticking to the previous paragraph (no mt-20).
  - "All posts" footer link colliding with last paragraph.
  - Code blocks with no vertical breathing room.

Fix: switch every spacing utility to its arbitrary-value form
(`mt-[80px]` instead of `mt-20`, `my-[28px]` instead of `my-7`,
etc.). Arbitrary-value classes get JIT-emitted from the literal
token in source, so they compile regardless of whether the named
scale has been brought into the build.

Also brings the [slug] page's max-width from 760px back up to
840px to match /changelog and /blog index, restoring the layout
boundary parity the user pointed out twice.

The compiled tailwind.css is gitignored (regenerated at deploy
time), so this commit ships only the source change. The classes
JIT correctly on the next `tailwindcss` invocation.

Bundle of post-grade improvements:
  - Code block padding moved inside the <code> with px-[24px]
    py-[20px], so overflow-x preserves padding on both sides.
  - List items use `before:content-['•']` absolute-positioned
    markers so bullets stay inside the layout column.
  - Title at clamp(36px, 6vw, 56px), description in serif italic.
  - Footer with mt-[128px] pt-[40px] for the "All posts" link.

* refactor(website): move blog + changelog logic into modules/, follow webjs's own convention

The website was stuffing file-reading, frontmatter parsing, and markdown
rendering directly inside `app/blog/page.ts`, `app/blog/[slug]/page.ts`,
and `app/changelog/page.ts`. That violates the layout we tell every
scaffolded webjs app to follow in AGENTS.md:

  app/                      ROUTING ONLY. Thin route adapters.
  modules/<feature>/        Feature-scoped queries + utils + types.
  lib/                      App-wide helpers.

Dogfooding fix. New layout:

  website/lib/frontmatter.ts                    Shared parser (browser-safe).
  website/modules/blog/queries/
    list-posts.server.ts                        Read all posts, return metadata.
    get-post.server.ts                          Read one post by slug, return body.
  website/modules/blog/utils/render-post.ts     Long-form markdown renderer.
  website/modules/blog/types.ts                 Post / PostWithBody.
  website/modules/changelog/queries/
    list-entries.server.ts                      Read all changelog entries.
  website/modules/changelog/utils/
    render-entry.ts                             Compact-card markdown renderer.
    pkg-badge.ts                                Color-coded package pill.
  website/modules/changelog/types.ts            Entry.

The route files at `website/app/blog/page.ts`,
`website/app/blog/[slug]/page.ts`, and `website/app/changelog/page.ts`
are now thin adapters that import from the modules and render the
result. None of them do file IO or string-parsing directly.

Same routes, same output. Logic moved to where AGENTS.md says it
should live.

The `'use server'` directive on each query file makes the file
source-protected (browser imports get a throw-at-load stub) and
RPC-callable (so a client component could in principle import
`listPosts` if it needed to, and the dev server would rewrite the
import into an RPC stub). For the current pages, both query files
are only called server-side from the page's default export.

Sharing across the two features: `parseFrontmatter()` is identical
for both, so it lives in `website/lib/frontmatter.ts` (lib/ scope
because it's cross-feature). The inline-markdown regexes diverge
between the two renderers (different code-block sizes, different
heading typography), so each module has its own renderer rather
than parameterizing a shared one. Two callers, two short
implementations, no premature abstraction.

* blog(why-webjs): replace 'started' with 'built' in opening

* blog(why-webjs): retitle to avoid duplicating the personal-blog title

* blog(why-webjs): drop 'small' from the title

* blog(why-webjs): reframe around 'wanted this framework, built it for myself'

The post leaned hard on critiquing other frameworks (stack traces in
minified bundles, convention drift between engineers, etc.). Reframed
per the user's direction: the story is "I wanted a framework close
to web standards with the Next.js-style DX I enjoy. Could not find
one I personally liked. Built one for myself. AI-first followed
naturally from building it from scratch in 2025."

New shape:

  - Open with what I wanted (web standards + Next.js DX), the search,
    the not-finding, the decision to build my own.
  - "Close to web standards" section explains the platform-first
    architecture (native web components, lit-shaped public API on
    top), without comparing other frameworks unfavorably.
  - "How small that lets the framework be" surfaces the concrete
    5-10% of Next.js size claim, with the feature parity list and
    the explanation: the platform does the heavy lifting (web
    components, Node 24 strip-types, HTTP/2 multiplex, CSS vars).
  - "Why AI-first followed naturally" reframes the AI-first content
    as the consequence of building from scratch in 2025, not the
    starting motivation. Same content, different positioning.

Removed:

  - "watching AI agents try to write code in those frameworks ..."
    paragraph that read as a critique of competitors.
  - "stack traces that pointed at minified bundle positions the
    agent could not read" line.
  - "conventions that two engineers would interpret differently"
    line, which read as a dig.
  - The "why web components, not React/Vue/Svelte/Solid?" framing.
    Replaced with "what close-to-standards means" which states the
    positive case without the comparison.

Title and intro keep the AI-era angle for SEO and for the inaugural-
post role of why-webjs.md.

* blog(lit): consolidate the two lit posts into one

The two posts (`betting-on-lits-mental-model.md` and
`why-not-lit-as-a-dependency.md`) argued the same point from two
angles, with substantial content overlap. Merged the strongest
material from both into one post and deleted the redundant file.

Kept the `betting-on-lits-mental-model` slug (better SEO surface,
nuanced title). Retitled to "Lit-shaped, without depending on lit"
to flag the dual angle directly.

The merged post is now structured as:

  1. The "minimal version: just re-export lit" code, and why I
     considered it for a week before writing my own runtime.
  2. What I wanted to KEEP from lit (the API surface the corpus
     already knows, with the four-agents experiment as evidence).
  3. Why I did NOT depend on lit, broken into four reasons in load-
     bearing order:
     a. SSR (the killer, with the four lit-ssr structural limits)
     b. The decorator + erasable-TypeScript conflict
     c. The AI-agent-reads-node_modules readability argument
     d. Fine-grained control over edge cases
  4. What an LLM sees when it reads webjs (the code-diff comparison)
  5. What the runtime ownership cost (lost lit bug fixes,
     lost cleverness, ~10 KB size delta)
  6. The "what if lit ships SSR + slots tomorrow" hypothetical
  7. Not a dig at lit
  8. Reading the actual implementation

* blog(light-dom-default): add post on why webjs picks light DOM as default

Most web-components frameworks default to shadow DOM (lit, Stencil,
FAST). webjs flips the default: every component renders in light DOM
unless it sets `static shadow = true`. The post walks through six
concrete benefits in load-bearing order:

  1. Tailwind utility classes apply (the load-bearing one for webjs).
  2. CSS stays cache-friendly: external stylesheet hit once by the
     browser, instead of inline `static styles` shipped per page.
  3. document.getElementById, querySelector, closest just work
     without shadow-piercing.
  4. Accessibility behaves the way ARIA + form association specs
     assume (aria-labelledby across roots, form data carrying
     light-DOM input names, no formAssociated/ElementInternals
     ceremony).
  5. Playwright / Puppeteer / Web Test Runner selectors work
     without `>>>` pierce syntax. Agents writing tests reuse the
     same selectors they write in components.
  6. SEO + crawler reach is more reliable in initial HTML. Modern
     Googlebot handles DSD correctly, but the long tail of
     crawlers, social-card scrapers, RSS readers, and archival
     bots is more variable. Light DOM is the lower-variance answer.

Counters the "but scoping!" argument by pointing at the two real
shadow-DOM use cases (third-party embeds, design-system primitives
meant to drop into hostile pages) and notes that Tailwind utilities
sidestep the leakage thought experiment for app code.

Links to the existing light-dom-slots-with-full-parity post for
the slot-projection story, which is what unblocks light DOM as a
serious default (most frameworks treat <slot> as a shadow-only
feature, webjs ships full parity in both modes).

Dated 2025-12-22, slotted between why-webjs (2025-12-15) and
light-dom-slots-with-full-parity (2025-12-30) so the
foundational decision lands before the slot deep-dive.

* blog(light-dom-default): correct the 'shadow DOM is the default' framing

Native web components default to LIGHT DOM. If a custom element does
not call attachShadow(...), there is no shadow root. Lit picked a
different default for its LitElement class (it attaches a shadow root
in the constructor unless you override createRenderRoot to return
this), and because lit is what most developers and most AI training
data treat as canonical web-components style, the perception has
shifted toward "shadow DOM is the default."

The opening of the post implied the latter. Rewritten to state the
platform-level fact accurately:

  - Native web components default to light DOM.
  - lit defaults to shadow DOM by attaching a shadow root in
    LitElement's constructor.
  - webjs aligns with the platform default, not lit's default.

This is also a sharper framing for the rest of the post: the
"benefits of light DOM" become "benefits the platform already gives
you that lit's default opts out of."

Description in the frontmatter updated to match.

* blog(light-dom-default): restore Stencil + FAST alongside lit in the framing

Web-verified the original claim: lit, Stencil, and FAST all default
to shadow DOM. With three independent data points, the framing is
sharper than "lit picked a different default" alone:

  - lit: LitElement attaches a shadow root in its constructor
    unless you override createRenderRoot to return this.
  - Stencil: components default to shadow DOM. The stencil generate
    CLI emits shadow-enabled components and the @component
    decorator's shadow field defaults to true.
  - FAST: FASTElement automatically attaches a ShadowRoot and
    renders the template into it.

All three are cited inline with links to their official docs.

The reframing is "the three libraries developers actually learn
web components from all default to shadow, and that's where the
'shadow is the default' perception comes from. The platform itself
does not share that default."

That is a stronger argument for webjs's choice than the
single-library version.

* blog(light-dom-default): correct Stencil's actual default

Stencil's @component decorator defaults shadow: false (light DOM).
You opt INTO shadow with @component({ shadow: true }). The earlier
claim that Stencil "defaults to shadow DOM" conflated the CLI
scaffolder default (the `stencil generate` template enables shadow)
with the framework's actual decorator default (which does not).

Verified by fetching both stencil.io/docs/styling and
stencil.io/docs/component:

  - styling: "To use the Shadow DOM in a Stencil component, you can
    set the shadow option to true in the component decorator."
  - component: "If shadow is set to false, the component will not
    use native shadow DOM encapsulation." Default is false.

The post now states the accurate picture:

  - lit defaults to shadow at the framework level
  - FAST defaults to shadow at the framework level
  - Stencil defaults to light at the framework level; the CLI
    scaffolder is what produces shadow-enabled components

The "popular libraries pick shadow" framing is now scoped to lit
and FAST. Stencil is called out as the precedent that backs webjs's
choice: same underlying default (light), shadow as opt-in.

This is actually a stronger argument for webjs than the previous
"everyone defaults to shadow" framing, because it shows there is
existing precedent in the ecosystem for light-DOM-by-default at the
framework level.

* blog(light-dom-default): clarify Tailwind is scaffold default, not requirement

The post was too Tailwind-centric. Easy to read it as "you need
Tailwind to use webjs," which is wrong: the framework is agnostic
about the styling story. Tailwind is the scaffold default because
it pairs well with the rest of the stack, but vanilla CSS, CSS
modules, BEM, hand-written stylesheets, or another utility framework
all work the same way.

Changes:

  - Added an explicit note after benefit #1: webjs does NOT require
    Tailwind, the benefit (external CSS cascades into light-DOM
    components) is general, Tailwind is just the concrete example
    in the post.
  - Benefit #2 ("CSS cache-friendly"): broadened from "tailwind.css"
    to "an external stylesheet (the scaffold's tailwind.css, or your
    own app.css, or whatever you write)."
  - Scoping section: added a mention of BEM / class-prefix
    discipline as a non-Tailwind way to avoid leakage. Linked to
    the framework's `light-dom-css-prefix` lint rule that catches
    unprefixed selectors in vanilla CSS for light-DOM components.
  - Summary bullets: "Tailwind utility classes apply" -> "External
    CSS applies without escape hatches: Tailwind, vanilla, CSS
    modules, BEM, whatever you bring."
  - Closing paragraph: "light DOM with Tailwind by default" ->
    "light DOM by default, with Tailwind as the scaffold default
    but no framework-level requirement to use it."

The argument is now framed as light-DOM-vs-shadow, not as
Tailwind-evangelism. Tailwind users still see Tailwind-flavored
examples throughout, but non-Tailwind users see the framework
working for them too.

* blog(file-based-routing): strip opening to two options, frame around personal DX

The opening listed three options (invent, Rails-shaped, Next.js).
Rails was never seriously in the running and the inclusion read as
filler. Stripped to two: custom or Next.js. The decision is now
framed primarily around the Next.js DX I personally enjoy, with
the corpus-priors argument as a secondary reason rather than the
load-bearing one.

* blog(naming-saga): rewrite 'the user pointed out' to first person

The post is bylined by Vivek and written in first-person voice. The
'the user pointed out' phrasing slipped through, treating someone
else as the source of the insight. Now reads 'I realized,' matching
the rest of the post's voice.

Audited the other blog posts for similar third-person 'user'
references. The remaining mentions are about end-users of the
framework (package.json size, function-wrapping callers, store
config etc.) which is the correct use of the word.

* blog(index): remove 'written as the project evolves' tagline

* blog([slug]): tighten footer spacing before 'All posts' link

Reduced mt from 128px to 72px and pt from 40px to 32px, halving
the gap between the last paragraph and the bottom 'All posts' link.
Earlier value was overcompensating after the user pointed out the
link sticking to the paragraph; this lands in the comfortable
middle.

* blog([slug]): walk back the footer spacing reduction

Previous change went from 168px to 104px which the user said was
too aggressive. Dialed to 140px (mt-[104px] pt-[36px]), a modest
~17% reduction from the original 168px rather than the 38% cut.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant