FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: implement Web Components SSR framework with Koa 3, DOM-Renderer, Parcel 2, and CLI by Copilot · Pull Request #1 · EasyWebApp/WebSSR · GitHub

feat: implement Web Components SSR framework with Koa 3, DOM-Renderer, Parcel 2, and CLI - #1

Draft
TechQuery with Copilot wants to merge 3 commits into
mainfrom
copilot/implement-web-components-ssr-framework
Draft

feat: implement Web Components SSR framework with Koa 3, DOM-Renderer, Parcel 2, and CLI#1
TechQuery with Copilot wants to merge 3 commits into
mainfrom
copilot/implement-web-components-ssr-framework

Conversation

Copilot AI commented Apr 4, 2026
edited
Loading

Copy link
Copy Markdown

Bootstraps the WebSSR framework: a Node.js 22 + Koa 3 server that renders Web Components pages via JSX → Declarative Shadow DOM HTML, with file-based routing (Next.js App Router convention), a Parcel 2 plugin for client component boundaries, a webssr CLI, CSS module support, and unit tests.

Core modules

  • source/polyfill.ts – ESM-native happy-dom bootstrap. Registers a Node.js ESM loader hook for CSS module stubs and injects DOM globals into globalThis. Pre-loaded via --import ./source/polyfill.ts so it runs before any module graph is constructed.
  • source/css-loader.ts – Node.js ESM loader hook that resolves .css / .module.css imports to an identity proxy (styles.hero → 'hero') for SSR. Parcel handles scoped class names on the client.
  • source/renderer.ts – Awaits the async page component, then uses DOMRenderer.renderToReadableStream for streaming HTML via an async function* generator. Wraps output in a full document shell injecting the declarative-shadow-dom-polyfill CDN script (https://unpkg.com/declarative-shadow-dom-polyfill).
  • source/router.ts – Recursively scans app/**/page.tsx, converts [param] segments to Koa :param, and streams the response using Readable.from(asyncGenerator).
  • source/server.ts – Entry point: router scan → Koa listen → optional static asset serving from .webssr/dist.
  • source/cli.ts – webssr CLI built with CommanderJSX (Command constructor API). Three commands:
    • webssr dev [-p port] [-a appDir] – starts the Koa SSR server and Parcel in watch mode (HMR on port+1)
    • webssr start [-p port] [-a appDir] – starts the production Koa server
    • webssr build [-a appDir] – runs a production Parcel build into .webssr/dist

Parcel 2 client component plugin

source/parcel-plugin/index.ts is a Parcel Transformer that intercepts import attributes at the client boundary:

// Page component (server-side)
import { MyButton } from './MyButton' with { runtime: 'client' };
  • Server build (env.context === 'node' or PARCEL_BUILD_TARGET=server): replaces the import with a stub { __clientComponent: true, specifier, name } so SSR runs without browser-only code.
  • Client build: import passes through unchanged for normal Parcel bundling.

CSS modules and sub-components

Page components can import CSS modules and reusable sub-components using standard syntax:

import styles from './styles/home.module.css';
import { NavBar } from '../components/NavBar';

export default async function Page({ searchParams }: PageProps) {
    return <main class={styles.hero}><NavBar links={[...]} /></main>;
}

The SSR CSS loader returns class names as-is (identity proxy); Parcel handles scoped names in the browser bundle.

Test fixtures and unit tests

Example pages have been moved from app/ to test/app/ as test fixtures. Unit tests cover:

  • test/renderer.test.tsx – 5 tests for renderPage and renderPageToStream (document structure, polyfill injection, searchParams interpolation, client script injection, async generator streaming)
  • test/router.test.ts – 2 tests for route discovery and [param] → :param conversion

Run with: node --import tsx/esm --import ./source/polyfill.ts --test 'test/**/*.test.ts' 'test/**/*.test.tsx'

Package changes

  • Package manager: switched to pnpm ("packageManager": "pnpm@10.33.0"); package-lock.json removed.
  • Exports: "." → ./dist/index.js, "./parcel-plugin" → ./dist/parcel-plugin/index.js (compiled ESM, not .ts source).
  • bin: webssr → ./dist/cli.js.
  • Koa ecosystem: upgraded to koa@^3.2.0, @koa/router@^15.4.0, @types/koa@^3.0.2; removed @types/koa__router (bundled in v15).
  • Parcel: @parcel/plugin@^2.16.4, @parcel/core@^2.16.4.
  • New deps: commander-jsx, koa-static, @parcel/core.

Dependency notes

  • happy-dom pinned to ^20.8.9 via overrides — earlier v14 range had critical CVEs (VM context escape, script tag RCE).
  • All node scripts use --import tsx/esm --import ./source/polyfill.ts to pre-load the DOM polyfill before any module graph is constructed. This fixes an ESM evaluation-order race where dom-renderer/jsx-runtime (implicitly added to .tsx files by tsx) would load declarative-shadow-dom-polyfill before XMLSerializer was available in globalThis.
Original prompt

实现一个 Web components 服务端渲染框架,遵循以下设计:

  1. HTML 序列化、DOM 水合基于声明式影子 DOM 标准,polyfill 补丁为 https://github.com/EasyWebApp/declarative-shadow-dom-polyfill
  2. 页面组件用异步函数返回 JSX,兼容 React 服务端组件写法,但不使用其私有协议
  3. https://github.com/EasyWebApp/DOM-Renderer 将 JSX 流式生成 HTML 字符串(已处理好自定义元素的标签)
  4. 页面路由采用文件目录式,兼容 Next.js app router,但初版只实现 page.tsx,后期再做 layout.tsx 等高级能力
  5. 通过 Parcel 2 插件做客户端组件依赖收集,并以 import { ClientComponent } from './client-component' with { runtime: 'client' } 作为边界,不使用 "use client" 声明
  6. HTTP 服务器采用 Node.js ^22 + Koa

…and Parcel 2 plugin

Agent-Logs-Url: https://github.com/EasyWebApp/WebSSR/sessions/2eff332b-4410-47ef-9e45-8763652062b4

Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement Web components server-side rendering framework feat: implement Web Components SSR framework with Koa, DOM-Renderer, and Parcel 2 plugin Apr 4, 2026
Copilot AI requested a review from TechQuery April 4, 2026 15:19
TechQuery added enhancement New feature or request feature labels Apr 4, 2026
github-project-automation Bot moved this to In progress in Upgrade & Refactor Apr 4, 2026

TechQuery left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

既然是参考 Next.js 的服务端渲染框架,那就得对外包装一个 CLI 让用户一键运行,CLI 入口架构用 http://github.com/TechQuery/CommanderJSX

Comment thread app/page.tsx Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

导入 CSS modules、<SubComponent /> 的能力也要实现,要满足日常开发的核心需求。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Implemented in commit b41a4bc:

  • SubComponent: test/components/NavBar.tsx is a reusable <NavBar links={...} /> component imported and used directly in test/app/page.tsx
  • CSS modules: test/styles/home.module.css is imported as import styles from '../styles/home.module.css'; a new Node.js ESM loader hook (source/css-loader.ts, registered in source/polyfill.ts) returns an identity proxy on the server (styles.hero → 'hero'), and source/css-modules.d.ts provides the ambient TypeScript type

The rendered <main class="hero"> confirms both features working end-to-end on the server.

Comment thread package-lock.json Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

用 PNPM,此文件删掉。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Done in commit b41a4bc – deleted package-lock.json, added "packageManager": "pnpm@10.33.0" to package.json, and committed pnpm-lock.yaml + pnpm-workspace.yaml.

Comment thread source/renderer.ts Outdated
Comment on lines +17 to +19
const POLYFILL_SCRIPT = `<script type="module">
import 'declarative-shadow-dom-polyfill';
</script>`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
const POLYFILL_SCRIPT = `<script type="module">
import 'declarative-shadow-dom-polyfill';
</script>`;
const POLYFILL_SCRIPT = `<script src="https://unpkg.com/declarative-shadow-dom-polyfill"></script>`;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Applied in commit b41a4bc – POLYFILL_SCRIPT is now:

<script src="https://unpkg.com/declarative-shadow-dom-polyfill"></script>

Comment thread source/renderer.ts Outdated
Comment on lines +65 to +97
return new ReadableStream<string>({
async start(controller) {
const enqueue = (chunk: string) => controller.enqueue(chunk);

enqueue(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
${POLYFILL_SCRIPT}
${clientScripts.map(src => `<script type="module" src="${src}"></script>`).join('\n')}
</head>
<body>
`);

const vnode = await Page(props);
const stream = renderer.renderToReadableStream(vnode);
const reader = stream.getReader();

try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
enqueue(value);
}
} finally {
reader.releaseLock();
}

enqueue('\n</body>\n</html>');
controller.close();
}
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

不要手写流,用异步生成器。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Rewritten in commit b41a4bc. renderPageToStream is now an async function* generator that yields HTML chunks:

export async function* renderPageToStream(Page, props, clientScripts) {
    yield `<!DOCTYPE html>...<body>\n`;
    const vnode = await Page(props);
    yield* renderer.renderToReadableStream(vnode);
    yield '\n</body>\n</html>';
}

The router converts it to a Node.js Readable with Readable.from(renderPageToStream(...)).

Comment thread source/router.ts
Comment on lines +74 to +96
router.get(koaPath, async ctx => {
const mod = await import(fileUrl);
const Page: PageComponent = mod.default;

if (typeof Page !== 'function') {
ctx.status = 404;
ctx.body = 'Page component not found';
return;
}

const params = ctx.params as Record<string, string>;
const searchParams = Object.fromEntries(
new URLSearchParams(ctx.querystring)
);

const stream = renderPageToStream(Page, { params, searchParams });

ctx.type = 'text/html; charset=utf-8';
// Pipe the web ReadableStream into the Koa (Node.js) response
ctx.body = Readable.fromWeb(
stream as import('stream/web').ReadableStream
);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Parcel 的资源打包在哪里?开发模式热更新在哪里?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Added in commit b41a4bc via the CLI (source/cli.ts):

  • Dev mode (webssr dev): starts Parcel in watch mode via @parcel/core alongside the Koa SSR server. Client entry points are discovered as *.client.tsx files; hot reload served on port+1. Bundled assets from .webssr/dist are served by koa-static.
  • Prod build (webssr build): runs @parcel/core in production mode, outputs to .webssr/dist.
  • Prod server (webssr start): serves SSR + static assets from .webssr/dist.

Comment thread package.json
Comment on lines +18 to +21
"exports": {
".": "./source/index.ts",
"./parcel-plugin": "./source/parcel-plugin/index.ts"
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

TypeScript 文件直接导出去没法用吧?至少编译成 ESM 啊!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Fixed in commit b41a4bc – package.json exports now point to compiled output:

"exports": {
    ".": "./dist/index.js",
    "./parcel-plugin": "./dist/parcel-plugin/index.js"
}

The bin field also points to ./dist/cli.js. Run npm run build (→ tsc) to compile source/ to dist/ before publishing.

Comment thread package.json Outdated
"start": "node --import tsx/esm source/server.ts",
"dev": "node --watch --import tsx/esm source/server.ts",
"build": "tsc",
"test": "node --import tsx/esm --test source/**/*.test.ts"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

你单元测试都没写啊!把 app/ 目录移到 test/ 里作为测试用示例代码。

Suggested change
"test": "node --import tsx/esm --test source/**/*.test.ts"
"test": "node --import tsx/esm --test test/**/*.test.ts"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Done in commit b41a4bc:

  • Moved app/ → test/app/ as example/fixture code
  • Added test/renderer.test.tsx (5 tests for renderPage + renderPageToStream) and test/router.test.ts (2 tests for route discovery)
  • All 7 tests pass via npm test (node --import tsx/esm --import ./source/polyfill.ts --test 'test/**/*.test.ts' 'test/**/*.test.tsx')

Comment thread package.json Outdated
Comment on lines +29 to +32
"@koa/router": "^13.1.0",
"dom-renderer": "^2.6.4",
"happy-dom": "^20.8.9",
"koa": "^2.15.4"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

升级到 Koa 3.x 及其生态库最新版!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Upgraded in commit b41a4bc – koa@^3.2.0, @koa/router@^15.4.0, @types/koa@^3.0.2. Removed @types/koa__router (no longer needed; @koa/router@15 ships its own types at ./dist/index.d.ts).

Comment thread package.json Outdated
"happy-dom": "^20.8.9"
},
"devDependencies": {
"@parcel/plugin": "^2.12.0",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Parcel 升到 2.16.4 最新版

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Upgraded to @parcel/plugin@^2.16.4 and @parcel/core@^2.16.4 in commit b41a4bc.

…, CSS modules, Koa 3.x

Agent-Logs-Url: https://github.com/EasyWebApp/WebSSR/sessions/a8094aaf-888b-452f-abeb-1b63b4b1ac60

Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>

Copilot AI commented Apr 4, 2026
edited
Loading

Copy link
Copy Markdown
Author

既然是参考 Next.js 的服务端渲染框架,那就得对外包装一个 CLI 让用户一键运行,CLI 入口架构用 http://github.com/TechQuery/CommanderJSX

Added in commit b41a4bc. source/cli.ts implements a webssr CLI using the CommanderJSX Command constructor API with three commands:

  • webssr dev [-p port] [-a appDir] – starts the Koa SSR server and Parcel in watch mode (HMR on port+1)
  • webssr start [-p port] [-a appDir] – starts the production Koa server
  • webssr build [-a appDir] – runs a production Parcel build into .webssr/dist

The bin field in package.json maps webssr → dist/cli.js.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • cdn.jsdelivr.net
    • Triggering command: /usr/bin/curl curl -s REDACTED (dns block)
  • https://api.github.com/repos/TechQuery/CommanderJSX/contents
    • Triggering command: /usr/bin/curl curl -s REDACTED (http block)
  • https://api.github.com/repos/TechQuery/CommanderJSX/contents/src
    • Triggering command: /usr/bin/curl curl -s REDACTED (http block)
  • https://api.github.com/repos/TechQuery/CommanderJSX/readme
    • Triggering command: /usr/bin/curl curl -s REDACTED -H Accept: application/vnd.github.v3.raw (http block)
  • unpkg.com
    • Triggering command: /usr/bin/curl curl -s REDACTED (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title feat: implement Web Components SSR framework with Koa, DOM-Renderer, and Parcel 2 plugin feat: implement Web Components SSR framework with Koa 3, DOM-Renderer, Parcel 2, and CLI Apr 4, 2026
Copilot AI requested a review from TechQuery April 4, 2026 17:24
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

Labels

enhancement New feature or request feature

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL