@fougere/cli 0.6.0-alpha.0 → 0.7.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/app/commands/BuildCommand.ts +1 -1
- package/app/commands/CallCommand.ts +3 -3
- package/app/commands/CheckCommand.ts +1 -1
- package/app/commands/DevtoolsCommand.ts +1 -1
- package/app/commands/ExplainCommand.ts +1 -1
- package/app/commands/FreezeCommand.ts +2 -2
- package/app/commands/GraphCommand.ts +1 -1
- package/app/commands/MigrateCommand.ts +1 -1
- package/dist/bin.js +3 -14
- package/dist/bin.js.map +1 -1
- package/dist/bridge.d.ts +1 -6
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +38 -39
- package/dist/bridge.js.map +1 -1
- package/dist/completion.d.ts +1 -8
- package/dist/completion.d.ts.map +1 -1
- package/dist/completion.js.map +1 -1
- package/dist/loader.d.ts +1 -8
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +1 -8
- package/dist/loader.js.map +1 -1
- package/dist/machine.d.ts +4 -10
- package/dist/machine.d.ts.map +1 -1
- package/dist/machine.js +4 -10
- package/dist/machine.js.map +1 -1
- package/dist/runner.d.ts +2 -7
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +4 -4
- package/dist/runner.js.map +1 -1
- package/dist/typescript/EntityTypes.d.ts +12 -0
- package/dist/typescript/EntityTypes.d.ts.map +1 -0
- package/dist/typescript/EntityTypes.js +76 -0
- package/dist/typescript/EntityTypes.js.map +1 -0
- package/dist/typescript/FacadeTypes.d.ts +19 -0
- package/dist/typescript/FacadeTypes.d.ts.map +1 -0
- package/dist/typescript/FacadeTypes.js +34 -0
- package/dist/typescript/FacadeTypes.js.map +1 -0
- package/dist/typescript/syntax.d.ts +5 -0
- package/dist/typescript/syntax.d.ts.map +1 -0
- package/dist/typescript/syntax.js +11 -0
- package/dist/typescript/syntax.js.map +1 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +1 -11
- package/dist/ui.js.map +1 -1
- package/fronds/analysis/handlers/CheckHandler.ts +31 -2
- package/fronds/analysis/handlers/DevtoolsHandler.ts +1 -1
- package/fronds/analysis/handlers/ExplainHandler.ts +2 -2
- package/fronds/scaffold/handlers/SyncHandler.ts +9 -7
- package/package.json +7 -7
- package/src/bin.ts +3 -14
- package/src/bridge.ts +52 -41
- package/src/completion.ts +1 -8
- package/src/loader.ts +1 -8
- package/src/machine.ts +4 -10
- package/src/runner.ts +7 -12
- package/src/typescript/EntityTypes.ts +86 -0
- package/src/typescript/FacadeTypes.ts +48 -0
- package/src/typescript/syntax.ts +11 -0
- package/src/ui.ts +1 -11
- package/templates/admin/fronds/admin/handlers/UserHandler.ts +1 -1
- package/templates/api/fronds/api/handlers/TaskHandler.ts +1 -1
- package/templates/blog/app/pages/index.vue +1 -1
- package/templates/blog/app/pages/posts/manage.vue +2 -2
- package/templates/blog/fronds/blog/handlers/PostHandler.ts +2 -2
- package/templates/flat/CLAUDE.md +4 -4
- package/templates/frond/CLAUDE.md +4 -4
- package/templates/fronds/blank/handlers/ItemHandler.ts +1 -1
- package/templates/workspace/CLAUDE.md +4 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SchemaDescriptor } from '@fougere/schema';
|
|
2
|
+
import { docCommentOf, propertyKey } from './syntax.js';
|
|
3
|
+
|
|
4
|
+
export interface FacadeTypesOptions {
|
|
5
|
+
name?: string;
|
|
6
|
+
exported?: boolean;
|
|
7
|
+
rowType?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OpDescriptor {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
output?: SchemaDescriptor;
|
|
14
|
+
cardinality?: 'one' | 'maybe' | 'many' | 'page' | 'none';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** So a consumer sees the cardinality in the type, not in a doc line. */
|
|
18
|
+
function returnTypeOf(op: OpDescriptor, rowType: string): string {
|
|
19
|
+
switch (op.cardinality) {
|
|
20
|
+
case 'many': return `${rowType}[]`;
|
|
21
|
+
case 'page': return `${rowType}[] & { total?: number; endCursor?: string; hasMore?: boolean }`;
|
|
22
|
+
case 'maybe': return `${rowType} | undefined`;
|
|
23
|
+
case 'one': return rowType;
|
|
24
|
+
case 'none': return 'unknown';
|
|
25
|
+
default: return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class FacadeTypes {
|
|
30
|
+
private constructor(private readonly operations: readonly OpDescriptor[]) {}
|
|
31
|
+
|
|
32
|
+
static of(operations: readonly OpDescriptor[]): FacadeTypes {
|
|
33
|
+
return new FacadeTypes(operations);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(options: FacadeTypesOptions = {}): string {
|
|
37
|
+
const name = options.name ?? 'Facade';
|
|
38
|
+
const exported = options.exported === false ? '' : 'export ';
|
|
39
|
+
const rowType = options.rowType ?? 'unknown';
|
|
40
|
+
const members = this.operations.map((operation) => {
|
|
41
|
+
const doc = docCommentOf(operation.description, ' ');
|
|
42
|
+
return `${doc} ${propertyKey(operation.name)}(invocation?: Invocation): Promise<${returnTypeOf(operation, rowType)}>;`;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (members.length === 0) return `${exported}interface ${name} {}`;
|
|
46
|
+
return `${exported}interface ${name} {\n${members.join('\n')}\n}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** So a name that is not an identifier is still written as a key. */
|
|
2
|
+
export function propertyKey(name: string): string {
|
|
3
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** So a sentence shows on hover — and cannot close the comment it sits in. */
|
|
7
|
+
export function docCommentOf(text: string | undefined, indent: string): string {
|
|
8
|
+
if (!text) return '';
|
|
9
|
+
|
|
10
|
+
return `${indent}/** ${text.replace(/\*\//g, '*\\/')} */\n`;
|
|
11
|
+
}
|
package/src/ui.ts
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fougere CLI UI — beautiful terminal interface.
|
|
3
|
-
*
|
|
4
|
-
* Wraps @clack/prompts + picocolors + consola into a cohesive API.
|
|
5
|
-
*
|
|
6
|
-
* This was `@fougere/cli-ui`, a published package with exactly one consumer —
|
|
7
|
-
* the CLI it is named after. A second name in the registry that nobody would
|
|
8
|
-
* ever install on purpose is a name, not a boundary. Same dependency profile,
|
|
9
|
-
* so it folds in as a module; a subpath export is one line the day something
|
|
10
|
-
* outside the CLI wants it.
|
|
11
|
-
*/
|
|
1
|
+
/** Fougere CLI UI — beautiful terminal interface. */
|
|
12
2
|
import * as clack from '@clack/prompts';
|
|
13
3
|
import pc from 'picocolors';
|
|
14
4
|
import { consola } from 'consola';
|
|
@@ -7,7 +7,7 @@ export class UserCard extends User.pick('id', 'name', 'status') {}
|
|
|
7
7
|
// Crud(User) gives list/create/update/delete for free — the accelerator.
|
|
8
8
|
// 'deactivate' is the business contract: a state transition, not a field write.
|
|
9
9
|
export default class UserHandler extends Crud(User) {
|
|
10
|
-
/** active→inactive — an operation, not a field write.
|
|
10
|
+
/** active→inactive — an operation, not a field write. Validate: active only. */
|
|
11
11
|
async deactivate(id: string): Promise<User> {
|
|
12
12
|
const user = await this.storage.findById(id);
|
|
13
13
|
if (!user) {
|
|
@@ -7,7 +7,7 @@ export class TaskCard extends Task.pick('id', 'title', 'status') {}
|
|
|
7
7
|
// Crud(Task) gives list/create/update/delete for free — the accelerator.
|
|
8
8
|
// 'complete' is the business contract: a state transition, not a field write.
|
|
9
9
|
export default class TaskHandler extends Crud(Task) {
|
|
10
|
-
/** open→done — an operation, not a field write.
|
|
10
|
+
/** open→done — an operation, not a field write. Validate: open only. */
|
|
11
11
|
async complete(id: string): Promise<Task> {
|
|
12
12
|
const task = await this.storage.findById(id);
|
|
13
13
|
if (!task) {
|
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
<NuxtLink to="/posts/manage">Drafts & publishing</NuxtLink> ·
|
|
8
8
|
<NuxtLink to="/posts/new">New draft</NuxtLink>
|
|
9
9
|
</p>
|
|
10
|
-
<p class="muted">The loop: create a draft → it stays private → publish it (a
|
|
10
|
+
<p class="muted">The loop: create a draft → it stays private → publish it (a validated operation, not a field write) → it appears in the published list, live.</p>
|
|
11
11
|
</main>
|
|
12
12
|
</template>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import Post from '@fronds/blog/entities/Post';
|
|
3
3
|
|
|
4
|
-
interface
|
|
5
|
-
const { items: posts, loading } = await useQuery<
|
|
4
|
+
interface PostValues { id: string; title: string; status: 'draft' | 'published' }
|
|
5
|
+
const { items: posts, loading } = await useQuery<PostValues>(Post, 'list');
|
|
6
6
|
const publish = useCommand(Post, 'publish');
|
|
7
7
|
const failed = ref('');
|
|
8
8
|
|
|
@@ -7,11 +7,11 @@ import Post from '../entities/Post.js';
|
|
|
7
7
|
export class PostCard extends Post.pick('id', 'title', 'status') {}
|
|
8
8
|
|
|
9
9
|
// Crud(Post) gives list/create/update/delete for free — the accelerator.
|
|
10
|
-
// 'publish' is the real business contract: a state transition that
|
|
10
|
+
// 'publish' is the real business contract: a state transition that validates
|
|
11
11
|
// before it realises — an operation, not a field write. The golden path.
|
|
12
12
|
export default class PostHandler extends Crud(Post) {
|
|
13
13
|
/**
|
|
14
|
-
* The draft→published transition.
|
|
14
|
+
* The draft→published transition. Validate: exists, draft only.
|
|
15
15
|
* Realise: the server flips the owned field.
|
|
16
16
|
*/
|
|
17
17
|
async publish(id: string): Promise<Post> {
|
package/templates/flat/CLAUDE.md
CHANGED
|
@@ -21,15 +21,15 @@ Two consequences worth stating, because they are what makes it hold:
|
|
|
21
21
|
a parallel entity that repeats the same fields. A field added to the entity is then accepted
|
|
22
22
|
without touching the view.
|
|
23
23
|
- **A handler writes what the input carries** (`{ ...attributes }`), it does not enumerate its
|
|
24
|
-
fields — otherwise a new field is
|
|
24
|
+
fields — otherwise a new field is validated, then silently not written.
|
|
25
25
|
|
|
26
26
|
If you are about to write the same constraint in two places, you have missed the derivation.
|
|
27
27
|
|
|
28
28
|
## A surface is a door, never a logic
|
|
29
29
|
|
|
30
|
-
Every door goes through the handler **façade**, which is
|
|
30
|
+
Every door goes through the handler **façade**, which is where validation sits: unknown-key refusal,
|
|
31
31
|
collectors. A resolver or route you wire yourself against the storage — or worse, against the database —
|
|
32
|
-
is a second door with no
|
|
32
|
+
is a second door with no validator behind it, and the rules declared in the entities stop applying there.
|
|
33
33
|
|
|
34
34
|
Before adding a surface, reach for its **projection**:
|
|
35
35
|
|
|
@@ -39,7 +39,7 @@ Before adding a surface, reach for its **projection**:
|
|
|
39
39
|
| GraphQL | `registerAll(builder, app)` then `registerGraphQL(router, builder.toSchema())` — `@fougere/adapter-graphql` |
|
|
40
40
|
|
|
41
41
|
Hand-writing the types (`buildSchema`, raw SDL, one Pothos resolver per field) rebuilds what the
|
|
42
|
-
projection already derives, and drops the
|
|
42
|
+
projection already derives, and drops the validator on the way. `registerType` / `registerOperations`
|
|
43
43
|
exist to add what a projection cannot derive — never to replace it.
|
|
44
44
|
|
|
45
45
|
## Reading data
|
|
@@ -21,15 +21,15 @@ Two consequences worth stating, because they are what makes it hold:
|
|
|
21
21
|
a parallel entity that repeats the same fields. A field added to the entity is then accepted
|
|
22
22
|
without touching the view.
|
|
23
23
|
- **A handler writes what the input carries** (`{ ...attributes }`), it does not enumerate its
|
|
24
|
-
fields — otherwise a new field is
|
|
24
|
+
fields — otherwise a new field is validated, then silently not written.
|
|
25
25
|
|
|
26
26
|
If you are about to write the same constraint in two places, you have missed the derivation.
|
|
27
27
|
|
|
28
28
|
## A surface is a door, never a logic
|
|
29
29
|
|
|
30
|
-
Every door goes through the handler **façade**, which is
|
|
30
|
+
Every door goes through the handler **façade**, which is where validation sits: unknown-key refusal,
|
|
31
31
|
collectors. A resolver or route you wire yourself against the storage — or worse, against the database —
|
|
32
|
-
is a second door with no
|
|
32
|
+
is a second door with no validator behind it, and the rules declared in the entities stop applying there.
|
|
33
33
|
|
|
34
34
|
Before adding a surface, reach for its **projection**:
|
|
35
35
|
|
|
@@ -39,7 +39,7 @@ Before adding a surface, reach for its **projection**:
|
|
|
39
39
|
| GraphQL | `registerAll(builder, app)` then `registerGraphQL(router, builder.toSchema())` — `@fougere/adapter-graphql` |
|
|
40
40
|
|
|
41
41
|
Hand-writing the types (`buildSchema`, raw SDL, one Pothos resolver per field) rebuilds what the
|
|
42
|
-
projection already derives, and drops the
|
|
42
|
+
projection already derives, and drops the validator on the way. `registerType` / `registerOperations`
|
|
43
43
|
exist to add what a projection cannot derive — never to replace it.
|
|
44
44
|
|
|
45
45
|
## Reading data
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Crud } from '@fougere/core';
|
|
2
2
|
import Item from '../entities/Item.js';
|
|
3
3
|
|
|
4
|
-
/** The five ops, derived. Redefine one to
|
|
4
|
+
/** The five ops, derived. Redefine one to validate it, add a method to name an operation. */
|
|
5
5
|
export default class ItemHandler extends Crud(Item) {}
|
|
@@ -21,15 +21,15 @@ Two consequences worth stating, because they are what makes it hold:
|
|
|
21
21
|
a parallel entity that repeats the same fields. A field added to the entity is then accepted
|
|
22
22
|
without touching the view.
|
|
23
23
|
- **A handler writes what the input carries** (`{ ...attributes }`), it does not enumerate its
|
|
24
|
-
fields — otherwise a new field is
|
|
24
|
+
fields — otherwise a new field is validated, then silently not written.
|
|
25
25
|
|
|
26
26
|
If you are about to write the same constraint in two places, you have missed the derivation.
|
|
27
27
|
|
|
28
28
|
## A surface is a door, never a logic
|
|
29
29
|
|
|
30
|
-
Every door goes through the handler **façade**, which is
|
|
30
|
+
Every door goes through the handler **façade**, which is where validation sits: unknown-key refusal,
|
|
31
31
|
collectors. A resolver or route you wire yourself against the storage — or worse, against the database —
|
|
32
|
-
is a second door with no
|
|
32
|
+
is a second door with no validator behind it, and the rules declared in the entities stop applying there.
|
|
33
33
|
|
|
34
34
|
Before adding a surface, reach for its **projection**:
|
|
35
35
|
|
|
@@ -39,7 +39,7 @@ Before adding a surface, reach for its **projection**:
|
|
|
39
39
|
| GraphQL | `registerAll(builder, app)` then `registerGraphQL(router, builder.toSchema())` — `@fougere/adapter-graphql` |
|
|
40
40
|
|
|
41
41
|
Hand-writing the types (`buildSchema`, raw SDL, one Pothos resolver per field) rebuilds what the
|
|
42
|
-
projection already derives, and drops the
|
|
42
|
+
projection already derives, and drops the validator on the way. `registerType` / `registerOperations`
|
|
43
43
|
exist to add what a projection cannot derive — never to replace it.
|
|
44
44
|
|
|
45
45
|
## Reading data
|