@emseepea/create-react-ui-server 0.0.4 → 0.0.5

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.
package/README.md CHANGED
@@ -25,6 +25,11 @@ This example makes the server-rendered form interactive with React and imports
25
25
  one compiled Em See Pea stylesheet. It needs no Tailwind configuration and uses
26
26
  the same sample states as the native example.
27
27
 
28
+ `src/server.tsx` assembles the server. HTTP handlers live in `src/routes/`,
29
+ where the filename supplies the method and path. The route files serve the page,
30
+ form response, stylesheet, and browser script. Add a direct Fastify route only
31
+ when a handler does not fit this simple file convention.
32
+
28
33
  ## Run
29
34
 
30
35
  From this directory:
@@ -7,6 +7,11 @@ This example makes the server-rendered form interactive with React and imports
7
7
  one compiled Em See Pea stylesheet. It needs no Tailwind configuration and uses
8
8
  the same sample states as the native example.
9
9
 
10
+ `src/server.tsx` assembles the server. HTTP handlers live in `src/routes/`,
11
+ where the filename supplies the method and path. The route files serve the page,
12
+ form response, stylesheet, and browser script. Add a direct Fastify route only
13
+ when a handler does not fit this simple file convention.
14
+
10
15
  ## Run
11
16
 
12
17
  From this directory:
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "private": true,
31
31
  "dependencies": {
32
- "@emseepea/react": "0.0.5",
33
- "@emseepea/server": "0.2.2",
32
+ "@emseepea/react": "0.0.6",
33
+ "@emseepea/server": "0.3.0",
34
34
  "@emseepea/tailwind": "0.0.2",
35
35
  "react": "19.2.8",
36
36
  "react-dom": "19.2.8"
@@ -0,0 +1,9 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import type { HttpRouteHandler } from "@emseepea/server";
4
+
5
+ const client = await readFile(new URL("../client.js", import.meta.url), "utf8");
6
+
7
+ export default (async (_request, reply) => {
8
+ await reply.type("text/javascript; charset=utf-8").header("cache-control", "no-store").send(client);
9
+ }) satisfies HttpRouteHandler;
@@ -0,0 +1,9 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import type { HttpRouteHandler } from "@emseepea/server";
4
+
5
+ const stylesheet = await readFile(new URL(import.meta.resolve("@emseepea/tailwind/styles.css")), "utf8");
6
+
7
+ export default (async (_request, reply) => {
8
+ await reply.type("text/css; charset=utf-8").header("cache-control", "no-store").send(stylesheet);
9
+ }) satisfies HttpRouteHandler;
@@ -0,0 +1,7 @@
1
+ import type { HttpRouteHandler } from "@emseepea/server";
2
+
3
+ import { pageFromQuery } from "../ui.js";
4
+
5
+ export default (async (request, reply) => {
6
+ await reply.type("text/html; charset=utf-8").send(pageFromQuery(request.query));
7
+ }) satisfies HttpRouteHandler;
@@ -0,0 +1,7 @@
1
+ import type { HttpRouteHandler } from "@emseepea/server";
2
+
3
+ import { submittedView } from "../ui.js";
4
+
5
+ export default (async (request, reply) => {
6
+ await reply.type("application/json; charset=utf-8").send(submittedView(request.body));
7
+ }) satisfies HttpRouteHandler;
@@ -1,16 +1,7 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { parse } from "node:querystring";
3
2
 
4
- import {
5
- fixtureForState,
6
- viewFromSubmission,
7
- } from "./ui-shared.js";
8
- import { ElicitationForm } from "@emseepea/react";
9
- import { createEmseepea, discoverCapabilities, serveEmseepea, type ElicitationView } from "@emseepea/server";
10
- import { renderToString } from "react-dom/server";
3
+ import { createEmseepea, discoverCapabilities, registerRoutes, serveEmseepea } from "@emseepea/server";
11
4
 
12
- const stylesheet = await readFile(new URL(import.meta.resolve("@emseepea/tailwind/styles.css")), "utf8");
13
- const client = await readFile(new URL("./client.js", import.meta.url), "utf8");
14
5
  const app = createEmseepea({
15
6
  name: "emseepea-react-ui-server",
16
7
  version: "0.0.0",
@@ -23,24 +14,7 @@ app.addContentTypeParser(
23
14
  { parseAs: "string" },
24
15
  (_request, body, done) => done(null, parse(body.toString())),
25
16
  );
26
- app.get("/emseepea.css", async (_request, reply) => {
27
- await reply.type("text/css; charset=utf-8").header("cache-control", "no-store").send(stylesheet);
28
- });
29
- app.get("/client.js", async (_request, reply) => {
30
- await reply.type("text/javascript; charset=utf-8").header("cache-control", "no-store").send(client);
31
- });
32
- app.get("/", async (request, reply) => {
33
- const query = record(request.query);
34
- const view = fixtureForState(query.state);
35
- await reply.type("text/html; charset=utf-8").send(page(
36
- view,
37
- first(query.theme) === "dark" ? "dark" : "light",
38
- first(query.style) !== "off",
39
- ));
40
- });
41
- app.post("/", async (request, reply) => {
42
- await reply.type("application/json; charset=utf-8").send(viewFromSubmission(request.body));
43
- });
17
+ await registerRoutes(app, new URL("./routes/", import.meta.url));
44
18
 
45
19
  const running = await serveEmseepea(app, { port: Number.parseInt(process.env.PORT ?? "3001", 10) });
46
20
  console.log(`Em See Pea React UI example listening at ${running.url}`);
@@ -51,25 +25,3 @@ async function shutdown(): Promise<void> {
51
25
  }
52
26
  process.once("SIGINT", () => void shutdown());
53
27
  process.once("SIGTERM", () => void shutdown());
54
-
55
- function page(view: ElicitationView, theme: "light" | "dark", styled: boolean): string {
56
- const markup = renderToString(<ElicitationForm view={view} headingLevel={2} />);
57
- return `<!doctype html><html lang="en" data-emseepea-theme="${theme}"><head>` +
58
- `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
59
- `<title>Pea planting plan preview - Em See Pea</title>${styled ? '<link rel="stylesheet" href="/emseepea.css">' : ""}` +
60
- `</head><body><a href="#main-content">Skip to main content</a><main id="main-content" tabindex="-1">` +
61
- `<h1>React and Tailwind form example</h1><div id="app">${markup}</div></main>` +
62
- `<script id="emseepea-view" type="application/json">${safeJson(view)}</script>` +
63
- `<script type="module" src="/client.js"></script></body></html>`;
64
- }
65
-
66
- function safeJson(value: unknown): string {
67
- return JSON.stringify(value).replaceAll("&", "\\u0026").replaceAll("<", "\\u003c")
68
- .replaceAll(">", "\\u003e").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
69
- }
70
- function record(value: unknown): Record<string, unknown> {
71
- return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
72
- }
73
- function first(value: unknown): string | undefined {
74
- return typeof value === "string" ? value : Array.isArray(value) && typeof value[0] === "string" ? value[0] : undefined;
75
- }
@@ -0,0 +1,41 @@
1
+ import { fixtureForState, viewFromSubmission } from "./ui-shared.js";
2
+ import { ElicitationForm } from "@emseepea/react";
3
+ import type { ElicitationView } from "@emseepea/server";
4
+ import { renderToString } from "react-dom/server";
5
+
6
+ export function pageFromQuery(value: unknown): string {
7
+ const query = record(value);
8
+ return page(
9
+ fixtureForState(query.state),
10
+ first(query.theme) === "dark" ? "dark" : "light",
11
+ first(query.style) !== "off",
12
+ );
13
+ }
14
+
15
+ export function submittedView(value: unknown): ElicitationView {
16
+ return viewFromSubmission(value);
17
+ }
18
+
19
+ function page(view: ElicitationView, theme: "light" | "dark", styled: boolean): string {
20
+ const markup = renderToString(<ElicitationForm view={view} headingLevel={2} />);
21
+ return `<!doctype html><html lang="en" data-emseepea-theme="${theme}"><head>` +
22
+ `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
23
+ `<title>Pea planting plan preview - Em See Pea</title>${styled ? '<link rel="stylesheet" href="/emseepea.css">' : ""}` +
24
+ `</head><body><a href="#main-content">Skip to main content</a><main id="main-content" tabindex="-1">` +
25
+ `<h1>React and Tailwind form example</h1><div id="app">${markup}</div></main>` +
26
+ `<script id="emseepea-view" type="application/json">${safeJson(view)}</script>` +
27
+ `<script type="module" src="/client.js"></script></body></html>`;
28
+ }
29
+
30
+ function safeJson(value: unknown): string {
31
+ return JSON.stringify(value).replaceAll("&", "\\u0026").replaceAll("<", "\\u003c")
32
+ .replaceAll(">", "\\u003e").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
33
+ }
34
+
35
+ function record(value: unknown): Record<string, unknown> {
36
+ return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
37
+ }
38
+
39
+ function first(value: unknown): string | undefined {
40
+ return typeof value === "string" ? value : Array.isArray(value) && typeof value[0] === "string" ? value[0] : undefined;
41
+ }
@@ -23,6 +23,7 @@ export function testUiExample(example) {
23
23
  if (message.type() === "error") errors.push(message.text());
24
24
  });
25
25
  page.on("pageerror", (error) => errors.push(error.message));
26
+ await assertRouteResponses(page.request, running.origin, example.react === true);
26
27
 
27
28
  for (const theme of ["light", "dark"]) {
28
29
  for (const state of ["ready", "invalid", "busy", "terminal"]) {
@@ -35,6 +36,8 @@ export function testUiExample(example) {
35
36
  `${example.name} ${theme} ${state} has axe violations`,
36
37
  );
37
38
  assert.equal(await page.locator("html[lang='en']").count(), 1);
39
+ assert.equal(await page.locator("meta[name='viewport'][content='width=device-width, initial-scale=1']").count(), 1);
40
+ assert.equal(await page.locator("a[href='#main-content']").innerText(), "Skip to main content");
38
41
  assert.equal(await page.locator("main#main-content").count(), 1);
39
42
  assert.equal(await page.locator("h1").count(), 1);
40
43
  assert.equal(await page.title(), "Pea planting plan preview - Em See Pea");
@@ -130,6 +133,32 @@ export function testUiExample(example) {
130
133
  });
131
134
  }
132
135
 
136
+ async function assertRouteResponses(request, origin, react) {
137
+ const document = await request.get(`${origin}/`);
138
+ assert.equal(document.status(), 200);
139
+ assert.match(document.headers()["content-type"], /^text\/html; charset=utf-8$/);
140
+
141
+ const stylesheet = await request.get(`${origin}/emseepea.css`);
142
+ assert.equal(stylesheet.status(), 200);
143
+ assert.match(stylesheet.headers()["content-type"], /^text\/css; charset=utf-8$/);
144
+ assert.equal(stylesheet.headers()["cache-control"], "no-store");
145
+
146
+ const submission = await request.post(`${origin}/`, { form: { title: "Route contract" } });
147
+ assert.equal(submission.status(), 200);
148
+ assert.match(
149
+ submission.headers()["content-type"],
150
+ react ? /^application\/json; charset=utf-8$/ : /^text\/html; charset=utf-8$/,
151
+ );
152
+
153
+ if (react) {
154
+ const client = await request.get(`${origin}/client.js`);
155
+ assert.equal(client.status(), 200);
156
+ assert.match(client.headers()["content-type"], /^text\/javascript; charset=utf-8$/);
157
+ assert.equal(client.headers()["cache-control"], "no-store");
158
+ assert.ok((await client.body()).byteLength > 0);
159
+ }
160
+ }
161
+
133
162
  async function assertNoEffectClaim(page) {
134
163
  const text = await page.locator("[data-emseepea-part='terminal']").innerText();
135
164
  assert.match(text, /No report was sent or stored\./);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/create-react-ui-server",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Create an Em See Pea server with an accessible React form.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,8 +18,8 @@
18
18
  "prepack": "npm run build:initializer"
19
19
  },
20
20
  "devDependencies": {
21
- "@emseepea/react": "0.0.5",
22
- "@emseepea/server": "0.2.2",
21
+ "@emseepea/react": "0.0.6",
22
+ "@emseepea/server": "0.3.0",
23
23
  "@emseepea/tailwind": "0.0.2",
24
24
  "react": "19.2.8",
25
25
  "react-dom": "19.2.8",