Skip to content

chore(blog): sync examples/blog with scaffold templates#15

Merged
vivek7405 merged 6 commits into
mainfrom
feat/sync-blog-example
May 19, 2026
Merged

chore(blog): sync examples/blog with scaffold templates#15
vivek7405 merged 6 commits into
mainfrom
feat/sync-blog-example

Conversation

@vivek7405
Copy link
Copy Markdown
Collaborator

Summary

The examples/blog/ reference app was missing all the enforcement plumbing that scaffolded webjs apps now ship with. This PR mirrors packages/cli/templates/ into examples/blog/ so the example accurately reflects what webjs create produces.

What got added

Category Files
Claude Code .claude/settings.json, .claude/hooks/block-prose-punctuation.sh, guard-branch-context.sh, nudge-uncommitted.sh, .claude.json
Gemini CLI .gemini/settings.json, .gemini/hooks/nudge-uncommitted.sh
Cursor 1.7+ .cursor/hooks.json, .cursor/hooks/nudge-uncommitted.sh
Cross-agent text rules .cursorrules, .windsurfrules, .github/copilot-instructions.md, .github/pull_request_template.md, .editorconfig
Conventions CONVENTIONS.md (with {{APP_NAME}} substituted to blog)
Git enforcement .hooks/pre-commit

All hook scripts are committed with the executable bit set.

Why per-commit, not one big blob

Six logical chunks, each a single ecosystem or concern:

  1. Claude Code config + hooks
  2. Gemini CLI hooks
  3. Cursor hooks
  4. Cross-agent text rules + editorconfig
  5. CONVENTIONS.md
  6. .hooks/pre-commit

Mirrors the same enforcement story PR #13 set up for the scaffold templates.

Notes

  • Within this monorepo, git's core.hooksPath is .hooks at the framework root (set in PR WEBJS_PUBLIC_* env vars accessible as process.env.* in the browser #14). The blog's local .hooks/pre-commit is dormant during monorepo commits but activates if someone copies the blog out and git inits.
  • .claude/hooks/, .gemini/hooks/, .cursor/hooks/ DO fire when an AI agent edits within examples/blog/, because those tools read project-scoped settings.
  • Remaining gaps (tailwind-browser.js, web-test-runner.config.js, scaffold's test/browser + test/e2e) are app-feature concerns, not enforcement, and out of scope for this PR.

Test plan

  • All six commits passed the framework pre-commit (npm test, 913/913)
  • diff -rq packages/cli/templates examples/blog now only shows the app-feature differences, not enforcement gaps
  • Standalone-copy scenario manually tested by copying examples/blog/ out and running git init (not done in this PR)

vivek7405 added 6 commits May 19, 2026 15:44
Mirrors what scaffolded apps get via webjs create. Three hooks
plus the project-level Claude Code settings:

* .claude/hooks/block-prose-punctuation.sh: enforces the
  em-dash / pause-punctuation rule
* .claude/hooks/guard-branch-context.sh: prompts before edits
  on main
* .claude/hooks/nudge-uncommitted.sh: commit-frequency nudge
  at threshold 4
* .claude/settings.json: registers all three on the right
  PreToolUse / PostToolUse events
* .claude.json: project-level Claude Code config (Playwright
  MCP, etc.)

The blog example serves as a reference for what a real webjs
app looks like, so it should match the scaffold output.
Same nudge-uncommitted enforcement as Claude Code, adapted for
Gemini CLI's AfterTool hook contract. Output shape is the
same hookSpecificOutput.additionalContext format both tools
accept.

Plus .gemini/settings.json registering the hook on AfterTool
for write_file and replace tools.
Same nudge-uncommitted enforcement, adapted for Cursor 1.7+'s
afterFileEdit contract. Output uses Cursor's snake_case
additional_context field at the top level.

Plus .cursor/hooks.json registering the hook.
For agents without a programmatic hooks API (Windsurf,
Copilot) and as a baseline source of truth for all agents:

* .cursorrules: Cursor's rule file
* .windsurfrules: Windsurf's rule file
* .github/copilot-instructions.md: GitHub Copilot's rule file
* .github/pull_request_template.md: PR template
* .editorconfig: editor settings (indent, line endings)

All copied verbatim from packages/cli/templates/, including
the strengthened commit-frequency wording and hook references
from PRs #12 and #13.
Copied from packages/cli/templates/CONVENTIONS.md and
substituted the {{APP_NAME}} placeholder with 'blog'. The
markdown covers architectural conventions (modules layout,
styling, testing, git workflow) and points readers at
webjs check --rules for the canonical lint rule catalogue.
For the case where someone copies the blog example out as a
standalone webjs app and runs git init: the pre-commit fires
and blocks direct commits to main, runs webjs test, and runs
webjs check.

Within this monorepo, git's core.hooksPath is set at the root
(.hooks/pre-commit at the framework level), so this file
serves the standalone-copy scenario, not commits made within
the monorepo.
@vivek7405 vivek7405 merged commit c4380e2 into main May 19, 2026
@vivek7405 vivek7405 deleted the feat/sync-blog-example branch May 19, 2026 10:21
vivek7405 added a commit that referenced this pull request May 21, 2026
Third slice. Wires the slot runtime into the WebComponent lifecycle and
refines render-client.js's slot bind so fallback content is captured
once at compile time and cloned freshly per instance.

component.js:

- Light-DOM activation now runs in two phases:
    Phase 1 (before _performRender): if not hydrating, call
      captureAuthoredChildren(host) to move authored children into the
      slot state's assignment table. The renderer's replaceChildren()
      call would otherwise destroy them on first render.
    Phase 2 (after _performRender, slots are now live DOM): if the
      host was hydrating, call adoptSSRAssignments(host) to record
      SSR-placed children so the first projection pass is a no-op.
      Then call attachSlotObservers(host) so future authored-child
      mutations and slot-name changes drive incremental projection.

- New __isHydrating() helper inspects this.firstChild for the
  framework's <!--webjs-hydrate--> marker and records the result
  on __hydratedAtActivate so phase two can branch correctly.

- disconnectedCallback now calls detachSlotObservers(host) for
  light-DOM hosts. The per-host slot state (assignment table,
  pending fragments, last snapshots) is preserved across disconnect,
  so a re-attached element picks up where it left off.

render-client.js:

- discoverSlots() now MOVES the slot's authored children into a
  fallbackTemplate DocumentFragment stored on the SLOT
  PartDescriptor. The cached templateEl's slot becomes empty, so
  every clone starts empty too. This eliminates the cloning-the-
  fallback-out-of-the-slot dance the previous bind step did.

- bindPart for slot now clones the descriptor's fallbackTemplate
  into a per-instance holding fragment and stamps it on the slot
  via SLOT_FALLBACK_FRAG for slot.js to swap in. The slot itself
  is left untouched at bind time, which makes hydration trivially
  correct (SSR-projected children stay in place; the slot-part
  just sets up its fallback supply for later transitions).

- PartDescriptor typedef gains an optional fallbackTemplate field.

Verified across 115 existing core unit tests (component, render-client,
render-server, directives, registry, css, html, context, task,
suspense, repeat, testing). All pass.

What remains: render-server.js injectDSD upgrade for SSR slot
substitution (Task #13), then the 62-case test suite (Task #14),
then docs (Task #15).
vivek7405 added a commit that referenced this pull request May 21, 2026
Fourth slice. Upgrades injectDSD to project authored children into
<slot> positions during server-side rendering for light-DOM
WebComponents, with full parity to the client-side projection rules
and the shadow-DOM <slot> spec.

When a light-DOM component's render() output contains <slot> tags,
injectDSD now:

  1. Finds the source HTML's matching closing tag for the custom
     element by walking forward with depth tracking for nested
     same-tag elements.
  2. Extracts the authored inner HTML between the element's opening
     and closing tags.
  3. Partitions the authored HTML by each top-level child's slot=""
     attribute. Text nodes, comment nodes, and elements without
     slot="" all route to the default-slot bucket.
  4. Walks the rendered template's <slot> tags in document order and
     substitutes each with a framework-marked
     <slot data-webjs-light data-projection="actual"|"fallback">
     element carrying either the projected children or the slot's
     authored fallback content. Multiple slots with the same name
     follow the first-wins rule per spec.
  5. Emits one edit spanning the entire opening-to-closing range of
     the source element. Inner custom elements among authored
     children are processed via the recursive injectDSD call on the
     substituted output, not by the outer loop (a new sort + overlap
     filter drops the duplicate inner edits that were enumerated
     against the original html before substitution).

When a component's render() output has NO <slot> tags, the old SSR
shape is preserved unchanged: edit at the opening tag only, leave
authored children adjacent to the rendered template, closing tag
untouched. This keeps existing components that never used slots
behaving exactly as before.

Shadow DOM components are completely unaffected. Their native <slot>
elements live inside the DSD <template shadowrootmode="open"> block
and the browser handles projection from the host's light-DOM children
into the shadow tree natively. No framework substitution there.

New helpers in render-server.js:

  isVoidElement(tag)                tag is a void element (br, img...).
  findClosingTagInString(html, ...) depth-tracked matching close tag.
  extractSlotAttr(attrsRaw)         pulls slot="..." value or null.
  partitionAuthoredBySlot(html)     groups authored inner HTML by slot.
  appendStringToMap(map, k, v)      concatenating map insert helper.
  substituteSlotsInRender(...)      walks <slot> tags, emits framework
                                    marker variants with projection or
                                    fallback content, first-wins per
                                    name across the document.

End-to-end smoke (server-only):

  <my-card>
    <h2 slot="header">Title</h2>
    <p>Body</p>
    <span slot="footer">Foot</span>
  </my-card>

renders to

  <my-card><!--webjs-hydrate-->
    <div class="card">
      <header><slot data-webjs-light data-projection="actual" name="header">
        <h2 slot="header">Title</h2>
      </slot></header>
      <main><slot data-webjs-light data-projection="actual">
        <p>Body</p>
      </slot></main>
      <footer><slot data-webjs-light data-projection="actual" name="footer">
        <span slot="footer">Foot</span>
      </slot></footer>
    </div>
  </my-card>

Fallback content surfaces correctly when no children match; first-wins
holds across duplicate same-named slots; shadow DOM passthrough is
unchanged. 127 existing unit tests across component, render-client,
render-server, directives, registry, css, html, context, task,
suspense, repeat, testing, blog-smoke, json-negotiation, and
light-dom-ssr all pass.

What remains: the 62-case test suite (Task #14) and docs +
convention rule (Task #15).
vivek7405 added a commit that referenced this pull request May 21, 2026
chore(blog): sync examples/blog with scaffold templates
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