@fougere/nuxt 0.1.0-alpha.0 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAK9D,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,EAAE,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAGD,QAAA,MAAM,MAAM,sFA4JV,CAAC;eAEY,MAAM;AAMrB,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,EAAE,EAClB,cAAc,EAAE,MAAM,GACrB,MAAM,CA+DR"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAK9D,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,EAAE,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAGD,QAAA,MAAM,MAAM,sFAoLV,CAAC;eAEY,MAAM;AAMrB,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,EAAE,EAClB,cAAc,EAAE,MAAM,GACrB,MAAM,CA+DR"}
package/dist/module.js CHANGED
@@ -72,6 +72,29 @@ const module = defineNuxtModule({
72
72
  : [frond.source.path];
73
73
  nuxt.options.watch.push(...watched);
74
74
  }
75
+ // ── 1b-bis. Keep entity names through minification ──────────────────────
76
+ //
77
+ // Designation is class + verb: `useQuery(Post, 'list')` reads `Post.name`, and
78
+ // that name travels — it is the JSON-RPC method (`post.list`) and the REST path.
79
+ //
80
+ // Rollup cannot keep `class Post extends entity({…})` as a hoisted declaration
81
+ // (its heritage clause is a CALL), so it emits `var Post = class extends …`.
82
+ // The class is ANONYMOUS; the name comes from JavaScript's inference on the
83
+ // variable. `keep_classnames` therefore protects nothing — measured, in
84
+ // `compress` and in `mangle` both. Reserving the IDENTIFIER is what works.
85
+ //
86
+ // The list is exact rather than guessed: the scan just ran, so these are the
87
+ // classes themselves, read before any bundler touched them. `@fougere/vite`
88
+ // does the same for the hosts that have no module to do it for them.
89
+ const entityNames = fronds.flatMap((frond) => frond.entities.map((e) => e.entityClass.name));
90
+ if (entityNames.length > 0) {
91
+ const vite = (nuxt.options.vite ??= {});
92
+ const build = (vite.build ??= {});
93
+ build.minify = 'terser';
94
+ const terser = (build.terserOptions ??= {});
95
+ const mangle = (terser.mangle ??= {});
96
+ mangle.reserved = [...new Set([...(mangle.reserved ?? []), ...entityNames])];
97
+ }
75
98
  // ── 1c. Register aliases for synced remote fronds (.fougere/remotes.json) ──
76
99
  const remotesPath = resolve(rootDir, '.fougere', 'remotes.json');
77
100
  if (existsSync(remotesPath)) {
@@ -145,7 +168,7 @@ const module = defineNuxtModule({
145
168
  const bootTpl = addTemplate({
146
169
  filename: 'fougere-boot.ts',
147
170
  write: true,
148
- getContents: () => generateBootPlugin(config, allSeeds, runtimeResolve('server/utils/fougereApp')),
171
+ getContents: () => generateBootPlugin(config, allSeeds, runtimeResolve('server/utils/boot')),
149
172
  });
150
173
  addServerPlugin(bootTpl.dst);
151
174
  },
@@ -1 +1 @@
1
- {"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "@fougere/nuxt",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.2.0-alpha.0",
4
4
  "description": "The Nuxt module: Fougere's client primitives and server surface.",
5
+ "keywords": [
6
+ "fougere",
7
+ "nuxt",
8
+ "nuxt-module",
9
+ "vue",
10
+ "typescript",
11
+ "fullstack",
12
+ "rpc"
13
+ ],
5
14
  "license": "MIT",
6
15
  "repository": {
7
16
  "type": "git",
@@ -24,20 +33,20 @@
24
33
  "dependencies": {
25
34
  "@nuxt/kit": "^4.5.1",
26
35
  "jiti": "^2.6.1",
27
- "@fougere/container-fougere": "0.1.0-alpha.0",
28
- "@fougere/runtime": "0.1.0-alpha.0",
29
- "@fougere/schema": "0.1.0-alpha.0",
30
- "@fougere/core": "0.1.0-alpha.0",
31
- "@fougere/transport-http": "0.1.0-alpha.0"
36
+ "@fougere/core": "0.2.0-alpha.0",
37
+ "@fougere/runtime": "0.2.0-alpha.0",
38
+ "@fougere/app": "0.2.0-alpha.0"
32
39
  },
33
40
  "devDependencies": {
34
41
  "@nuxt/schema": "^4.5.1",
42
+ "h3": "^1.15.11",
35
43
  "nitropack": "^2.13.2",
36
44
  "nuxt": "^4.5.1",
37
45
  "vue": "^3.5.40",
38
46
  "vitest": "^4.1.0"
39
47
  },
40
48
  "peerDependencies": {
49
+ "h3": "^1.15.0",
41
50
  "nuxt": "^4.0.0",
42
51
  "vue": "^3.5.0"
43
52
  },
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { useState, useRequestFetch } from '#imports';
10
10
  import { computed } from 'vue';
11
- import type { SessionView } from '../session/view.js';
11
+ import type { SessionView } from '@fougere/app/client';
12
12
 
13
13
  export function useCurrentUser<TUser = Record<string, unknown>>() {
14
14
  const session = useState<SessionView>('fougere:session', () => ({ user: null }));
@@ -11,7 +11,7 @@
11
11
  import { reactive, computed } from 'vue';
12
12
  import { FougereError, ErrorCode, toRegistrationName } from '@fougere/core/contract';
13
13
  import { useCommand } from './useFougereData.js';
14
- import { formFieldsOf, payloadOf, errorsByField, type FormEntity, type FormField } from '../form/fields.js';
14
+ import { formFieldsOf, payloadOf, errorsByField, type FormEntity, type FormField } from '@fougere/app/client';
15
15
 
16
16
  export interface FormOptions {
17
17
  /** Command the submit rides. Default: 'create'. */
@@ -1,52 +1,38 @@
1
1
  /**
2
- * The couple — useQuery (reads) and useCommand (writes), the two dual
3
- * gestures of a page talking to a Frond. Designation is class + verb:
4
- * the imported entity class carries the metadata, its name carries the
5
- * registration key.
2
+ * The couple — useQuery (reads) and useCommand (writes), the two dual gestures of
3
+ * a page talking to a Frond, in Vue.
6
4
  *
7
- * Both gestures ride the call envelope: the browser POSTs JSON-RPC to
8
- * /_fougere/call; during SSR Nuxt collapses the same call to an
9
- * in-process fetch (no network, no port).
5
+ * What the gestures ARE lives in `@fougere/app/client`: designation is class +
6
+ * verb, the key derives from designation plus input, and a successful command on
7
+ * an entity revalidates every mounted query on that entity. What lives here is
8
+ * Vue — `useAsyncData` for the read, refs for the pending flags, `onScopeDispose`
9
+ * for unregistration. The React adapter states the same three facts against its
10
+ * own primitives, and neither restates the rules.
10
11
  *
11
- * The link: a successful command on an entity revalidates every mounted
12
- * query on that entity designation gives the entity on both sides,
13
- * nothing to declare.
12
+ * Both gestures ride the call envelope: the browser POSTs JSON-RPC to
13
+ * /_fougere/call; during SSR Nuxt collapses the same call to an in-process fetch
14
+ * (no network, no port).
14
15
  */
15
16
  import { useAsyncData, useRequestFetch, refreshNuxtData } from '#imports';
16
17
  import { ref, computed, toValue, onScopeDispose, type MaybeRefOrGetter, type Ref } from 'vue';
18
+ import type { FougereError } from '@fougere/core/contract';
17
19
  import {
18
- FougereError,
19
- ErrorCode,
20
- toRegistrationName,
21
- type InvocationContext,
22
- type FrondCall,
23
- } from '@fougere/core/contract';
24
- import { frameCall, unframeResponse, type RpcResponse } from '@fougere/transport-http/client';
25
-
26
- /** An entity class is a designation: its name is the registration key. */
27
- type EntityClass = { name: string };
20
+ callOf,
21
+ entityKeyOf,
22
+ invocationOf,
23
+ queryKeyOf,
24
+ sendCall,
25
+ trackQuery,
26
+ mountedKeys,
27
+ itemsOf,
28
+ pageOf,
29
+ asFougereError,
30
+ type CallInput,
31
+ type EntityClass,
32
+ type Fetcher,
33
+ } from '@fougere/app/client';
28
34
 
29
- /** What a page provides of an invocation — the rest is stamped server-side. */
30
- export type CallInput = Partial<Pick<InvocationContext, 'params' | 'query' | 'body'>>;
31
-
32
- let nextId = 1;
33
-
34
- /** Mounted queries per entity — the command side of the link reads this. */
35
- const mounted = new Map<string, Set<string>>();
36
-
37
- function invocationOf(input?: CallInput): InvocationContext {
38
- return { params: {}, query: {}, body: undefined, state: {}, ...input };
39
- }
40
-
41
- type Fetcher = <T>(url: string, options: { method: 'POST'; body: unknown }) => Promise<T>;
42
-
43
- async function send(fetcher: Fetcher, call: FrondCall, invocation: InvocationContext): Promise<unknown> {
44
- const response = await fetcher<RpcResponse>('/_fougere/call', {
45
- method: 'POST',
46
- body: frameCall(call, invocation, nextId++),
47
- });
48
- return unframeResponse(response, call);
49
- }
35
+ export type { CallInput };
50
36
 
51
37
  export async function useQuery<T = Record<string, unknown>>(
52
38
  entity: EntityClass,
@@ -54,46 +40,35 @@ export async function useQuery<T = Record<string, unknown>>(
54
40
  input?: MaybeRefOrGetter<CallInput | undefined>,
55
41
  opts?: { immediate?: boolean },
56
42
  ) {
57
- const entityKey = toRegistrationName(entity.name);
58
- const call: FrondCall = { entity: entityKey, op };
59
- const key = `fougere:${entityKey}.${op}:${JSON.stringify(toValue(input) ?? {})}`;
43
+ const entityKey = entityKeyOf(entity);
44
+ const call = callOf(entity, op);
45
+ const key = queryKeyOf(entityKey, op, toValue(input));
60
46
  const fetcher = useRequestFetch() as Fetcher;
61
47
 
62
48
  // Register before any await — the link and scope cleanup need the setup scope.
63
49
  if (import.meta.client) {
64
- const keys = mounted.get(entityKey) ?? new Set<string>();
65
- keys.add(key);
66
- mounted.set(entityKey, keys);
67
- onScopeDispose(() => keys.delete(key));
50
+ onScopeDispose(trackQuery(entityKey, key));
68
51
  }
69
52
 
70
53
  const { data, pending, error, refresh } = await useAsyncData(
71
54
  key,
72
- () => send(fetcher, call, invocationOf(toValue(input))),
55
+ () => sendCall(fetcher, call, invocationOf(toValue(input))),
73
56
  {
74
57
  ...(input === undefined ? {} : { watch: [() => toValue(input)] }),
75
58
  ...(opts?.immediate === false ? { immediate: false } : {}),
76
59
  },
77
60
  );
78
61
 
79
- // A list result reads as items/total/hasMore whatever the wire delivered.
80
- const items = computed<T[]>(() => {
81
- const v = data.value as unknown;
82
- if (Array.isArray(v)) return v as T[];
83
- if (v && typeof v === 'object' && Array.isArray((v as { items?: unknown }).items)) {
84
- return (v as { items: T[] }).items;
85
- }
86
- return [];
87
- });
88
- const total = computed(() => (data.value as { total?: number } | null)?.total);
89
- const hasMore = computed(() => (data.value as { hasMore?: boolean } | null)?.hasMore);
62
+ const items = computed<T[]>(() => itemsOf<T>(data.value));
63
+ const total = computed(() => pageOf(data.value).total);
64
+ const hasMore = computed(() => pageOf(data.value).hasMore);
90
65
 
91
66
  return { data: data as Ref<T | null>, items, total, hasMore, loading: pending, error, refresh };
92
67
  }
93
68
 
94
69
  export function useCommand<T = unknown>(entity: EntityClass, op: string) {
95
- const entityKey = toRegistrationName(entity.name);
96
- const call: FrondCall = { entity: entityKey, op };
70
+ const entityKey = entityKeyOf(entity);
71
+ const call = callOf(entity, op);
97
72
  const fetcher = useRequestFetch() as Fetcher;
98
73
  const loading = ref(false);
99
74
  const error = ref<FougereError | null>(null);
@@ -102,22 +77,13 @@ export function useCommand<T = unknown>(entity: EntityClass, op: string) {
102
77
  loading.value = true;
103
78
  error.value = null;
104
79
  try {
105
- const result = (await send(fetcher, call, invocationOf(input))) as T;
80
+ const result = (await sendCall(fetcher, call, invocationOf(input))) as T;
106
81
  // The link: same entity designated on both sides → revalidate its queries.
107
- const keys = mounted.get(entityKey);
108
- if (keys?.size) await refreshNuxtData([...keys]);
82
+ const keys = mountedKeys(entityKey);
83
+ if (keys.length) await refreshNuxtData(keys);
109
84
  return result;
110
85
  } catch (err) {
111
- error.value =
112
- err instanceof FougereError
113
- ? err
114
- : new FougereError({
115
- code: ErrorCode.SERVICE_UNAVAILABLE,
116
- message: (err as Error)?.message ?? String(err),
117
- entity: entityKey,
118
- operation: op,
119
- cause: err,
120
- });
86
+ error.value = asFougereError(err, entityKey, op);
121
87
  throw error.value;
122
88
  } finally {
123
89
  loading.value = false;
@@ -5,7 +5,7 @@
5
5
  * page arrives already knowing its user — no round-trip.
6
6
  */
7
7
  import { defineNuxtPlugin, useRequestEvent, useState } from '#imports';
8
- import { sessionViewOf, type SessionView } from '../session/view.js';
8
+ import { sessionViewOf, type SessionView } from '@fougere/app/client';
9
9
 
10
10
  export default defineNuxtPlugin(() => {
11
11
  const event = useRequestEvent();
@@ -1,86 +1,49 @@
1
1
  /**
2
- * REST catch-all — a thin bridge: parse the URL into a call value,
3
- * invoke it, format the result for HTTP. Dispatch and errors belong to
4
- * the runner; only static frond metadata is consulted here.
2
+ * REST catch-all — the h3 half of a door whose decisions live in `@fougere/app`.
3
+ *
4
+ * It used to derive its own answers: its own `pluralize`, its own "segment = op or
5
+ * id", and its own method rule — which was no rule at all. A known operation name
6
+ * won whatever the verb, so `GET /api/blog/posts/delete?id=…` reached the mutating
7
+ * method with the session cookie a browser attaches to any navigation, and a
8
+ * body-less `DELETE /api/blog/posts` was read as `create`.
9
+ *
10
+ * `schema-rest` already answers all three in one place, `exposed === false`
11
+ * included — so the fix was not a guard added here, it was the removal of the
12
+ * second answer. `serveRest` is now the third: the same decision serves Next, and
13
+ * neither adapter restates it.
14
+ *
15
+ * What stays here is translation: read the h3 event, write the h3 answer. The state
16
+ * is stamped from `event.context` — what the server resolved, never the wire.
5
17
  */
6
- import { defineEventHandler, readBody, getQuery, createError } from 'h3';
7
- import { toHttpError } from '@fougere/core';
8
- import { useFougereApp } from '../utils/fougereApp';
9
- import { invoke } from '../utils/invoke';
10
-
11
- function pluralize(name: string): string {
12
- return name.endsWith('y') ? name.slice(0, -1) + 'ies' : name + 's';
13
- }
18
+ import { defineEventHandler, readBody, getQuery, createError, setResponseStatus, setResponseHeaders } from 'h3';
19
+ import { serveRest, useFougereApp } from '@fougere/app';
14
20
 
15
21
  export default defineEventHandler(async (event) => {
16
22
  const app = await useFougereApp();
17
23
 
18
- // ── parse: /api/{frondName}/{plural}[/{idOrOp}] → FrondCall + invocation
19
- const path = event.path.replace(/^\/api\//, '').replace(/\?.*$/, '');
20
- const segments = path.split('/').filter(Boolean);
21
- if (segments.length < 2) return;
22
-
23
- const frond = app.fronds.find((f) => f.name === segments[0]);
24
- if (!frond) return;
25
- const entity = frond.entities.find((e) => pluralize(e.name) === segments[1]);
26
- if (!entity) return;
27
-
28
- // The default handler, explicitly: this catch-all IS the default surface, and
29
- // the call below lands on the default façade. Without `!h.surface` the op list
30
- // that arbitrates "segment = op or id" could come from a surface handler while
31
- // the call went somewhere else — two answers for one route.
32
- const ops = [
33
- ...(frond.handlers.find((h) => h.address === entity.name && !h.surface)?.operations.keys() ?? []),
34
- ];
35
-
36
24
  const method = event.method.toUpperCase();
37
- const extra: string | undefined = segments[2];
38
- const params: Record<string, string> = {};
39
- let op: string;
40
-
41
- if (!extra) {
42
- op = method === 'GET' ? 'list' : 'create';
43
- } else {
44
- // /{idOrOp}: kebab-case → camelCase, known operation wins, else it's an id
45
- const asOp = extra.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
46
- if (ops.includes(asOp)) {
47
- op = asOp;
48
- } else {
49
- params.id = extra;
50
- if (method === 'GET') op = 'findById';
51
- else if (method === 'PUT' || method === 'PATCH') op = 'update';
52
- else if (method === 'DELETE') op = 'delete';
53
- else throw createError({ statusCode: 405, message: 'Method not allowed' });
54
- }
55
- }
56
-
57
- const query = getQuery(event) as Record<string, string>;
58
25
  const hasBody = method === 'POST' || method === 'PUT' || method === 'PATCH';
59
- const body = hasBody ? await readBody(event) : undefined;
60
-
61
- // ── invoke: the runner routes it — local façade or remote doublure
62
- let result: unknown;
63
- try {
64
- result = await invoke({ entity: entity.name, op }, { params, query, body });
65
- } catch (err) {
66
- const { status, body: payload } = toHttpError(err);
67
- throw createError({ statusCode: status, message: payload.message, data: payload });
68
- }
69
-
70
- // ── format
71
- if (result === null) {
72
- throw createError({ statusCode: 404, message: 'Not found' });
73
- }
74
26
 
75
- // ListResult serialize as { items, total, hasMore, endCursor }
76
- if (op === 'list' && Array.isArray(result)) {
77
- return {
78
- items: [...result],
79
- total: (result as any).total,
80
- hasMore: (result as any).hasMore,
81
- endCursor: (result as any).endCursor,
82
- };
27
+ const outcome = await serveRest(app, {
28
+ method,
29
+ path: event.path.replace(/^\/api\//, '').replace(/\?.*$/, ''),
30
+ query: getQuery(event) as Record<string, string>,
31
+ body: hasBody ? await readBody(event) : undefined,
32
+ state: (event.context ?? {}) as Record<string, unknown>,
33
+ });
34
+
35
+ // Nothing here is ours — an app's own /api/* routes must still reach their handler.
36
+ if (outcome.kind === 'pass') return;
37
+
38
+ if (outcome.kind === 'error') {
39
+ if (outcome.headers) setResponseHeaders(event, outcome.headers);
40
+ throw createError({
41
+ statusCode: outcome.status,
42
+ message: outcome.body.message,
43
+ data: outcome.body,
44
+ });
83
45
  }
84
46
 
85
- return result;
47
+ setResponseStatus(event, outcome.status);
48
+ return outcome.body;
86
49
  });
@@ -1,5 +1,5 @@
1
1
  import { defineEventHandler, getRequestHeader } from 'h3';
2
- import { useFougereAuth } from '../../utils/fougereAuth';
2
+ import { useFougereAuth } from '@fougere/app';
3
3
 
4
4
  /**
5
5
  * Resolves the current session/user from the request cookie and exposes them
@@ -1,5 +1,5 @@
1
1
  import { defineEventHandler } from 'h3';
2
- import { useFougereAuth } from '../../../utils/fougereAuth';
2
+ import { useFougereAuth } from '@fougere/app';
3
3
 
4
4
  export default defineEventHandler(async (event) => {
5
5
  const auth = await useFougereAuth();
@@ -1,17 +1,14 @@
1
1
  /**
2
- * Receiving end for the browser — same wire as process-to-process
3
- * (POST /_fougere/call, JSON-RPC), different trust boundary: the browser
4
- * sits outside the topology, so `state` is stamped from the server-resolved
5
- * session (event.context), never taken from the wire.
2
+ * Receiving end for the browser — the h3 half. The decision (which audience, which
3
+ * runner, what a parse failure answers) lives in `@fougere/app`; what stays here is
4
+ * getting a JSON payload out of an h3 event, which is the one thing that genuinely
5
+ * differs between hosts. Next reads `await request.json()` and needs none of this.
6
6
  *
7
- * The runner follows the app's topology: local façades and remote
8
- * doublures alike the browser never knows where a Frond lives.
7
+ * Trust boundary, unchanged: the browser sits outside the topology, so `state` is
8
+ * stamped from the server-resolved session (event.context), never taken from the wire.
9
9
  */
10
10
  import { defineEventHandler } from 'h3';
11
- import { handleRpc, PARSE_ERROR } from '@fougere/transport-http';
12
- import { createAppRunner } from '@fougere/core';
13
- import type { Transport } from '@fougere/core';
14
- import { useFougereApp } from '../utils/fougereApp';
11
+ import { serveRpc, rpcParseError, useFougereApp } from '@fougere/app';
15
12
 
16
13
  type NodeReq = {
17
14
  body?: unknown;
@@ -122,37 +119,16 @@ async function readJsonBody(event: { req?: unknown; node?: { req?: unknown } }):
122
119
  return {};
123
120
  }
124
121
 
125
- /**
126
- * The audience this door serves — the path segment after `/_fougere/call`.
127
- *
128
- * The envelope is a surface like REST and GraphQL, so it selects its audience like they
129
- * do; the difference is only that it takes it from the path instead of an option, because
130
- * a door is mounted, not called. The same word names the directory
131
- * (`handlers/public/`), the config key (`surfaces: { public: [...] }`) and this segment —
132
- * derived, never configured.
133
- *
134
- * No escalation to guard: a named surface serves the entities it names and nothing else
135
- * (closed by naming), so every one of them is a subset of what the bare path already
136
- * serves.
137
- */
138
- function surfaceOf(path: string): string | undefined {
139
- const named = /^\/_fougere\/call\/([A-Za-z0-9_-]+)/.exec(path.replace(/\?.*$/, ''));
140
- return named?.[1];
141
- }
142
-
143
122
  export default defineEventHandler(async (event) => {
144
123
  const app = await useFougereApp();
145
- const runner = createAppRunner(app, surfaceOf(event.path));
146
- const stamped: Transport = (call, invocation) =>
147
- runner(call, { ...invocation, state: (event.context ?? {}) as Record<string, unknown> });
148
124
  try {
149
- return handleRpc(stamped, await readJsonBody(event));
125
+ return await serveRpc(app, {
126
+ path: event.path,
127
+ body: await readJsonBody(event),
128
+ state: (event.context ?? {}) as Record<string, unknown>,
129
+ });
150
130
  } catch (err) {
151
131
  if ((err as { statusCode?: number })?.statusCode === 413) throw err;
152
- return {
153
- jsonrpc: '2.0' as const,
154
- id: null,
155
- error: { code: PARSE_ERROR, message: 'Parse error' },
156
- };
132
+ return rpcParseError();
157
133
  }
158
134
  });
@@ -4,7 +4,7 @@
4
4
  * family as /_fougere/call.
5
5
  */
6
6
  import { defineEventHandler } from 'h3';
7
- import { sessionViewOf } from '../../session/view.js';
7
+ import { sessionViewOf } from '@fougere/app';
8
8
 
9
9
  export default defineEventHandler((event) =>
10
10
  sessionViewOf((event.context ?? {}) as Record<string, unknown>),
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The boot, re-exported under a path inside this package.
3
+ *
4
+ * Every runtime file here reaches `@fougere/app` by name, because it is resolved
5
+ * from this package's own node_modules. The generated Nitro plugin cannot: it is
6
+ * written into the user's `.nuxt/`, so a bare `@fougere/app` would be resolved from
7
+ * the USER's project, where a transitive dependency is not visible under pnpm.
8
+ *
9
+ * So the codegen imports this file by absolute path, and this file — which does sit
10
+ * in `@fougere/nuxt` — does the resolving. One indirection, one reason.
11
+ */
12
+ export { configureFougere, useFougereApp, type FougereServerConfig } from '@fougere/app';
@@ -1,20 +1,15 @@
1
1
  /**
2
- * The server dual of the couple — same designation (class + verb), same
3
- * return, same errors. Fabricates the call value and hands it to the app
4
- * runner: local façadedirect in-memory execution, frond in `remotes`
5
- * JSON-RPC on the wire. The caller never knows which.
2
+ * The server dual of the couple — same designation (class + verb), same return,
3
+ * same errors. `invokeOn` places the call (local façade direct in-memory
4
+ * execution, a frond in `remotes` JSON-RPC on the wire); what this file owns is
5
+ * the one host-specific part: finding the current request.
6
6
  *
7
7
  * In request context the current session rides along (event.context →
8
8
  * invocation.state); outside a request, state is empty or explicit.
9
9
  */
10
10
  import { useEvent } from 'nitropack/runtime';
11
- import {
12
- createAppRunner,
13
- callValueOf,
14
- type FrondCall,
15
- type InvocationContext,
16
- } from '@fougere/core';
17
- import { useFougereApp } from './fougereApp';
11
+ import { invokeOn, useFougereApp } from '@fougere/app';
12
+ import type { FrondCall, InvocationContext } from '@fougere/core';
18
13
 
19
14
  type EntityClass = { name: string };
20
15
  type CallInput = Partial<InvocationContext>;
@@ -26,11 +21,8 @@ export async function invoke<T = unknown>(
26
21
  opOrInput?: string | CallInput,
27
22
  input?: CallInput,
28
23
  ): Promise<T> {
29
- const given = typeof opOrInput === 'string' ? input : opOrInput;
30
- const { call, invocation } = callValueOf(target, opOrInput, input);
31
- const state = given?.state ?? requestState();
32
24
  const app = await useFougereApp();
33
- return (await createAppRunner(app)(call, { ...invocation, state })) as T;
25
+ return invokeOn<T>(app, target, opOrInput, input, requestState());
34
26
  }
35
27
 
36
28
  function requestState(): Record<string, unknown> {
@@ -1,185 +0,0 @@
1
- /**
2
- * Form contract, pure part — derives what a create/edit form is made of
3
- * from the entity's field axes. No Vue, no Nuxt: testable headless,
4
- * usable by any renderer (the page owns the widgets).
5
- */
6
- import { inputFields } from '@fougere/schema';
7
- import type { ValidationError } from '@fougere/schema';
8
-
9
- /** What an entity class exposes to a form — the schema statics it already has. */
10
- export interface FormEntity {
11
- name: string;
12
- getFields(): Record<string, FieldLike>;
13
- validate(input: unknown): { success: true; data: unknown } | { success: false; errors: ValidationError[] };
14
- }
15
-
16
- interface FieldLike {
17
- shape?: {
18
- type?: unknown;
19
- enum?: readonly unknown[];
20
- format?: string;
21
- properties?: unknown;
22
- minLength?: number;
23
- maxLength?: number;
24
- minimum?: number;
25
- maximum?: number;
26
- pattern?: string;
27
- };
28
- lifecycle?: { create?: unknown };
29
- role?: { primary?: boolean; relation?: { kind: string } };
30
- }
31
-
32
- /**
33
- * The literal a field is born with, when it declares one.
34
- *
35
- * `text({ default: 'x' })` and `oneOf('a', 'b', { default: 'a' })` both compile to
36
- * `lifecycle.create = { value }` — the create rule that answers the field's absence.
37
- * The other create rules ('now', { generate }, 'optional') name no literal: their value
38
- * is decided at write time, so a form has nothing to show for them.
39
- */
40
- function defaultOf(field: FieldLike): unknown {
41
- const create = field.lifecycle?.create;
42
- return create !== null && typeof create === 'object' && 'value' in create
43
- ? (create as { value: unknown }).value
44
- : undefined;
45
- }
46
-
47
- export interface FormField {
48
- name: string;
49
- /** Rendering hint derived from the shape — the page maps it to widgets. */
50
- control: 'text' | 'email' | 'url' | 'number' | 'boolean' | 'date' | 'select';
51
- required: boolean;
52
- /** i18n key by convention: `entity.field`. The schema never carries display text. */
53
- labelKey: string;
54
- /** Fallback label when no i18n message fills the key. */
55
- label: string;
56
- /** Enum values, when control is 'select'. */
57
- options?: string[];
58
- /**
59
- * What the browser enforces, under the names it already knows — spread this on the
60
- * input and the page states no rule of its own.
61
- *
62
- * The shape holds `minLength`/`maximum`/`pattern`; a browser holds `minlength`/
63
- * `max`/`pattern` and enforces them with no JavaScript at all. Carrying them here
64
- * is a projection, not a second rule: the judge reads the same shape, and a form
65
- * that ignores these still gets the same verdict — it just gets it later, and a
66
- * screen reader never gets it at all.
67
- *
68
- * `type` is part of the contract, not decoration: `email` and `url` are formats the
69
- * shape states and the browser checks live, per field, as one types. A page writing
70
- * `type="email"` by hand is spelling a second time what the card already said.
71
- *
72
- * Three deliberate absences, each one a place where the attribute would mean
73
- * something the shape does not say:
74
- * - a `date` field gets no `type` — neither `date` nor `datetime-local` produces the
75
- * RFC 3339 string a `date-time` shape judges, so the browser would accept what the
76
- * judge refuses;
77
- * - `select` and `boolean` are not inputs — the page picks the widget, `control` says
78
- * which;
79
- * - a required `boolean` gets no `required` — on a checkbox that attribute means
80
- * "must be CHECKED", where the shape only says the value must be supplied.
81
- */
82
- attrs?: {
83
- type?: 'text' | 'email' | 'url' | 'number';
84
- required?: boolean;
85
- minlength?: number;
86
- maxlength?: number;
87
- min?: number;
88
- max?: number;
89
- pattern?: string;
90
- };
91
- /**
92
- * The value the field is born with — the literal its `lifecycle.create` rule names.
93
- * Present so the form can SHOW what is about to be written; the storage realizes it
94
- * either way, so a form that ignores this still produces the same row.
95
- */
96
- default?: unknown;
97
- }
98
-
99
- /** The base JSON type of a shape — unwraps the `[T,'null']` union. */
100
- function baseType(type: unknown): string {
101
- if (Array.isArray(type)) return (type.find((t) => t !== 'null') as string) ?? 'string';
102
- return (type as string) ?? 'string';
103
- }
104
-
105
- /** The formats a browser has an input type for — the rest stay `text`, judged later. */
106
- const CONTROL_BY_FORMAT: Record<string, FormField['control']> = {
107
- 'date-time': 'date',
108
- email: 'email',
109
- uri: 'url',
110
- };
111
-
112
- function controlOf(field: FieldLike): FormField['control'] {
113
- const shape = field.shape ?? {};
114
- if (Array.isArray(shape.enum) && shape.enum.length) return 'select';
115
- const base = baseType(shape.type);
116
- if (base === 'number' || base === 'integer') return 'number';
117
- if (base === 'boolean') return 'boolean';
118
- if (base === 'string' && shape.format) return CONTROL_BY_FORMAT[shape.format] ?? 'text';
119
- return 'text';
120
- }
121
-
122
- /** Controls that ARE an `<input type>` — see the two absences on {@link FormField.attrs}. */
123
- const INPUT_TYPES = new Set(['text', 'email', 'url', 'number']);
124
-
125
- /** The shape's bounds, under the names a browser already enforces. */
126
- function attrsOf(field: FieldLike, control: FormField['control'], required: boolean): NonNullable<FormField['attrs']> {
127
- const s = field.shape ?? {};
128
- const attrs = {
129
- type: INPUT_TYPES.has(control) ? control : undefined,
130
- required: (required && control !== 'boolean') || undefined,
131
- minlength: s.minLength,
132
- maxlength: s.maxLength,
133
- min: s.minimum,
134
- max: s.maximum,
135
- pattern: s.pattern,
136
- };
137
- return Object.fromEntries(Object.entries(attrs).filter(([, v]) => v !== undefined));
138
- }
139
-
140
- /**
141
- * The fields a create form is made of: membership from the io projection
142
- * (`inputFields` — what a client may supply), requiredness from the
143
- * lifecycle axis (any create rule makes absence legal).
144
- */
145
- export function formFieldsOf(entity: FormEntity, entityKey: string): FormField[] {
146
- return Object.entries(inputFields(entity.getFields() as never)).map(([name, field]) => {
147
- const f = field as FieldLike;
148
- const control = controlOf(f);
149
- const required = f.lifecycle?.create === undefined;
150
- const attrs = attrsOf(f, control, required);
151
- return {
152
- name,
153
- control,
154
- required,
155
- labelKey: `${entityKey}.${name}`,
156
- label: name.charAt(0).toUpperCase() + name.slice(1),
157
- ...(Array.isArray(f.shape?.enum)
158
- ? { options: f.shape.enum.filter((value): value is string => typeof value === 'string') }
159
- : {}),
160
- ...(Object.keys(attrs).length ? { attrs } : {}),
161
- ...(defaultOf(f) !== undefined ? { default: defaultOf(f) } : {}),
162
- };
163
- });
164
- }
165
-
166
- /**
167
- * The wire body of the form's values — an empty control is an absent value
168
- * at the create boundary (absence is judged by the lifecycle axis, an empty
169
- * string would be judged as a present bad value).
170
- */
171
- export function payloadOf(values: Record<string, unknown>): Record<string, unknown> {
172
- return Object.fromEntries(
173
- Object.entries(values).filter(([, v]) => v !== undefined && v !== ''),
174
- );
175
- }
176
-
177
- /** Index judge errors by field — local judge and remote judge share this shape. */
178
- export function errorsByField(errors: ValidationError[]): Record<string, string> {
179
- const byField: Record<string, string> = {};
180
- for (const err of errors) {
181
- const field = err.path.split('.')[0] || err.path;
182
- byField[field] ??= err.message;
183
- }
184
- return byField;
185
- }
@@ -1,209 +0,0 @@
1
- /**
2
- * Fougere server bootstrap — single entry point for app lifecycle in Nuxt.
3
- *
4
- * Default path (zero-config): fougere.config.ts declares `db: 'sqlite'` and
5
- * everything else; this module auto-resolves the storage handle, builds an
6
- * ormFactory, runs auto-DDL from the entities, and boots the app.
7
- *
8
- * Escape hatch: `configureFougere({ db, ormFactory })` can be called from a
9
- * Nitro plugin if the user wants a custom data layer (alternative driver,
10
- * managed migrations, etc.).
11
- */
12
- import { createApp, loadCascadedConfig, setModuleLoader, frondAliases, Logger } from '@fougere/core';
13
- import { createContainer } from '@fougere/container-fougere';
14
- import type { App, EntityOrm, FougereConfig, Transport } from '@fougere/core';
15
- import { applyCreate, applyUpdate, type SchemaLike } from '@fougere/schema';
16
-
17
- // ── Public types ─────────────────────────────────
18
-
19
- export interface FougereServerConfig {
20
- /** Storage handle. Forwarded to the auth provider via AuthContext.db. */
21
- db?: unknown;
22
- /** Per-entity ORM factory. */
23
- ormFactory?: (entity: SchemaLike, name: string) => EntityOrm;
24
- /** Called after app is created. Use for migrations, seeding, etc. */
25
- afterBoot?: (app: App) => void | Promise<void>;
26
- }
27
-
28
- // ── State ────────────────────────────────────────
29
-
30
- let _config: FougereServerConfig = {};
31
- let _appPromise: Promise<App> | null = null;
32
-
33
- // ── Public API ───────────────────────────────────
34
-
35
- /**
36
- * Override the data layer — only needed if you don't want the convention-driven
37
- * setup based on `config.db` in fougere.config.ts.
38
- */
39
- export function configureFougere(config: FougereServerConfig) {
40
- _config = config;
41
- _appPromise = null;
42
- }
43
-
44
- /** Get the booted Fougere app. Lazy — boots on first call, then caches. */
45
- export function useFougereApp(): Promise<App> {
46
- if (!_appPromise) {
47
- _appPromise = boot();
48
- }
49
- return _appPromise;
50
- }
51
-
52
- // ── Boot ─────────────────────────────────────────
53
-
54
- async function boot(): Promise<App> {
55
- const bootStart = performance.now();
56
- const log = new Logger('boot', { level: 'debug' });
57
-
58
- log.info('booting (Nuxt/Nitro)');
59
-
60
- const { createJiti } = await import('jiti');
61
- // Nitro serves from a bundle, but the scan still reads frond sources from disk — so the
62
- // named form a frond uses for its neighbour has to resolve here too.
63
- const jiti = createJiti(import.meta.url, {
64
- interopDefault: true,
65
- alias: await frondAliases(process.env.FOUGERE_ROOT ?? process.cwd()),
66
- });
67
- setModuleLoader((filePath) => jiti.import(filePath) as Promise<Record<string, unknown>>);
68
-
69
- // Config cascades along the workspace→app frontier: the workspace root (via
70
- // FOUGERE_ROOT, where `remotes`/shared db live) is the base, the app (cwd)
71
- // overrides. Same boundary the fronds cascade along. No `root` → both equal.
72
- const configRoot = process.cwd();
73
- const root = process.env.FOUGERE_ROOT ?? configRoot;
74
- const fileConfig: FougereConfig = await loadCascadedConfig(root, configRoot);
75
-
76
- // Auto-resolve the data layer from config.db when the user didn't provide a
77
- // custom one via configureFougere. The resolution itself lives in @fougere/runtime
78
- // — this host must not know which storage package backs `db:`.
79
- let db = _config.db;
80
- let ormFactory = _config.ormFactory;
81
- let migrateSchema: ((app: never) => Promise<void> | void) | undefined;
82
- if (!ormFactory) {
83
- const { resolveStorage } = await import('@fougere/runtime');
84
- const storage = resolveStorage(fileConfig.db as never);
85
- if (storage.ormFactory) {
86
- log.debug('auto-resolving storage from config.db');
87
- db = storage.db;
88
- ormFactory = storage.ormFactory;
89
- migrateSchema = storage.afterBoot as never;
90
- } else {
91
- log.debug('no db declared — falling back to in-memory ORM');
92
- ormFactory = createMemoryOrm;
93
- }
94
- }
95
-
96
- // Layer-2 wiring: `remotes: { catalog: 'http://...' }` in fougere.config.ts
97
- // is all the user writes — the default transport comes from here.
98
- let remoteTransport: ((url: string) => Transport) | undefined;
99
- if (Object.keys(fileConfig.remotes ?? {}).length > 0) {
100
- log.debug(`remotes declared (${Object.keys(fileConfig.remotes!).join(', ')}) — wiring HTTP transport`);
101
- const { createHttpTransport } = await import('@fougere/transport-http');
102
- remoteTransport = (url) => createHttpTransport(url);
103
- }
104
-
105
- const app = await createApp({
106
- root,
107
- createContainer,
108
- ormFactory,
109
- db,
110
- auth: fileConfig.auth,
111
- remotes: fileConfig.remotes,
112
- remoteTransport,
113
- });
114
-
115
- // Bring the schema up to date — creates missing tables and adds columns an
116
- // entity gained. Auth entities travel with the app, no synthetic frond needed.
117
- if (migrateSchema) {
118
- log.debug('migrating schema from entities');
119
- await migrateSchema(app as never);
120
- }
121
-
122
- if (_config.afterBoot) {
123
- log.debug('running afterBoot');
124
- await _config.afterBoot(app);
125
- log.info('afterBoot done');
126
- }
127
-
128
- const ms = (performance.now() - bootStart).toFixed(0);
129
- log.info(`ready in ${ms}ms — ${app.fronds.length} frond(s)${app.auth ? ` + auth (${app.auth.basePath})` : ''}`);
130
-
131
- return app;
132
- }
133
-
134
- // ── Fallback ORM ─────────────────────────────────
135
-
136
- /**
137
- * The store an app with no `db` runs on.
138
- *
139
- * It used to ignore both its arguments — `(_entity, _name)` — so it forced the field
140
- * name `id`, minted a uuid whatever the entity declared, and realized none of the
141
- * lifecycle rules: `auto()` stamped nothing, a declared default stayed absent. The
142
- * same page therefore behaved one way here and another way on SQLite.
143
- *
144
- * It reads the axes now, through the one realization every storage shares.
145
- */
146
- export function createMemoryOrm(entity: SchemaLike, name: string): EntityOrm {
147
- const fields = entity.getFields();
148
- const pk = Object.entries(fields).find(([, field]) => field.role?.primary)?.[0] ?? 'id';
149
- const store = new Map<string, Record<string, unknown>>();
150
- // `EntityOrm.findById(id: string)` — but a key can hold a number, and a Map keyed on
151
- // `1` does not answer `'1'`. SQL never had the question; here the divergence was
152
- // silent and only on this storage.
153
- const keyOf = (value: unknown) => String(value);
154
- const matches = (row: Record<string, unknown>, criteria: Record<string, unknown>) =>
155
- Object.entries(criteria).every(([key, value]) => Object.is(row[key], value));
156
- return {
157
- client: store,
158
- async list(options?: any) {
159
- let items = [...store.values()];
160
- if (options?.where) items = items.filter((row) => matches(row, options.where));
161
- const limit = options?.limit;
162
- const offset = options?.page && limit ? (options.page - 1) * limit : options?.offset ?? 0;
163
- if (offset > 0) items = items.slice(offset);
164
- const hasMore = limit ? items.length > limit : false;
165
- if (limit) items = items.slice(0, limit);
166
- const result = items as any;
167
- result.hasMore = hasMore;
168
- result.endCursor = items.length > 0 ? String((items[items.length - 1] as any)[pk] ?? '') : undefined;
169
- if (options?.count) result.total = store.size;
170
- return result;
171
- },
172
- async findById(id: string) { return store.get(keyOf(id)); },
173
- async findBy(criteria: Record<string, unknown>) {
174
- return [...store.values()].find((row) => matches(row, criteria));
175
- },
176
- async findAllBy(criteria: Record<string, unknown>) {
177
- return [...store.values()].filter((row) => matches(row, criteria));
178
- },
179
- async create(input: Partial<Record<string, unknown>>) {
180
- const record = applyCreate(fields, input);
181
- const id = record[pk] as string | undefined;
182
- // `primary(text())` declares no generator, so nothing fills the hole and the
183
- // caller has to. Keying on `undefined` would let the second create overwrite the
184
- // first, in silence — the old version hid this by inventing an `id` field the
185
- // entity never declared.
186
- if (id === undefined) {
187
- throw new Error(`${name}.create: '${pk}' is the primary key and nothing supplied it — this entity declares no generator for it.`);
188
- }
189
- // A create is not an upsert. `Map.set` overwrites, so a second create under the
190
- // same key answered "created" while destroying the previous row — SQL answers a
191
- // constraint violation, and a store that loses data silently is worse than one
192
- // that fails.
193
- if (store.has(keyOf(id))) {
194
- throw new Error(`${name}.create: '${pk}' ${JSON.stringify(id)} already exists.`);
195
- }
196
- store.set(keyOf(id), record);
197
- return record;
198
- },
199
- async update(id: string, input: Partial<Record<string, unknown>>) {
200
- const existing = store.get(keyOf(id));
201
- if (!existing) throw new Error(`Not found: ${id}`);
202
- const updated = { ...existing, ...applyUpdate(fields, input), [pk]: existing[pk] };
203
- store.set(keyOf(id), updated);
204
- return updated;
205
- },
206
- async delete(id: string) { return store.delete(keyOf(id)); },
207
- output(_schema: SchemaLike) { return this; },
208
- };
209
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Auth accessor — reads the AuthRuntime that the core mounted on the app.
3
- *
4
- * Auth is no longer a separate singleton: it lives on `app.auth`, built once
5
- * during boot from the `auth` field of fougere.config.ts. This accessor is a
6
- * thin async helper for server code (middleware, routes, /api/me).
7
- */
8
- import type { AuthRuntime } from '@fougere/core';
9
- import { useFougereApp } from './fougereApp';
10
-
11
- /** Get the auth runtime resolved at boot. Throws if no `auth` was declared in fougere.config.ts. */
12
- export async function useFougereAuth(): Promise<AuthRuntime> {
13
- const app = await useFougereApp();
14
- if (!app.auth) {
15
- throw new Error(
16
- 'Auth not configured — declare `auth: { provider, ... }` in fougere.config.ts.',
17
- );
18
- }
19
- return app.auth;
20
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * The session view — the one place that turns the server-resolved
3
- * request context (filled by the auth middleware) into what the client
4
- * is allowed to see. One resolution, three readers: the page by
5
- * hydration, the refresh route over the wire, handlers by invocation.
6
- *
7
- * The app-declared context (viewer enrichment) will attach here when
8
- * a real case lands — this function is the seam.
9
- */
10
-
11
- export interface SessionView {
12
- user: Record<string, unknown> | null;
13
- }
14
-
15
- export function sessionViewOf(context: Record<string, unknown>): SessionView {
16
- const raw = context.user as Record<string, unknown> | undefined;
17
- if (!raw) return { user: null };
18
- const { passwordHash: _passwordHash, ...user } = raw;
19
- return { user };
20
- }