redweb 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ import { action, component, defineApp, page, state } from 'redweb';
2
+
3
+ @component()
4
+ class Counter {
5
+ @state() count = 0;
6
+
7
+ @action()
8
+ increment() { this.count += 1; }
9
+
10
+ render() {
11
+ return <button rw-click="increment">Count {this.count}</button>;
12
+ }
13
+ }
14
+
15
+ @page('/')
16
+ class CountersPage {
17
+ first = new Counter();
18
+ second = new Counter();
19
+
20
+ render() { return <main>{this.first}{this.second}</main>; }
21
+ }
22
+
23
+ const app = defineApp({ pages: [CountersPage] });
24
+ app.run();
@@ -0,0 +1,16 @@
1
+ import { action, defineApp, page, state } from 'redweb';
2
+
3
+ @page('/', { shared: true })
4
+ class CounterPage {
5
+ @state() count = 0;
6
+
7
+ @action()
8
+ increment() { this.count += 1; }
9
+
10
+ render() {
11
+ return <button rw-click="increment">Count {this.count}</button>;
12
+ }
13
+ }
14
+
15
+ const app = defineApp({ pages: [CounterPage] });
16
+ app.run();
@@ -1,8 +1,8 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { page, start, BaseHandler, SocketRoute, RedWebSocket, RedWebRequest, LivePageRequestContext } from 'redweb';
2
+ import { page, defineApp, BaseHandler, SocketRoute, RedWebSocket, RedWebRequest, LivePageRequestContext } from 'redweb';
3
3
 
4
4
  // A runnable local demonstration, not a production credential store.
5
- export function createApp(port = 8181) {
5
+ export async function createApp(port = 8181) {
6
6
  const token = randomBytes(32).toString('base64url');
7
7
  let enabled = true;
8
8
  const authenticate = (request: Pick<RedWebRequest, 'headers'>) =>
@@ -28,9 +28,9 @@ export function createApp(port = 8181) {
28
28
  }
29
29
  }
30
30
 
31
- const app = start(Home, { listen: false, authenticate, logger: null });
32
- const team = app.sockets!.addRoute(Team);
33
- app.server.listen(port, '127.0.0.1');
31
+ const app = defineApp({ pages: [Home], sockets: [Team], authenticate, port, bind: '127.0.0.1', logger: null });
32
+ await app.run();
33
+ const team = app.sockets!.routes.find(route => route instanceof Team)!;
34
34
  return {
35
35
  app, team, token,
36
36
  async revoke() {
@@ -42,10 +42,9 @@ export function createApp(port = 8181) {
42
42
  };
43
43
  }
44
44
 
45
- if (require.main === module) {
46
- const demo = createApp();
47
- console.log('Local demo: http://127.0.0.1:8181/ and ws://127.0.0.1:8181/team');
48
- console.log(`Authorization: Bearer ${demo.token}`); // One fresh local-demo credential per run.
49
- process.once('SIGTERM', () => void demo.shutdown().catch(console.error));
50
- process.once('SIGINT', () => void demo.shutdown().catch(console.error));
51
- }
45
+ if (require.main === module) {
46
+ createApp().then(demo => {
47
+ console.log('Local demo: http://127.0.0.1:8181/ and ws://127.0.0.1:8181/team');
48
+ console.log(`Authorization: Bearer ${demo.token}`); // One fresh local-demo credential per run.
49
+ });
50
+ }
@@ -0,0 +1,2 @@
1
+ body { max-width: 50rem; margin: 3rem auto; padding: 0 1rem; font-family: system-ui, sans-serif; }
2
+ nav { display: flex; gap: 1rem; margin-bottom: 2rem; }
@@ -0,0 +1,22 @@
1
+ import { defineApp, defineSite } from 'redweb';
2
+
3
+ const site = defineSite({
4
+ css: 'site.css',
5
+ layout: content => <body>
6
+ <nav><a href="/">Home</a> · <a href="/about">About</a></nav>
7
+ <main>{content}</main>
8
+ </body>,
9
+ });
10
+
11
+ @site.page('/', { head: { title: 'Home' } })
12
+ class HomePage {
13
+ render() { return <h1>Welcome to Redweb</h1>; }
14
+ }
15
+
16
+ @site.page('/about', { head: { title: 'About' } })
17
+ class AboutPage {
18
+ render() { return <p>Two pages, one layout, no browser framework.</p>; }
19
+ }
20
+
21
+ const app = defineApp({ pages: [HomePage, AboutPage] });
22
+ app.run();
package/docs/topics.json CHANGED
@@ -2,7 +2,7 @@
2
2
  { "id": "getting-started", "title": "Choose a starter and build a working app", "summary": "Requirements, fit, development, tests, and production boundaries.", "source": "docs/GETTING_STARTED.md" },
3
3
  { "id": "application", "title": "One application, one listener", "summary": "Define pages, socket routes and application services together; run and shut down one owned HTTP/WebSocket listener.", "source": "docs/APPLICATION.md" },
4
4
  { "id": "guides/realtime-dashboard", "title": "Build a private realtime dashboard", "summary": "Persistent SQLite cards, account-private updates and sign-out across tabs, with explicit single-process limits.", "source": "docs/guides/realtime-dashboard.md", "recipe": { "template": "dashboard", "file": "src/cards.tsx" } },
5
- { "id": "guides/jsx-without-react", "title": "Render JSX without React", "summary": "TypeScript pages, a shared layout and external CSS, rendered on the server without browser framework code.", "source": "docs/guides/jsx-without-react.md", "recipe": { "template": "site", "file": "src/app.tsx" } },
5
+ { "id": "guides/jsx-without-react", "title": "Render JSX without React", "summary": "TypeScript pages, a shared layout and external CSS, rendered on the server without browser framework code.", "source": "docs/guides/jsx-without-react.md", "codeSource": "docs/snippets/site.tsx", "recipe": { "template": "site", "file": "src/app.tsx" } },
6
6
  { "id": "guides/chatroom", "title": "Build a chatroom with live presence", "summary": "Reusable server-side components, validated forms and disconnect-aware presence, without custom browser socket glue.", "source": "docs/guides/chatroom.md", "recipe": { "template": "chat", "file": "src/chatroom.tsx" } },
7
7
  { "id": "guides/typed-websockets", "title": "Share typed WebSocket contracts", "summary": "One match route, separate join/move/resume handlers and validated client/server payloads from the same schema.", "source": "docs/guides/typed-websockets.md", "recipe": { "template": "socket", "file": "src/handlers.ts" } },
8
8
  { "id": "guides/http-websocket", "title": "Serve HTTP and WebSockets on one port", "summary": "An Express endpoint and raw socket route share one listener with one explicit shutdown owner.", "source": "docs/guides/http-websocket.md", "recipe": { "template": "http-ws", "file": "src/app.tsx" } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redweb",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "A small Node.js foundation for HTTP, WebSockets, multiplayer services, and server-rendered HTML",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -104,21 +104,28 @@ class Documentation {
104
104
  return { id: `recipes/${template}`, title: `${template[0].toUpperCase()}${template.slice(1)} starter`, summary: explanation.split('\n').find(line => line && !line.startsWith('#')), source, markdown, files };
105
105
  }
106
106
 
107
- recipeCode(entry) {
107
+ recipeCode(entry) {
108
108
  const files = projectFiles(this.manifest.version, entry.template, this.root);
109
109
  const file = files.find(file => file.path === entry.file);
110
110
  if (!file) throw new Error(`Unknown documentation recipe file: ${entry.template}/${entry.file}`);
111
- return normalize(file.content);
112
- }
111
+ return normalize(file.content);
112
+ }
113
+
114
+ code(entry, field) {
115
+ return entry.recipe ? this.recipeCode(entry.recipe) : entry.codeSource ? this.read(entry.codeSource) : entry[field];
116
+ }
113
117
 
114
118
  topic(topic) {
115
- let markdown = `${this.notice()}\n\n${this.links(this.read(topic.source), topic.source)}`;
119
+ let markdown = `${this.notice()}\n\n${this.links(this.read(topic.source), topic.source)}`;
120
+ markdown = markdown.replace(/<!-- source: ([\w./-]+) -->/g,
121
+ (_match, file) => fence(this.read(file), language(file)));
116
122
  if (topic.recipe) {
117
123
  const { template, file } = topic.recipe;
118
124
  markdown += [
119
125
  '\n## Build and run the complete application', this.setup(template),
120
- `The [complete ${template} recipe](${this.basePath}/recipes/${template}.md) contains every generated file, its real acceptance tests, and deployment instructions. The source below is one of those files, not a standalone program; initialize the whole project before modifying it.`,
121
- `## Source walkthrough: ${file}`, fence(this.recipeCode(topic.recipe), language(file)),
126
+ `The [complete ${template} recipe](${this.basePath}/recipes/${template}.md) contains every generated file, its real acceptance tests, and deployment instructions.`,
127
+ ...(topic.codeSource ? [] : [`The source below is one of those files, not a standalone program; initialize the whole project before modifying it.`,
128
+ `## Source walkthrough: ${file}`, fence(this.recipeCode(topic.recipe), language(file))]),
122
129
  ].join('\n\n') + '\n';
123
130
  }
124
131
  return { ...topic, markdown };
@@ -128,8 +135,8 @@ class Documentation {
128
135
  const pages = this.topics.map(topic => this.topic(topic));
129
136
  pages.push(...TEMPLATES.map(template => this.recipe(template)));
130
137
  const reference = this.reference;
131
- const api = reference.api.map(section => ({ ...section, usage: section.recipe ? this.recipeCode(section.recipe) : section.usage }));
132
- const examples = reference.examples.map(example => ({ ...example, code: example.recipe ? this.recipeCode(example.recipe) : example.codeSource ? this.read(example.codeSource) : example.code }));
138
+ const api = reference.api.map(section => ({ ...section, usage: this.code(section, 'usage') }));
139
+ const examples = reference.examples.map(example => ({ ...example, code: this.code(example, 'code') }));
133
140
  for (const example of examples) {
134
141
  pages.push({ id: `examples/${example.id}`, title: example.title, summary: example.summary, source: 'docs/reference.json', markdown: [
135
142
  `# ${example.title}`, this.notice(), example.summary,