create-yerd 0.0.0-stage → 0.2.2
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/LICENSE +21 -0
- package/README.md +84 -2
- package/bin/create-yerd.js +17 -0
- package/package.json +42 -4
- package/src/args.js +140 -0
- package/src/cli.js +180 -0
- package/src/pm.js +33 -0
- package/src/prompt.js +284 -0
- package/src/render.js +159 -0
- package/src/style.js +33 -0
- package/src/target.js +68 -0
- package/src/versions.js +14 -0
- package/templates/_shared/_env.example +9 -0
- package/templates/_shared/_gitignore +6 -0
- package/templates/_shared/vitest.config.ts +8 -0
- package/templates/api/README.md +39 -0
- package/templates/api/src/app.ts +9 -0
- package/templates/api/src/main.ts +5 -0
- package/templates/api/src/notes.ts +65 -0
- package/templates/api/src/routes/notes.ts +49 -0
- package/templates/api/test/notes.test.ts +68 -0
- package/templates/api/tsconfig.json +16 -0
- package/templates/api/vite.config.ts +7 -0
- package/templates/full/README.md +46 -0
- package/templates/full/e2e/shop.spec.ts +18 -0
- package/templates/full/playwright.config.ts +16 -0
- package/templates/full/public/favicon.svg +1 -0
- package/templates/full/src/app.ts +8 -0
- package/templates/full/src/cart.ts +50 -0
- package/templates/full/src/catalog.ts +59 -0
- package/templates/full/src/components.tsx +31 -0
- package/templates/full/src/layouts.tsx +67 -0
- package/templates/full/src/main.ts +5 -0
- package/templates/full/src/orders.ts +52 -0
- package/templates/full/src/routes/api.ts +34 -0
- package/templates/full/src/routes/auth.tsx +71 -0
- package/templates/full/src/routes/cart.tsx +66 -0
- package/templates/full/src/routes/checkout.tsx +101 -0
- package/templates/full/src/routes/products.tsx +79 -0
- package/templates/full/src/styles.css +72 -0
- package/templates/full/src/users.ts +41 -0
- package/templates/full/test/shop.test.ts +130 -0
- package/templates/full/tsconfig.json +18 -0
- package/templates/full/vite.config.ts +7 -0
- package/templates/minimal/README.md +26 -0
- package/templates/minimal/public/favicon.svg +1 -0
- package/templates/minimal/src/app.ts +7 -0
- package/templates/minimal/src/layouts.tsx +32 -0
- package/templates/minimal/src/main.ts +5 -0
- package/templates/minimal/src/routes/home.tsx +44 -0
- package/templates/minimal/src/styles.css +32 -0
- package/templates/minimal/test/app.test.ts +35 -0
- package/templates/minimal/tsconfig.json +18 -0
- package/templates/minimal/vite.config.ts +7 -0
- package/templates/tailwind/_shared/vite.config.ts +8 -0
- package/templates/tailwind/full/src/styles.css +49 -0
- package/templates/tailwind/minimal/src/layouts.tsx +33 -0
- package/templates/tailwind/minimal/src/routes/home.tsx +48 -0
- package/templates/tailwind/minimal/src/styles.css +2 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Region, action, get, route, session, type Session } from "@toyz/yerd";
|
|
2
|
+
|
|
3
|
+
/** What this page keeps in the visitor's session. No cookie until it changes. */
|
|
4
|
+
class Visit {
|
|
5
|
+
clicks = 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The count, as a Region: the action sends back just this. */
|
|
9
|
+
function Clicks({ count }: { count: number }) {
|
|
10
|
+
return (
|
|
11
|
+
<Region id="clicks" as="p">
|
|
12
|
+
Clicked {count} {count === 1 ? "time" : "times"}
|
|
13
|
+
</Region>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@route("/", { meta: { title: "Home" } })
|
|
18
|
+
export class Home {
|
|
19
|
+
@session(Visit) accessor visit!: Session<Visit>;
|
|
20
|
+
|
|
21
|
+
@get page() {
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<h1>Hello from yerd</h1>
|
|
25
|
+
<p>
|
|
26
|
+
Edit <code>src/routes/home.tsx</code> and save to see it change.
|
|
27
|
+
</p>
|
|
28
|
+
<Clicks count={this.visit.clicks} />
|
|
29
|
+
{/* Posts without a reload when the runtime is loaded, as a plain form when it isn't. */}
|
|
30
|
+
<form action={this.click}>
|
|
31
|
+
<button>Click</button>
|
|
32
|
+
</form>
|
|
33
|
+
<p>
|
|
34
|
+
<a href="https://toyz.github.io/yerd/">Read the guides</a>
|
|
35
|
+
</p>
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@action click() {
|
|
41
|
+
this.visit.clicks++;
|
|
42
|
+
return <Clicks count={this.visit.clicks} />;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Linked with <Stylesheet href="src/styles.css" />; hashed by `vite build`. */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #fafaf9;
|
|
4
|
+
--text: #1c1917;
|
|
5
|
+
--muted: #78716c;
|
|
6
|
+
--accent: #c2410c;
|
|
7
|
+
--accent-ink: #ffffff;
|
|
8
|
+
color-scheme: light dark;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@media (prefers-color-scheme: dark) {
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #1c1917;
|
|
14
|
+
--text: #f5f5f4;
|
|
15
|
+
--muted: #a8a29e;
|
|
16
|
+
--accent: #fb923c;
|
|
17
|
+
--accent-ink: #1c1917;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
22
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 17px/1.6 ui-sans-serif, system-ui, sans-serif; }
|
|
23
|
+
main { max-width: 640px; margin: 0 auto; padding: 64px 20px; }
|
|
24
|
+
h1 { font-size: 2.4rem; line-height: 1.1; margin: 0 0 16px; }
|
|
25
|
+
a { color: var(--accent); }
|
|
26
|
+
code { font-size: 0.9em; }
|
|
27
|
+
button {
|
|
28
|
+
font: inherit; padding: 8px 18px; border: 0; border-radius: 8px;
|
|
29
|
+
background: var(--accent); color: var(--accent-ink); cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
#clicks { font-variant-numeric: tabular-nums; color: var(--muted); }
|
|
32
|
+
.error .status { font-size: 3rem; font-weight: 700; margin: 0; color: var(--muted); }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, it } from "vitest";
|
|
2
|
+
import { createApp } from "../src/app.js";
|
|
3
|
+
import "../src/routes/home.js";
|
|
4
|
+
|
|
5
|
+
// The app main.ts serves, with the routes this file imports. No port is opened.
|
|
6
|
+
const app = createApp({ handleSignals: false });
|
|
7
|
+
beforeAll(() => app.boot());
|
|
8
|
+
afterAll(() => app.stop());
|
|
9
|
+
|
|
10
|
+
const html = { accept: "text/html" };
|
|
11
|
+
|
|
12
|
+
it("renders the home page inside the layout", async () => {
|
|
13
|
+
const res = await app.request("/", { headers: html });
|
|
14
|
+
expect(res.status).toBe(200);
|
|
15
|
+
const body = await res.text();
|
|
16
|
+
expect(body).toContain("<title>Home · {{name}}</title>");
|
|
17
|
+
expect(body).toContain("Clicked 0 times");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("counts clicks in the visitor's session", async () => {
|
|
21
|
+
const client = app.client();
|
|
22
|
+
await client.action("/", {});
|
|
23
|
+
const res = await client.action("/", {});
|
|
24
|
+
expect(res.status).toBe(200);
|
|
25
|
+
expect(await res.text()).toContain("Clicked 2 times");
|
|
26
|
+
|
|
27
|
+
// Another visitor has a session of their own.
|
|
28
|
+
expect(await app.client().page("/")).toContain("Clicked 0 times");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("answers an unknown path with the error page", async () => {
|
|
32
|
+
const res = await app.request("/nope", { headers: html });
|
|
33
|
+
expect(res.status).toBe(404);
|
|
34
|
+
expect(await res.text()).toContain("Page not found");
|
|
35
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"lib": ["ES2023", "esnext.decorators", "DOM"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"jsxImportSource": "@toyz/yerd"
|
|
16
|
+
},
|
|
17
|
+
"include": ["src", "test", "*.config.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import yerd from "@toyz/yerd/vite";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
// `client`: files `vite build` hashes; <Stylesheet> links them through the manifest.
|
|
6
|
+
plugins: [yerd({ entry: "/src/main.ts", client: ["src/styles.css"] })],
|
|
7
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
3
|
+
import yerd from "@toyz/yerd/vite";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
// Tailwind builds src/styles.css; `client` makes `vite build` hash it for <Stylesheet>.
|
|
7
|
+
plugins: [tailwindcss(), yerd({ entry: "/src/main.ts", client: ["src/styles.css"] })],
|
|
8
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Linked with <Stylesheet href="src/styles.css" />. Tailwind builds it; `vite build` hashes it.
|
|
3
|
+
* The pages keep short class names, made of utilities here; utilities work in the TSX too.
|
|
4
|
+
*/
|
|
5
|
+
@import "tailwindcss";
|
|
6
|
+
|
|
7
|
+
@layer base {
|
|
8
|
+
body { @apply bg-stone-50 text-stone-900 antialiased dark:bg-stone-950 dark:text-stone-100; }
|
|
9
|
+
h1 { @apply mb-4 text-3xl font-bold tracking-tight; }
|
|
10
|
+
a { @apply text-sky-800 underline-offset-2 hover:underline dark:text-sky-300; }
|
|
11
|
+
input { @apply rounded-lg border border-stone-300 bg-white px-3 py-1.5 dark:border-stone-700 dark:bg-stone-900; }
|
|
12
|
+
button { @apply cursor-pointer rounded-lg bg-sky-800 px-3.5 py-1.5 font-medium text-white hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-sky-300 dark:text-sky-950 dark:hover:bg-sky-200; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@layer components {
|
|
16
|
+
.button { @apply inline-block rounded-lg bg-sky-800 px-3.5 py-1.5 font-medium text-white no-underline hover:bg-sky-700 hover:no-underline dark:bg-sky-300 dark:text-sky-950 dark:hover:bg-sky-200; }
|
|
17
|
+
button.link { @apply bg-transparent p-0 font-normal text-sky-800 underline hover:bg-transparent dark:bg-transparent dark:text-sky-300 dark:hover:bg-transparent; }
|
|
18
|
+
|
|
19
|
+
.top { @apply mx-auto flex max-w-3xl items-center gap-4 px-4 py-5; }
|
|
20
|
+
.brand { @apply mr-auto text-xl font-bold text-stone-900 no-underline hover:no-underline dark:text-stone-100; }
|
|
21
|
+
.cart-badge { @apply no-underline hover:no-underline; }
|
|
22
|
+
.badge { @apply inline-block min-w-[1.6em] rounded-full bg-sky-800 px-1.5 text-center text-white dark:bg-sky-300 dark:text-sky-950; }
|
|
23
|
+
.account { @apply flex items-center gap-2; }
|
|
24
|
+
main { @apply mx-auto max-w-3xl px-4 pb-12; }
|
|
25
|
+
.narrow { @apply max-w-md; }
|
|
26
|
+
|
|
27
|
+
.search { @apply mb-4 flex gap-2; }
|
|
28
|
+
.search input { @apply flex-1; }
|
|
29
|
+
.products { @apply grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-3; }
|
|
30
|
+
.card { @apply grid content-start gap-1 rounded-xl border border-stone-200 bg-white p-3.5 dark:border-stone-800 dark:bg-stone-900; }
|
|
31
|
+
.price { @apply tabular-nums; }
|
|
32
|
+
.sold-out, .empty, .hint, .stock { @apply text-stone-500 dark:text-stone-400; }
|
|
33
|
+
.hint { @apply mt-4; }
|
|
34
|
+
|
|
35
|
+
.product { @apply grid justify-items-start gap-2; }
|
|
36
|
+
.product h1 { @apply m-0; }
|
|
37
|
+
|
|
38
|
+
.lines { @apply grid gap-2; }
|
|
39
|
+
.lines li { @apply flex flex-wrap items-center gap-3 border-b border-stone-200 py-2.5 dark:border-stone-800; }
|
|
40
|
+
.lines li > a { @apply flex-1; }
|
|
41
|
+
.total { @apply my-3 font-bold; }
|
|
42
|
+
.summary { @apply list-disc pl-5; }
|
|
43
|
+
|
|
44
|
+
.field { @apply mb-3 grid gap-1; }
|
|
45
|
+
.field-error, .form-error { @apply text-sm text-red-700 empty:hidden dark:text-red-400; }
|
|
46
|
+
[aria-invalid="true"] { @apply border-red-700 dark:border-red-400; }
|
|
47
|
+
|
|
48
|
+
.error-page .status { @apply m-0 text-5xl font-bold text-stone-500; }
|
|
49
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Scripts, Stylesheet, type ErrorPageProps, type LayoutProps } from "@toyz/yerd";
|
|
2
|
+
|
|
3
|
+
/** The document around every HTML response, error pages included. */
|
|
4
|
+
export function RootLayout({ children, meta }: LayoutProps) {
|
|
5
|
+
return (
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8" />
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
11
|
+
<title>{meta.title ? `${meta.title} · {{name}}` : "{{name}}"}</title>
|
|
12
|
+
{/* Tailwind's output: served by Vite in dev, the hashed file after `vite build`. */}
|
|
13
|
+
<Stylesheet href="src/styles.css" />
|
|
14
|
+
<Scripts />
|
|
15
|
+
</head>
|
|
16
|
+
<body class="min-h-dvh bg-stone-50 text-stone-900 antialiased dark:bg-stone-950 dark:text-stone-100">
|
|
17
|
+
<main class="mx-auto max-w-2xl px-5 py-16">{children}</main>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** app.errorPage(): 404s and crashes render here, inside RootLayout. */
|
|
24
|
+
export function ErrorPage({ status, message }: ErrorPageProps) {
|
|
25
|
+
return (
|
|
26
|
+
<section class="flex flex-col gap-3">
|
|
27
|
+
<p class="text-5xl font-bold text-stone-400">{status}</p>
|
|
28
|
+
<h1 class="text-3xl font-bold">{status === 404 ? "Page not found" : "Something went wrong"}</h1>
|
|
29
|
+
<p class="text-stone-600 dark:text-stone-400">{message}</p>
|
|
30
|
+
<a class="text-orange-700 underline dark:text-orange-400" href="/">Home</a>
|
|
31
|
+
</section>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Region, action, get, route, session, type Session } from "@toyz/yerd";
|
|
2
|
+
|
|
3
|
+
/** What this page keeps in the visitor's session. No cookie until it changes. */
|
|
4
|
+
class Visit {
|
|
5
|
+
clicks = 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The count, as a Region: the action sends back just this. */
|
|
9
|
+
function Clicks({ count }: { count: number }) {
|
|
10
|
+
return (
|
|
11
|
+
<Region id="clicks" as="p" class="mt-8 tabular-nums text-stone-500 dark:text-stone-400">
|
|
12
|
+
Clicked {count} {count === 1 ? "time" : "times"}
|
|
13
|
+
</Region>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@route("/", { meta: { title: "Home" } })
|
|
18
|
+
export class Home {
|
|
19
|
+
@session(Visit) accessor visit!: Session<Visit>;
|
|
20
|
+
|
|
21
|
+
@get page() {
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">
|
|
25
|
+
Hello from <span class="text-orange-600">yerd</span>
|
|
26
|
+
</h1>
|
|
27
|
+
<p class="mt-4 text-lg text-stone-600 dark:text-stone-400">
|
|
28
|
+
Edit <code class="font-mono text-base">src/routes/home.tsx</code> and save to see it change.
|
|
29
|
+
</p>
|
|
30
|
+
<Clicks count={this.visit.clicks} />
|
|
31
|
+
{/* Posts without a reload when the runtime is loaded, as a plain form when it isn't. */}
|
|
32
|
+
<form class="mt-3" action={this.click}>
|
|
33
|
+
<button class="rounded-lg bg-orange-600 px-4 py-2 font-semibold text-white shadow-sm transition hover:bg-orange-500 active:translate-y-px">
|
|
34
|
+
Click
|
|
35
|
+
</button>
|
|
36
|
+
</form>
|
|
37
|
+
<p class="mt-10">
|
|
38
|
+
<a class="text-orange-700 underline dark:text-orange-400" href="https://toyz.github.io/yerd/">Read the guides</a>
|
|
39
|
+
</p>
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@action click() {
|
|
45
|
+
this.visit.clicks++;
|
|
46
|
+
return <Clicks count={this.visit.clicks} />;
|
|
47
|
+
}
|
|
48
|
+
}
|