@fougere/core 0.3.0-alpha.0 → 0.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/boot/Emissions.js +1 -1
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/HandlerFacade.js +2 -2
- package/dist/boot/HandlerFacade.js.map +1 -1
- package/dist/boot/apply.d.ts.map +1 -1
- package/dist/boot/apply.js +2 -12
- package/dist/boot/apply.js.map +1 -1
- package/dist/boot/binding.d.ts +1 -1
- package/dist/boot/binding.d.ts.map +1 -1
- package/dist/boot/binding.js +4 -4
- package/dist/boot/binding.js.map +1 -1
- package/dist/boot/boot.d.ts +1 -1
- package/dist/boot/boot.d.ts.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +26 -17
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/frame.d.ts +1 -1
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js +5 -14
- package/dist/boot/frame.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 +5 -5
- 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 +3 -3
- 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/together.d.ts.map +1 -1
- package/dist/boot/together.js +6 -6
- package/dist/boot/together.js.map +1 -1
- package/dist/boot/types.d.ts +17 -4
- 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 +9 -4
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +12 -5
- 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/dispatch/InputValidator.d.ts.map +1 -1
- package/dist/dispatch/InputValidator.js +3 -2
- package/dist/dispatch/InputValidator.js.map +1 -1
- package/dist/dispatch/OutputProjector.js +2 -2
- package/dist/dispatch/OutputProjector.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +2 -2
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/effective-operation.d.ts.map +1 -1
- package/dist/effective-operation.js +8 -5
- package/dist/effective-operation.js.map +1 -1
- package/dist/emit.d.ts +2 -2
- package/dist/emit.d.ts.map +1 -1
- package/dist/emit.js +2 -2
- package/dist/emit.js.map +1 -1
- package/dist/identity.js +1 -1
- package/dist/identity.js.map +1 -1
- package/dist/imports.d.ts +1 -1
- package/dist/imports.d.ts.map +1 -1
- package/dist/imports.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 +2 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -0
- package/dist/node.js.map +1 -1
- package/dist/orm.d.ts.map +1 -1
- package/dist/orm.js +4 -4
- package/dist/orm.js.map +1 -1
- package/dist/prefab/collector.js +2 -2
- package/dist/prefab/collector.js.map +1 -1
- package/dist/prefab/mirror.d.ts.map +1 -1
- package/dist/prefab/mirror.js +2 -2
- package/dist/prefab/mirror.js.map +1 -1
- package/dist/prefab/presenter.d.ts.map +1 -1
- package/dist/prefab/presenter.js +2 -2
- package/dist/prefab/presenter.js.map +1 -1
- package/dist/prefab/repository.d.ts.map +1 -1
- package/dist/prefab/repository.js +2 -2
- package/dist/prefab/repository.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/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/emit.d.ts.map +1 -1
- package/dist/scan/emit.js +3 -2
- package/dist/scan/emit.js.map +1 -1
- package/dist/scan/handler-parser.d.ts +2 -0
- package/dist/scan/handler-parser.d.ts.map +1 -1
- package/dist/scan/handler-parser.js +52 -13
- package/dist/scan/handler-parser.js.map +1 -1
- package/dist/scan/scanner.d.ts.map +1 -1
- package/dist/scan/scanner.js +37 -11
- 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/verify.js +2 -2
- package/dist/verify.js.map +1 -1
- package/dist/wire/call.d.ts +19 -8
- package/dist/wire/call.d.ts.map +1 -1
- package/dist/wire/call.js +17 -6
- 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 +6 -4
- package/src/boot/AppLifecycle.ts +50 -0
- package/src/boot/Emissions.ts +263 -0
- package/src/boot/HandlerFacade.ts +242 -0
- package/src/boot/ambient-port.ts +41 -0
- package/src/boot/ambient.als.ts +43 -0
- package/src/boot/ambient.queue.ts +75 -0
- package/src/boot/apply.ts +47 -0
- package/src/boot/auth.ts +55 -0
- package/src/boot/binding.ts +182 -0
- package/src/boot/boot.ts +125 -0
- package/src/boot/bootstrap.ts +847 -0
- package/src/boot/frame.ts +210 -0
- package/src/boot/hosted.ts +77 -0
- package/src/boot/ownership.ts +137 -0
- package/src/boot/ports.ts +66 -0
- package/src/boot/remote.ts +179 -0
- package/src/boot/seed.ts +158 -0
- package/src/boot/statement-drift.ts +60 -0
- package/src/boot/together.ts +215 -0
- package/src/boot/types.ts +274 -0
- package/src/builtins/config.ts +19 -0
- package/src/builtins/logger.ts +175 -0
- package/src/config-loader.ts +146 -0
- package/src/contract/Call.ts +18 -0
- package/src/contract/CallLog.ts +55 -0
- package/src/contract/Invocation.ts +72 -0
- package/src/contract/RouteAddress.ts +49 -0
- package/src/contract.ts +83 -0
- package/src/crypto/encoding.ts +59 -0
- package/src/crypto/node.ts +30 -0
- package/src/crypto/port.ts +45 -0
- package/src/crypto/webcrypto.ts +55 -0
- package/src/define.ts +10 -0
- package/src/dispatch/ArgumentResolver.ts +17 -0
- package/src/dispatch/ArrayResult.ts +10 -0
- package/src/dispatch/DispatchEvent.ts +38 -0
- package/src/dispatch/DispatchLifecycle.ts +43 -0
- package/src/dispatch/DispatchPort.ts +6 -0
- package/src/dispatch/Dispatcher.ts +48 -0
- package/src/dispatch/InFlight.ts +45 -0
- package/src/dispatch/InputValidator.ts +28 -0
- package/src/dispatch/LocalRoutePolicy.ts +34 -0
- package/src/dispatch/OperationExecutor.ts +60 -0
- package/src/dispatch/OperationRoute.ts +18 -0
- package/src/dispatch/OutputProjector.ts +32 -0
- package/src/dispatch/OutputView.ts +9 -0
- package/src/dispatch/PresenterArgumentResolver.ts +25 -0
- package/src/dispatch/PresenterExecutor.ts +55 -0
- package/src/dispatch/RemoteRouteResolver.ts +21 -0
- package/src/dispatch/Route.ts +11 -0
- package/src/dispatch/RouteNotFoundError.ts +29 -0
- package/src/dispatch/RoutePolicy.ts +8 -0
- package/src/dispatch/RouteRegistry.ts +73 -0
- package/src/dispatch/RouteResolver.ts +7 -0
- package/src/dispatch/StorageGuard.ts +71 -0
- package/src/effective-operation.ts +601 -0
- package/src/emit.ts +90 -0
- package/src/entry/DynamicFacade.ts +28 -0
- package/src/entry/FacadeEntry.ts +32 -0
- package/src/entry/TransportEntry.ts +30 -0
- package/src/frond-config.ts +138 -0
- package/src/graph.ts +180 -0
- package/src/identity-keys.ts +43 -0
- package/src/identity.ts +235 -0
- package/src/imports.ts +123 -0
- package/src/index.ts +120 -0
- package/src/loader.ts +41 -0
- package/src/node.ts +30 -0
- package/src/orm.ts +252 -0
- package/src/prefab/collector.ts +36 -0
- package/src/prefab/crud.ts +188 -0
- package/src/prefab/mirror.ts +122 -0
- package/src/prefab/prefab.ts +46 -0
- package/src/prefab/presenter.ts +63 -0
- package/src/prefab/repository.ts +170 -0
- package/src/scan/Fronds.ts +73 -0
- package/src/scan/bundling.ts +25 -0
- package/src/scan/conventions.ts +122 -0
- package/src/scan/declare.ts +188 -0
- package/src/scan/emit.ts +231 -0
- package/src/scan/frond.ts +235 -0
- package/src/scan/handler-parser.ts +817 -0
- package/src/scan/scanner.ts +757 -0
- package/src/scan/statement.ts +126 -0
- package/src/verify.ts +193 -0
- package/src/wire/call.ts +381 -0
- package/src/wire/drift.ts +131 -0
- package/src/wire/errors.ts +124 -0
- package/src/wire/http-error.ts +77 -0
- package/src/wire/loggerMiddleware.ts +29 -0
- package/src/wire/middleware.ts +47 -0
- package/src/wire/operation.ts +258 -0
- 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/boot/egress.d.ts +0 -8
- package/dist/boot/egress.d.ts.map +0 -1
- package/dist/boot/egress.js +0 -11
- package/dist/boot/egress.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
|
@@ -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/verify.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { FrondDescriptor } from './scan/frond.js';
|
|
2
|
+
import { lowerFirst } from '@fougere/schema';
|
|
3
|
+
import { repositoryKeyOf } from './prefab/repository.js';
|
|
4
|
+
import { ormKeyOf } from './orm.js';
|
|
5
|
+
import { presenterKeyOf } from './prefab/presenter.js';
|
|
6
|
+
import { collectorKeyOf } from './prefab/collector.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What a rule found in an app.
|
|
10
|
+
*
|
|
11
|
+
* `message` is the sentence the rule replaces — the prose a human used to have
|
|
12
|
+
* to remember. `Known issues` says "Keep collectors in the consuming frond";
|
|
13
|
+
* this is that sentence, told about a specific handler, by something that runs.
|
|
14
|
+
*/
|
|
15
|
+
export interface Violation {
|
|
16
|
+
/** Rule name, stable — 'cross-frond-dependency'. */
|
|
17
|
+
rule: string;
|
|
18
|
+
/**
|
|
19
|
+
* What it costs, decided by the rule that raises it — never by whoever renders it.
|
|
20
|
+
*
|
|
21
|
+
* The two rules here are not the same animal: a cross-frond dependency resolves
|
|
22
|
+
* today and stops resolving the day the other frond answers over the wire, while a
|
|
23
|
+
* collector declared elsewhere is already wrong in one process. A reader holding a
|
|
24
|
+
* table of rule names would be a second opinion on a fact the rule already has.
|
|
25
|
+
*/
|
|
26
|
+
severity: 'blocking' | 'warning';
|
|
27
|
+
/** The frond the subject lives in. */
|
|
28
|
+
frond: string;
|
|
29
|
+
/** What violates it — 'PostHandler'. */
|
|
30
|
+
subject: string;
|
|
31
|
+
/** Where to go and look. */
|
|
32
|
+
filePath: string;
|
|
33
|
+
/**
|
|
34
|
+
* What the subject reaches for, and where it actually lives.
|
|
35
|
+
*
|
|
36
|
+
* `frond` above is the CONSUMER; the target used to exist only inside the
|
|
37
|
+
* sentence. A reader that has to parse prose to learn which frond a violation
|
|
38
|
+
* points at is a second opinion on a fact the checker already held — so the
|
|
39
|
+
* boot can ask "does this violation target a frond named in `remotes:`?" by
|
|
40
|
+
* reading a field.
|
|
41
|
+
*/
|
|
42
|
+
dependsOn: { key: string; frond: string; kind: string };
|
|
43
|
+
/** What breaks, and what the caller gets instead. */
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A dependency declared in a frond's scope, and what kind of thing it is. */
|
|
48
|
+
type Registration = { frond: string; kind: string };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The container keys a frond registers in its own scope, keyed as a handler's
|
|
52
|
+
* `deps` spell them — DI resolves by type name, so both sides are PascalCase.
|
|
53
|
+
*
|
|
54
|
+
* Every key comes from the function that states it, never from a derivation
|
|
55
|
+
* respelled here: a second reader that spells one differently finds nothing and
|
|
56
|
+
* reports nothing wrong, which is the failure mode a checker must not have.
|
|
57
|
+
*/
|
|
58
|
+
function registrationsOf(frond: FrondDescriptor): Map<string, Registration> {
|
|
59
|
+
const out = new Map<string, Registration>();
|
|
60
|
+
const put = (key: string, kind: string) => out.set(key, { frond: frond.name, kind });
|
|
61
|
+
|
|
62
|
+
for (const p of frond.providers) put(p.ctor.name, 'provider');
|
|
63
|
+
for (const p of frond.presenters) put(presenterKeyOf(p.entityName), 'presenter');
|
|
64
|
+
for (const c of frond.collectors) put(collectorKeyOf(c.typeName), 'collector');
|
|
65
|
+
for (const e of frond.entities) {
|
|
66
|
+
put(ormKeyOf(e.name), 'ORM');
|
|
67
|
+
put(repositoryKeyOf(e.name), 'repository');
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Everything in a frond that is constructed by DI, so carries `deps`.
|
|
74
|
+
* Handlers, presenters and collectors are all built from a constructor whose
|
|
75
|
+
* argument types the scan read; each one can therefore reach across.
|
|
76
|
+
*/
|
|
77
|
+
function injectablesOf(frond: FrondDescriptor) {
|
|
78
|
+
return [
|
|
79
|
+
...frond.handlers.map((h) => ({ name: h.ctor.name, deps: h.deps, filePath: h.filePath })),
|
|
80
|
+
...frond.presenters.map((p) => ({ name: p.ctor.name, deps: p.deps, filePath: p.filePath })),
|
|
81
|
+
...frond.collectors.map((c) => ({ name: c.ctor.name, deps: c.deps, filePath: c.filePath })),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Does this app survive being split?
|
|
87
|
+
*
|
|
88
|
+
* A frond runs in-process or in its own process behind JSON-RPC with identical
|
|
89
|
+
* user code — that is the whole claim. What the claim does not say is that
|
|
90
|
+
* *every* app survives the move: a dependency that crosses a frond boundary
|
|
91
|
+
* resolves today because both scopes live in one container, and resolves to
|
|
92
|
+
* nothing the day the other frond answers over the wire.
|
|
93
|
+
*
|
|
94
|
+
* The question only exists where a module boundary is meant to become a process
|
|
95
|
+
* boundary later. Where every inter-module call is already remote there is
|
|
96
|
+
* nothing to check, and where no exit is planned there is nothing to check
|
|
97
|
+
* either. Here it means something, and until now it had no answer.
|
|
98
|
+
*
|
|
99
|
+
* Pure over `app.fronds`: no mount, no process, no file. Call it from a test.
|
|
100
|
+
*/
|
|
101
|
+
export function verify(app: { fronds: readonly FrondDescriptor[] }): Violation[] {
|
|
102
|
+
const index = new Map<string, Registration>();
|
|
103
|
+
for (const frond of app.fronds) {
|
|
104
|
+
for (const [key, reg] of registrationsOf(frond)) index.set(key, reg);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Which frond declares a collector for which entity. Collectors are the one
|
|
108
|
+
// thing a handler asks for by OPERATION PARAMETER, never by constructor, so
|
|
109
|
+
// they need their own index and their own rule.
|
|
110
|
+
const collectorFronds = new Map<string, string>();
|
|
111
|
+
for (const frond of app.fronds) {
|
|
112
|
+
for (const c of frond.collectors) collectorFronds.set(c.typeName, frond.name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const violations: Violation[] = [];
|
|
116
|
+
for (const frond of app.fronds) {
|
|
117
|
+
// Rule 1 — a constructor dependency declared in another frond's scope.
|
|
118
|
+
// Façade keys are exempt by construction, not by omission: a `Facade<X>`
|
|
119
|
+
// dependency is spelled camelCase (`articleHandler`) while every type key
|
|
120
|
+
// here is PascalCase, so the two namespaces cannot collide. The façade IS
|
|
121
|
+
// the sanctioned crossing — see tests/cross-frond.test.ts.
|
|
122
|
+
for (const subject of injectablesOf(frond)) {
|
|
123
|
+
for (const dep of subject.deps) {
|
|
124
|
+
const declared = index.get(dep);
|
|
125
|
+
// Not in the index at all = a builtin (Logger, Config), a
|
|
126
|
+
// façade key, or an unresolved name. None is a boundary crossing, and
|
|
127
|
+
// an unresolved dependency is the container's complaint, not this rule's.
|
|
128
|
+
if (!declared || declared.frond === frond.name) continue;
|
|
129
|
+
violations.push({
|
|
130
|
+
rule: 'cross-frond-dependency',
|
|
131
|
+
severity: 'warning',
|
|
132
|
+
frond: frond.name,
|
|
133
|
+
subject: subject.name,
|
|
134
|
+
filePath: subject.filePath,
|
|
135
|
+
dependsOn: { key: dep, frond: declared.frond, kind: declared.kind },
|
|
136
|
+
message:
|
|
137
|
+
`${subject.name} depends on ${dep}, the ${declared.kind} of frond '${declared.frond}'. ` +
|
|
138
|
+
`Each frond registers into its own scope, so this does not resolve here. ` +
|
|
139
|
+
`Declare ${dep} in '${frond.name}', or reach '${declared.frond}' through its ` +
|
|
140
|
+
`façade — the one sanctioned crossing.`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Rule 2 — an operation parameter that wanted a collector this frond has not
|
|
146
|
+
// got. The preliminary binding convention can only see this frond's collector
|
|
147
|
+
// set, so it produces body evidence. EffectiveOperation refuses that evidence
|
|
148
|
+
// before boot: another frond's collector proves the provenance is not a body.
|
|
149
|
+
const own = new Set(frond.collectors.map((c) => c.typeName));
|
|
150
|
+
for (const handler of frond.handlers) {
|
|
151
|
+
for (const [opName, op] of handler.operations) {
|
|
152
|
+
for (const param of op.signature?.params ?? []) {
|
|
153
|
+
// Same key the scan writes and the binding plan looks up — `toLowerCase()`
|
|
154
|
+
// here missed a two-word type in BOTH directions: neither confirmed the
|
|
155
|
+
// collector was local nor found it elsewhere, so the rule reported nothing.
|
|
156
|
+
const wanted = lowerFirst(param.type.name);
|
|
157
|
+
if (own.has(wanted)) continue;
|
|
158
|
+
const elsewhere = collectorFronds.get(wanted);
|
|
159
|
+
// No collector anywhere for that type may be an ordinary body parameter.
|
|
160
|
+
// Only a collector that exists in the wrong place makes that interpretation
|
|
161
|
+
// provably false.
|
|
162
|
+
if (!elsewhere) continue;
|
|
163
|
+
violations.push({
|
|
164
|
+
rule: 'collector-in-another-frond',
|
|
165
|
+
severity: 'blocking',
|
|
166
|
+
frond: frond.name,
|
|
167
|
+
subject: `${handler.ctor.name}.${opName}(${param.name})`,
|
|
168
|
+
filePath: handler.filePath,
|
|
169
|
+
dependsOn: { key: collectorKeyOf(wanted), frond: elsewhere, kind: 'collector' },
|
|
170
|
+
message:
|
|
171
|
+
`'${param.name}' is typed ${param.type.name}, and the collector that produces one ` +
|
|
172
|
+
`is declared in frond '${elsewhere}'. A binding plan only sees its own frond's ` +
|
|
173
|
+
`collectors, so the preliminary body interpretation is invalid. Fougere refuses ` +
|
|
174
|
+
`the operation before it can receive what the caller sent. ` +
|
|
175
|
+
`Move the collector into '${frond.name}'.`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return violations;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The same verdict, as an assertion. Throws naming every rule that failed, so a
|
|
186
|
+
* test is one line and its output is the sentence — not a diff of two objects.
|
|
187
|
+
*/
|
|
188
|
+
export function assertSplittable(app: { fronds: readonly FrondDescriptor[] }): void {
|
|
189
|
+
const violations = verify(app);
|
|
190
|
+
if (violations.length === 0) return;
|
|
191
|
+
const lines = violations.map((v) => ` [${v.rule}] ${v.frond}/${v.subject}\n ${v.message}\n ${v.filePath}`);
|
|
192
|
+
throw new Error(`${violations.length} violation(s) — this app does not survive a split:\n${lines.join('\n')}`);
|
|
193
|
+
}
|