@fougere/core 0.4.0-alpha.0 → 0.5.0-alpha.1
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/dist/boot/Emissions.js +1 -1
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/binding.d.ts +1 -1
- package/dist/boot/binding.d.ts.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +12 -4
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/hosted.d.ts +29 -0
- package/dist/boot/hosted.d.ts.map +1 -0
- package/dist/boot/hosted.js +38 -0
- package/dist/boot/hosted.js.map +1 -0
- package/dist/boot/ownership.d.ts +0 -3
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/remote.d.ts +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +1 -1
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +24 -0
- package/dist/boot/statement-drift.d.ts.map +1 -0
- package/dist/boot/statement-drift.js +38 -0
- package/dist/boot/statement-drift.js.map +1 -0
- package/dist/boot/types.d.ts +15 -2
- package/dist/boot/types.d.ts.map +1 -1
- package/dist/contract/CallLog.d.ts +57 -0
- package/dist/contract/CallLog.d.ts.map +1 -0
- package/dist/contract/CallLog.js +2 -0
- package/dist/contract/CallLog.js.map +1 -0
- package/dist/contract.d.ts +8 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +9 -2
- package/dist/contract.js.map +1 -1
- package/dist/dispatch/DispatchLifecycle.d.ts +9 -1
- package/dist/dispatch/DispatchLifecycle.d.ts.map +1 -1
- package/dist/dispatch/DispatchLifecycle.js +17 -2
- package/dist/dispatch/DispatchLifecycle.js.map +1 -1
- package/dist/dispatch/InFlight.d.ts +0 -1
- package/dist/dispatch/InFlight.d.ts.map +1 -1
- package/dist/dispatch/InFlight.js +0 -3
- package/dist/dispatch/InFlight.js.map +1 -1
- package/dist/effective-operation.d.ts.map +1 -1
- package/dist/effective-operation.js +4 -0
- package/dist/effective-operation.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +4 -2
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +4 -2
- package/dist/node.js.map +1 -1
- package/dist/scan/bundling.d.ts +14 -0
- package/dist/scan/bundling.d.ts.map +1 -0
- package/dist/scan/bundling.js +26 -0
- package/dist/scan/bundling.js.map +1 -0
- package/dist/scan/conventions.d.ts +8 -12
- package/dist/scan/conventions.d.ts.map +1 -1
- package/dist/scan/conventions.js +0 -13
- package/dist/scan/conventions.js.map +1 -1
- package/dist/scan/declare.d.ts +71 -0
- package/dist/scan/declare.d.ts.map +1 -0
- package/dist/scan/declare.js +125 -0
- package/dist/scan/declare.js.map +1 -0
- package/dist/scan/handler-parser.js +0 -1
- package/dist/scan/handler-parser.js.map +1 -1
- package/dist/scan/scanner.d.ts +17 -0
- package/dist/scan/scanner.d.ts.map +1 -1
- package/dist/scan/scanner.js +17 -1
- package/dist/scan/scanner.js.map +1 -1
- package/dist/scan/statement.d.ts +4 -0
- package/dist/scan/statement.d.ts.map +1 -0
- package/dist/scan/statement.js +112 -0
- package/dist/scan/statement.js.map +1 -0
- package/dist/wire/call.d.ts +12 -1
- package/dist/wire/call.d.ts.map +1 -1
- package/dist/wire/call.js +11 -0
- package/dist/wire/call.js.map +1 -1
- package/dist/wire/drift.d.ts +55 -0
- package/dist/wire/drift.d.ts.map +1 -0
- package/dist/wire/drift.js +121 -0
- package/dist/wire/drift.js.map +1 -0
- package/dist/wire/middleware.d.ts +1 -1
- package/dist/wire/middleware.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/boot/Emissions.ts +1 -1
- package/src/boot/HandlerFacade.ts +1 -1
- package/src/boot/binding.ts +1 -1
- package/src/boot/bootstrap.ts +12 -4
- package/src/boot/hosted.ts +77 -0
- package/src/boot/ownership.ts +1 -4
- package/src/boot/remote.ts +1 -1
- package/src/boot/statement-drift.ts +60 -0
- package/src/boot/types.ts +15 -2
- package/src/contract/CallLog.ts +55 -0
- package/src/contract.ts +12 -3
- package/src/dispatch/DispatchLifecycle.ts +22 -2
- package/src/dispatch/InFlight.ts +0 -4
- package/src/effective-operation.ts +4 -0
- package/src/index.ts +16 -2
- package/src/node.ts +4 -2
- package/src/scan/bundling.ts +25 -0
- package/src/scan/conventions.ts +0 -18
- package/src/scan/declare.ts +188 -0
- package/src/scan/handler-parser.ts +0 -1
- package/src/scan/scanner.ts +22 -1
- package/src/scan/statement.ts +126 -0
- package/src/wire/call.ts +13 -1
- package/src/wire/drift.ts +131 -0
- package/src/wire/middleware.ts +1 -1
- package/dist/boot/Lifecycle.d.ts +0 -4
- package/dist/boot/Lifecycle.d.ts.map +0 -1
- package/dist/boot/Lifecycle.js +0 -3
- package/dist/boot/Lifecycle.js.map +0 -1
- package/dist/scan/scan-cache.d.ts +0 -16
- package/dist/scan/scan-cache.d.ts.map +0 -1
- package/dist/scan/scan-cache.js +0 -99
- package/dist/scan/scan-cache.js.map +0 -1
- package/dist/wire/invocation.d.ts +0 -4
- package/dist/wire/invocation.d.ts.map +0 -1
- package/dist/wire/invocation.js +0 -3
- package/dist/wire/invocation.js.map +0 -1
- package/src/boot/Lifecycle.ts +0 -3
- package/src/scan/scan-cache.ts +0 -115
- package/src/wire/invocation.ts +0 -10
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DispatchEvent } from '../dispatch/DispatchEvent.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What crosses the door: the address, the route it took, and the verdict.
|
|
5
|
+
*
|
|
6
|
+
* It lives here rather than beside its producer for the reason `TopologyReport` does: it
|
|
7
|
+
* crosses a process boundary, so a reader that never installed `@fougere/calls` — the CLI
|
|
8
|
+
* — needs its shape, and putting it there is what produces a hand-copied duplicate.
|
|
9
|
+
*
|
|
10
|
+
* Never the body. Same rule the topology report states — a remote destination is named,
|
|
11
|
+
* not disclosed — and it holds here for the same reason: this answer leaves the process.
|
|
12
|
+
*/
|
|
13
|
+
export interface CallRecord {
|
|
14
|
+
/** Monotonic, and the whole cursor protocol: a reader asks for what is above its own. */
|
|
15
|
+
seq: number;
|
|
16
|
+
frond?: string;
|
|
17
|
+
entity: string;
|
|
18
|
+
operation: string;
|
|
19
|
+
surface?: string;
|
|
20
|
+
/** Known at `resolved`, so absent on a call that never found a route. */
|
|
21
|
+
route?: NonNullable<DispatchEvent['routeKind']>;
|
|
22
|
+
/**
|
|
23
|
+
* The traceparent the invocation carried, when one did.
|
|
24
|
+
*
|
|
25
|
+
* One panel reads one process, so this is what lets two of them be sewn: the process
|
|
26
|
+
* that sent and the process that executed share it, and the gap between their two
|
|
27
|
+
* durations IS the cost of the wire rather than a deduction.
|
|
28
|
+
*/
|
|
29
|
+
trace?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The peer that established this call, when one did.
|
|
32
|
+
*
|
|
33
|
+
* A hosted frond serves several consumers, so its own ring mixes them: this is what
|
|
34
|
+
* separates them. Absent on a call nobody signed — a loopback dev boot has no identity.
|
|
35
|
+
*/
|
|
36
|
+
caller?: string;
|
|
37
|
+
startedAt: number;
|
|
38
|
+
/** Known at `settled`. */
|
|
39
|
+
ms?: number;
|
|
40
|
+
verdict: 'running' | 'ok' | 'failed';
|
|
41
|
+
refusal?: { code?: string; message: string };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One page of the ring.
|
|
46
|
+
*
|
|
47
|
+
* `dropped` is what the ring could not keep — an absence is named rather than left to
|
|
48
|
+
* look like a quiet period.
|
|
49
|
+
*/
|
|
50
|
+
export interface CallPage {
|
|
51
|
+
calls: CallRecord[];
|
|
52
|
+
cursor: number;
|
|
53
|
+
inFlight: number;
|
|
54
|
+
dropped: number;
|
|
55
|
+
}
|
package/src/contract.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* is allowed in — measured, it imports no node builtin either.
|
|
10
10
|
*/
|
|
11
11
|
import { lowerFirst } from '@fougere/schema';
|
|
12
|
-
import { canonicalInvocation, type InvocationContext } from './
|
|
12
|
+
import { canonicalInvocation, type InvocationContext } from './contract/Invocation.js';
|
|
13
13
|
import type { FrondCall } from './wire/call.js';
|
|
14
14
|
|
|
15
15
|
export { FougereError, ErrorCode, validationErrorsOf } from './wire/errors.js';
|
|
@@ -17,14 +17,23 @@ export { FougereError, ErrorCode, validationErrorsOf } from './wire/errors.js';
|
|
|
17
17
|
// core's boot — reaching it through the main entry dragged the scanner into a bundle.
|
|
18
18
|
export { toPublicError } from './wire/http-error.js';
|
|
19
19
|
export type { FougereErrorOptions } from './wire/errors.js';
|
|
20
|
-
export { canonicalInvocation, EMPTY_INVOCATION } from './
|
|
21
|
-
export type { InvocationContext } from './
|
|
20
|
+
export { canonicalInvocation, EMPTY_INVOCATION } from './contract/Invocation.js';
|
|
21
|
+
export type { InvocationContext } from './contract/Invocation.js';
|
|
22
22
|
export { Invocation } from './contract/Invocation.js';
|
|
23
23
|
export type { InvocationInput } from './contract/Invocation.js';
|
|
24
24
|
export { Call } from './contract/Call.js';
|
|
25
25
|
export { RouteAddress } from './contract/RouteAddress.js';
|
|
26
26
|
export type { RouteAddressInput } from './contract/RouteAddress.js';
|
|
27
27
|
export type { FrondCall, Transport, SignedCall } from './wire/call.js';
|
|
28
|
+
export { MAX_BODY_BYTES } from './wire/call.js';
|
|
29
|
+
// The reserved entity, VALUE and not type: a consumer that wants to leave it alone — a
|
|
30
|
+
// call log ignoring its own reader — has to be able to name it.
|
|
31
|
+
export { RPC_ENTITY } from './wire/call.js';
|
|
32
|
+
export type { CallPage, CallRecord } from './contract/CallLog.js';
|
|
33
|
+
// The comparison of two cards, which a consumer runs about a producer — browser-safe on
|
|
34
|
+
// purpose: a panel showing the drift holds only the two cards, never the app.
|
|
35
|
+
export { driftOf, agrees, explain } from './wire/drift.js';
|
|
36
|
+
export type { CardDrift } from './wire/drift.js';
|
|
28
37
|
|
|
29
38
|
/**
|
|
30
39
|
* What `rpc.discover` answers. It belongs here and not to the runtime: the
|
|
@@ -2,10 +2,30 @@ import type { DispatchEvent, DispatchObserver } from './DispatchEvent.js';
|
|
|
2
2
|
|
|
3
3
|
/** Publishes dispatch transitions without participating in dispatch decisions. */
|
|
4
4
|
export class DispatchLifecycle {
|
|
5
|
+
private readonly observers: DispatchObserver[];
|
|
6
|
+
|
|
5
7
|
constructor(
|
|
6
|
-
|
|
8
|
+
observers: readonly DispatchObserver[] = [],
|
|
7
9
|
private readonly diagnose: (error: unknown, event: DispatchEvent) => void = () => {},
|
|
8
|
-
) {
|
|
10
|
+
) {
|
|
11
|
+
this.observers = [...observers];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe after the dispatcher was built, and get the unsubscription back.
|
|
16
|
+
*
|
|
17
|
+
* The list used to be settled at construction, so only the caller of `createApp` could
|
|
18
|
+
* observe — an extension, which runs in `up(app)`, could not. `app.use` already adds a
|
|
19
|
+
* middleware this late; this is its dual, and the pair is *participate* / *watch*.
|
|
20
|
+
*/
|
|
21
|
+
add(observer: DispatchObserver): () => void {
|
|
22
|
+
this.observers.push(observer);
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
const at = this.observers.indexOf(observer);
|
|
26
|
+
if (at !== -1) this.observers.splice(at, 1);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
9
29
|
|
|
10
30
|
publish(event: DispatchEvent): void {
|
|
11
31
|
for (const observer of this.observers) {
|
package/src/dispatch/InFlight.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* container, filesystem, migration or handler construction is involved.
|
|
7
7
|
*/
|
|
8
8
|
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
9
|
+
import { statementDrift } from './boot/statement-drift.js';
|
|
9
10
|
import type { BindingPlan } from './boot/binding.js';
|
|
10
11
|
import { targetOf } from './prefab/prefab.js';
|
|
11
12
|
import type {
|
|
@@ -166,6 +167,9 @@ export function resolveEffectiveOperations(
|
|
|
166
167
|
const effective = new Map<string, EffectiveOperation>();
|
|
167
168
|
byHandler.set(handler, effective);
|
|
168
169
|
const contracts = resolveContracts(handler, frond.operationsOverrides, collectorNames);
|
|
170
|
+
// A statement wins over the scan on purpose; saying so out loud is what keeps the
|
|
171
|
+
// win from hiding a rename. Compared here, where both readings are in hand.
|
|
172
|
+
resolutionDiagnostics.push(...statementDrift(frond, handler));
|
|
169
173
|
|
|
170
174
|
for (const [name, rawContract] of contracts) {
|
|
171
175
|
const subject = `${handler.ctor.name}.${name}`;
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,12 @@ export { RouteAddress } from './contract/RouteAddress.js';
|
|
|
31
31
|
export type { RouteAddressInput } from './contract/RouteAddress.js';
|
|
32
32
|
export { DispatchLifecycle } from './dispatch/DispatchLifecycle.js';
|
|
33
33
|
export { Dispatcher } from './dispatch/Dispatcher.js';
|
|
34
|
-
|
|
34
|
+
// The class and not only its type: an observer RECEIVES these, so a package that folds
|
|
35
|
+
// them — or a test of one — has to be able to make one through the door.
|
|
36
|
+
export { DispatchEvent } from './dispatch/DispatchEvent.js';
|
|
37
|
+
export type { CallPage, CallRecord } from './contract/CallLog.js';
|
|
38
|
+
export { driftOf, agrees, explain, type CardDrift } from './wire/drift.js';
|
|
39
|
+
export type { DispatchObserver } from './dispatch/DispatchEvent.js';
|
|
35
40
|
export type { DispatchPort } from './dispatch/DispatchPort.js';
|
|
36
41
|
export {
|
|
37
42
|
inferOperationKind,
|
|
@@ -62,7 +67,16 @@ export type {
|
|
|
62
67
|
export type { ModuleLoader } from './loader.js';
|
|
63
68
|
export { FougereError, ErrorCode, validationErrorsOf, type FougereErrorOptions } from './wire/errors.js';
|
|
64
69
|
export type { OperationContext, AppNext, AppMiddleware } from './wire/middleware.js';
|
|
65
|
-
export { createLocalRunner, createAppRunner, assertIdentityCard } from './wire/call.js';
|
|
70
|
+
export { createLocalRunner, createAppRunner, assertIdentityCard, RPC_ENTITY, MAX_BODY_BYTES } from './wire/call.js';
|
|
71
|
+
|
|
72
|
+
// A frond an app STATES, for a boot that will not scan. No disk, no `typescript` — which
|
|
73
|
+
// is why it sits here and not in `/node`, beside the scanner that reads a project.
|
|
74
|
+
export { frond } from './scan/declare.js';
|
|
75
|
+
export type { FrondDeclaration, Declared, DeclaredSubject } from './scan/declare.js';
|
|
76
|
+
// What this app would answer to `rpc.discover`. Served by the boot, and reached until now
|
|
77
|
+
// only by tests through a deep path — a consumer comparing its own card to a producer's
|
|
78
|
+
// had to dispatch a call to ask a question about itself.
|
|
79
|
+
export { identityCardOf } from './wire/call.js';
|
|
66
80
|
export type {
|
|
67
81
|
FrondCall, Transport, IdentityCard, CardOp, Facade, RpcAnswer,
|
|
68
82
|
TopologyReport, FrondPlacement, Edge,
|
package/src/node.ts
CHANGED
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { boot } from './boot/boot.js';
|
|
13
13
|
|
|
14
|
-
export { scanProject, frondAliases } from './scan/scanner.js';
|
|
14
|
+
export { scanProject, frondAliases, watchPathsOf } from './scan/scanner.js';
|
|
15
15
|
export {
|
|
16
16
|
type Conventions, type ConventionsInput, DEFAULT_CONVENTIONS,
|
|
17
|
-
resolveConventions, frondPackage, frondDirsOf, providerDirsOf,
|
|
17
|
+
resolveConventions, frondPackage, frondDirsOf, providerDirsOf,
|
|
18
18
|
} from './scan/conventions.js';
|
|
19
|
+
export { RUNTIME_PACKAGES } from './scan/bundling.js';
|
|
19
20
|
export { emitScan, type EmitOptions } from './scan/emit.js';
|
|
21
|
+
export { emitStatement } from './scan/statement.js';
|
|
20
22
|
export { setModuleLoader, getModuleLoader } from './loader.js';
|
|
21
23
|
export { loadConfig, loadCascadedConfig } from './config-loader.js';
|
|
22
24
|
export { defineFrond } from './frond-config.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Fougere boot loads at RUNTIME, so a bundler must leave it alone.
|
|
3
|
+
*
|
|
4
|
+
* Stated once because it was stated twice and the two had already drifted: the Vite plugin
|
|
5
|
+
* listed `@fougere/schema`, `@fougere/adapter-graphql` and `@fougere/auth-better`, the Next
|
|
6
|
+
* one did not, and BOTH omitted the two packages `@fougere/app`'s boot actually imports
|
|
7
|
+
* dynamically — `@fougere/defaults` and `@fougere/transport-http`. A list a bundler reads
|
|
8
|
+
* has no way to notice it is wrong: nothing fails at build time, the app fails at boot.
|
|
9
|
+
*
|
|
10
|
+
* It sits here rather than beside the dynamic imports because both readers are BUILD-TIME
|
|
11
|
+
* plugins that already depend on core, and neither may pull the boot into a bundler config.
|
|
12
|
+
*/
|
|
13
|
+
export const RUNTIME_PACKAGES: readonly string[] = [
|
|
14
|
+
'@fougere/app',
|
|
15
|
+
'@fougere/core',
|
|
16
|
+
'@fougere/schema',
|
|
17
|
+
'@fougere/defaults',
|
|
18
|
+
'@fougere/transport-http',
|
|
19
|
+
'@fougere/adapter-sql',
|
|
20
|
+
'@fougere/adapter-graphql',
|
|
21
|
+
'@fougere/auth-better',
|
|
22
|
+
'better-sqlite3',
|
|
23
|
+
'jiti',
|
|
24
|
+
'typescript',
|
|
25
|
+
];
|
package/src/scan/conventions.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* literals in five packages, so a project could not move any of them and `frondsDir` was
|
|
7
7
|
* a declared key with no reader.
|
|
8
8
|
*/
|
|
9
|
-
import { join } from 'node:path';
|
|
10
9
|
|
|
11
10
|
export interface Conventions {
|
|
12
11
|
/**
|
|
@@ -103,20 +102,3 @@ export function frondDirsOf(conventions: Conventions): string[] {
|
|
|
103
102
|
entities, handlers, presenters, collectors, seeds, versions, ...providerDirsOf(conventions),
|
|
104
103
|
])];
|
|
105
104
|
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* What changes when a frond's domain changes — the paths a dev loop watches.
|
|
109
|
-
*
|
|
110
|
-
* The root frond IS the scan root, so watching its path would match every write in the
|
|
111
|
-
* project: `.nuxt/`, `node_modules/`, the build output. Its convention directories are
|
|
112
|
-
* the frond, and they are what a scan re-reads.
|
|
113
|
-
*/
|
|
114
|
-
export function watchPathsOf(
|
|
115
|
-
frond: { source: { path: string } },
|
|
116
|
-
scanRoot: string,
|
|
117
|
-
conventions: Conventions,
|
|
118
|
-
): string[] {
|
|
119
|
-
return frond.source.path === scanRoot
|
|
120
|
-
? frondDirsOf(conventions).map((dir) => join(scanRoot, dir))
|
|
121
|
-
: [frond.source.path];
|
|
122
|
-
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A frond stated by its author, for an app that will not scan.
|
|
3
|
+
*
|
|
4
|
+
* The scan reads the type checker because a signature IS a declaration — and that reading
|
|
5
|
+
* has to happen while the types still exist, which is build time. An app that wants
|
|
6
|
+
* neither a build step nor `typescript` at runtime says the same thing here instead, and
|
|
7
|
+
* `createApp` cannot tell the difference: it consumes a `ScanResult`, never a scanner.
|
|
8
|
+
*
|
|
9
|
+
* What it does NOT ask for is the point. Every name the scan derives from a class is
|
|
10
|
+
* derived here the same way — `PostHandler` answers at `post`, `Post` is stored as `post`
|
|
11
|
+
* — so a declaration states classes and nothing else. `filePath` is empty because there is
|
|
12
|
+
* no file to point at, and a diagnostic that would have quoted one says so plainly.
|
|
13
|
+
*
|
|
14
|
+
* Measured on `demos/nuxt-blog`: 23 of its 29 operations need no word here at all, because
|
|
15
|
+
* `Crud.__ops` declares them at runtime. Only the six the author wrote by hand cost
|
|
16
|
+
* anything, and they cost `frond.config.ts` — where what is not derivable is stated.
|
|
17
|
+
*/
|
|
18
|
+
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
19
|
+
import type {
|
|
20
|
+
CollectorEntry, EntityEntry, FrondDescriptor, HandlerEntry,
|
|
21
|
+
PresenterEntry, ProviderEntry, SeedEntry,
|
|
22
|
+
} from './frond.js';
|
|
23
|
+
import { DEFAULT_CONVENTIONS } from './conventions.js';
|
|
24
|
+
import { getPresenterFields } from '../prefab/presenter.js';
|
|
25
|
+
|
|
26
|
+
/** A class, as a declaration hands it over: the constructor itself. */
|
|
27
|
+
type Ctor = new (...args: never[]) => unknown;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What a subject needs beyond its class, when its constructor names a frame or a port.
|
|
31
|
+
*
|
|
32
|
+
* `deps` is the one thing no runtime can recover: TypeScript erases the parameter types,
|
|
33
|
+
* and `registerFrames` reads them to know which frames to build — asking for one IS
|
|
34
|
+
* declaring it. A handler that takes only its own entity's ORM needs nothing here.
|
|
35
|
+
*/
|
|
36
|
+
export interface DeclaredSubject {
|
|
37
|
+
ctor: Ctor;
|
|
38
|
+
deps?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A handler, and the surface it answers on when it is not the default one. */
|
|
42
|
+
export interface DeclaredHandler extends DeclaredSubject {
|
|
43
|
+
/**
|
|
44
|
+
* The scan reads this from the directory (`handlers/public/`), so a statement has to
|
|
45
|
+
* say it: two handlers over one entity collide on their address otherwise, and the
|
|
46
|
+
* refusal names the same route twice.
|
|
47
|
+
*/
|
|
48
|
+
surface?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A class on its own, or a class with what it asks for. */
|
|
52
|
+
export type Declared = Ctor | DeclaredSubject;
|
|
53
|
+
|
|
54
|
+
const ctorOf = (d: Declared): Ctor => (typeof d === 'function' ? d : d.ctor);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What a prefab was BUILT ON — `Presenter(Post)` and `Collector(User)` both keep it under
|
|
58
|
+
* `__entity`, which is the only place it survives: nothing in the FORM of `PostPresenter`
|
|
59
|
+
* says `Post`. Absent, the class did not come through the prefab, and the refusal says so
|
|
60
|
+
* rather than yielding a frond whose presenter belongs to no entity.
|
|
61
|
+
*/
|
|
62
|
+
function subjectOf(ctor: Ctor, kind: string): { name: string } {
|
|
63
|
+
const subject = (ctor as unknown as { __entity?: { name: string } }).__entity;
|
|
64
|
+
if (!subject?.name) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${ctor.name} is declared as a ${kind} but does not extend ${kind === 'presenter' ? 'Presenter(Entity)' : 'Collector(Type)'}, `
|
|
67
|
+
+ `so what it is about cannot be read.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return subject;
|
|
72
|
+
}
|
|
73
|
+
const depsOf = (d: Declared): string[] => (typeof d === 'function' ? [] : d.deps ?? []);
|
|
74
|
+
|
|
75
|
+
/** `PostHandler` answers at `post` — the same rule the scan applies to a file it found. */
|
|
76
|
+
function addressOf(className: string): string {
|
|
77
|
+
const base = className.endsWith('Handler') ? className.slice(0, -7) : className;
|
|
78
|
+
|
|
79
|
+
return lowerFirst(base);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What a declaration states about one frond. Everything else is derived from the classes. */
|
|
83
|
+
export interface FrondDeclaration {
|
|
84
|
+
entities?: SchemaView[];
|
|
85
|
+
handlers?: (Ctor | DeclaredHandler)[];
|
|
86
|
+
presenters?: (Ctor | DeclaredSubject)[];
|
|
87
|
+
collectors?: (Ctor | DeclaredSubject)[];
|
|
88
|
+
providers?: Declared[];
|
|
89
|
+
seeds?: { entityName: string; data: SeedEntry['data'] }[];
|
|
90
|
+
/** Per-surface entity lists — the same key `frond.config.ts` states. */
|
|
91
|
+
surfaces?: Record<string, string[]>;
|
|
92
|
+
/** The import scope this frond answers under. Defaults to the conventional one. */
|
|
93
|
+
scope?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* State a frond without reading a disk.
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* createApp({
|
|
101
|
+
* scan: { fronds: Fronds.scanned([frond('blog', { entities: [Post], handlers: [PostHandler] })]), diagnostics: [] },
|
|
102
|
+
* })
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function frond(name: string, declared: FrondDeclaration = {}): FrondDescriptor {
|
|
106
|
+
const scope = declared.scope ?? DEFAULT_CONVENTIONS.scope;
|
|
107
|
+
|
|
108
|
+
const entities: EntityEntry[] = (declared.entities ?? []).map((entityClass) => ({
|
|
109
|
+
name: lowerFirst((entityClass as { name: string }).name),
|
|
110
|
+
entityClass,
|
|
111
|
+
filePath: '',
|
|
112
|
+
exposed: true,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const handlers: HandlerEntry[] = (declared.handlers ?? []).map((h) => {
|
|
116
|
+
const ctor = ctorOf(h);
|
|
117
|
+
const surface = typeof h === 'function' ? undefined : h.surface;
|
|
118
|
+
const address = addressOf(ctor.name);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: ctor.name,
|
|
122
|
+
address,
|
|
123
|
+
ctor,
|
|
124
|
+
// A handler about no stored row is ordinary — the address is not a promise that an
|
|
125
|
+
// entity carries it, which is why this is not looked up.
|
|
126
|
+
entityName: address,
|
|
127
|
+
operations: new Map(),
|
|
128
|
+
deps: depsOf(h),
|
|
129
|
+
filePath: '',
|
|
130
|
+
exposed: true,
|
|
131
|
+
...(surface ? { surface } : {}),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Read off the class, never restated: `Presenter(Post)` keeps `Post` in `__entity` and a
|
|
136
|
+
// computed field IS a method, which `getPresenterFields` already reads from the prototype.
|
|
137
|
+
// Asking for `entityName` and `fields` made a statement copy what the class carries — and
|
|
138
|
+
// a copy that drifts silently, since nothing compares the two.
|
|
139
|
+
const presenters: PresenterEntry[] = (declared.presenters ?? []).map((p) => {
|
|
140
|
+
const ctor = ctorOf(p);
|
|
141
|
+
const fields = getPresenterFields(ctor);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
entityName: lowerFirst(subjectOf(ctor, 'presenter').name),
|
|
145
|
+
ctor,
|
|
146
|
+
fields,
|
|
147
|
+
fieldMeta: fields.map((field) => ({ name: field })),
|
|
148
|
+
deps: depsOf(p),
|
|
149
|
+
filePath: '',
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// `Collector(User)` keeps `User` the same way — the type a handler names to receive it.
|
|
154
|
+
const collectors: CollectorEntry[] = (declared.collectors ?? []).map((c) => {
|
|
155
|
+
const ctor = ctorOf(c);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
typeName: subjectOf(ctor, 'collector').name,
|
|
159
|
+
ctor,
|
|
160
|
+
deps: depsOf(c),
|
|
161
|
+
filePath: '',
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const providers: ProviderEntry[] = (declared.providers ?? []).map((p) => ({
|
|
166
|
+
ctor: ctorOf(p),
|
|
167
|
+
deps: depsOf(p),
|
|
168
|
+
filePath: '',
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
const seeds: SeedEntry[] = (declared.seeds ?? []).map((s) => ({
|
|
172
|
+
entityName: s.entityName,
|
|
173
|
+
data: s.data,
|
|
174
|
+
filePath: '',
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
name,
|
|
179
|
+
source: { path: '', package: `${scope}/${name}` },
|
|
180
|
+
providers,
|
|
181
|
+
entities,
|
|
182
|
+
handlers,
|
|
183
|
+
presenters,
|
|
184
|
+
collectors,
|
|
185
|
+
seeds,
|
|
186
|
+
...(declared.surfaces ? { surfaces: declared.surfaces } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -800,7 +800,6 @@ function parseClassMethods(
|
|
|
800
800
|
const cls = findDefaultClass(source);
|
|
801
801
|
if (!cls) return { methods: [], unresolvedHeritage: unresolved };
|
|
802
802
|
|
|
803
|
-
// Parse child class methods
|
|
804
803
|
const childMethods = extractClassMethods(cls, source, skip, checker);
|
|
805
804
|
const childNames = new Set(childMethods.map((m) => m.name));
|
|
806
805
|
|
package/src/scan/scanner.ts
CHANGED
|
@@ -458,7 +458,6 @@ async function toPresenterEntry(filePath: string): Promise<PresenterEntry | null
|
|
|
458
458
|
const presenterParams = await ctorParamsOf(filePath);
|
|
459
459
|
const deps = presenterParams.map((p) => depKeyOf(p.type));
|
|
460
460
|
|
|
461
|
-
// Parse method return types from source
|
|
462
461
|
let fieldMeta: PresenterEntry['fieldMeta'] = [];
|
|
463
462
|
try {
|
|
464
463
|
const parsed = await presenterMethodsOf(filePath);
|
|
@@ -756,3 +755,25 @@ export async function scanProject(
|
|
|
756
755
|
|
|
757
756
|
return { fronds, diagnostics };
|
|
758
757
|
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* What changes when a frond's domain changes — the paths a dev loop watches.
|
|
761
|
+
*
|
|
762
|
+
* It lives beside the scan rather than beside the conventions it reads, because it turns
|
|
763
|
+
* names into DISK paths: `join` is `node:path`, and `conventions.ts` is reached from
|
|
764
|
+
* `index` through `frond()`, which an edge bundle imports. Measured — esbuild refused the
|
|
765
|
+
* Worker with `Could not resolve "node:path"`.
|
|
766
|
+
*
|
|
767
|
+
* The root frond IS the scan root, so watching its path would match every write in the
|
|
768
|
+
* project: `.nuxt/`, `node_modules/`, the build output. Its convention directories are
|
|
769
|
+
* the frond, and they are what a scan re-reads.
|
|
770
|
+
*/
|
|
771
|
+
export function watchPathsOf(
|
|
772
|
+
frond: { source: { path: string } },
|
|
773
|
+
scanRoot: string,
|
|
774
|
+
conventions: Conventions,
|
|
775
|
+
): string[] {
|
|
776
|
+
return frond.source.path === scanRoot
|
|
777
|
+
? frondDirsOf(conventions).map((dir) => join(scanRoot, dir))
|
|
778
|
+
: [frond.source.path];
|
|
779
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scan, written as the STATEMENT an author would have written.
|
|
3
|
+
*
|
|
4
|
+
* A scan runs at build and the app runs elsewhere — two runtimes, no shared memory — so
|
|
5
|
+
* something has to cross. What used to cross was the scan RESULT: every operation's
|
|
6
|
+
* TypeScript signature re-serialized as JSON, beside the five CRUD ops `Crud.__ops`
|
|
7
|
+
* declares at runtime anyway. A second writing of what the classes carry, and one that
|
|
8
|
+
* drifts, since nothing compares the two.
|
|
9
|
+
*
|
|
10
|
+
* What crosses here is what `fronds.ts` holds: imports that bring the ORIGINAL classes
|
|
11
|
+
* back, and `frond()` calls. Everything else `frond()` derives — `Post` is stored as
|
|
12
|
+
* `post`, `PostHandler` answers at `post`, a computed field IS a method, and
|
|
13
|
+
* `Presenter(Post)` keeps its subject. Only what TypeScript erases is named: a
|
|
14
|
+
* constructor's parameter types (`deps`), and the surface a handler answers on, which the
|
|
15
|
+
* scan read from its directory.
|
|
16
|
+
*
|
|
17
|
+
* Measured on demos/nuxt-blog: 102 lines of scan against 31 of statement, and the same app
|
|
18
|
+
* — same rows, same computed fields, and no `typescript` loaded at boot.
|
|
19
|
+
*/
|
|
20
|
+
import { relative } from 'node:path';
|
|
21
|
+
import type { FrondDescriptor, ScanResult } from './frond.js';
|
|
22
|
+
|
|
23
|
+
type Live = { name?: string };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A file becomes the specifier the PROJECT already uses for it: `@fronds/blog/…`, the
|
|
27
|
+
* import scope a handler names its neighbour by, and which the Nuxt module registers as an
|
|
28
|
+
* alias for every frond it found.
|
|
29
|
+
*
|
|
30
|
+
* A relative path would name the same file by a second route, and two routes to one module
|
|
31
|
+
* are two modules — so `Post` would not equal `Post` and every identity check would fail
|
|
32
|
+
* quietly. It also puts the file outside what the alias resolves, which is what made Node,
|
|
33
|
+
* not the bundler, read the frond's `.ts` and answer 500 on `../entities/Post.js`.
|
|
34
|
+
*
|
|
35
|
+
* `.js`, because the source is `.ts` and the project spells a TypeScript source that way
|
|
36
|
+
* everywhere else — the alias resolves through the same rule as a hand-written import.
|
|
37
|
+
*/
|
|
38
|
+
function specifierOf(filePath: string, frond: FrondDescriptor): string {
|
|
39
|
+
const inside = relative(frond.source.path, filePath).replace(/\.tsx?$/, '.js');
|
|
40
|
+
|
|
41
|
+
return `${frond.source.package}/${inside}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One alias per file, so a class imported twice is one binding and one identity. */
|
|
45
|
+
class Imports {
|
|
46
|
+
private readonly byPath = new Map<string, string>();
|
|
47
|
+
private readonly lines: string[] = [];
|
|
48
|
+
|
|
49
|
+
default(filePath: string, frond: FrondDescriptor): string {
|
|
50
|
+
const known = this.byPath.get(filePath);
|
|
51
|
+
if (known) return known;
|
|
52
|
+
const alias = `_${this.byPath.size}`;
|
|
53
|
+
this.lines.push(`import ${alias} from '${specifierOf(filePath, frond)}';`);
|
|
54
|
+
this.byPath.set(filePath, alias);
|
|
55
|
+
|
|
56
|
+
return alias;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render(): string {
|
|
60
|
+
return this.lines.join('\n');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `{ ctor: X, deps: [...] }` when there is something to say, the bare class otherwise. */
|
|
65
|
+
function subject(alias: string, deps: string[], extra = ''): string {
|
|
66
|
+
const parts = [
|
|
67
|
+
...(deps.length ? [`deps: ${JSON.stringify(deps)}`] : []),
|
|
68
|
+
...(extra ? [extra] : []),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
return parts.length ? `{ ctor: ${alias}, ${parts.join(', ')} }` : alias;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function frondOf(frond: FrondDescriptor, imports: Imports): string {
|
|
75
|
+
const list = (items: string[]): string => `[${items.join(', ')}]`;
|
|
76
|
+
const members: string[] = [];
|
|
77
|
+
|
|
78
|
+
if (frond.entities.length) {
|
|
79
|
+
members.push(`entities: ${list(frond.entities.map((e) => imports.default(e.filePath, frond)))}`);
|
|
80
|
+
}
|
|
81
|
+
if (frond.handlers.length) {
|
|
82
|
+
members.push(`handlers: ${list(frond.handlers.map((h) =>
|
|
83
|
+
subject(imports.default(h.filePath, frond), h.deps, h.surface ? `surface: ${JSON.stringify(h.surface)}` : '')))}`);
|
|
84
|
+
}
|
|
85
|
+
if (frond.presenters.length) {
|
|
86
|
+
members.push(`presenters: ${list(frond.presenters.map((p) => subject(imports.default(p.filePath, frond), p.deps)))}`);
|
|
87
|
+
}
|
|
88
|
+
if (frond.collectors.length) {
|
|
89
|
+
members.push(`collectors: ${list(frond.collectors.map((c) => subject(imports.default(c.filePath, frond), c.deps)))}`);
|
|
90
|
+
}
|
|
91
|
+
if (frond.providers.length) {
|
|
92
|
+
members.push(`providers: ${list(frond.providers.map((p) => subject(imports.default(p.filePath, frond), p.deps)))}`);
|
|
93
|
+
}
|
|
94
|
+
// A seed is DATA, not a class — the one member a statement cannot derive from an import.
|
|
95
|
+
if (frond.seeds.length) {
|
|
96
|
+
members.push(`seeds: ${JSON.stringify(frond.seeds.map((s) => ({ entityName: s.entityName, data: s.data })))}`);
|
|
97
|
+
}
|
|
98
|
+
if (frond.surfaces) members.push(`surfaces: ${JSON.stringify(frond.surfaces)}`);
|
|
99
|
+
|
|
100
|
+
const scope = (frond.source.package as string | undefined)?.split('/')[0];
|
|
101
|
+
if (scope) members.push(`scope: ${JSON.stringify(scope)}`);
|
|
102
|
+
|
|
103
|
+
return ` frond(${JSON.stringify(frond.name)}, {\n ${members.join(',\n ')},\n })`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The scan as a list of `frond()` calls — the file an author writes, written for them. */
|
|
107
|
+
// Nothing is written relative to the destination, so where the file SITS is not a
|
|
108
|
+
// parameter — the alias resolves the same from anywhere.
|
|
109
|
+
export function emitStatement(scan: ScanResult): string {
|
|
110
|
+
const imports = new Imports();
|
|
111
|
+
const fronds = scan.fronds.map((f) => frondOf(f as FrondDescriptor & { source: Live }, imports));
|
|
112
|
+
|
|
113
|
+
return [
|
|
114
|
+
'// Generated by @fougere/nuxt — what the scan found, as the statement you would write.',
|
|
115
|
+
'//',
|
|
116
|
+
'// Classes are IMPORTED, never restated: `frond()` derives every name from them. Write',
|
|
117
|
+
'// your own `fronds.ts` beside `fougere.config.ts` to state this by hand instead.',
|
|
118
|
+
"import { frond } from '@fougere/core';",
|
|
119
|
+
imports.render(),
|
|
120
|
+
'',
|
|
121
|
+
'export default [',
|
|
122
|
+
`${fronds.join(',\n')},`,
|
|
123
|
+
'];',
|
|
124
|
+
'',
|
|
125
|
+
].join('\n');
|
|
126
|
+
}
|
package/src/wire/call.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Card, type SchemaDescriptor } from '@fougere/schema';
|
|
10
10
|
import { factsAnnouncedBy } from '../emit.js';
|
|
11
|
-
import type { InvocationContext } from '
|
|
11
|
+
import type { InvocationContext } from '../contract/Invocation.js';
|
|
12
12
|
import { FougereError, ErrorCode } from './errors.js';
|
|
13
13
|
import type { App } from '../boot/types.js';
|
|
14
14
|
import { createTransportEntry } from '../entry/TransportEntry.js';
|
|
@@ -27,6 +27,18 @@ export interface FrondCall {
|
|
|
27
27
|
export type Transport = (call: FrondCall, invocation: InvocationContext) => Promise<unknown>;
|
|
28
28
|
|
|
29
29
|
/** Reserved namespace — calls the runner answers itself, never a façade. */
|
|
30
|
+
/**
|
|
31
|
+
* What a receiver accepts before it stops reading a body.
|
|
32
|
+
*
|
|
33
|
+
* Stated once because it was stated FOUR times — `transport/http/policy.ts` said so in its
|
|
34
|
+
* own comment ("spelled in four packages") while three doors kept their own literal, so
|
|
35
|
+
* raising the cap here used to leave three of them refusing at the old one.
|
|
36
|
+
*
|
|
37
|
+
* `@fougere/http` keeps a fourth copy on purpose: it declares no Fougere dependency at all,
|
|
38
|
+
* and giving a leaf port a dependency on the kernel to share a number is the wrong trade.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_BODY_BYTES = 1024 * 1024;
|
|
41
|
+
|
|
30
42
|
export const RPC_ENTITY = 'rpc';
|
|
31
43
|
|
|
32
44
|
/**
|