@webjsdev/cli 0.10.18 → 0.10.19
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/lib/create.js +32 -20
- package/lib/saas-template.js +29 -27
- package/package.json +1 -1
- package/templates/AGENTS.md +11 -11
- package/templates/CONVENTIONS.md +12 -11
- package/templates/test/hello/hello.test.ts +1 -1
package/lib/create.js
CHANGED
|
@@ -85,11 +85,10 @@ const UI_REGISTRY_ROOT = resolveUiRegistryRoot();
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Read a single @webjsdev/ui registry component, rewrite its relative import
|
|
88
|
-
* of `../lib/utils.ts`
|
|
89
|
-
* `components/ui/<name>.ts
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* utils/ folder: `../../lib/utils/cn.ts`.
|
|
88
|
+
* of `../lib/utils.ts` to the scaffolded app's aliased path so it resolves
|
|
89
|
+
* when written to `components/ui/<name>.ts`. The scaffold puts cn() at
|
|
90
|
+
* `lib/utils/cn.ts` (folder-grouped with other browser-safe helpers), so the
|
|
91
|
+
* alias form is `#lib/utils/cn.ts` (#555/#556).
|
|
93
92
|
*
|
|
94
93
|
* @param {string} name component name without `.ts` (e.g. 'button')
|
|
95
94
|
* @returns {Promise<string>} source with import rewritten
|
|
@@ -97,9 +96,11 @@ const UI_REGISTRY_ROOT = resolveUiRegistryRoot();
|
|
|
97
96
|
async function readUiComponent(name) {
|
|
98
97
|
const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
|
|
99
98
|
const raw = await readFile(src, 'utf8');
|
|
99
|
+
// The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
|
|
100
|
+
// it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
|
|
100
101
|
return raw
|
|
101
|
-
.replaceAll("'../lib/utils.ts'", "'
|
|
102
|
-
.replaceAll('"../lib/utils.ts"', '"
|
|
102
|
+
.replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
|
|
103
|
+
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"');
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/**
|
|
@@ -286,6 +287,17 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
286
287
|
version: '0.1.0',
|
|
287
288
|
type: 'module',
|
|
288
289
|
private: true,
|
|
290
|
+
// Native package.json subpath aliases (#555): write app-internal imports as
|
|
291
|
+
// `#lib/...`, `#components/...`, `#db/...`, `#<any-dir>/...` instead of deep
|
|
292
|
+
// `../../../` relatives. ONE catch-all key, so a new top-level folder is
|
|
293
|
+
// aliased with no config change. Node 24+ and Bun both resolve `#*` natively
|
|
294
|
+
// (a `#/`-prefixed key is rejected by Bun, so this slash-free form is the
|
|
295
|
+
// cross-runtime-safe shape; no build step, no tsconfig paths). The webjs
|
|
296
|
+
// server expands the same map for the import graph + browser importmap. Opt
|
|
297
|
+
// out by using a plain relative import.
|
|
298
|
+
imports: {
|
|
299
|
+
'#*': './*',
|
|
300
|
+
},
|
|
289
301
|
scripts: {
|
|
290
302
|
// No `predev` / `prestart` hooks (#550): the `webjs` block below holds
|
|
291
303
|
// the start orchestration (`webjs db migrate`), run INSIDE `webjs start`,
|
|
@@ -724,8 +736,8 @@ export async function createUser(input: { name: string; email: string }) {
|
|
|
724
736
|
* /api/users: thin route wrapper over typed server actions.
|
|
725
737
|
* Business logic lives in modules/users/, not here.
|
|
726
738
|
*/
|
|
727
|
-
import { listUsers } from '
|
|
728
|
-
import { createUser } from '
|
|
739
|
+
import { listUsers } from '#modules/users/queries/list-users.server.ts';
|
|
740
|
+
import { createUser } from '#modules/users/actions/create-user.server.ts';
|
|
729
741
|
|
|
730
742
|
export async function GET() {
|
|
731
743
|
return Response.json(await listUsers());
|
|
@@ -742,8 +754,8 @@ export async function POST(req: Request) {
|
|
|
742
754
|
await writeFile(join(appDir, 'test', 'unit', 'users.test.ts'), `import { test } from 'node:test';
|
|
743
755
|
import assert from 'node:assert/strict';
|
|
744
756
|
|
|
745
|
-
import { listUsers } from '
|
|
746
|
-
import { createUser } from '
|
|
757
|
+
import { listUsers } from '#modules/users/queries/list-users.server.ts';
|
|
758
|
+
import { createUser } from '#modules/users/actions/create-user.server.ts';
|
|
747
759
|
|
|
748
760
|
test('listUsers returns an array', async () => {
|
|
749
761
|
const users = await listUsers();
|
|
@@ -833,16 +845,16 @@ export type ActionResult<T> =
|
|
|
833
845
|
await writeFile(join(appDir, 'app', 'layout.ts'), `// webjs-scaffold-placeholder. This is the example app chrome (brand, nav, content-width container). Adapt it to your app, then delete this line. webjs check fails while the marker remains.
|
|
834
846
|
import { html, cspNonce } from '@webjsdev/core';
|
|
835
847
|
import '@webjsdev/core/client-router';
|
|
836
|
-
import '
|
|
848
|
+
import '#components/theme-toggle.ts';
|
|
837
849
|
// Webjs UI components are tiered:
|
|
838
850
|
// - Tier 1 (button, card, input, label, alert, badge, separator, etc.) are
|
|
839
851
|
// class-helper FUNCTIONS, with no custom element to register. Each page
|
|
840
852
|
// imports the specific helpers it needs (e.g.
|
|
841
|
-
// \`import { buttonClass } from '
|
|
853
|
+
// \`import { buttonClass } from '#components/ui/button.ts'\`).
|
|
842
854
|
// - Tier 2 (dialog, popover, tooltip, tabs, accordion, etc.) ARE custom
|
|
843
855
|
// elements. Register them by side-effect-importing here once so they
|
|
844
856
|
// work transitively across every page:
|
|
845
|
-
// import '
|
|
857
|
+
// import '#components/ui/dialog.ts';
|
|
846
858
|
// The example app/page.ts below uses only Tier-1 helpers, so nothing
|
|
847
859
|
// extra needs to be registered. Add Tier-2 imports as you 'webjs ui add'.
|
|
848
860
|
|
|
@@ -1014,18 +1026,18 @@ ${SHADCN_THEME}
|
|
|
1014
1026
|
|
|
1015
1027
|
await writeFile(join(appDir, 'app', 'page.ts'), `// webjs-scaffold-placeholder. This is the example homepage. Replace it with your app's real page, then delete this line. webjs check fails while the marker remains.
|
|
1016
1028
|
import { html } from '@webjsdev/core';
|
|
1017
|
-
import { rubric, displayH1, accentLink } from '
|
|
1018
|
-
import { buttonClass } from '
|
|
1019
|
-
import { badgeClass } from '
|
|
1029
|
+
import { rubric, displayH1, accentLink } from '#lib/utils/ui.ts';
|
|
1030
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
1031
|
+
import { badgeClass } from '#components/ui/badge.ts';
|
|
1020
1032
|
import {
|
|
1021
1033
|
cardClass,
|
|
1022
1034
|
cardHeaderClass,
|
|
1023
1035
|
cardTitleClass,
|
|
1024
1036
|
cardDescriptionClass,
|
|
1025
1037
|
cardContentClass,
|
|
1026
|
-
} from '
|
|
1027
|
-
import { alertClass, alertTitleClass, alertDescriptionClass } from '
|
|
1028
|
-
import { separatorClass } from '
|
|
1038
|
+
} from '#components/ui/card.ts';
|
|
1039
|
+
import { alertClass, alertTitleClass, alertDescriptionClass } from '#components/ui/alert.ts';
|
|
1040
|
+
import { separatorClass } from '#components/ui/separator.ts';
|
|
1029
1041
|
|
|
1030
1042
|
export const metadata = {
|
|
1031
1043
|
title: '${name}: built with webjs',
|
package/lib/saas-template.js
CHANGED
|
@@ -14,17 +14,19 @@ const UI_REGISTRY_ROOT = resolve(
|
|
|
14
14
|
);
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Read a registry component and rewrite its `'
|
|
18
|
-
* the scaffolded app's `components/ui/<name>.ts` layout (
|
|
19
|
-
* Mirrors the helper in `create.js`, kept private here to avoid coupling.
|
|
17
|
+
* Read a registry component and rewrite its `'#lib/utils.ts'` import for
|
|
18
|
+
* the scaffolded app's `components/ui/<name>.ts` layout (the `#lib/utils/cn.ts`
|
|
19
|
+
* alias). Mirrors the helper in `create.js`, kept private here to avoid coupling.
|
|
20
20
|
*/
|
|
21
21
|
async function readUiComponent(name) {
|
|
22
22
|
const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
|
|
23
23
|
if (!existsSync(src)) return null;
|
|
24
24
|
const raw = await readFile(src, 'utf8');
|
|
25
|
+
// The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
|
|
26
|
+
// it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
|
|
25
27
|
return raw
|
|
26
|
-
.replaceAll("'../lib/utils.ts'", "'
|
|
27
|
-
.replaceAll('"../lib/utils.ts"', '"
|
|
28
|
+
.replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
|
|
29
|
+
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"');
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** Copy named registry components into `<appDir>/components/ui/`. */
|
|
@@ -79,7 +81,7 @@ export async function writeSaasFiles(appDir) {
|
|
|
79
81
|
// lib/auth.server.ts
|
|
80
82
|
await writeFile(join(appDir, 'lib', 'auth.server.ts'), [
|
|
81
83
|
"import { createAuth, Credentials } from '@webjsdev/server';",
|
|
82
|
-
"import { db } from '
|
|
84
|
+
"import { db } from '#db/connection.server.ts';",
|
|
83
85
|
"import { compare } from './password.server.ts';",
|
|
84
86
|
"",
|
|
85
87
|
"export const { auth, signIn, signOut, handlers } = createAuth({",
|
|
@@ -124,9 +126,9 @@ export async function writeSaasFiles(appDir) {
|
|
|
124
126
|
await writeFile(join(appDir, 'modules', 'auth', 'actions', 'signup.server.ts'), [
|
|
125
127
|
"'use server';",
|
|
126
128
|
"",
|
|
127
|
-
"import { db } from '
|
|
128
|
-
"import { users } from '
|
|
129
|
-
"import { hash } from '
|
|
129
|
+
"import { db } from '#db/connection.server.ts';",
|
|
130
|
+
"import { users } from '#db/schema.server.ts';",
|
|
131
|
+
"import { hash } from '#lib/password.server.ts';",
|
|
130
132
|
"",
|
|
131
133
|
"export async function signup(input: { name: string; email: string; password: string }) {",
|
|
132
134
|
" const exists = await db.query.users.findFirst({ where: { email: input.email }, columns: { id: true } });",
|
|
@@ -141,7 +143,7 @@ export async function writeSaasFiles(appDir) {
|
|
|
141
143
|
await writeFile(join(appDir, 'modules', 'auth', 'queries', 'current-user.server.ts'), [
|
|
142
144
|
"'use server';",
|
|
143
145
|
"",
|
|
144
|
-
"import { auth } from '
|
|
146
|
+
"import { auth } from '#lib/auth.server.ts';",
|
|
145
147
|
"",
|
|
146
148
|
"export async function currentUser() {",
|
|
147
149
|
" const session = await auth();",
|
|
@@ -263,7 +265,7 @@ export async function writeSaasFiles(appDir) {
|
|
|
263
265
|
// app/api/auth/[...path]/route.ts
|
|
264
266
|
await mkdir(join(appDir, 'app', 'api', 'auth', '[...path]'), { recursive: true });
|
|
265
267
|
await writeFile(join(appDir, 'app', 'api', 'auth', '[...path]', 'route.ts'), [
|
|
266
|
-
"import { handlers } from '
|
|
268
|
+
"import { handlers } from '#lib/auth.server.ts';",
|
|
267
269
|
"export const GET = handlers.GET;",
|
|
268
270
|
"export const POST = handlers.POST;",
|
|
269
271
|
"",
|
|
@@ -273,10 +275,10 @@ export async function writeSaasFiles(appDir) {
|
|
|
273
275
|
await mkdir(join(appDir, 'app', 'login'), { recursive: true });
|
|
274
276
|
await writeFile(join(appDir, 'app', 'login', 'page.ts'), [
|
|
275
277
|
"import { html } from '@webjsdev/core';",
|
|
276
|
-
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '
|
|
277
|
-
"import { buttonClass } from '
|
|
278
|
-
"import { inputClass } from '
|
|
279
|
-
"import { labelClass } from '
|
|
278
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '#components/ui/card.ts';",
|
|
279
|
+
"import { buttonClass } from '#components/ui/button.ts';",
|
|
280
|
+
"import { inputClass } from '#components/ui/input.ts';",
|
|
281
|
+
"import { labelClass } from '#components/ui/label.ts';",
|
|
280
282
|
"",
|
|
281
283
|
"export const metadata = { title: 'Login' };",
|
|
282
284
|
"",
|
|
@@ -315,11 +317,11 @@ export async function writeSaasFiles(appDir) {
|
|
|
315
317
|
await mkdir(join(appDir, 'app', 'signup'), { recursive: true });
|
|
316
318
|
await writeFile(join(appDir, 'app', 'signup', 'page.ts'), [
|
|
317
319
|
"import { html } from '@webjsdev/core';",
|
|
318
|
-
"import { signup } from '
|
|
319
|
-
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '
|
|
320
|
-
"import { buttonClass } from '
|
|
321
|
-
"import { inputClass } from '
|
|
322
|
-
"import { labelClass } from '
|
|
320
|
+
"import { signup } from '#modules/auth/actions/signup.server.ts';",
|
|
321
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '#components/ui/card.ts';",
|
|
322
|
+
"import { buttonClass } from '#components/ui/button.ts';",
|
|
323
|
+
"import { inputClass } from '#components/ui/input.ts';",
|
|
324
|
+
"import { labelClass } from '#components/ui/label.ts';",
|
|
323
325
|
"",
|
|
324
326
|
"export const metadata = { title: 'Sign up' };",
|
|
325
327
|
"",
|
|
@@ -387,7 +389,7 @@ export async function writeSaasFiles(appDir) {
|
|
|
387
389
|
// app/dashboard/middleware.ts
|
|
388
390
|
await mkdir(join(appDir, 'app', 'dashboard', 'settings'), { recursive: true });
|
|
389
391
|
await writeFile(join(appDir, 'app', 'dashboard', 'middleware.ts'), [
|
|
390
|
-
"import { auth } from '
|
|
392
|
+
"import { auth } from '#lib/auth.server.ts';",
|
|
391
393
|
"",
|
|
392
394
|
"export default async function requireAuth(req: Request, next: () => Promise<Response>) {",
|
|
393
395
|
" const session = await auth();",
|
|
@@ -402,10 +404,10 @@ export async function writeSaasFiles(appDir) {
|
|
|
402
404
|
// app/dashboard/page.ts
|
|
403
405
|
await writeFile(join(appDir, 'app', 'dashboard', 'page.ts'), [
|
|
404
406
|
"import { html } from '@webjsdev/core';",
|
|
405
|
-
"import { currentUser } from '
|
|
406
|
-
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '
|
|
407
|
-
"import { buttonClass } from '
|
|
408
|
-
"import { badgeClass } from '
|
|
407
|
+
"import { currentUser } from '#modules/auth/queries/current-user.server.ts';",
|
|
408
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '#components/ui/card.ts';",
|
|
409
|
+
"import { buttonClass } from '#components/ui/button.ts';",
|
|
410
|
+
"import { badgeClass } from '#components/ui/badge.ts';",
|
|
409
411
|
"",
|
|
410
412
|
"export const metadata = { title: 'Dashboard' };",
|
|
411
413
|
"",
|
|
@@ -433,8 +435,8 @@ export async function writeSaasFiles(appDir) {
|
|
|
433
435
|
// app/dashboard/settings/page.ts
|
|
434
436
|
await writeFile(join(appDir, 'app', 'dashboard', 'settings', 'page.ts'), [
|
|
435
437
|
"import { html } from '@webjsdev/core';",
|
|
436
|
-
"import { currentUser } from '
|
|
437
|
-
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '
|
|
438
|
+
"import { currentUser } from '#modules/auth/queries/current-user.server.ts';",
|
|
439
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '#components/ui/card.ts';",
|
|
438
440
|
"",
|
|
439
441
|
"export const metadata = { title: 'Settings' };",
|
|
440
442
|
"",
|
package/package.json
CHANGED
package/templates/AGENTS.md
CHANGED
|
@@ -202,10 +202,10 @@ Pure functions that return Tailwind class strings. You apply them to
|
|
|
202
202
|
import {
|
|
203
203
|
cardClass, cardHeaderClass, cardTitleClass,
|
|
204
204
|
cardContentClass, cardFooterClass,
|
|
205
|
-
} from '
|
|
206
|
-
import { inputClass } from '
|
|
207
|
-
import { labelClass } from '
|
|
208
|
-
import { buttonClass } from '
|
|
205
|
+
} from '#components/ui/card.ts';
|
|
206
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
207
|
+
import { labelClass } from '#components/ui/label.ts';
|
|
208
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
209
209
|
|
|
210
210
|
return html`
|
|
211
211
|
<div class=${cardClass()}>
|
|
@@ -237,13 +237,13 @@ elements. Import them once (typically in `app/layout.ts`) and use
|
|
|
237
237
|
|
|
238
238
|
```ts
|
|
239
239
|
// app/layout.ts (registers the custom elements for every page)
|
|
240
|
-
import '
|
|
241
|
-
import '
|
|
240
|
+
import '#components/ui/dialog.ts';
|
|
241
|
+
import '#components/ui/tabs.ts';
|
|
242
242
|
```
|
|
243
243
|
|
|
244
244
|
```ts
|
|
245
245
|
// app/some-page/page.ts (uses the registered elements)
|
|
246
|
-
import { buttonClass } from '
|
|
246
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
247
247
|
|
|
248
248
|
return html`
|
|
249
249
|
<ui-dialog>
|
|
@@ -463,7 +463,7 @@ singleton avoids opening a new connection on every dev-server reload), and
|
|
|
463
463
|
the tables from `db/schema.server.ts`:
|
|
464
464
|
|
|
465
465
|
```ts
|
|
466
|
-
import { db } from '
|
|
466
|
+
import { db } from '#db/connection.server.ts';
|
|
467
467
|
const users = await db.query.users.findMany();
|
|
468
468
|
```
|
|
469
469
|
|
|
@@ -754,8 +754,8 @@ legitimately use `static styles = css\`\`` for scoped CSS.
|
|
|
754
754
|
```ts
|
|
755
755
|
// modules/posts/actions/create-post.server.ts
|
|
756
756
|
'use server';
|
|
757
|
-
import { db } from '
|
|
758
|
-
import { posts } from '
|
|
757
|
+
import { db } from '#db/connection.server.ts';
|
|
758
|
+
import { posts } from '#db/schema.server.ts';
|
|
759
759
|
|
|
760
760
|
export async function createPost(input: { title: string; body: string }) {
|
|
761
761
|
if (!input.title) return { success: false, error: 'title required', status: 400 };
|
|
@@ -806,7 +806,7 @@ with a `route.ts` POST handler:
|
|
|
806
806
|
```ts
|
|
807
807
|
// app/posts/route.ts
|
|
808
808
|
import { redirect, html } from '@webjsdev/core';
|
|
809
|
-
import { createPost } from '
|
|
809
|
+
import { createPost } from '#modules/posts/actions/create-post.server.ts';
|
|
810
810
|
|
|
811
811
|
export async function POST(req: Request) {
|
|
812
812
|
const form = await req.formData();
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -281,7 +281,7 @@ codegen, so there is no dev `before` step.
|
|
|
281
281
|
preferences (theme, sidebar collapsed, etc.).
|
|
282
282
|
5. To add a model: edit `db/schema.server.ts`, then `npm run db:generate`
|
|
283
283
|
and `npm run db:migrate`. Access via `import { db } from
|
|
284
|
-
'
|
|
284
|
+
'#db/connection.server.ts'` (and the tables from
|
|
285
285
|
`db/schema.server.ts`) **only inside `.server.{js,ts}` files,
|
|
286
286
|
`route.ts` handlers, or `middleware.ts`**. Components, pages, and
|
|
287
287
|
layouts call into the wrapped server query instead; the framework
|
|
@@ -388,6 +388,7 @@ modules/
|
|
|
388
388
|
```
|
|
389
389
|
|
|
390
390
|
**Rules:**
|
|
391
|
+
- **Prefer the `#` root alias over deep relatives.** Write `import { db } from '#db/connection.server.ts'`, `import { Button } from '#components/ui/button.ts'`, `#lib/...`, `#modules/...` instead of `../../../`. It is native `package.json "imports"` (the single `"#*": "./*"` key covers every top-level folder, so a new folder needs no config), resolved by Node and Bun with no build step. There is no slash after the `#` (`#lib/...`, not `#/lib/...`). A same-directory import stays relative (`./sibling.ts`).
|
|
391
392
|
- One exported function per server action/query file
|
|
392
393
|
- Server actions need BOTH the `.server.{js,ts}` extension AND a `'use server'` directive at the top. Extension alone marks a server-only utility (source-protected, not RPC-callable). Directive alone is a lint violation (`use-server-needs-extension`).
|
|
393
394
|
- Components must call `Class.register('tag')`
|
|
@@ -588,8 +589,8 @@ the module once (typically in `app/layout.ts`) and use the tag.
|
|
|
588
589
|
```ts
|
|
589
590
|
// Tier 1: class helpers on native elements (use this for forms,
|
|
590
591
|
// dashboards, cards, layouts, anywhere the value is purely visual)
|
|
591
|
-
import { buttonClass } from '
|
|
592
|
-
import { inputClass } from '
|
|
592
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
593
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
593
594
|
return html`
|
|
594
595
|
<button class=${buttonClass({ size: 'lg' })}>Save</button>
|
|
595
596
|
<input class=${inputClass()} placeholder="Email">
|
|
@@ -773,7 +774,7 @@ Consume:
|
|
|
773
774
|
|
|
774
775
|
```ts
|
|
775
776
|
// app/page.ts
|
|
776
|
-
import { rubric } from '
|
|
777
|
+
import { rubric } from '#lib/utils/ui.ts';
|
|
777
778
|
|
|
778
779
|
export default function Home() {
|
|
779
780
|
return html`
|
|
@@ -925,8 +926,8 @@ route imports and calls it.
|
|
|
925
926
|
```ts
|
|
926
927
|
// modules/posts/actions/create-post.server.ts
|
|
927
928
|
'use server';
|
|
928
|
-
import { db } from '
|
|
929
|
-
import { posts } from '
|
|
929
|
+
import { db } from '#db/connection.server.ts';
|
|
930
|
+
import { posts } from '#db/schema.server.ts';
|
|
930
931
|
export async function createPost({ title, body }) {
|
|
931
932
|
const [post] = await db.insert(posts).values({ title, body }).returning();
|
|
932
933
|
return post;
|
|
@@ -936,7 +937,7 @@ export async function createPost({ title, body }) {
|
|
|
936
937
|
```ts
|
|
937
938
|
// app/api/posts/route.ts
|
|
938
939
|
import { route } from '@webjsdev/server';
|
|
939
|
-
import { createPost } from '
|
|
940
|
+
import { createPost } from '#modules/posts/actions/create-post.server.ts';
|
|
940
941
|
// The route() adapter merges query + route params + JSON body into one input
|
|
941
942
|
// object and JSON-responds the result. Pass { validate } to guard the input.
|
|
942
943
|
export const POST = route(createPost);
|
|
@@ -1039,8 +1040,8 @@ Where the data lives, where to read it:
|
|
|
1039
1040
|
```ts
|
|
1040
1041
|
// modules/posts/actions/create-post.server.ts
|
|
1041
1042
|
'use server';
|
|
1042
|
-
import { db } from '
|
|
1043
|
-
import { posts } from '
|
|
1043
|
+
import { db } from '#db/connection.server.ts';
|
|
1044
|
+
import { posts } from '#db/schema.server.ts';
|
|
1044
1045
|
import type { ActionResult } from '../types.ts';
|
|
1045
1046
|
|
|
1046
1047
|
export async function createPost(input: {
|
|
@@ -1167,8 +1168,8 @@ Routes are thin wrappers over typed server actions. Business logic lives in
|
|
|
1167
1168
|
|
|
1168
1169
|
```ts
|
|
1169
1170
|
// app/api/users/route.ts: thin wrapper
|
|
1170
|
-
import { listUsers } from '
|
|
1171
|
-
import { createUser } from '
|
|
1171
|
+
import { listUsers } from '#modules/users/queries/list-users.server.ts';
|
|
1172
|
+
import { createUser } from '#modules/users/actions/create-user.server.ts';
|
|
1172
1173
|
|
|
1173
1174
|
export async function GET() { return Response.json(await listUsers()); }
|
|
1174
1175
|
export async function POST(req: Request) {
|
|
@@ -16,7 +16,7 @@ test('html template renders correctly', async () => {
|
|
|
16
16
|
|
|
17
17
|
test('example: your first server action test', async () => {
|
|
18
18
|
// Import your server action:
|
|
19
|
-
// import { createPost } from '
|
|
19
|
+
// import { createPost } from '#modules/posts/actions/create-post.server.ts';
|
|
20
20
|
//
|
|
21
21
|
// const result = await createPost({ title: 'Test', body: 'Content' });
|
|
22
22
|
// assert.equal(result.success, true);
|