@fougere/compiler 0.9.2-alpha.0 → 0.10.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/dist/boot.d.ts.map +1 -1
- package/dist/boot.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/scan/Aliases.d.ts +16 -0
- package/dist/scan/Aliases.d.ts.map +1 -0
- package/dist/scan/Aliases.js +2 -0
- package/dist/scan/Aliases.js.map +1 -0
- package/dist/scan/FacadeOptions.d.ts +5 -0
- package/dist/scan/FacadeOptions.d.ts.map +1 -0
- package/dist/scan/FacadeOptions.js +2 -0
- package/dist/scan/FacadeOptions.js.map +1 -0
- package/dist/scan/{contract.d.ts → Live.d.ts} +2 -16
- package/dist/scan/Live.d.ts.map +1 -0
- package/dist/scan/{contract.js → Live.js} +5 -1
- package/dist/scan/Live.js.map +1 -0
- package/dist/scan/Served.d.ts +47 -0
- package/dist/scan/Served.d.ts.map +1 -0
- package/dist/scan/Served.js +111 -0
- package/dist/scan/Served.js.map +1 -0
- package/dist/scan/emit.d.ts.map +1 -1
- package/dist/scan/emit.js +1 -1
- package/dist/scan/emit.js.map +1 -1
- package/dist/scan/handler-parser.d.ts +8 -0
- package/dist/scan/handler-parser.d.ts.map +1 -1
- package/dist/scan/handler-parser.js +24 -6
- package/dist/scan/handler-parser.js.map +1 -1
- package/dist/scan/names.d.ts +7 -0
- package/dist/scan/names.d.ts.map +1 -0
- package/dist/scan/names.js +93 -0
- package/dist/scan/names.js.map +1 -0
- package/dist/scan/refusals.d.ts +33 -0
- package/dist/scan/refusals.d.ts.map +1 -0
- package/dist/scan/refusals.js +118 -0
- package/dist/scan/refusals.js.map +1 -0
- package/dist/scan/scanner.d.ts.map +1 -1
- package/dist/scan/scanner.js +38 -3
- package/dist/scan/scanner.js.map +1 -1
- package/dist/scan/statement.d.ts.map +1 -1
- package/dist/scan/statement.js +1 -1
- package/dist/scan/statement.js.map +1 -1
- package/package.json +6 -6
- package/src/boot.ts +1 -1
- package/src/index.ts +3 -0
- package/src/scan/Aliases.ts +16 -0
- package/src/scan/FacadeOptions.ts +4 -0
- package/src/scan/{contract.ts → Live.ts} +4 -16
- package/src/scan/Served.ts +135 -0
- package/src/scan/emit.ts +2 -1
- package/src/scan/handler-parser.ts +34 -7
- package/src/scan/names.ts +101 -0
- package/src/scan/refusals.ts +167 -0
- package/src/scan/scanner.ts +49 -7
- package/src/scan/statement.ts +2 -1
- package/dist/scan/contract.d.ts.map +0 -1
- package/dist/scan/contract.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statement.d.ts","sourceRoot":"","sources":["../../src/scan/statement.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"statement.d.ts","sourceRoot":"","sources":["../../src/scan/statement.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AA0IhD,2FAA2F;AAG3F,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiBtD"}
|
package/dist/scan/statement.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { nameOf } from '@fougere/core/descriptor';
|
|
2
2
|
import { relative } from 'node:path';
|
|
3
|
-
import { operationsOf } from './
|
|
3
|
+
import { operationsOf } from './Live.js';
|
|
4
4
|
/**
|
|
5
5
|
* A file becomes the specifier the PROJECT already uses for it: `@fronds/blog/…`, the import scope
|
|
6
6
|
* a handler names its neighbour by, and which the Nuxt module registers as an alias for every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statement.js","sourceRoot":"","sources":["../../src/scan/statement.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAwB,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"statement.js","sourceRoot":"","sources":["../../src/scan/statement.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0-alpha.0",
|
|
4
4
|
"description": "The scan: Fougere reading a project's source to produce the descriptors core boots from. Needs a filesystem and a TypeScript program.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
"@typescript/typescript6": "^6.0.2"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@fougere/core": "^0.
|
|
37
|
-
"@fougere/container": "^0.
|
|
36
|
+
"@fougere/core": "^0.10.0-alpha.0",
|
|
37
|
+
"@fougere/container": "^0.10.0-alpha.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"vitest": "^4.1.0",
|
|
41
|
-
"@fougere/
|
|
42
|
-
"@fougere/
|
|
43
|
-
"@fougere/container": "0.
|
|
41
|
+
"@fougere/core": "0.10.0-alpha.0",
|
|
42
|
+
"@fougere/schema": "0.10.0-alpha.0",
|
|
43
|
+
"@fougere/container": "0.10.0-alpha.0"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
package/src/boot.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger, applyConfig, createApp,
|
|
1
|
+
import { Logger, applyConfig, createApp, type App, type CreateAppOptions, type Extension, type FougereConfig, type Transport } from '@fougere/core';
|
|
2
2
|
import { loadConfig } from '@fougere/core/node';
|
|
3
3
|
import { scanProject } from './scan/scanner.js';
|
|
4
4
|
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,9 @@ export { scanProject, frondAliases, watchPathsOf } from './scan/scanner.js';
|
|
|
4
4
|
export { RUNTIME_PACKAGES } from './scan/bundling.js';
|
|
5
5
|
export { emitScan } from './scan/emit.js';
|
|
6
6
|
export { emitStatement } from './scan/statement.js';
|
|
7
|
+
// The third projection of one scan: the facades as TYPES, so a client can narrow a refusal.
|
|
8
|
+
export { emitFacade, facadeModule, type Served } from './scan/Served.js';
|
|
9
|
+
export { emitNames, type NamesOptions } from './scan/names.js';
|
|
7
10
|
export { adaptersOf } from './scan/adapters.js';
|
|
8
11
|
export { crossFrondImports } from './imports.js';
|
|
9
12
|
export { handlerDeclarations } from './declarations.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Live } from './Live.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the writer needs of an emitter's imports, and nothing more.
|
|
5
|
+
*
|
|
6
|
+
* The two emitters index theirs differently — one by value against a relative path, one
|
|
7
|
+
* by file against the package specifier a project already uses — and neither is wrong for
|
|
8
|
+
* the module it writes. What they agree on is this: a value already imported has an
|
|
9
|
+
* alias, a class can be imported by name, and an entity can be found by its class name.
|
|
10
|
+
*/
|
|
11
|
+
export interface Aliases {
|
|
12
|
+
aliasOf(value: Live): string | undefined;
|
|
13
|
+
named(value: Live, filePath: string, name: string): string;
|
|
14
|
+
/** The entity class this name belongs to — what a `Partial<X>` names as its source. */
|
|
15
|
+
classNamed(name: string): Live | undefined;
|
|
16
|
+
}
|
|
@@ -1,25 +1,10 @@
|
|
|
1
|
-
/** An operation contract, written down — the half two emitters say the same way. */
|
|
2
1
|
import { type OperationContract } from '@fougere/core/descriptor';
|
|
3
2
|
import { ANONYMOUS_SCHEMA_NAME, Card, type SchemaView } from '@fougere/schema';
|
|
3
|
+
import type { Aliases } from './Aliases.js';
|
|
4
4
|
|
|
5
5
|
/** A value that ends up as an import in the module being written. */
|
|
6
6
|
export type Live = object;
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* What the writer needs of an emitter's imports, and nothing more.
|
|
10
|
-
*
|
|
11
|
-
* The two emitters index theirs differently — one by value against a relative path, one
|
|
12
|
-
* by file against the package specifier a project already uses — and neither is wrong for
|
|
13
|
-
* the module it writes. What they agree on is this: a value already imported has an
|
|
14
|
-
* alias, a class can be imported by name, and an entity can be found by its class name.
|
|
15
|
-
*/
|
|
16
|
-
export interface Aliases {
|
|
17
|
-
aliasOf(value: Live): string | undefined;
|
|
18
|
-
named(value: Live, filePath: string, name: string): string;
|
|
19
|
-
/** The entity class this name belongs to — what a `Partial<X>` names as its source. */
|
|
20
|
-
classNamed(name: string): Live | undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
8
|
export const lit = (value: unknown): string => JSON.stringify(value ?? null);
|
|
24
9
|
|
|
25
10
|
/** What a schema slot becomes in the generated module. */
|
|
@@ -62,6 +47,9 @@ export function contractOf(
|
|
|
62
47
|
if (contract.description !== undefined) parts.push(`description: ${lit(contract.description)}`);
|
|
63
48
|
if (contract.cardinality !== undefined) parts.push(`cardinality: ${lit(contract.cardinality)}`);
|
|
64
49
|
if (contract.signature !== undefined) parts.push(`signature: ${lit(contract.signature)}`);
|
|
50
|
+
// Written down like every other member: a scan on disk is what an edge deployment reads, and
|
|
51
|
+
// a contract that loses a field there answers differently depending on how it was booted.
|
|
52
|
+
if (contract.errors !== undefined) parts.push(`errors: ${lit(contract.errors)}`);
|
|
65
53
|
|
|
66
54
|
return `[${lit(op)}, { ${parts.join(', ')} }]`;
|
|
67
55
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { dirname, relative } from 'node:path';
|
|
2
|
+
import { refusalsOf, type ScanResult } from '@fougere/core';
|
|
3
|
+
import { servedSurfaces } from '@fougere/core/descriptor';
|
|
4
|
+
import type { FacadeOptions } from './FacadeOptions.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The specifier a handler is reached by, from where the generated file sits.
|
|
8
|
+
*
|
|
9
|
+
* The type is written WITHOUT `typeof`: that names the CONSTRUCTOR, whose `keyof` is
|
|
10
|
+
* `'prototype'`, and every operation would fail to compile against it.
|
|
11
|
+
*/
|
|
12
|
+
function specifierOf(filePath: string, outFile: string): string {
|
|
13
|
+
const path = relative(dirname(outFile), filePath).replace(/\.tsx?$/, '.js');
|
|
14
|
+
|
|
15
|
+
return path.startsWith('.') ? path : `./${path}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A union of enum MEMBERS — BOTH halves of what a call can come back refusing.
|
|
20
|
+
*
|
|
21
|
+
* `refusalsOf` is core's own table, read here rather than restated: what an op declares is
|
|
22
|
+
* walked from its throw sites, and what the framework adds follows from `kind` and `input`.
|
|
23
|
+
* Writing only the first half would tell a page that an op refuses nothing, when every op can
|
|
24
|
+
* meet a draining facade.
|
|
25
|
+
*
|
|
26
|
+
* `ErrorCode` is a string enum, so `'CONFLICT'` is not assignable to it — a literal would read
|
|
27
|
+
* as the right thing and refuse to narrow `FougereError<Code>`, which is the whole point.
|
|
28
|
+
*/
|
|
29
|
+
function codesOf(contract: { kind?: 'query' | 'command'; input?: unknown; errors?: readonly string[] }): string {
|
|
30
|
+
const codes = refusalsOf(contract);
|
|
31
|
+
if (codes.length === 0) return 'never';
|
|
32
|
+
|
|
33
|
+
return codes.map((code) => `ErrorCode.${code}`).join(' | ');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* An augmentation of `FougereOperations`, keyed the way a facade is ADDRESSED — `post.publish`, and
|
|
38
|
+
* `public:post.publish` for a named surface.
|
|
39
|
+
*
|
|
40
|
+
* It AUGMENTS rather than declares, so a page reaches the keys through the package it already
|
|
41
|
+
* imports: `useQuery('checkout', 'pay')` narrows against them without naming this file. The
|
|
42
|
+
* interface it fills is empty in `@fougere/core/contract` — the facade is addressed the same way
|
|
43
|
+
* on both sides of the wire, and a backend project builds without a front-end package.
|
|
44
|
+
*
|
|
45
|
+
* The prefix is `facadeKeyOf`'s own spelling, because it is the same distinction: a surface is a
|
|
46
|
+
* second facade in front of the same handler, serving a subset of its operations. What each op can
|
|
47
|
+
* refuse does not change with the surface — the code that throws is the same — but WHICH ops
|
|
48
|
+
* exist does, and a client pointed at `/_fougere/call/public` must not be offered the rest.
|
|
49
|
+
*
|
|
50
|
+
* A browser reaches the default surface unless it is handed another endpoint, which is why the
|
|
51
|
+
* unprefixed keys are the ones a composable narrows against.
|
|
52
|
+
*/
|
|
53
|
+
export function emitFacade(scan: ScanResult, options: FacadeOptions): string {
|
|
54
|
+
const served: Served[] = [];
|
|
55
|
+
|
|
56
|
+
for (const frond of scan.fronds) {
|
|
57
|
+
for (const handler of frond.handlers) {
|
|
58
|
+
// NOT `handler.surface`: a surface is also declared in `frond.config.ts`, which names an
|
|
59
|
+
// address without opening a directory for it. `servedSurfaces` is the one reader of both,
|
|
60
|
+
// and it answers `undefined` for the default the way `facadeKeyOf` spells it.
|
|
61
|
+
for (const surface of servedSurfaces(frond, handler)) {
|
|
62
|
+
served.push({
|
|
63
|
+
at: surface ? `${surface}:${handler.address}` : handler.address,
|
|
64
|
+
handler: `import('${specifierOf(handler.filePath, options.outFile)}').default`,
|
|
65
|
+
ops: [...(handler.operations ?? [])].map(([name, contract]) => ({ name, codes: codesOf(contract) })),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return facadeModule(served, 'what the scan found');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One address, who answers there, and what each of its operations can refuse. */
|
|
75
|
+
export interface Served {
|
|
76
|
+
at: string;
|
|
77
|
+
/**
|
|
78
|
+
* The TYPE EXPRESSION the handler is reached by, whole — `import('./x.js').default` for a
|
|
79
|
+
* scanned class, `import('./x.js').PostHandler` for the synthetic interface `sync` writes.
|
|
80
|
+
*
|
|
81
|
+
* The expression and not a specifier: a scan's handler is a default export and a synced one
|
|
82
|
+
* is named, and a producer that hands over only the path leaves the reader to guess which.
|
|
83
|
+
*/
|
|
84
|
+
handler: string;
|
|
85
|
+
ops: { name: string; codes: string }[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The module itself — written once, from a scan or from a card.
|
|
90
|
+
*
|
|
91
|
+
* `fougere sync` has the same three facts about a frond in ANOTHER repository: the card names
|
|
92
|
+
* the addresses, the operations, and what each one refuses. What it does not have is the
|
|
93
|
+
* handler's class, so it writes a synthetic interface and points here at that instead. Two
|
|
94
|
+
* producers, one spelling — a second copy of this format would drift the day one gained a
|
|
95
|
+
* member.
|
|
96
|
+
*/
|
|
97
|
+
export function facadeModule(served: readonly Served[], source: string): string {
|
|
98
|
+
const rows = served.flatMap((one) => one.ops.map((op) => ` '${one.at}.${op.name}': { errors: ${op.codes} };`));
|
|
99
|
+
const handlers = new Map(served.map((one) => [one.at, one.handler]));
|
|
100
|
+
|
|
101
|
+
// A named surface shares its handler's class name, so two of them would declare one const.
|
|
102
|
+
// A browser reaches the DEFAULT surface unless it was handed another endpoint, which is the
|
|
103
|
+
// one a page names; the prefixed keys stay in `FougereOperations` for whoever reads them.
|
|
104
|
+
const facades = [...handlers].filter(([at]) => !at.includes(':')).sort();
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
`// Generated by Fougere from ${source} — do not edit, and do not commit.`,
|
|
108
|
+
'// Rewritten whenever the app boots, so it cannot drift from the handlers it was read off.',
|
|
109
|
+
'',
|
|
110
|
+
"import type { ErrorCode, FacadeName } from '@fougere/core/contract';",
|
|
111
|
+
'',
|
|
112
|
+
"declare module '@fougere/core/contract' {",
|
|
113
|
+
' /** Every facade this app serves, and what each one refuses beyond what `kind` implies. */',
|
|
114
|
+
' interface FougereOperations {',
|
|
115
|
+
...(rows.length > 0 ? rows.sort().map((row) => ` ${row}`) : [' // This app serves no facade.']),
|
|
116
|
+
' }',
|
|
117
|
+
'',
|
|
118
|
+
' /** The class that answers at each address — its operations, and what each one answers. */',
|
|
119
|
+
' interface FougereHandlers {',
|
|
120
|
+
...[...handlers].sort().map(([at, type]) => ` '${at}': ${type};`),
|
|
121
|
+
' }',
|
|
122
|
+
'}',
|
|
123
|
+
'',
|
|
124
|
+
'// One facade per address. A page IMPORTS it, so a project that never generated this file',
|
|
125
|
+
"// fails to resolve rather than falling back to `string` in silence. The value is the",
|
|
126
|
+
'// address and nothing else: the handler is reached as a TYPE, and no server code travels.',
|
|
127
|
+
'//',
|
|
128
|
+
'// Named after the ADDRESS, never after the class: what a page holds is a facade, and calling',
|
|
129
|
+
"// it `PostHandler` would be a value claiming to be a class it is not. A page whose rows are",
|
|
130
|
+
'// already called `post` aliases it on the import, the way any other collision is settled.',
|
|
131
|
+
...facades.map(([at, type]) =>
|
|
132
|
+
`export const ${at}: FacadeName<${type}, '${at}'> = { address: '${at}' };`),
|
|
133
|
+
'',
|
|
134
|
+
].join('\n');
|
|
135
|
+
}
|
package/src/scan/emit.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { type ScanResult } from '@fougere/core';
|
|
|
3
3
|
import { nameOf, type CollectorEntry, type EntityEntry, type FrondDescriptor, type HandlerEntry, type MiddlewareEntry, type PresenterEntry, type ProviderEntry, type SeedEntry } from '@fougere/core/descriptor';
|
|
4
4
|
import { dirname, relative } from 'node:path';
|
|
5
5
|
|
|
6
|
-
import { type Aliases
|
|
6
|
+
import { type Aliases } from './Aliases.js';
|
|
7
|
+
import { lit, operationsOf, schemaRef, type Live } from './Live.js';
|
|
7
8
|
|
|
8
9
|
export interface EmitOptions {
|
|
9
10
|
/** Where the generated module will sit. Imports are written relative to it. */
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** Handler signature parser — extracts method signatures from handler source files. */
|
|
2
2
|
import { type Param, type Signature, type TypeRef } from '@fougere/core/descriptor';
|
|
3
|
+
import { type ErrorCode } from '@fougere/core';
|
|
3
4
|
|
|
4
5
|
import type ts from '@typescript/typescript6';
|
|
6
|
+
import { refusalsIn } from './refusals.js';
|
|
5
7
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
6
8
|
import { join, dirname, resolve as resolvePath } from 'node:path';
|
|
7
9
|
|
|
@@ -120,7 +122,8 @@ export async function seedTypeProgram(filePaths: readonly string[], projectRoot?
|
|
|
120
122
|
|
|
121
123
|
for (const filePath of filePaths) {
|
|
122
124
|
const absolute = resolvePath(filePath);
|
|
123
|
-
const
|
|
125
|
+
const typescript = getTS();
|
|
126
|
+
const configured = compilerProjectOf(absolute, projectRoot);
|
|
124
127
|
const group = grouped.get(configured.key) ?? { options: configured.options, paths: [] };
|
|
125
128
|
group.paths.push(absolute);
|
|
126
129
|
grouped.set(configured.key, group);
|
|
@@ -133,8 +136,8 @@ export async function seedTypeProgram(filePaths: readonly string[], projectRoot?
|
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
/** The program a file belongs to, built once and widened as more files are asked for. */
|
|
140
|
+
function projectOf(filePath: string, projectRoot?: string): { program: ts.Program; absolute: string } {
|
|
138
141
|
const absolute = resolvePath(filePath);
|
|
139
142
|
const configured = compilerProjectOf(absolute, projectRoot);
|
|
140
143
|
let project = typeProjects.get(configured.key);
|
|
@@ -153,9 +156,15 @@ function checkedSourceOf(filePath: string, projectRoot?: string): { source: ts.S
|
|
|
153
156
|
project.program = builtProgram(configured.key, [...project.roots], project.options);
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
return { program: project.program, absolute };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function checkedSourceOf(filePath: string, projectRoot?: string): { source: ts.SourceFile; checker: ts.TypeChecker } {
|
|
163
|
+
const { program, absolute } = projectOf(filePath, projectRoot);
|
|
164
|
+
const source = program.getSourceFile(absolute);
|
|
157
165
|
if (!source) throw new Error(`TypeScript did not include '${absolute}' in its program.`);
|
|
158
|
-
|
|
166
|
+
|
|
167
|
+
return { source, checker: program.getTypeChecker() };
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
/** A file, opened. Five places read and parsed one, each spelling the same two calls. */
|
|
@@ -455,7 +464,7 @@ function extractClassMethods(
|
|
|
455
464
|
// `private`/`protected` is a statement about the surface, and the AST carries it —
|
|
456
465
|
// it was read past. Every helper a handler names by intent (`mustOwn`, `readMany`,
|
|
457
466
|
// `refuse`) became a callable op: measured on the bench, `list.readMany` executed
|
|
458
|
-
// and `list.mayPublish` validated its argument. A
|
|
467
|
+
// and `list.mayPublish` validated its argument. A facade is what the author declares
|
|
459
468
|
// public, and TypeScript already has the word for it. `#name` is private too, but
|
|
460
469
|
// it is not an identifier, so it never reached here in the first place.
|
|
461
470
|
if (member.modifiers?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword)) continue;
|
|
@@ -519,7 +528,7 @@ function inheritedFromBase(
|
|
|
519
528
|
if (skip.has(property.name)) continue;
|
|
520
529
|
|
|
521
530
|
// `private`/`protected` is a statement about the surface, and the declaration carries
|
|
522
|
-
// it. A
|
|
531
|
+
// it. A facade is what the author declares public.
|
|
523
532
|
const declaration = property.declarations?.[0];
|
|
524
533
|
if (declaration && typescript.canHaveModifiers(declaration)
|
|
525
534
|
&& typescript.getModifiers(declaration)?.some((m) => m.kind === typescript.SyntaxKind.PrivateKeyword
|
|
@@ -635,6 +644,24 @@ export async function parseAllHandlerMethods(filePath: string, projectRoot?: str
|
|
|
635
644
|
return parseClassMethods(filePath, CONSTRUCTOR_ONLY, projectRoot);
|
|
636
645
|
}
|
|
637
646
|
|
|
647
|
+
/**
|
|
648
|
+
* What every operation of a file's program can refuse, walked from the refusals upward.
|
|
649
|
+
*
|
|
650
|
+
* It reads the SAME program the signatures came from, so nothing is opened twice and a
|
|
651
|
+
* dependency resolves through the checker rather than by name.
|
|
652
|
+
*/
|
|
653
|
+
export async function parseRefusals(
|
|
654
|
+
filePath: string,
|
|
655
|
+
isOperation: (name: string) => boolean,
|
|
656
|
+
projectRoot?: string,
|
|
657
|
+
): Promise<Map<string, ErrorCode[]>> {
|
|
658
|
+
const typescript = await loadTS();
|
|
659
|
+
const { program } = projectOf(filePath, projectRoot);
|
|
660
|
+
const { byMethod } = refusalsIn(typescript, program, isOperation);
|
|
661
|
+
|
|
662
|
+
return new Map([...byMethod].map(([method, codes]) => [method, [...codes].sort()]));
|
|
663
|
+
}
|
|
664
|
+
|
|
638
665
|
/** Parse a presenter source file and extract all method signatures. */
|
|
639
666
|
export async function parsePresenterMethods(filePath: string, projectRoot?: string): Promise<Signature[]> {
|
|
640
667
|
await loadTS();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names this project contains, as TYPES — what an operator may write in a config file.
|
|
3
|
+
*
|
|
4
|
+
* A fourth projection of one scan, beside the facade, the emitted module and the stated frond.
|
|
5
|
+
* It exists because `fougere.config.ts` and `frond.config.ts` designate classes, entities and
|
|
6
|
+
* fronds by STRING, and a string is the one thing in this framework that nothing reads back:
|
|
7
|
+
* `ports: { Payment: 'StripePayment' }` survives renaming `StripePayment`, and says so at boot.
|
|
8
|
+
*
|
|
9
|
+
* Ports are written as a RELATION rather than one more union. Two unions would admit
|
|
10
|
+
* `ports: { Payment: 'FileStorage' }` — both names exist, and nothing would say that one does
|
|
11
|
+
* not answer the other.
|
|
12
|
+
*
|
|
13
|
+
* Documented: [ports](https://fougere.dev/docs/business/ports).
|
|
14
|
+
*/
|
|
15
|
+
import { basesOf, type FrondDescriptor } from '@fougere/core/descriptor';
|
|
16
|
+
import { type ScanResult } from '@fougere/core';
|
|
17
|
+
|
|
18
|
+
export interface NamesOptions {
|
|
19
|
+
/** The keys of `sources:`, which the config states and no scan can find. */
|
|
20
|
+
sources?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Sorted, deduplicated, and quoted — the union as it is written. */
|
|
24
|
+
function union(names: Iterable<string>): string {
|
|
25
|
+
const all = [...new Set(names)].sort();
|
|
26
|
+
|
|
27
|
+
return all.length > 0 ? all.map((name) => `'${name}'`).join(' | ') : 'never';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The classes a frond declares, whatever convention directory they came from.
|
|
32
|
+
*
|
|
33
|
+
* `basesOf` asks whether a name is answered, which the boot settles against its container. A
|
|
34
|
+
* scan has no container, so what stands in for it is the project itself: a port is a class this
|
|
35
|
+
* scan read. That is the same line drawn from the other side — `RepositoryBase` is returned by a
|
|
36
|
+
* call and declared in no file, so it is excluded here for the reason the boot excludes it.
|
|
37
|
+
*/
|
|
38
|
+
function declaredIn(frond: FrondDescriptor): Set<string> {
|
|
39
|
+
return new Set([
|
|
40
|
+
...frond.providers.map((one) => one.ctor.name),
|
|
41
|
+
...frond.handlers.map((one) => one.ctor.name),
|
|
42
|
+
...frond.middlewares.map((one) => one.ctor.name),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function emitNames(scan: ScanResult, options: NamesOptions = {}): string {
|
|
47
|
+
const fronds = new Set<string>();
|
|
48
|
+
const entities = new Set<string>();
|
|
49
|
+
const handlers = new Set<string>();
|
|
50
|
+
const providers = new Set<string>();
|
|
51
|
+
const middlewares = new Set<string>();
|
|
52
|
+
const ports = new Map<string, Set<string>>();
|
|
53
|
+
|
|
54
|
+
for (const frond of scan.fronds) {
|
|
55
|
+
fronds.add(frond.name);
|
|
56
|
+
// BOTH spellings, because the boot admits both: `sources:` lowercases what it is
|
|
57
|
+
// given (`lowerFirst`), so `['Ledger']` and `['ledger']` name one entity. A union of
|
|
58
|
+
// one form would be stricter than what runs, and refuse code that works.
|
|
59
|
+
for (const entity of frond.entities) {
|
|
60
|
+
entities.add(entity.name);
|
|
61
|
+
entities.add(entity.entityClass.name);
|
|
62
|
+
}
|
|
63
|
+
for (const handler of frond.handlers) handlers.add(handler.ctor.name);
|
|
64
|
+
for (const provider of frond.providers) providers.add(provider.ctor.name);
|
|
65
|
+
for (const middleware of frond.middlewares) middlewares.add(middleware.ctor.name);
|
|
66
|
+
|
|
67
|
+
const declared = declaredIn(frond);
|
|
68
|
+
for (const [port, answering] of basesOf(frond.providers, (name) => declared.has(name))) {
|
|
69
|
+
const known = ports.get(port) ?? new Set<string>();
|
|
70
|
+
for (const one of answering) known.add(one.ctor.name);
|
|
71
|
+
ports.set(port, known);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
'// Generated by Fougere from what the scan found — do not edit, and do not commit.',
|
|
77
|
+
'// Rewritten whenever the app boots, so it cannot drift from the project it was read off.',
|
|
78
|
+
'',
|
|
79
|
+
"declare module '@fougere/core' {",
|
|
80
|
+
' /** Every name this project declares, by kind. A config may write no other. */',
|
|
81
|
+
' interface FougereNames {',
|
|
82
|
+
` frond: ${union(fronds)};`,
|
|
83
|
+
` entity: ${union(entities)};`,
|
|
84
|
+
` handler: ${union(handlers)};`,
|
|
85
|
+
` provider: ${union(providers)};`,
|
|
86
|
+
` middleware: ${union(middlewares)};`,
|
|
87
|
+
` source: ${union(options.sources ?? [])};`,
|
|
88
|
+
' }',
|
|
89
|
+
'',
|
|
90
|
+
' /** Each port, and what extends it — so a config cannot answer one with the other. */',
|
|
91
|
+
' interface FougerePorts {',
|
|
92
|
+
...(ports.size > 0
|
|
93
|
+
? [...ports].sort().map(([port, answering]) => ` ${port}: ${union(answering)};`)
|
|
94
|
+
: [' // This project declares no port.']),
|
|
95
|
+
' }',
|
|
96
|
+
'}',
|
|
97
|
+
'',
|
|
98
|
+
'export {};',
|
|
99
|
+
'',
|
|
100
|
+
].join('\n');
|
|
101
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an operation can refuse, read by walking UP from every refusal to whoever reaches it.
|
|
3
|
+
*
|
|
4
|
+
* Reading a handler's own body answers "at least these", which is not a contract: a guard moved
|
|
5
|
+
* into a helper — a refactor with no change of behaviour — would silently shrink what an app
|
|
6
|
+
* promises. Measured on `site/fronds/blog`, the body of `publish` holds ONE throw and the walk
|
|
7
|
+
* finds four, because the three that matter live in module functions beside it.
|
|
8
|
+
*
|
|
9
|
+
* Walking up rather than down, for the same reason a router indexes forward: there are a handful
|
|
10
|
+
* of refusal sites and many operations, so the search starts from the few. An address resolved
|
|
11
|
+
* through a port lands on the base's method and the union of what its realizations refuse, which
|
|
12
|
+
* OVER-approximates — the honest direction for a contract, where a code that cannot happen costs
|
|
13
|
+
* a dead branch and a code that can costs a surprise in production.
|
|
14
|
+
*
|
|
15
|
+
* It stops at the frond. What another frond refuses is published by ITS card.
|
|
16
|
+
*/
|
|
17
|
+
import type ts from '@typescript/typescript6';
|
|
18
|
+
import { ErrorCode } from '@fougere/core';
|
|
19
|
+
|
|
20
|
+
/** Where a refusal was written, and which one. */
|
|
21
|
+
interface Site {
|
|
22
|
+
/** `ClassName.method`, or `<fn>.name` for a module function. */
|
|
23
|
+
at: string;
|
|
24
|
+
code: ErrorCode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Refusals {
|
|
28
|
+
/** `Handler.method` → the codes any path from it can reach. */
|
|
29
|
+
byMethod: Map<string, Set<ErrorCode>>;
|
|
30
|
+
/** How many refusal sites the walk started from — zero is a claim, not a failure. */
|
|
31
|
+
sites: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A refusal this package raises about itself never reaches a caller, so it is not a contract. */
|
|
35
|
+
const MASKED = new Set(['INTERNAL_ERROR']);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The name a call and a declaration agree on. A method carries its class so two `list` methods
|
|
39
|
+
* are two subjects; a module function has no owner and says so rather than borrowing one.
|
|
40
|
+
*/
|
|
41
|
+
function nameOf(typescript: typeof ts, node: ts.Node): string | undefined {
|
|
42
|
+
const named = node as { name?: ts.Node };
|
|
43
|
+
const own = named.name && 'getText' in named.name ? (named.name as ts.Identifier).getText() : undefined;
|
|
44
|
+
if (!own) return undefined;
|
|
45
|
+
|
|
46
|
+
const owner = typescript.isMethodDeclaration(node) || typescript.isMethodSignature(node)
|
|
47
|
+
? (node.parent as { name?: ts.Identifier })?.name?.getText()
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
return `${owner ?? '<fn>'}.${own}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The method or function a node sits inside — where a refusal is charged. */
|
|
54
|
+
function holderOf(typescript: typeof ts, node: ts.Node): string | undefined {
|
|
55
|
+
let at: ts.Node | undefined = node;
|
|
56
|
+
while (at && !typescript.isMethodDeclaration(at) && !typescript.isFunctionDeclaration(at)) at = at.parent;
|
|
57
|
+
|
|
58
|
+
return at ? nameOf(typescript, at) : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `new FougereError({ code: ErrorCode.CONFLICT })` → `CONFLICT`, and nothing for a computed one.
|
|
63
|
+
*
|
|
64
|
+
* The member is read as TEXT, so it is judged against the enum before it travels: a walk reaches
|
|
65
|
+
* files the project never compiles, and a name that is not a code would otherwise be published on
|
|
66
|
+
* a card as one.
|
|
67
|
+
*/
|
|
68
|
+
function codeIn(typescript: typeof ts, node: ts.NewExpression): ErrorCode | undefined {
|
|
69
|
+
if (node.expression.getText() !== 'FougereError') return undefined;
|
|
70
|
+
const first = node.arguments?.[0];
|
|
71
|
+
if (!first || !typescript.isObjectLiteralExpression(first)) return undefined;
|
|
72
|
+
|
|
73
|
+
const written = first.properties.find((one) => one.name?.getText() === 'code');
|
|
74
|
+
const value = written && typescript.isPropertyAssignment(written) ? written.initializer.getText() : undefined;
|
|
75
|
+
if (!value?.startsWith('ErrorCode.')) return undefined;
|
|
76
|
+
|
|
77
|
+
const member = value.slice('ErrorCode.'.length);
|
|
78
|
+
|
|
79
|
+
return member in ErrorCode ? ErrorCode[member as keyof typeof ErrorCode] : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read one program: every refusal, and every edge from a caller to what it calls.
|
|
84
|
+
*
|
|
85
|
+
* The checker is what makes an edge exact — `this.posts.findById()` resolves to the declaration
|
|
86
|
+
* on `PostRepository`, which is the same node a refusal inside it was charged to. No name
|
|
87
|
+
* matching, and no guess about what a dependency holds.
|
|
88
|
+
*/
|
|
89
|
+
export function refusalsIn(typescript: typeof ts, program: ts.Program, isOperation: (name: string) => boolean): Refusals {
|
|
90
|
+
const checker = program.getTypeChecker();
|
|
91
|
+
const sites: Site[] = [];
|
|
92
|
+
/** callee → the callers that reach it. */
|
|
93
|
+
const callers = new Map<string, Set<string>>();
|
|
94
|
+
|
|
95
|
+
for (const file of program.getSourceFiles()) {
|
|
96
|
+
if (file.isDeclarationFile || file.fileName.includes('node_modules')) continue;
|
|
97
|
+
|
|
98
|
+
const visit = (node: ts.Node): void => {
|
|
99
|
+
if (typescript.isNewExpression(node)) {
|
|
100
|
+
const code = codeIn(typescript, node);
|
|
101
|
+
const at = code ? holderOf(typescript, node) : undefined;
|
|
102
|
+
if (code && at && !MASKED.has(code)) sites.push({ at, code });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typescript.isCallExpression(node)) {
|
|
106
|
+
const callee = calleeOf(typescript, checker, node);
|
|
107
|
+
const caller = callee ? holderOf(typescript, node) : undefined;
|
|
108
|
+
if (callee && caller && callee !== caller) {
|
|
109
|
+
(callers.get(callee) ?? callers.set(callee, new Set()).get(callee)!).add(caller);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
typescript.forEachChild(node, visit);
|
|
114
|
+
};
|
|
115
|
+
visit(file);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { sites: sites.length, byMethod: reached(sites, callers, isOperation) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The declaration a call resolves to, named the way a refusal site is. */
|
|
122
|
+
function calleeOf(typescript: typeof ts, checker: ts.TypeChecker, node: ts.CallExpression): string | undefined {
|
|
123
|
+
const target = node.expression;
|
|
124
|
+
const found = checker.getSymbolAtLocation(target)
|
|
125
|
+
?? (typescript.isPropertyAccessExpression(target) ? checker.getSymbolAtLocation(target.name) : undefined);
|
|
126
|
+
|
|
127
|
+
// An IMPORTED function resolves to its import specifier, which is callable in no sense the
|
|
128
|
+
// test below admits — so the edge was dropped and the refusal never climbed out of its file.
|
|
129
|
+
// Measured on `site/fronds/blog`: moving three guards into a neighbouring module, which
|
|
130
|
+
// `fougere check` asks for, cut what `publish` promises from five codes to two.
|
|
131
|
+
const symbol = found && found.flags & typescript.SymbolFlags.Alias ? checker.getAliasedSymbol(found) : found;
|
|
132
|
+
|
|
133
|
+
const declared = symbol?.declarations?.[0];
|
|
134
|
+
if (!declared) return undefined;
|
|
135
|
+
|
|
136
|
+
const callable = typescript.isMethodDeclaration(declared)
|
|
137
|
+
|| typescript.isMethodSignature(declared)
|
|
138
|
+
|| typescript.isFunctionDeclaration(declared);
|
|
139
|
+
|
|
140
|
+
return callable ? nameOf(typescript, declared) : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** From each refusal, climb its callers until an operation holds it. */
|
|
144
|
+
function reached(
|
|
145
|
+
sites: readonly Site[],
|
|
146
|
+
callers: Map<string, Set<string>>,
|
|
147
|
+
isOperation: (name: string) => boolean,
|
|
148
|
+
): Map<string, Set<ErrorCode>> {
|
|
149
|
+
const found = new Map<string, Set<ErrorCode>>();
|
|
150
|
+
|
|
151
|
+
for (const site of sites) {
|
|
152
|
+
const walked = new Set([site.at]);
|
|
153
|
+
const queue = [site.at];
|
|
154
|
+
|
|
155
|
+
while (queue.length > 0) {
|
|
156
|
+
const at = queue.shift()!;
|
|
157
|
+
if (isOperation(at)) (found.get(at) ?? found.set(at, new Set()).get(at)!).add(site.code);
|
|
158
|
+
for (const caller of callers.get(at) ?? []) {
|
|
159
|
+
if (walked.has(caller)) continue;
|
|
160
|
+
walked.add(caller);
|
|
161
|
+
queue.push(caller);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return found;
|
|
167
|
+
}
|