@fougere/cli 0.2.0-alpha.2 → 0.3.0-alpha.0
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 +10 -1
- package/app/commands/BuildCommand.ts +39 -0
- package/app/commands/CallCommand.ts +2 -2
- package/app/commands/CheckCommand.ts +2 -1
- package/app/commands/ExplainCommand.ts +77 -0
- package/app/commands/FreezeCommand.ts +107 -0
- package/app/commands/GrantCommand.ts +44 -0
- package/app/commands/KeysCommand.ts +56 -0
- package/app/commands/MigrateCommand.ts +54 -0
- package/app/commands/NewCommand.ts +5 -5
- package/app/commands/ServeCommand.ts +83 -7
- package/app/commands/grant-material.ts +5 -0
- package/dist/bin.js +53 -7
- package/dist/bin.js.map +1 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +4 -4
- package/dist/bridge.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +6 -6
- package/dist/runner.js.map +1 -1
- package/fronds/analysis/entities/Build.ts +7 -0
- package/fronds/analysis/entities/Explain.ts +8 -0
- package/fronds/analysis/entities/Freeze.ts +7 -0
- package/fronds/analysis/entities/Migrate.ts +7 -0
- package/fronds/analysis/handlers/BuildHandler.ts +60 -0
- package/fronds/analysis/handlers/CheckHandler.ts +40 -41
- package/fronds/analysis/handlers/ExplainHandler.ts +214 -0
- package/fronds/analysis/handlers/FreezeHandler.ts +172 -0
- package/fronds/analysis/handlers/MigrateHandler.ts +97 -0
- package/fronds/analysis/services/ProjectScan.ts +23 -7
- package/fronds/analysis/versions.ts +58 -0
- package/fronds/scaffold/entities/Grant.ts +6 -0
- package/fronds/scaffold/entities/Keys.ts +4 -0
- package/fronds/scaffold/entities/Serve.ts +2 -1
- package/fronds/scaffold/handlers/BuildFrondHandler.ts +11 -13
- package/fronds/scaffold/handlers/GrantHandler.ts +8 -0
- package/fronds/scaffold/handlers/KeysHandler.ts +8 -0
- package/fronds/scaffold/handlers/SyncHandler.ts +27 -24
- package/fronds/scaffold/services/ProjectWriter.ts +9 -8
- package/package.json +8 -7
- package/templates/admin/fronds/admin/handlers/UserHandler.ts +3 -5
- package/templates/admin/fronds/admin/package.json +1 -1
- package/templates/api/fronds/api/handlers/TaskHandler.ts +3 -5
- package/templates/api/fronds/api/package.json +1 -1
- package/templates/apps/nuxt/app/pages/index.vue +1 -1
- package/templates/blog/app/pages/posts/index.vue +1 -1
- package/templates/blog/app/pages/posts/manage.vue +1 -1
- package/templates/blog/app/pages/posts/new.vue +1 -1
- package/templates/blog/fronds/blog/handlers/PostHandler.ts +3 -5
- package/templates/blog/fronds/blog/package.json +1 -1
- package/templates/flat/AGENTS.md +14 -0
- package/templates/flat/CLAUDE.md +25 -3
- package/templates/frond/AGENTS.md +14 -0
- package/templates/frond/CLAUDE.md +25 -3
- package/templates/frond/fronds/__name__/handlers/PostHandler.ts +3 -5
- package/templates/frond/fronds/__name__/package.json +1 -1
- package/templates/frond/serve.mjs +3 -2
- package/templates/fronds/blank/package.json +1 -1
- package/templates/fronds/blog/handlers/PostHandler.ts +2 -4
- package/templates/fronds/blog/package.json +1 -1
- package/templates/workspace/AGENTS.md +14 -0
- package/templates/workspace/CLAUDE.md +25 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cpSync, existsSync, renameSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { type Conventions, DEFAULT_CONVENTIONS, frondPackage } from '@fougere/core/node';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The monorepo's `packages/`, found by its workspace marker rather than counted
|
|
@@ -67,7 +68,7 @@ export default class ProjectWriter {
|
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* Put a frond template's directories at the project root. Only the directories: at the
|
|
70
|
-
* root the app's own `package.json` is the frond's, and `@
|
|
71
|
+
* root the app's own `package.json` is the frond's, and `@fronds/<name>` comes from the
|
|
71
72
|
* directory through the Nuxt module's alias, so the template's package would only
|
|
72
73
|
* duplicate it under a second name.
|
|
73
74
|
*/
|
|
@@ -81,8 +82,8 @@ export default class ProjectWriter {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/** Add a frond (business hexagon) under fronds/<name>. */
|
|
84
|
-
addFrond(wsDir: string, template: string, name: string): { path: string } {
|
|
85
|
-
const dest = join(wsDir,
|
|
85
|
+
addFrond(wsDir: string, template: string, name: string, conventions: Conventions = DEFAULT_CONVENTIONS): { path: string } {
|
|
86
|
+
const dest = join(wsDir, conventions.fronds, name);
|
|
86
87
|
cpSync(join(TEMPLATES, 'fronds', template), dest, { recursive: true });
|
|
87
88
|
// Only the import name. Carrying the convention is what makes a frond — the scan
|
|
88
89
|
// reads directories. `fougere.frond` IS read now (`scanner.ts`, `frondNameOf`), but
|
|
@@ -90,7 +91,7 @@ export default class ProjectWriter {
|
|
|
90
91
|
const pkgPath = join(dest, 'package.json');
|
|
91
92
|
if (existsSync(pkgPath)) {
|
|
92
93
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { name: string };
|
|
93
|
-
pkg.name =
|
|
94
|
+
pkg.name = frondPackage(name, conventions);
|
|
94
95
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
95
96
|
}
|
|
96
97
|
return { path: dest };
|
|
@@ -119,11 +120,11 @@ export default class ProjectWriter {
|
|
|
119
120
|
* A template cannot carry it: the frond is named at composition time (`blog:catalog`),
|
|
120
121
|
* so a dependency written into `templates/apps/nuxt` would name the template instead
|
|
121
122
|
* and resolve to nothing. Which is what happened — the generated app imported
|
|
122
|
-
* `@
|
|
123
|
+
* `@fronds/blog` whatever you had called it, and did not start.
|
|
123
124
|
*
|
|
124
125
|
* `fronds/` and `apps/` are the registry, like `listTemplates`: nothing to declare.
|
|
125
126
|
*/
|
|
126
|
-
linkFronds(wsDir: string): void {
|
|
127
|
+
linkFronds(wsDir: string, conventions: Conventions = DEFAULT_CONVENTIONS): void {
|
|
127
128
|
const dirs = (kind: string): string[] => {
|
|
128
129
|
const dir = join(wsDir, kind);
|
|
129
130
|
if (!existsSync(dir)) return [];
|
|
@@ -131,7 +132,7 @@ export default class ProjectWriter {
|
|
|
131
132
|
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
132
133
|
};
|
|
133
134
|
|
|
134
|
-
const fronds = dirs(
|
|
135
|
+
const fronds = dirs(conventions.fronds);
|
|
135
136
|
if (fronds.length === 0) return;
|
|
136
137
|
|
|
137
138
|
for (const app of dirs('apps')) {
|
|
@@ -140,7 +141,7 @@ export default class ProjectWriter {
|
|
|
140
141
|
|
|
141
142
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { dependencies?: Record<string, string> };
|
|
142
143
|
pkg.dependencies ??= {};
|
|
143
|
-
for (const frond of fronds) pkg.dependencies[
|
|
144
|
+
for (const frond of fronds) pkg.dependencies[frondPackage(frond, conventions)] = 'workspace:*';
|
|
144
145
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
145
146
|
}
|
|
146
147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha.0",
|
|
4
4
|
"description": "The Fougere CLI — compose a workspace, serve a frond, call an operation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -40,11 +40,12 @@
|
|
|
40
40
|
"consola": "^3.4.2",
|
|
41
41
|
"jiti": "^2.4.2",
|
|
42
42
|
"picocolors": "^1.1.1",
|
|
43
|
-
"@fougere/
|
|
44
|
-
"@fougere/
|
|
45
|
-
"@fougere/
|
|
46
|
-
"@fougere/
|
|
47
|
-
"@fougere/
|
|
43
|
+
"@fougere/container": "0.3.0-alpha.0",
|
|
44
|
+
"@fougere/schema": "0.3.0-alpha.0",
|
|
45
|
+
"@fougere/adapter-sql": "0.3.0-alpha.0",
|
|
46
|
+
"@fougere/transport-http": "0.3.0-alpha.0",
|
|
47
|
+
"@fougere/core": "0.3.0-alpha.0",
|
|
48
|
+
"@fougere/defaults": "0.3.0-alpha.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"vitest": "^4.1.0"
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
"access": "public"
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
56
|
-
"build": "rm -rf dist && tsc && chmod +x dist/bin.js",
|
|
57
|
+
"build": "rm -rf dist && tsc && chmod +x dist/bin.js && node dist/bin.js build",
|
|
57
58
|
"test": "vitest run",
|
|
58
59
|
"test:watch": "vitest",
|
|
59
60
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.runtime.json && tsc -p tsconfig.templates.json"
|
|
@@ -13,7 +13,7 @@ export default class UserHandler extends Crud(User) {
|
|
|
13
13
|
if (!user) {
|
|
14
14
|
throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `User '${id}' not found`, entity: 'user', operation: 'deactivate' });
|
|
15
15
|
}
|
|
16
|
-
if (
|
|
16
|
+
if (user.status === 'inactive') {
|
|
17
17
|
throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already inactive', entity: 'user', operation: 'deactivate' });
|
|
18
18
|
}
|
|
19
19
|
return this.orm.update(id, { status: 'inactive' });
|
|
@@ -21,9 +21,7 @@ export default class UserHandler extends Crud(User) {
|
|
|
21
21
|
|
|
22
22
|
/** Active users, projected to the card contract. */
|
|
23
23
|
async active(): Promise<UserCard[]> {
|
|
24
|
-
const
|
|
25
|
-
return
|
|
26
|
-
.filter((u) => (u as { status?: string }).status === 'active')
|
|
27
|
-
.map((u) => ({ id: String(u.id), name: String(u.name), status: 'active' })) as UserCard[];
|
|
24
|
+
const users = await this.orm.list({ where: { status: 'active' } });
|
|
25
|
+
return users.map(({ id, name, status }) => ({ id, name, status }));
|
|
28
26
|
}
|
|
29
27
|
}
|
|
@@ -13,7 +13,7 @@ export default class TaskHandler extends Crud(Task) {
|
|
|
13
13
|
if (!task) {
|
|
14
14
|
throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `Task '${id}' not found`, entity: 'task', operation: 'complete' });
|
|
15
15
|
}
|
|
16
|
-
if (
|
|
16
|
+
if (task.status === 'done') {
|
|
17
17
|
throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already done', entity: 'task', operation: 'complete' });
|
|
18
18
|
}
|
|
19
19
|
return this.orm.update(id, { status: 'done' });
|
|
@@ -21,9 +21,7 @@ export default class TaskHandler extends Crud(Task) {
|
|
|
21
21
|
|
|
22
22
|
/** Still-open tasks, projected to the card contract. */
|
|
23
23
|
async open(): Promise<TaskCard[]> {
|
|
24
|
-
const
|
|
25
|
-
return
|
|
26
|
-
.filter((t) => (t as { status?: string }).status === 'open')
|
|
27
|
-
.map((t) => ({ id: String(t.id), title: String(t.title), status: 'open' })) as TaskCard[];
|
|
24
|
+
const tasks = await this.orm.list({ where: { status: 'open' } });
|
|
25
|
+
return tasks.map(({ id, title, status }) => ({ id, title, status }));
|
|
28
26
|
}
|
|
29
27
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// They take an ENTITY, so this page names none: what is composed here is yours, and a
|
|
4
4
|
// scaffold that guessed at an entity shipped a page that could not run.
|
|
5
5
|
//
|
|
6
|
-
// import Post from '@
|
|
6
|
+
// import Post from '@fronds/<your-frond>/entities/Post'
|
|
7
7
|
// const { items, loading } = await useQuery(Post, 'list')
|
|
8
8
|
// const { values, errors, submit } = useFormFor(Post)
|
|
9
9
|
// const { execute } = useCommand(Post, 'publish')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import Post from '@
|
|
2
|
+
import Post from '@fronds/blog/entities/Post';
|
|
3
3
|
|
|
4
4
|
interface Card { id: string; title: string; status: string }
|
|
5
5
|
const { items: posts, loading, error } = await useQuery<Card>(Post, 'published');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import Post from '@
|
|
2
|
+
import Post from '@fronds/blog/entities/Post';
|
|
3
3
|
|
|
4
4
|
interface Row { id: string; title: string; status: 'draft' | 'published' }
|
|
5
5
|
const { items: posts, loading } = await useQuery<Row>(Post, 'list');
|
|
@@ -19,7 +19,7 @@ export default class PostHandler extends Crud(Post) {
|
|
|
19
19
|
if (!post) {
|
|
20
20
|
throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `Post '${id}' not found`, entity: 'post', operation: 'publish' });
|
|
21
21
|
}
|
|
22
|
-
if (
|
|
22
|
+
if (post.status === 'published') {
|
|
23
23
|
throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published', entity: 'post', operation: 'publish' });
|
|
24
24
|
}
|
|
25
25
|
return this.orm.update(id, { status: 'published' });
|
|
@@ -27,9 +27,7 @@ export default class PostHandler extends Crud(Post) {
|
|
|
27
27
|
|
|
28
28
|
/** Only published posts exist for the outside world, projected to the card. */
|
|
29
29
|
async published(): Promise<PostCard[]> {
|
|
30
|
-
const
|
|
31
|
-
return
|
|
32
|
-
.filter((p) => (p as { status?: string }).status === 'published')
|
|
33
|
-
.map((p) => ({ id: String(p.id), title: String(p.title), status: 'published' })) as PostCard[];
|
|
30
|
+
const posts = await this.orm.list({ where: { status: 'published' } });
|
|
31
|
+
return posts.map(({ id, title, status }) => ({ id, title, status }));
|
|
34
32
|
}
|
|
35
33
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Working in this application
|
|
2
|
+
|
|
3
|
+
This project is built with **Fougere**. Read `CLAUDE.md` for the model and architecture guidance
|
|
4
|
+
that applies to every coding agent.
|
|
5
|
+
|
|
6
|
+
## Required verification workflow
|
|
7
|
+
|
|
8
|
+
After every change to handlers, entities, Fronds, configuration, or topology:
|
|
9
|
+
|
|
10
|
+
1. Run `fougere check`.
|
|
11
|
+
2. Fix every deterministic error it reports before continuing.
|
|
12
|
+
3. Run the relevant tests, then run `pnpm typecheck`.
|
|
13
|
+
|
|
14
|
+
`fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
|
package/templates/flat/CLAUDE.md
CHANGED
|
@@ -44,7 +44,18 @@ exist to add what a projection cannot derive — never to replace it.
|
|
|
44
44
|
|
|
45
45
|
## Reading data
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Storage is reached through a repository. Never inject `EntityOrm` directly into a handler,
|
|
48
|
+
presenter or collector — the boot refuses it. With no repository file, ask for the default shape:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import type { RepositoryOf } from '@fougere/core';
|
|
52
|
+
import Product from '../entities/Product.js';
|
|
53
|
+
|
|
54
|
+
constructor(private products: RepositoryOf<Product>) {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the handler extends `Crud(Product)`, its inherited `this.orm` is already backed by that
|
|
58
|
+
repository; do not add a constructor. The repository forwards the guarded storage gestures:
|
|
48
59
|
|
|
49
60
|
```
|
|
50
61
|
list(options?) every row — `options.where` filters, plus paging and sorting
|
|
@@ -56,10 +67,21 @@ create / update / delete
|
|
|
56
67
|
|
|
57
68
|
Read a relation with `findAllBy`. Never read a whole table to filter it in memory.
|
|
58
69
|
|
|
70
|
+
When a query deserves a domain name, add `repositories/ProductRepository.ts` with
|
|
71
|
+
`class ProductRepository extends Repository(Product)`, put the query there, and inject
|
|
72
|
+
`ProductRepository`. A repository is registered as a provider and remains the only route to storage.
|
|
73
|
+
|
|
59
74
|
## Checking your work
|
|
60
75
|
|
|
76
|
+
After every change to handlers, entities, Fronds, configuration, or topology:
|
|
77
|
+
|
|
78
|
+
1. Run `fougere check`.
|
|
79
|
+
2. Fix every deterministic error it reports before continuing.
|
|
80
|
+
3. Run the relevant tests, then run the project typecheck.
|
|
81
|
+
|
|
61
82
|
```bash
|
|
62
|
-
|
|
83
|
+
fougere check
|
|
84
|
+
pnpm typecheck
|
|
63
85
|
```
|
|
64
86
|
|
|
65
|
-
|
|
87
|
+
`fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Working in this application
|
|
2
|
+
|
|
3
|
+
This project is built with **Fougere**. Read `CLAUDE.md` for the model and architecture guidance
|
|
4
|
+
that applies to every coding agent.
|
|
5
|
+
|
|
6
|
+
## Required verification workflow
|
|
7
|
+
|
|
8
|
+
After every change to handlers, entities, Fronds, configuration, or topology:
|
|
9
|
+
|
|
10
|
+
1. Run `fougere check`.
|
|
11
|
+
2. Fix every deterministic error it reports before continuing.
|
|
12
|
+
3. Run the relevant tests, then run `npx tsc -p fronds --noEmit`.
|
|
13
|
+
|
|
14
|
+
`fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
|
|
@@ -44,7 +44,18 @@ exist to add what a projection cannot derive — never to replace it.
|
|
|
44
44
|
|
|
45
45
|
## Reading data
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Storage is reached through a repository. Never inject `EntityOrm` directly into a handler,
|
|
48
|
+
presenter or collector — the boot refuses it. With no repository file, ask for the default shape:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import type { RepositoryOf } from '@fougere/core';
|
|
52
|
+
import Product from '../entities/Product.js';
|
|
53
|
+
|
|
54
|
+
constructor(private products: RepositoryOf<Product>) {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the handler extends `Crud(Product)`, its inherited `this.orm` is already backed by that
|
|
58
|
+
repository; do not add a constructor. The repository forwards the guarded storage gestures:
|
|
48
59
|
|
|
49
60
|
```
|
|
50
61
|
list(options?) every row — `options.where` filters, plus paging and sorting
|
|
@@ -56,10 +67,21 @@ create / update / delete
|
|
|
56
67
|
|
|
57
68
|
Read a relation with `findAllBy`. Never read a whole table to filter it in memory.
|
|
58
69
|
|
|
70
|
+
When a query deserves a domain name, add `repositories/ProductRepository.ts` with
|
|
71
|
+
`class ProductRepository extends Repository(Product)`, put the query there, and inject
|
|
72
|
+
`ProductRepository`. A repository is registered as a provider and remains the only route to storage.
|
|
73
|
+
|
|
59
74
|
## Checking your work
|
|
60
75
|
|
|
76
|
+
After every change to handlers, entities, Fronds, configuration, or topology:
|
|
77
|
+
|
|
78
|
+
1. Run `fougere check`.
|
|
79
|
+
2. Fix every deterministic error it reports before continuing.
|
|
80
|
+
3. Run the relevant tests, then run the project typecheck.
|
|
81
|
+
|
|
61
82
|
```bash
|
|
62
|
-
|
|
83
|
+
fougere check
|
|
84
|
+
npx tsc -p fronds --noEmit
|
|
63
85
|
```
|
|
64
86
|
|
|
65
|
-
|
|
87
|
+
`fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
|
|
@@ -10,7 +10,7 @@ export default class PostHandler extends Crud(Post) {
|
|
|
10
10
|
if (!post) {
|
|
11
11
|
throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `Post '${id}' not found`, entity: 'post', operation: 'publish' });
|
|
12
12
|
}
|
|
13
|
-
if (
|
|
13
|
+
if (post.status === 'published') {
|
|
14
14
|
throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published', entity: 'post', operation: 'publish' });
|
|
15
15
|
}
|
|
16
16
|
return this.orm.update(id, { status: 'published' });
|
|
@@ -18,9 +18,7 @@ export default class PostHandler extends Crud(Post) {
|
|
|
18
18
|
|
|
19
19
|
/** Only published posts, projected to the card. */
|
|
20
20
|
async published(): Promise<PostCard[]> {
|
|
21
|
-
const
|
|
22
|
-
return
|
|
23
|
-
.filter((p) => (p as { status?: string }).status === 'published')
|
|
24
|
-
.map((p) => ({ id: String(p.id), title: String(p.title), status: 'published' })) as PostCard[];
|
|
21
|
+
const posts = await this.orm.list({ where: { status: 'published' } });
|
|
22
|
+
return posts.map(({ id, title, status }) => ({ id, title, status }));
|
|
25
23
|
}
|
|
26
24
|
}
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* Nothing in the frond changes — only where it runs.
|
|
9
9
|
*/
|
|
10
10
|
import { createJiti } from 'jiti';
|
|
11
|
-
import { createLocalRunner,
|
|
11
|
+
import { createLocalRunner, Logger } from '@fougere/core';
|
|
12
|
+
import { setModuleLoader, frondAliases } from '@fougere/core/node';
|
|
12
13
|
import { bootAppFromConfig } from '@fougere/defaults';
|
|
13
14
|
import { serve } from '@fougere/transport-http';
|
|
14
15
|
|
|
15
|
-
// `frondAliases` is what makes `@
|
|
16
|
+
// `frondAliases` is what makes `@fronds/<neighbour>/entities/X.js` resolve — the
|
|
16
17
|
// named form a frond uses for its neighbour. A bare jiti loads frond sources but
|
|
17
18
|
// not that convention, so a collector or handler importing across fronds dies here
|
|
18
19
|
// while the same code works in-process.
|
|
@@ -30,9 +30,7 @@ export default class PostHandler extends Crud(Post) {
|
|
|
30
30
|
|
|
31
31
|
/** Only published posts, projected to the card. */
|
|
32
32
|
async published(): Promise<PostCard[]> {
|
|
33
|
-
const
|
|
34
|
-
return
|
|
35
|
-
.filter((p) => p.status === 'published')
|
|
36
|
-
.map(({ id, title, status }) => ({ id, title, status }));
|
|
33
|
+
const posts = await this.orm.list({ where: { status: 'published' } });
|
|
34
|
+
return posts.map(({ id, title, status }) => ({ id, title, status }));
|
|
37
35
|
}
|
|
38
36
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Working in this application
|
|
2
|
+
|
|
3
|
+
This project is built with **Fougere**. Read `CLAUDE.md` for the model and architecture guidance
|
|
4
|
+
that applies to every coding agent.
|
|
5
|
+
|
|
6
|
+
## Required verification workflow
|
|
7
|
+
|
|
8
|
+
After every change to handlers, entities, Fronds, configuration, or topology:
|
|
9
|
+
|
|
10
|
+
1. Run `fougere check`.
|
|
11
|
+
2. Fix every deterministic error it reports before continuing.
|
|
12
|
+
3. Run the relevant tests, then run `pnpm typecheck`.
|
|
13
|
+
|
|
14
|
+
`fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
|
|
@@ -44,7 +44,18 @@ exist to add what a projection cannot derive — never to replace it.
|
|
|
44
44
|
|
|
45
45
|
## Reading data
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Storage is reached through a repository. Never inject `EntityOrm` directly into a handler,
|
|
48
|
+
presenter or collector — the boot refuses it. With no repository file, ask for the default shape:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import type { RepositoryOf } from '@fougere/core';
|
|
52
|
+
import Product from '../entities/Product.js';
|
|
53
|
+
|
|
54
|
+
constructor(private products: RepositoryOf<Product>) {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the handler extends `Crud(Product)`, its inherited `this.orm` is already backed by that
|
|
58
|
+
repository; do not add a constructor. The repository forwards the guarded storage gestures:
|
|
48
59
|
|
|
49
60
|
```
|
|
50
61
|
list(options?) every row — `options.where` filters, plus paging and sorting
|
|
@@ -56,10 +67,21 @@ create / update / delete
|
|
|
56
67
|
|
|
57
68
|
Read a relation with `findAllBy`. Never read a whole table to filter it in memory.
|
|
58
69
|
|
|
70
|
+
When a query deserves a domain name, add `repositories/ProductRepository.ts` with
|
|
71
|
+
`class ProductRepository extends Repository(Product)`, put the query there, and inject
|
|
72
|
+
`ProductRepository`. A repository is registered as a provider and remains the only route to storage.
|
|
73
|
+
|
|
59
74
|
## Checking your work
|
|
60
75
|
|
|
76
|
+
After every change to handlers, entities, Fronds, configuration, or topology:
|
|
77
|
+
|
|
78
|
+
1. Run `fougere check`.
|
|
79
|
+
2. Fix every deterministic error it reports before continuing.
|
|
80
|
+
3. Run the relevant tests, then run the project typecheck.
|
|
81
|
+
|
|
61
82
|
```bash
|
|
62
|
-
|
|
83
|
+
fougere check
|
|
84
|
+
pnpm typecheck
|
|
63
85
|
```
|
|
64
86
|
|
|
65
|
-
|
|
87
|
+
`fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
|