feat(examples): add streaming-ssr example with 5 pages#37
Merged
Conversation
Contributor
Coverage Report
File CoverageNo changed files found. |
Create `examples/streaming-ssr/` — a focused second example demonstrating HTTP streaming SSR with the Scratchy framework. Key features: - Uses `createStreamingSSRHandler()` for all page routes instead of the non-streaming `createSSRHandler()` from the starter example - No database or auth dependencies — focused purely on the streaming rendering pipeline - 5 Qwik City client routes: Home, About, Features, Blog, Contact - Per-route `getProps` showing how to pass server-side data to the worker - Shared layout with active-link highlighting for all 5 nav links - 12 integration tests covering streaming HTML, props embedding, CORS, health check, and all 5 page routes - `README.md` with ASCII sequence diagram explaining the streaming flow Co-authored-by: Asjas <3828967+Asjas@users.noreply.github.com> Agent-Logs-Url: https://github.com/Asjas/scratchyjs/sessions/887b4a0a-7c81-4104-ab1c-62e40bc63b26
The worker HTML-escapes the JSON props it embeds in the script block to prevent XSS. The test now checks for the HTML-escaped form ("page" etc.) directly instead of trying to re-parse the escaped JSON, which avoids the CodeQL js/double-escaping alert. Co-authored-by: Asjas <3828967+Asjas@users.noreply.github.com> Agent-Logs-Url: https://github.com/Asjas/scratchyjs/sessions/887b4a0a-7c81-4104-ab1c-62e40bc63b26
Copilot
AI
changed the title
[WIP] Add second streaming SSR starter example with multiple pages
feat(examples): add streaming-ssr example with 5 pages
Mar 23, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new examples/streaming-ssr/ workspace package to demonstrate Scratchy’s streaming SSR route handler pattern (Fastify + Piscina worker pool), alongside a richer set of Qwik City route source files for a multi-page example.
Changes:
- Added
examples/streaming-ssr/example app wired tocreateStreamingSSRHandler()with five named routes plus a catch-all. - Added integration tests validating HTML responses, security headers (
x-content-type-options), escaped embedded props, CORS behavior, and health check. - Updated
pnpm-lock.yamlto include the new workspace importer and associated dependency graph changes.
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds the new workspace importer (examples/streaming-ssr) and updates dependency snapshots. |
| examples/streaming-ssr/vite.config.ts | Vite config using createScratchyConfig() with Tailwind enabled and backend proxy settings. |
| examples/streaming-ssr/tsconfig.json | Server-side TS config for the new example (mirrors starter example pattern). |
| examples/streaming-ssr/src/types/fastify.d.ts | Pulls in renderer Fastify module augmentations for runTask/piscina. |
| examples/streaming-ssr/src/server.ts | Builds the Fastify server, registers renderer plugin + streaming SSR routes with per-route props. |
| examples/streaming-ssr/src/server.test.ts | Integration tests covering health, headers, streaming SSR responses, props escaping, and CORS. |
| examples/streaming-ssr/src/renderer/worker.ts | Piscina worker entrypoint re-exporting @scratchyjs/renderer/worker. |
| examples/streaming-ssr/src/index.ts | Main entrypoint: loads config, builds server, sets up shutdown, listens on configured host/port. |
| examples/streaming-ssr/src/config.ts | Thin wrapper around @scratchyjs/core config loading/typing. |
| examples/streaming-ssr/src/client/tsconfig.json | Client TS config for Qwik routes/components (currently minimal). |
| examples/streaming-ssr/src/client/styles/global.css | Tailwind entry + base layer global styles. |
| examples/streaming-ssr/src/client/routes/layout.tsx | Shared layout with navigation and active-link highlighting. |
| examples/streaming-ssr/src/client/routes/index.tsx | Home page Qwik route + loader providing hero/stats/highlights. |
| examples/streaming-ssr/src/client/routes/features/index.tsx | Features page route with loader data and detailed feature cards. |
| examples/streaming-ssr/src/client/routes/contact/index.tsx | Contact page route with contact channels + client-side-only form state. |
| examples/streaming-ssr/src/client/routes/blog/index.tsx | Blog listing route with loader-provided posts/tags metadata. |
| examples/streaming-ssr/src/client/routes/about/index.tsx | About page route with loader-provided story/values/team sections. |
| examples/streaming-ssr/public/robots.txt | Allows indexing (simple robots.txt). |
| examples/streaming-ssr/package.json | Declares the new example workspace package, dependencies, and dev script. |
| examples/streaming-ssr/README.md | Documentation for the new example, route list, and setup instructions. |
| examples/streaming-ssr/.env.example | Default environment variables for running the example on port 3001. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Contributor
|
📚 Docs preview deployed https://scratchyjs-docs-pr-37.asjas.workers.dev Updates automatically on every push to this PR. |
Contributor
⚡ Benchmark Results
|
| Suite — Benchmark | ops/sec | mean (µs) | p99 (µs) | ±rme | vs main |
|---|---|---|---|---|---|
| SharedRingBuffer – small payload (64 B) — write 64 bytes | 552.5 K | 0.0018 | 0.0074 | ±0.96% | ~0% |
| SharedRingBuffer – small payload (64 B) — write + read 64 bytes | 481.8 K | 0.0021 | 0.0087 | ±0.52% | -2.3% |
| SharedRingBuffer – medium payload (1 KB) — write 1 KB | 276.5 K | 0.0036 | 0.0162 | ±0.57% | -4.6% |
| SharedRingBuffer – medium payload (1 KB) — write + read 1 KB | 238.7 K | 0.0042 | 0.0221 | ±0.53% | +12.7% 🚀 |
| SharedRingBuffer – large payload (16 KB) — write 16 KB | 38.8 K | 0.0258 | 0.0486 | ±0.58% | -16.6% |
| SharedRingBuffer – large payload (16 KB) — write + read 16 KB | 86.9 K | 0.0115 | 0.0452 | ±2.64% | -4.3% |
| SharedRingBuffer – sequential throughput (100 × 64 B) — 100 write + read cycles | 39.4 K | 0.0254 | 0.0576 | ±0.48% | -4.6% |
| SharedRingBuffer – introspection — availableToRead | 14.69 M | 0.0001 | 0.0001 | ±0.24% | ~0% |
| SharedRingBuffer – introspection — isEmpty | 14.98 M | 0.0001 | 0.0001 | ±0.09% | ~0% |
| SharedRingBuffer – introspection — isFull | 14.47 M | 0.0001 | 0.0001 | ±0.21% | -1.9% |
benchmarks/renderer/shared-buffer.bench.ts
| Suite — Benchmark | ops/sec | mean (µs) | p99 (µs) | ±rme | vs main |
|---|---|---|---|---|---|
| SharedBuffer – allocation — createSharedBuffer(4 KB) | 642.2 K | 0.0016 | 0.0064 | ±0.64% | +10.7% 🚀 |
| SharedBuffer – allocation — createSharedBuffer(64 KB) | 43.9 K | 0.0228 | 0.0425 | ±17.98% | +5.1% |
| SharedBuffer – small payload round-trip — write small JSON | 336.0 K | 0.0030 | 0.0127 | ±0.41% | +10.5% 🚀 |
| SharedBuffer – small payload round-trip — write + read small JSON | 245.1 K | 0.0041 | 0.0132 | ±1.30% | +5.4% |
| SharedBuffer – medium payload round-trip — write medium JSON (~2 KB) | 100.8 K | 0.0099 | 0.0235 | ±0.38% | +3.7% |
| SharedBuffer – medium payload round-trip — write + read medium JSON (~2 KB) | 56.7 K | 0.0176 | 0.0427 | ±0.44% | ~0% |
| SharedBuffer – large payload round-trip — write large JSON (~10 KB) | 15.4 K | 0.0648 | 0.1092 | ±0.40% | ~0% |
| SharedBuffer – large payload round-trip — write + read large JSON (~10 KB) | 7.5 K | 0.1333 | 0.2503 | ±0.53% | -1.8% |
benchmarks/utils/ip-address.bench.ts
| Suite — Benchmark | ops/sec | mean (µs) | p99 (µs) | ±rme | vs main |
|---|---|---|---|---|---|
| getClientIPAddress – no IP headers — no IP-related headers → null | 2.68 M | 0.0004 | 0.0007 | ±1.10% | ~0% |
| getClientIPAddress – single header — cf-connecting-ip (Cloudflare) | 1.72 M | 0.0006 | 0.0010 | ±0.30% | +1.7% |
| getClientIPAddress – single header — x-forwarded-for (simple) | 1.95 M | 0.0005 | 0.0009 | ±1.76% | +1.5% |
| getClientIPAddress – single header — x-real-ip | 1.50 M | 0.0007 | 0.0012 | ±1.22% | ~0% |
| getClientIPAddress – single header — true-client-ip (Akamai / Cloudflare Enterprise) | 1.57 M | 0.0006 | 0.0010 | ±1.20% | +5.0% |
| getClientIPAddress – x-forwarded-for multi-hop — 2-hop chain | 1.83 M | 0.0005 | 0.0008 | ±3.18% | -1.3% |
| getClientIPAddress – x-forwarded-for multi-hop — 4-hop chain | 1.69 M | 0.0006 | 0.0009 | ±0.37% | +3.9% |
| getClientIPAddress – Forwarded header (RFC 7239) — simple for= directive | 1.01 M | 0.0010 | 0.0014 | ±1.69% | -1.0% |
| getClientIPAddress – Forwarded header (RFC 7239) — for= with port | 758.3 K | 0.0013 | 0.0021 | ±0.37% | +1.6% |
| getClientIPAddress – Forwarded header (RFC 7239) — IPv6 literal | 765.9 K | 0.0013 | 0.0022 | ±1.33% | +2.5% |
| getClientIPAddress – Forwarded header (RFC 7239) — multi-hop Forwarded | 846.2 K | 0.0012 | 0.0019 | ±0.35% | ~0% |
| getClientIPAddress – IPv6 addresses — x-forwarded-for IPv6 | 1.17 M | 0.0009 | 0.0014 | ±1.48% | +2.6% |
benchmarks/utils/promise.bench.ts
| Suite — Benchmark | ops/sec | mean (µs) | p99 (µs) | ±rme | vs main |
|---|---|---|---|---|---|
| promiseHash – concurrent resolution — 2 already-resolved promises | 856.6 K | 0.0012 | 0.0031 | ±1.05% | +9.0% |
| promiseHash – concurrent resolution — 5 already-resolved promises | 527.8 K | 0.0019 | 0.0027 | ±1.59% | ~0% |
| promiseHash – concurrent resolution — 10 already-resolved promises | 314.4 K | 0.0032 | 0.0049 | ±1.39% | +6.9% |
| promiseHash – concurrent resolution — 5 promises with object values | 491.8 K | 0.0020 | 0.0027 | ±0.29% | +2.8% |
| timeout – wrapping fast promises — timeout wrapping an already-resolved promise (1 s budget) | 945.8 K | 0.0011 | 0.0017 | ±0.39% | +4.2% |
| timeout – wrapping fast promises — timeout wrapping an already-resolved object (5 s budget) | 873.1 K | 0.0011 | 0.0020 | ±1.71% | +3.7% |
benchmarks/utils/safe-redirect.bench.ts
| Suite — Benchmark | ops/sec | mean (µs) | p99 (µs) | ±rme | vs main |
|---|---|---|---|---|---|
| safeRedirect – valid paths — root path / | 6.98 M | 0.0001 | 0.0002 | ±0.18% | +2.8% |
| safeRedirect – valid paths — simple path /dashboard | 3.43 M | 0.0003 | 0.0003 | ±1.28% | +2.0% |
| safeRedirect – valid paths — nested path /settings/profile | 3.07 M | 0.0003 | 0.0004 | ±1.24% | +2.3% |
| safeRedirect – valid paths — path with query string /search?q=hello | 3.42 M | 0.0003 | 0.0003 | ±1.27% | +2.7% |
| safeRedirect – valid paths — path with hash /docs#section | 3.36 M | 0.0003 | 0.0003 | ±0.10% | ~0% |
| safeRedirect – rejected inputs — absolute URL https://evil.com | 4.63 M | 0.0002 | 0.0003 | ±0.12% | +1.7% |
| safeRedirect – rejected inputs — protocol-relative URL //evil.com | 4.71 M | 0.0002 | 0.0002 | ±0.14% | +2.1% |
| safeRedirect – rejected inputs — backslash-relative /\evil.com | 4.66 M | 0.0002 | 0.0004 | ±0.10% | +2.4% |
| safeRedirect – rejected inputs — path traversal /../etc/passwd | 4.11 M | 0.0002 | 0.0003 | ±0.10% | +4.8% |
| safeRedirect – rejected inputs — null input | 16.18 M | 0.0001 | 0.0001 | ±0.10% | ~0% |
| safeRedirect – rejected inputs — undefined input | 15.75 M | 0.0001 | 0.0001 | ±0.18% | -2.2% |
| safeRedirect – rejected inputs — empty string | 14.62 M | 0.0001 | 0.0001 | ±0.10% | -9.0% |
| safeRedirect – percent-encoded bypass — percent-encoded // (%2F%2F) | 4.60 M | 0.0002 | 0.0003 | ±0.10% | +5.7% |
| safeRedirect – percent-encoded bypass — percent-encoded path traversal (%2e%2e) | 4.89 M | 0.0002 | 0.0002 | ±0.09% | +7.4% |
| safeRedirect – percent-encoded bypass — mixed percent-encoded absolute URL | 4.60 M | 0.0002 | 0.0002 | ±0.09% | +6.2% |
| safeRedirect – custom default redirect — valid path with custom default | 3.66 M | 0.0003 | 0.0003 | ±0.26% | +3.1% |
| safeRedirect – custom default redirect — invalid input with custom default | 4.73 M | 0.0002 | 0.0002 | ±0.20% | +2.3% |
🔬 Compared against the latest benchmark run on
main.⚠️ = >10% slower · 🚀 = >10% faster.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: A-J Roos <asjasroos@pm.me>
Replace the root-extending tsconfig with a standalone bundler-oriented config matching examples/starter/src/client/tsconfig.json exactly: - module: ESNext + moduleResolution: Bundler (instead of NodeNext) - target: ES2024, strict, noEmit, skipLibCheck, isolatedModules - verbatimModuleSyntax: true - types: ["vite/client"] This avoids NodeNext-specific resolution issues in .tsx route files and keeps editor/tsc behavior consistent across both examples. Co-authored-by: Asjas <3828967+Asjas@users.noreply.github.com> Agent-Logs-Url: https://github.com/Asjas/scratchyjs/sessions/9b343b7d-1629-48a3-8d65-b8f076f8551a
Copilot stopped work on behalf of
Asjas due to an error
March 23, 2026 16:44
Copilot stopped work on behalf of
Asjas due to an error
March 23, 2026 17:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
examples/streaming-ssr/— a second example app focused exclusively on the streaming SSR rendering pipeline, with no database or auth layers to obscure the core pattern.Structure
src/server.ts— 5 named page routes each usingcreateStreamingSSRHandler()with per-routegetProps, plus a catch-all. Runs on port 3001 to avoid colliding with the starter.src/renderer/worker.ts— re-exports@scratchyjs/renderer/worker(same pattern as starter).src/config.ts— thin alias over coreConfig; no extra env vars needed.Pages (5 Qwik City routes)
//about/features/blog/contactShared
layout.tsxprovides a nav bar with active-link highlighting across all pages.Key difference from
examples/starterTests
12 integration tests cover: all 5 page routes return valid HTML,
x-content-type-options: nosniffon every route, HTML-escaped props embedded in the__PROPS__script block, CORS preflight, and health check.Original prompt
create a second example. make it a streaming ssr starter. and create more pages, 4 or 5 different pages.
[Chronological Review: The conversation began with the user attempting to run an example app using pnpm, which resulted in a module not found error. The user then inquired about missing drizzle commands for generating and migrating databases. They expressed concerns about the drift between the example app and the expected behavior of the `create-scratchy-app` package. The user requested patches and fixes for migration issues, updated environment variables, and raised questions about database existence and configuration. They also reported issues with rendering content and JSX types, and requested to kill the server on port 3000. The user expressed frustration over the lack of certain secrets in the configuration and the need for specific formatting rules in Prettier. The conversation culminated in a request to create a second example app with multiple pages.][Intent Mapping:
create-scratchy-apppackage?" - User wants to understand the relationship between the example app and the package.[Technical Inventory:
[Code Archaeology:
[Progress Assessment:
[Context Validation: All critical information for continuation is captured, including user frustrations, specific requests for fixes, and the need for additional examples.]
[Recent Commands Analysis:
1. Conversation Overview: - Primary Objectives: User aims to run an example app, fix migration issues, validate configurations, and create a new example app with multiple pages. - Session Context: The conversation has evolved from running an example app to addressing various technical issues, including database configurations, rendering problems, and Prettier formatting rules. - User Intent Evolution: The user has shifted from seeking help with running the app to demanding specific fixes and enhancements, reflecting increasing frustration with the development process.- Technical Foundation:
- Node.js: Version 25.8.1, used for running the application.
- PostgreSQL: Database management system, with issues related to database existence and configurations.
- Prettier: Code formatting tool, with specific requests to ignore certain files.
- Scratchy: SSR framework for rendering applications.
- Codebase Status:
- /home/codewizard/git/scratchyjs/examples/starter/src/index.ts:
- Purpose: Entry point for the example app.
- Current State: Encountering module n...
Created from VS Code.
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.