@avijs/compiler 0.2.1 → 0.3.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/check.d.ts +7 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +15 -11
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +28 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +415 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/src/check.ts +16 -11
- package/src/cli.ts +44 -4
- package/src/client.ts +519 -0
- package/src/index.ts +2 -0
package/dist/check.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
1
2
|
export interface Diagnostic {
|
|
2
3
|
readonly file: string;
|
|
3
4
|
readonly line: number;
|
|
@@ -12,4 +13,10 @@ export interface Diagnostic {
|
|
|
12
13
|
* Decorated classes get this from the compiler already; this covers undecorated ones too.
|
|
13
14
|
*/
|
|
14
15
|
export declare function checkInjectLists(project: string): Diagnostic[];
|
|
16
|
+
/** The project's program, and its own files (absolute paths). */
|
|
17
|
+
export declare function readProgram(project: string): {
|
|
18
|
+
program: ts.Program;
|
|
19
|
+
roots: Set<string>;
|
|
20
|
+
};
|
|
21
|
+
export declare function unwrap(expression: ts.Expression | undefined): ts.Expression | undefined;
|
|
15
22
|
//# sourceMappingURL=check.d.ts.map
|
package/dist/check.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAiB9D;AAED,iEAAiE;AACjE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAWxF;AAuFD,wBAAgB,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,GAAG,EAAE,CAAC,UAAU,GAAG,SAAS,CAYvF"}
|
package/dist/check.js
CHANGED
|
@@ -5,17 +5,8 @@ import ts from 'typescript';
|
|
|
5
5
|
* Decorated classes get this from the compiler already; this covers undecorated ones too.
|
|
6
6
|
*/
|
|
7
7
|
export function checkInjectLists(project) {
|
|
8
|
-
const
|
|
9
|
-
...ts.sys,
|
|
10
|
-
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
|
|
11
|
-
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
if (!config)
|
|
15
|
-
throw new Error(`Cannot read ${project}`);
|
|
16
|
-
const program = ts.createProgram({ rootNames: config.fileNames, options: config.options });
|
|
8
|
+
const { program, roots } = readProgram(project);
|
|
17
9
|
const checker = program.getTypeChecker();
|
|
18
|
-
const roots = new Set(config.fileNames.map((file) => path.resolve(file)));
|
|
19
10
|
const diagnostics = [];
|
|
20
11
|
for (const source of program.getSourceFiles()) {
|
|
21
12
|
if (source.isDeclarationFile || !roots.has(path.resolve(source.fileName)))
|
|
@@ -29,6 +20,19 @@ export function checkInjectLists(project) {
|
|
|
29
20
|
}
|
|
30
21
|
return diagnostics.sort((a, b) => a.file === b.file ? a.line - b.line || a.column - b.column : a.file.localeCompare(b.file));
|
|
31
22
|
}
|
|
23
|
+
/** The project's program, and its own files (absolute paths). */
|
|
24
|
+
export function readProgram(project) {
|
|
25
|
+
const config = ts.getParsedCommandLineOfConfigFile(path.resolve(project), undefined, {
|
|
26
|
+
...ts.sys,
|
|
27
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
|
|
28
|
+
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (!config)
|
|
32
|
+
throw new Error(`Cannot read ${project}`);
|
|
33
|
+
const program = ts.createProgram({ rootNames: config.fileNames, options: config.options });
|
|
34
|
+
return { program, roots: new Set(config.fileNames.map((file) => path.resolve(file))) };
|
|
35
|
+
}
|
|
32
36
|
function checkClass(node, source, checker, out) {
|
|
33
37
|
const inject = node.members.find((member) => ts.isPropertyDeclaration(member) &&
|
|
34
38
|
(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static) !== 0 &&
|
|
@@ -81,7 +85,7 @@ function checkClass(node, source, checker, out) {
|
|
|
81
85
|
}
|
|
82
86
|
});
|
|
83
87
|
}
|
|
84
|
-
function unwrap(expression) {
|
|
88
|
+
export function unwrap(expression) {
|
|
85
89
|
let current = expression;
|
|
86
90
|
while (current &&
|
|
87
91
|
(ts.isAsExpression(current) ||
|
package/dist/check.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,YAAY,CAAC;AAY5B;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAeA,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA8CnE"}
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { parseArgs } from 'node:util';
|
|
2
4
|
import { checkInjectLists } from './check.js';
|
|
5
|
+
import { generateClient } from './client.js';
|
|
3
6
|
import { writeManifest } from './manifest.js';
|
|
4
7
|
const USAGE = `Usage:
|
|
5
8
|
avi-compile check [--project tsconfig.json]
|
|
6
9
|
Type-check every static inject list against its constructor.
|
|
7
10
|
avi-compile manifest <entry.js> [--export AppModule] [--out avi.manifest.json]
|
|
8
|
-
Describe the app's modules, providers and routes without starting it
|
|
11
|
+
Describe the app's modules, providers and routes without starting it.
|
|
12
|
+
avi-compile client [--project tsconfig.json] [--out src/api.gen.ts] [--root file.ts#AppModule] [--check]
|
|
13
|
+
Generate the Api type for @avijs/client. --check fails if the file is out of date.`;
|
|
9
14
|
export async function main(argv) {
|
|
10
15
|
const [command, ...rest] = argv;
|
|
11
16
|
const { values, positionals } = parseArgs({
|
|
@@ -14,7 +19,9 @@ export async function main(argv) {
|
|
|
14
19
|
options: {
|
|
15
20
|
project: { type: 'string', default: 'tsconfig.json' },
|
|
16
21
|
export: { type: 'string', default: 'AppModule' },
|
|
17
|
-
out: { type: 'string'
|
|
22
|
+
out: { type: 'string' },
|
|
23
|
+
root: { type: 'string' },
|
|
24
|
+
check: { type: 'boolean', default: false },
|
|
18
25
|
},
|
|
19
26
|
});
|
|
20
27
|
if (command === 'check') {
|
|
@@ -25,13 +32,17 @@ export async function main(argv) {
|
|
|
25
32
|
console.log(`${String(errors)} error(s), ${String(diagnostics.length - errors)} warning(s) in static inject lists`);
|
|
26
33
|
return errors > 0 ? 1 : 0;
|
|
27
34
|
}
|
|
35
|
+
if (command === 'client') {
|
|
36
|
+
return client(values.project, values.out ?? 'src/api.gen.ts', values.root, values.check);
|
|
37
|
+
}
|
|
28
38
|
if (command === 'manifest' && positionals[0]) {
|
|
39
|
+
const out = values.out ?? 'avi.manifest.json';
|
|
29
40
|
const description = await writeManifest({
|
|
30
41
|
entry: positionals[0],
|
|
31
42
|
exportName: values.export,
|
|
32
|
-
out
|
|
43
|
+
out,
|
|
33
44
|
});
|
|
34
|
-
console.log(`Wrote ${
|
|
45
|
+
console.log(`Wrote ${out}: ${String(description.modules.length)} modules, ${String(description.routes.length)} routes`);
|
|
35
46
|
for (const controller of description.dynamic) {
|
|
36
47
|
console.log(`note: ${controller} creates its routes at runtime; they are not listed`);
|
|
37
48
|
}
|
|
@@ -40,6 +51,26 @@ export async function main(argv) {
|
|
|
40
51
|
console.error(USAGE);
|
|
41
52
|
return 2;
|
|
42
53
|
}
|
|
54
|
+
async function client(project, out, root, check) {
|
|
55
|
+
const result = generateClient({ project, out, root });
|
|
56
|
+
for (const d of result.diagnostics)
|
|
57
|
+
console.log(format(d));
|
|
58
|
+
if (result.diagnostics.some((d) => d.severity === 'error'))
|
|
59
|
+
return 1;
|
|
60
|
+
const routes = `${String(result.routes.length)} routes`;
|
|
61
|
+
if (check) {
|
|
62
|
+
const current = await readFile(out, 'utf8').catch(() => undefined);
|
|
63
|
+
// Formatters may rewrap the file; only the content counts.
|
|
64
|
+
const same = current !== undefined && squash(current) === squash(result.source);
|
|
65
|
+
console.log(same ? `${out} is up to date (${routes})` : `${out} is out of date: run avi client`);
|
|
66
|
+
return same ? 0 : 1;
|
|
67
|
+
}
|
|
68
|
+
await mkdir(path.dirname(path.resolve(out)), { recursive: true });
|
|
69
|
+
await writeFile(out, result.source);
|
|
70
|
+
console.log(`Wrote ${out}: ${routes}`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
const squash = (text) => text.replace(/[\s,;]+/g, '');
|
|
43
74
|
function format(d) {
|
|
44
75
|
return `${d.file}:${String(d.line)}:${String(d.column)} ${d.severity} ${d.code}: ${d.message}`;
|
|
45
76
|
}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAmB,MAAM,YAAY,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,KAAK,GAAG
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Diagnostic } from './check.js';
|
|
2
|
+
export interface ClientRoute {
|
|
3
|
+
readonly method: string;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
/** The type the generated `Api` gives this route. */
|
|
6
|
+
readonly type: string;
|
|
7
|
+
}
|
|
8
|
+
export interface GeneratedClient {
|
|
9
|
+
/** The generated file's contents. */
|
|
10
|
+
readonly source: string;
|
|
11
|
+
readonly routes: readonly ClientRoute[];
|
|
12
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
13
|
+
}
|
|
14
|
+
export interface GenerateClientOptions {
|
|
15
|
+
/** Path to tsconfig.json. */
|
|
16
|
+
readonly project: string;
|
|
17
|
+
/** Where the file will be written; imports are relative to it. */
|
|
18
|
+
readonly out: string;
|
|
19
|
+
/** `file.ts#Export` of the root module. Default: found from `AviFactory.create` or `register`. */
|
|
20
|
+
readonly root?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads an app's modules and controllers statically and generates the `Api` type for
|
|
24
|
+
* `@avijs/client`. The type references the app's own classes and controllers, so it stays exact
|
|
25
|
+
* and only changes when routes do.
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateClient(options: GenerateClientOptions): GeneratedClient;
|
|
28
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,qBAAqB;IACpC,6BAA6B;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAID;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,eAAe,CAY9E"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { readProgram, unwrap } from './check.js';
|
|
4
|
+
const ROUTE_DECORATORS = new Set(['Get', 'Post', 'Put', 'Patch', 'Delete', 'Head', 'Options']);
|
|
5
|
+
/**
|
|
6
|
+
* Reads an app's modules and controllers statically and generates the `Api` type for
|
|
7
|
+
* `@avijs/client`. The type references the app's own classes and controllers, so it stays exact
|
|
8
|
+
* and only changes when routes do.
|
|
9
|
+
*/
|
|
10
|
+
export function generateClient(options) {
|
|
11
|
+
const { program, roots } = readProgram(options.project);
|
|
12
|
+
const checker = program.getTypeChecker();
|
|
13
|
+
const out = path.resolve(options.out);
|
|
14
|
+
const generator = new Generator(checker, roots, out);
|
|
15
|
+
const appRoots = options.root
|
|
16
|
+
? generator.namedRoot(program, options.root)
|
|
17
|
+
: generator.findRoots(program);
|
|
18
|
+
for (const root of appRoots)
|
|
19
|
+
generator.module(root);
|
|
20
|
+
return generator.finish();
|
|
21
|
+
}
|
|
22
|
+
class Generator {
|
|
23
|
+
checker;
|
|
24
|
+
roots;
|
|
25
|
+
out;
|
|
26
|
+
#routes = new Map();
|
|
27
|
+
#diagnostics = [];
|
|
28
|
+
#imports = new Map();
|
|
29
|
+
#visited = new Set();
|
|
30
|
+
constructor(checker, roots, out) {
|
|
31
|
+
this.checker = checker;
|
|
32
|
+
this.roots = roots;
|
|
33
|
+
this.out = out;
|
|
34
|
+
}
|
|
35
|
+
/** Module expressions passed to `AviFactory.create` or `app.register` outside tests. */
|
|
36
|
+
findRoots(program) {
|
|
37
|
+
const found = [];
|
|
38
|
+
let first;
|
|
39
|
+
for (const source of program.getSourceFiles()) {
|
|
40
|
+
if (!this.#inProject(source) || /\.(test|spec)\.[cm]?tsx?$/.test(source.fileName))
|
|
41
|
+
continue;
|
|
42
|
+
first ??= source;
|
|
43
|
+
const visit = (node) => {
|
|
44
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
45
|
+
const name = node.expression.name.text;
|
|
46
|
+
const target = node.expression.expression;
|
|
47
|
+
const arg = node.arguments[0];
|
|
48
|
+
if (arg && name === 'create' && this.#symbolName(target) === 'AviFactory') {
|
|
49
|
+
found.push(arg);
|
|
50
|
+
}
|
|
51
|
+
else if (arg && name === 'register' && this.#typeName(target) === 'AviApp') {
|
|
52
|
+
found.push(arg);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
ts.forEachChild(node, visit);
|
|
56
|
+
};
|
|
57
|
+
visit(source);
|
|
58
|
+
}
|
|
59
|
+
if (found.length === 0) {
|
|
60
|
+
this.#report(first, undefined, 'error', 'AVI101', 'No app found: pass --root file.ts#AppModule, or call AviFactory.create or app.register');
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
namedRoot(program, root) {
|
|
65
|
+
const [file = '', name = 'AppModule'] = root.split('#');
|
|
66
|
+
const source = program.getSourceFile(path.resolve(file));
|
|
67
|
+
const symbol = source && this.checker.getSymbolAtLocation(source);
|
|
68
|
+
const exported = symbol && this.checker.getExportsOfModule(symbol).find((s) => s.name === name);
|
|
69
|
+
const declaration = exported && this.#declaration(exported);
|
|
70
|
+
if (!declaration) {
|
|
71
|
+
this.#report(source, undefined, 'error', 'AVI101', `Cannot find export ${name} in ${file}`);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const expression = ts.isVariableDeclaration(declaration)
|
|
75
|
+
? declaration.initializer
|
|
76
|
+
: ts.isClassDeclaration(declaration) && declaration.name
|
|
77
|
+
? declaration.name
|
|
78
|
+
: undefined;
|
|
79
|
+
return expression ? [expression] : [];
|
|
80
|
+
}
|
|
81
|
+
/** Walks a module: `@Module` class, `defineModule(...)`, or a module options object. */
|
|
82
|
+
module(expression) {
|
|
83
|
+
const options = this.#moduleOptions(expression);
|
|
84
|
+
if (!options || this.#visited.has(options))
|
|
85
|
+
return;
|
|
86
|
+
this.#visited.add(options);
|
|
87
|
+
const prefix = this.#stringProperty(options, 'prefix') ?? '/';
|
|
88
|
+
for (const ref of this.#arrayProperty(options, 'imports'))
|
|
89
|
+
this.module(ref);
|
|
90
|
+
for (const ref of this.#arrayProperty(options, 'controllers'))
|
|
91
|
+
this.#controller(ref, prefix);
|
|
92
|
+
}
|
|
93
|
+
finish() {
|
|
94
|
+
const routes = [...this.#routes.values()].sort((a, b) => a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path));
|
|
95
|
+
const used = new Set(routes.map((r) => (r.type.startsWith('Method') ? 'Method' : 'Route')));
|
|
96
|
+
const helpers = [
|
|
97
|
+
used.has('Method') && 'MethodEndpoint',
|
|
98
|
+
used.has('Route') && 'RouteEndpoint',
|
|
99
|
+
].filter(Boolean);
|
|
100
|
+
const imports = [...this.#imports.values()]
|
|
101
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.alias.localeCompare(b.alias))
|
|
102
|
+
.map(({ name, file, alias }) => {
|
|
103
|
+
const named = name === alias ? name : `${name} as ${alias}`;
|
|
104
|
+
return `import type { ${named} } from '${this.#specifier(file)}';`;
|
|
105
|
+
});
|
|
106
|
+
const lines = [
|
|
107
|
+
'// Generated by `avi client`. Do not edit: run it again after changing routes.',
|
|
108
|
+
...(helpers.length > 0 ? [`import type { ${helpers.join(', ')} } from '@avijs/core';`] : []),
|
|
109
|
+
...imports,
|
|
110
|
+
'',
|
|
111
|
+
'export interface Api {',
|
|
112
|
+
...routes.map((r) => ` '${r.method} ${r.path}': ${r.type};`),
|
|
113
|
+
'}',
|
|
114
|
+
'',
|
|
115
|
+
];
|
|
116
|
+
const diagnostics = this.#diagnostics.sort((a, b) => a.file === b.file ? a.line - b.line || a.column - b.column : a.file.localeCompare(b.file));
|
|
117
|
+
return { source: lines.join('\n'), routes, diagnostics };
|
|
118
|
+
}
|
|
119
|
+
#moduleOptions(expression) {
|
|
120
|
+
const node = unwrap(expression);
|
|
121
|
+
if (!node)
|
|
122
|
+
return undefined;
|
|
123
|
+
if (ts.isObjectLiteralExpression(node))
|
|
124
|
+
return node;
|
|
125
|
+
if (ts.isCallExpression(node) && this.#symbolName(node.expression) === 'defineModule') {
|
|
126
|
+
const arg = unwrap(node.arguments[0]);
|
|
127
|
+
return arg && ts.isObjectLiteralExpression(arg) ? arg : undefined;
|
|
128
|
+
}
|
|
129
|
+
if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) {
|
|
130
|
+
const declaration = this.#declarationOf(node);
|
|
131
|
+
// Modules from libraries, such as a config token's module, have no app routes.
|
|
132
|
+
if (!declaration || !this.#inProject(declaration.getSourceFile()))
|
|
133
|
+
return undefined;
|
|
134
|
+
if (ts.isClassDeclaration(declaration)) {
|
|
135
|
+
const options = this.#decoratorArgs(declaration, (name) => name === 'Module')?.[0];
|
|
136
|
+
const object = unwrap(options);
|
|
137
|
+
if (object && ts.isObjectLiteralExpression(object))
|
|
138
|
+
return object;
|
|
139
|
+
if (!options)
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
else if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
143
|
+
return this.#moduleOptions(declaration.initializer);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.#report(node.getSourceFile(), node, 'warning', 'AVI104', `Cannot read module ${node.getText()} statically; its routes are left out`);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
#controller(expression, modulePrefix) {
|
|
150
|
+
const node = unwrap(expression);
|
|
151
|
+
if (!node)
|
|
152
|
+
return;
|
|
153
|
+
const declaration = ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)
|
|
154
|
+
? this.#declarationOf(node)
|
|
155
|
+
: undefined;
|
|
156
|
+
if (declaration && ts.isClassDeclaration(declaration) && declaration.name) {
|
|
157
|
+
this.#classController(declaration, declaration.name.text, modulePrefix);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const call = declaration && ts.isVariableDeclaration(declaration) ? unwrap(declaration.initializer) : node;
|
|
161
|
+
if (call &&
|
|
162
|
+
ts.isCallExpression(call) &&
|
|
163
|
+
this.#symbolName(call.expression) === 'defineController') {
|
|
164
|
+
if (!declaration || !ts.isVariableDeclaration(declaration)) {
|
|
165
|
+
this.#report(node.getSourceFile(), node, 'warning', 'AVI102', 'Move this controller into an exported const so the client can reference it');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.#definedController(declaration, call, modulePrefix);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.#report(node.getSourceFile(), node, 'warning', 'AVI103', `Cannot read controller ${node.getText()} statically; its routes are left out`);
|
|
172
|
+
}
|
|
173
|
+
#classController(node, name, modulePrefix) {
|
|
174
|
+
const args = this.#decoratorArgs(node, (n) => n === 'Controller');
|
|
175
|
+
if (!args)
|
|
176
|
+
return;
|
|
177
|
+
const prefix = args[0] ? this.#literal(args[0]) : '/';
|
|
178
|
+
if (prefix === undefined)
|
|
179
|
+
return;
|
|
180
|
+
const alias = this.#import(node, name);
|
|
181
|
+
if (!alias)
|
|
182
|
+
return;
|
|
183
|
+
const base = joinPath(modulePrefix, prefix);
|
|
184
|
+
for (const member of node.members) {
|
|
185
|
+
if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name))
|
|
186
|
+
continue;
|
|
187
|
+
for (const decorator of ts.getDecorators(member) ?? []) {
|
|
188
|
+
const call = decorator.expression;
|
|
189
|
+
if (!ts.isCallExpression(call))
|
|
190
|
+
continue;
|
|
191
|
+
const method = this.#symbolName(call.expression);
|
|
192
|
+
if (!method || !ROUTE_DECORATORS.has(method))
|
|
193
|
+
continue;
|
|
194
|
+
const routePath = call.arguments[0] ? this.#literal(call.arguments[0]) : '';
|
|
195
|
+
if (routePath === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
this.#checkSchemaVisible(call, member);
|
|
198
|
+
const full = joinPath(base, routePath);
|
|
199
|
+
this.#add(method.toUpperCase(), full, `MethodEndpoint<'${full}', ${alias}['${member.name.text}']>`, call);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
#definedController(declaration, call, modulePrefix) {
|
|
204
|
+
const options = unwrap(call.arguments[0]);
|
|
205
|
+
if (!options || !ts.isObjectLiteralExpression(options)) {
|
|
206
|
+
this.#dynamic(call, 'its options are not an object literal');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const prefix = this.#stringProperty(options, 'prefix') ?? '/';
|
|
210
|
+
const routes = this.#routeList(options);
|
|
211
|
+
if (!routes || !ts.isIdentifier(declaration.name))
|
|
212
|
+
return;
|
|
213
|
+
const alias = this.#import(declaration, declaration.name.text);
|
|
214
|
+
if (!alias)
|
|
215
|
+
return;
|
|
216
|
+
const base = joinPath(modulePrefix, prefix);
|
|
217
|
+
routes.elements.forEach((element, index) => {
|
|
218
|
+
let object = unwrap(element);
|
|
219
|
+
if (object &&
|
|
220
|
+
ts.isCallExpression(object) &&
|
|
221
|
+
this.#symbolName(object.expression) === 'route') {
|
|
222
|
+
object = unwrap(object.arguments[0]);
|
|
223
|
+
}
|
|
224
|
+
if (!object || !ts.isObjectLiteralExpression(object)) {
|
|
225
|
+
this.#dynamic(element, 'this route is not an object literal or route({...})');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const method = this.#stringProperty(object, 'method');
|
|
229
|
+
const routePath = this.#stringProperty(object, 'path') ?? '';
|
|
230
|
+
if (method === undefined)
|
|
231
|
+
return;
|
|
232
|
+
const full = joinPath(base, routePath);
|
|
233
|
+
this.#add(method, full, `RouteEndpoint<'${full}', typeof ${alias}, ${String(index)}>`, element);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** The routes array: a literal, or a factory whose body returns one. */
|
|
237
|
+
#routeList(options) {
|
|
238
|
+
const value = unwrap(this.#property(options, 'routes'));
|
|
239
|
+
let list = value;
|
|
240
|
+
if (value && (ts.isArrowFunction(value) || ts.isFunctionExpression(value))) {
|
|
241
|
+
if (ts.isBlock(value.body)) {
|
|
242
|
+
const returns = value.body.statements.filter(ts.isReturnStatement);
|
|
243
|
+
list = returns.length === 1 ? returns[0]?.expression : undefined;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
list = value.body;
|
|
247
|
+
}
|
|
248
|
+
list = unwrap(list);
|
|
249
|
+
}
|
|
250
|
+
if (!list || !ts.isArrayLiteralExpression(list) || list.elements.some(ts.isSpreadElement)) {
|
|
251
|
+
this.#dynamic(value ?? options, 'its routes are not a literal array');
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
return list;
|
|
255
|
+
}
|
|
256
|
+
/** Warns when a decorator schema cannot reach the client because ctx is not a RouteContext. */
|
|
257
|
+
#checkSchemaVisible(call, method) {
|
|
258
|
+
const options = unwrap(call.arguments[1]);
|
|
259
|
+
const schema = options && ts.isObjectLiteralExpression(options) && this.#property(options, 'schema');
|
|
260
|
+
if (!schema)
|
|
261
|
+
return;
|
|
262
|
+
// The response type comes from the handler anyway; only inputs need the schema.
|
|
263
|
+
const fields = this.checker.getTypeAtLocation(schema);
|
|
264
|
+
if (!['params', 'query', 'body'].some((name) => fields.getProperty(name)))
|
|
265
|
+
return;
|
|
266
|
+
const param = method.parameters[0];
|
|
267
|
+
const type = param && this.checker.getTypeAtLocation(param);
|
|
268
|
+
if (type?.getProperty('~schema'))
|
|
269
|
+
return;
|
|
270
|
+
this.#report(method.getSourceFile(), method.name, 'warning', 'AVI105', `Type ctx as RouteContext<typeof schema> so the client sees the schema of ${method.name.getText()}`);
|
|
271
|
+
}
|
|
272
|
+
#add(method, full, type, at) {
|
|
273
|
+
const key = `${method} ${full}`;
|
|
274
|
+
if (this.#routes.has(key)) {
|
|
275
|
+
this.#report(at.getSourceFile(), at, 'warning', 'AVI106', `Duplicate route ${key}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
this.#routes.set(key, { method, path: full, type });
|
|
279
|
+
}
|
|
280
|
+
/** Imports a declaration's name into the generated file, or reports why it cannot. */
|
|
281
|
+
#import(node, name) {
|
|
282
|
+
const source = node.getSourceFile();
|
|
283
|
+
const symbol = this.checker.getSymbolAtLocation(source);
|
|
284
|
+
const exported = symbol &&
|
|
285
|
+
this.checker
|
|
286
|
+
.getExportsOfModule(symbol)
|
|
287
|
+
.some((s) => s.name === name && this.#declaration(s) === node);
|
|
288
|
+
if (!exported) {
|
|
289
|
+
this.#report(source, node, 'warning', 'AVI102', `Export ${name} so the client can reference it; its routes are left out`);
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
const key = `${source.fileName}#${name}`;
|
|
293
|
+
const existing = this.#imports.get(key);
|
|
294
|
+
if (existing)
|
|
295
|
+
return existing.alias;
|
|
296
|
+
const taken = new Set([...this.#imports.values()].map((i) => i.alias));
|
|
297
|
+
let alias = name;
|
|
298
|
+
for (let n = 2; taken.has(alias); n++)
|
|
299
|
+
alias = `${name}_${String(n)}`;
|
|
300
|
+
this.#imports.set(key, { name, file: source.fileName, alias });
|
|
301
|
+
return alias;
|
|
302
|
+
}
|
|
303
|
+
#specifier(file) {
|
|
304
|
+
let relative = path.relative(path.dirname(this.out), file).split(path.sep).join('/');
|
|
305
|
+
if (!relative.startsWith('.'))
|
|
306
|
+
relative = `./${relative}`;
|
|
307
|
+
return relative.replace(/\.(m|c)?tsx?$/, (_, kind) => `.${kind ?? ''}js`);
|
|
308
|
+
}
|
|
309
|
+
#decoratorArgs(node, match) {
|
|
310
|
+
for (const decorator of ts.getDecorators(node) ?? []) {
|
|
311
|
+
const call = decorator.expression;
|
|
312
|
+
if (ts.isCallExpression(call)) {
|
|
313
|
+
const name = this.#symbolName(call.expression);
|
|
314
|
+
if (name && match(name))
|
|
315
|
+
return call.arguments;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
#property(object, name) {
|
|
321
|
+
for (const property of object.properties) {
|
|
322
|
+
if (ts.isPropertyAssignment(property) && property.name.getText() === name) {
|
|
323
|
+
return property.initializer;
|
|
324
|
+
}
|
|
325
|
+
if (ts.isShorthandPropertyAssignment(property) && property.name.text === name) {
|
|
326
|
+
return property.name;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
#stringProperty(object, name) {
|
|
332
|
+
const value = this.#property(object, name);
|
|
333
|
+
return value ? this.#literal(value) : undefined;
|
|
334
|
+
}
|
|
335
|
+
#arrayProperty(object, name) {
|
|
336
|
+
const value = this.#property(object, name);
|
|
337
|
+
if (!value)
|
|
338
|
+
return [];
|
|
339
|
+
const list = this.#resolveValue(value);
|
|
340
|
+
if (!list || !ts.isArrayLiteralExpression(list)) {
|
|
341
|
+
this.#dynamic(value, `${name} is not a literal array`);
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
return list.elements.filter((element) => {
|
|
345
|
+
if (!ts.isSpreadElement(element))
|
|
346
|
+
return true;
|
|
347
|
+
this.#dynamic(element, `spreads in ${name} cannot be followed`);
|
|
348
|
+
return false;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/** A string literal, directly or through a const. */
|
|
352
|
+
#literal(expression) {
|
|
353
|
+
const value = this.#resolveValue(expression);
|
|
354
|
+
if (value && ts.isStringLiteralLike(value))
|
|
355
|
+
return value.text;
|
|
356
|
+
this.#dynamic(expression, `${expression.getText()} is not a string literal`);
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
/** Follows a const identifier to its initializer. */
|
|
360
|
+
#resolveValue(expression) {
|
|
361
|
+
const node = unwrap(expression);
|
|
362
|
+
if (node && ts.isIdentifier(node)) {
|
|
363
|
+
const declaration = this.#declarationOf(node);
|
|
364
|
+
if (declaration &&
|
|
365
|
+
ts.isVariableDeclaration(declaration) &&
|
|
366
|
+
declaration.initializer &&
|
|
367
|
+
(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0) {
|
|
368
|
+
return unwrap(declaration.initializer);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return node;
|
|
372
|
+
}
|
|
373
|
+
#dynamic(at, why) {
|
|
374
|
+
this.#report(at.getSourceFile(), at, 'warning', 'AVI103', `Cannot read routes: ${why}`);
|
|
375
|
+
}
|
|
376
|
+
#declarationOf(node) {
|
|
377
|
+
const symbol = this.checker.getSymbolAtLocation(node);
|
|
378
|
+
return symbol && this.#declaration(symbol);
|
|
379
|
+
}
|
|
380
|
+
#declaration(symbol) {
|
|
381
|
+
const target = symbol.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(symbol) : symbol;
|
|
382
|
+
return target.valueDeclaration ?? target.declarations?.[0];
|
|
383
|
+
}
|
|
384
|
+
#symbolName(node) {
|
|
385
|
+
const symbol = this.checker.getSymbolAtLocation(node);
|
|
386
|
+
if (!symbol)
|
|
387
|
+
return undefined;
|
|
388
|
+
const target = symbol.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(symbol) : symbol;
|
|
389
|
+
return target.name;
|
|
390
|
+
}
|
|
391
|
+
#typeName(node) {
|
|
392
|
+
const type = this.checker.getTypeAtLocation(node);
|
|
393
|
+
return (type.aliasSymbol ?? type.getSymbol())?.name;
|
|
394
|
+
}
|
|
395
|
+
#inProject(source) {
|
|
396
|
+
return !source.isDeclarationFile && this.roots.has(path.resolve(source.fileName));
|
|
397
|
+
}
|
|
398
|
+
#report(source, at, severity, code, message) {
|
|
399
|
+
const position = source && at ? source.getLineAndCharacterOfPosition(at.getStart(source)) : undefined;
|
|
400
|
+
this.#diagnostics.push({
|
|
401
|
+
file: source?.fileName ?? '',
|
|
402
|
+
line: (position?.line ?? 0) + 1,
|
|
403
|
+
column: (position?.character ?? 0) + 1,
|
|
404
|
+
severity,
|
|
405
|
+
code,
|
|
406
|
+
message,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Same as `joinPath` in @avijs/core.
|
|
411
|
+
function joinPath(prefix, routePath) {
|
|
412
|
+
const joined = `${prefix}/${routePath}`.replace(/\/{2,}/g, '/');
|
|
413
|
+
return joined.length > 1 && joined.endsWith('/') ? joined.slice(0, -1) : joined;
|
|
414
|
+
}
|
|
415
|
+
//# sourceMappingURL=client.js.map
|