@avijs/compiler 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/check.d.ts +15 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +102 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +46 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +13 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/package.json +53 -3
- package/src/bin.ts +4 -0
- package/src/check.ts +150 -0
- package/src/cli.ts +54 -0
- package/src/index.ts +4 -0
- package/src/manifest.ts +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christopher Robin Chase
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @avijs/compiler
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Optional build-time checks and manifests for Avi.js apps (avi-compile).
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @avijs/compiler
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Part of [Avi.js](https://github.com/chrischase011/avi-js), a TypeScript framework for Node.js with
|
|
10
|
+
modules, controllers and dependency injection, without `reflect-metadata`. See the
|
|
11
|
+
[tutorial](https://github.com/chrischase011/avi-js/blob/main/docs/tutorial/README.md) to get started.
|
|
12
|
+
|
|
13
|
+
Requires Node.js 22.12 or newer. MIT licensed.
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Diagnostic {
|
|
2
|
+
readonly file: string;
|
|
3
|
+
readonly line: number;
|
|
4
|
+
readonly column: number;
|
|
5
|
+
readonly severity: 'error' | 'warning';
|
|
6
|
+
/** AVI001 too few, AVI002 not checkable, AVI003 unknown token, AVI004 wrong type. */
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Type-checks every `static inject` list in a TypeScript project against its class constructor.
|
|
12
|
+
* Decorated classes get this from the compiler already; this covers undecorated ones too.
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkInjectLists(project: string): Diagnostic[];
|
|
15
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAGA,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,CA0B9D"}
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
/**
|
|
4
|
+
* Type-checks every `static inject` list in a TypeScript project against its class constructor.
|
|
5
|
+
* Decorated classes get this from the compiler already; this covers undecorated ones too.
|
|
6
|
+
*/
|
|
7
|
+
export function checkInjectLists(project) {
|
|
8
|
+
const config = ts.getParsedCommandLineOfConfigFile(path.resolve(project), undefined, {
|
|
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 });
|
|
17
|
+
const checker = program.getTypeChecker();
|
|
18
|
+
const roots = new Set(config.fileNames.map((file) => path.resolve(file)));
|
|
19
|
+
const diagnostics = [];
|
|
20
|
+
for (const source of program.getSourceFiles()) {
|
|
21
|
+
if (source.isDeclarationFile || !roots.has(path.resolve(source.fileName)))
|
|
22
|
+
continue;
|
|
23
|
+
const visit = (node) => {
|
|
24
|
+
if (ts.isClassLike(node))
|
|
25
|
+
checkClass(node, source, checker, diagnostics);
|
|
26
|
+
ts.forEachChild(node, visit);
|
|
27
|
+
};
|
|
28
|
+
visit(source);
|
|
29
|
+
}
|
|
30
|
+
return diagnostics.sort((a, b) => a.file === b.file ? a.line - b.line || a.column - b.column : a.file.localeCompare(b.file));
|
|
31
|
+
}
|
|
32
|
+
function checkClass(node, source, checker, out) {
|
|
33
|
+
const inject = node.members.find((member) => ts.isPropertyDeclaration(member) &&
|
|
34
|
+
(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static) !== 0 &&
|
|
35
|
+
member.name.getText(source) === 'inject');
|
|
36
|
+
if (!inject)
|
|
37
|
+
return;
|
|
38
|
+
const className = node.name?.text ?? '(anonymous class)';
|
|
39
|
+
const report = (at, severity, code, message) => {
|
|
40
|
+
const { line, character } = source.getLineAndCharacterOfPosition(at.getStart(source));
|
|
41
|
+
out.push({
|
|
42
|
+
file: source.fileName,
|
|
43
|
+
line: line + 1,
|
|
44
|
+
column: character + 1,
|
|
45
|
+
severity,
|
|
46
|
+
code,
|
|
47
|
+
message,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const list = unwrap(inject.initializer);
|
|
51
|
+
if (!list || !ts.isArrayLiteralExpression(list)) {
|
|
52
|
+
report(inject, 'warning', 'AVI002', `${className}.inject is not an array literal and cannot be checked`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (list.elements.some(ts.isSpreadElement)) {
|
|
56
|
+
report(list, 'warning', 'AVI002', `${className}.inject uses a spread and cannot be checked`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const ctor = node.members.find(ts.isConstructorDeclaration);
|
|
60
|
+
const params = ctor?.parameters ?? ts.factory.createNodeArray();
|
|
61
|
+
const required = params.filter((p) => !p.questionToken && !p.initializer && !p.dotDotDotToken).length;
|
|
62
|
+
if (list.elements.length < required) {
|
|
63
|
+
report(list, 'error', 'AVI001', `${className} needs ${String(required)} constructor arguments but inject lists ${String(list.elements.length)}`);
|
|
64
|
+
}
|
|
65
|
+
params.forEach((param, i) => {
|
|
66
|
+
const element = list.elements[i];
|
|
67
|
+
if (!element || param.dotDotDotToken)
|
|
68
|
+
return;
|
|
69
|
+
const provided = tokenType(element, checker);
|
|
70
|
+
if (!provided) {
|
|
71
|
+
report(element, 'warning', 'AVI003', `Cannot tell what ${element.getText(source)} resolves to`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const expected = checker.getTypeAtLocation(param);
|
|
75
|
+
if (!checker.isTypeAssignableTo(provided, expected)) {
|
|
76
|
+
report(element, 'error', 'AVI004', `${className}.inject[${String(i)}] provides ${checker.typeToString(provided)}, but parameter "${param.name.getText(source)}" expects ${checker.typeToString(expected)}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function unwrap(expression) {
|
|
81
|
+
let current = expression;
|
|
82
|
+
while (current &&
|
|
83
|
+
(ts.isAsExpression(current) ||
|
|
84
|
+
ts.isSatisfiesExpression(current) ||
|
|
85
|
+
ts.isParenthesizedExpression(current) ||
|
|
86
|
+
ts.isTypeAssertionExpression(current))) {
|
|
87
|
+
current = current.expression;
|
|
88
|
+
}
|
|
89
|
+
return current;
|
|
90
|
+
}
|
|
91
|
+
/** A class token resolves to its instance type; an `InjectionToken<T>` to `T`. */
|
|
92
|
+
function tokenType(element, checker) {
|
|
93
|
+
const type = checker.getTypeAtLocation(element);
|
|
94
|
+
const construct = type.getConstructSignatures()[0];
|
|
95
|
+
if (construct)
|
|
96
|
+
return construct.getReturnType();
|
|
97
|
+
if (type.getSymbol()?.getName() === 'InjectionToken') {
|
|
98
|
+
return checker.getTypeArguments(type)[0];
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAUA,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCnE"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { checkInjectLists } from './check.js';
|
|
3
|
+
import { writeManifest } from './manifest.js';
|
|
4
|
+
const USAGE = `Usage:
|
|
5
|
+
avi-compile check [--project tsconfig.json]
|
|
6
|
+
Type-check every static inject list against its constructor.
|
|
7
|
+
avi-compile manifest <entry.js> [--export AppModule] [--out avi.manifest.json]
|
|
8
|
+
Describe the app's modules, providers and routes without starting it.`;
|
|
9
|
+
export async function main(argv) {
|
|
10
|
+
const [command, ...rest] = argv;
|
|
11
|
+
const { values, positionals } = parseArgs({
|
|
12
|
+
args: [...rest],
|
|
13
|
+
allowPositionals: true,
|
|
14
|
+
options: {
|
|
15
|
+
project: { type: 'string', default: 'tsconfig.json' },
|
|
16
|
+
export: { type: 'string', default: 'AppModule' },
|
|
17
|
+
out: { type: 'string', default: 'avi.manifest.json' },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
if (command === 'check') {
|
|
21
|
+
const diagnostics = checkInjectLists(values.project);
|
|
22
|
+
for (const d of diagnostics)
|
|
23
|
+
console.log(format(d));
|
|
24
|
+
const errors = diagnostics.filter((d) => d.severity === 'error').length;
|
|
25
|
+
console.log(`${String(errors)} error(s), ${String(diagnostics.length - errors)} warning(s) in static inject lists`);
|
|
26
|
+
return errors > 0 ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
if (command === 'manifest' && positionals[0]) {
|
|
29
|
+
const description = await writeManifest({
|
|
30
|
+
entry: positionals[0],
|
|
31
|
+
exportName: values.export,
|
|
32
|
+
out: values.out,
|
|
33
|
+
});
|
|
34
|
+
console.log(`Wrote ${values.out}: ${String(description.modules.length)} modules, ${String(description.routes.length)} routes`);
|
|
35
|
+
for (const controller of description.dynamic) {
|
|
36
|
+
console.log(`note: ${controller} creates its routes at runtime; they are not listed`);
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
console.error(USAGE);
|
|
41
|
+
return 2;
|
|
42
|
+
}
|
|
43
|
+
function format(d) {
|
|
44
|
+
return `${d.file}:${String(d.line)}:${String(d.column)} ${d.severity} ${d.code}: ${d.message}`;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +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;;;;4EAI8D,CAAC;AAE7E,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAuB;IAChD,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;QACxC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,gBAAgB,EAAE,IAAI;QACtB,OAAO,EAAE;YACP,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,EAAE;YACrD,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE;YAChD,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,mBAAmB,EAAE;SACtD;KACF,CAAC,CAAC;IAEH,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;QACxB,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACrD,KAAK,MAAM,CAAC,IAAI,WAAW;YAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;QACxE,OAAO,CAAC,GAAG,CACT,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,MAAM,CAAC,oCAAoC,CACvG,CAAC;QACF,OAAO,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI,OAAO,KAAK,UAAU,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC;YACtC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC;YACrB,UAAU,EAAE,MAAM,CAAC,MAAM;YACzB,GAAG,EAAE,MAAM,CAAC,GAAG;SAChB,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CACT,SAAS,MAAM,CAAC,GAAG,KAAK,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAClH,CAAC;QACF,KAAK,MAAM,UAAU,IAAI,WAAW,CAAC,OAAO,EAAE,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,SAAS,UAAU,qDAAqD,CAAC,CAAC;QACxF,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,MAAM,CAAC,CAAa;IAC3B,OAAO,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;AACjG,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AppDescription } from '@avijs/core';
|
|
2
|
+
/** Serializes a description the same way every time, so manifests diff cleanly. */
|
|
3
|
+
export declare function serializeManifest(description: AppDescription): string;
|
|
4
|
+
/**
|
|
5
|
+
* Imports the compiled entry module, describes the exported root module and writes the
|
|
6
|
+
* manifest. Importing runs the module's top-level code, but no provider is created.
|
|
7
|
+
*/
|
|
8
|
+
export declare function writeManifest(options: {
|
|
9
|
+
entry: string;
|
|
10
|
+
exportName?: string;
|
|
11
|
+
out: string;
|
|
12
|
+
}): Promise<AppDescription>;
|
|
13
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,cAAc,EAAkB,MAAM,aAAa,CAAC;AAK/E,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,cAAc,GAAG,MAAM,CAErE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,cAAc,CAAC,CAa1B"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describeApp } from '@avijs/core';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
/** Serializes a description the same way every time, so manifests diff cleanly. */
|
|
6
|
+
export function serializeManifest(description) {
|
|
7
|
+
return `${JSON.stringify(description, null, 2)}\n`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Imports the compiled entry module, describes the exported root module and writes the
|
|
11
|
+
* manifest. Importing runs the module's top-level code, but no provider is created.
|
|
12
|
+
*/
|
|
13
|
+
export async function writeManifest(options) {
|
|
14
|
+
const exportName = options.exportName ?? 'AppModule';
|
|
15
|
+
const loaded = (await import(pathToFileURL(path.resolve(options.entry)).href));
|
|
16
|
+
const root = loaded[exportName];
|
|
17
|
+
if (root === undefined) {
|
|
18
|
+
throw new Error(`${options.entry} has no export named ${exportName}`);
|
|
19
|
+
}
|
|
20
|
+
const description = describeApp(root);
|
|
21
|
+
await writeFile(options.out, serializeManifest(description));
|
|
22
|
+
return description;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAuC,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,mFAAmF;AACnF,MAAM,UAAU,iBAAiB,CAAC,WAA2B;IAC3D,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AACrD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAInC;IACC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,WAAW,CAAC;IACrD,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAG5E,CAAC;IACF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IAChC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,wBAAwB,UAAU,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,WAAW,GAAG,WAAW,CAAC,IAAiB,CAAC,CAAC;IACnD,MAAM,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC,CAAC;IAC7D,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avijs/compiler",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Optional build-time checks and manifests for Avi.js apps.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"avi",
|
|
7
|
+
"avi.js",
|
|
8
|
+
"compiler",
|
|
9
|
+
"typescript",
|
|
10
|
+
"manifest"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Christopher Robin Chase",
|
|
14
|
+
"homepage": "https://github.com/chrischase011/avi-js#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/chrischase011/avi-js/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/chrischase011/avi-js.git",
|
|
21
|
+
"directory": "packages/compiler"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.12"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"bin": {
|
|
29
|
+
"avi-compile": "./dist/bin.js"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"@avijs/source": "./src/index.ts",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"src",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@avijs/core": "0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"typescript": ">=5.8"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
55
|
+
}
|
|
6
56
|
}
|
package/src/bin.ts
ADDED
package/src/check.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
export interface Diagnostic {
|
|
5
|
+
readonly file: string;
|
|
6
|
+
readonly line: number;
|
|
7
|
+
readonly column: number;
|
|
8
|
+
readonly severity: 'error' | 'warning';
|
|
9
|
+
/** AVI001 too few, AVI002 not checkable, AVI003 unknown token, AVI004 wrong type. */
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type-checks every `static inject` list in a TypeScript project against its class constructor.
|
|
16
|
+
* Decorated classes get this from the compiler already; this covers undecorated ones too.
|
|
17
|
+
*/
|
|
18
|
+
export function checkInjectLists(project: string): Diagnostic[] {
|
|
19
|
+
const config = ts.getParsedCommandLineOfConfigFile(path.resolve(project), undefined, {
|
|
20
|
+
...ts.sys,
|
|
21
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
|
|
22
|
+
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (!config) throw new Error(`Cannot read ${project}`);
|
|
26
|
+
|
|
27
|
+
const program = ts.createProgram({ rootNames: config.fileNames, options: config.options });
|
|
28
|
+
const checker = program.getTypeChecker();
|
|
29
|
+
const roots = new Set(config.fileNames.map((file) => path.resolve(file)));
|
|
30
|
+
const diagnostics: Diagnostic[] = [];
|
|
31
|
+
|
|
32
|
+
for (const source of program.getSourceFiles()) {
|
|
33
|
+
if (source.isDeclarationFile || !roots.has(path.resolve(source.fileName))) continue;
|
|
34
|
+
const visit = (node: ts.Node): void => {
|
|
35
|
+
if (ts.isClassLike(node)) checkClass(node, source, checker, diagnostics);
|
|
36
|
+
ts.forEachChild(node, visit);
|
|
37
|
+
};
|
|
38
|
+
visit(source);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return diagnostics.sort((a, b) =>
|
|
42
|
+
a.file === b.file ? a.line - b.line || a.column - b.column : a.file.localeCompare(b.file),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checkClass(
|
|
47
|
+
node: ts.ClassLikeDeclaration,
|
|
48
|
+
source: ts.SourceFile,
|
|
49
|
+
checker: ts.TypeChecker,
|
|
50
|
+
out: Diagnostic[],
|
|
51
|
+
): void {
|
|
52
|
+
const inject = node.members.find(
|
|
53
|
+
(member): member is ts.PropertyDeclaration =>
|
|
54
|
+
ts.isPropertyDeclaration(member) &&
|
|
55
|
+
(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static) !== 0 &&
|
|
56
|
+
member.name.getText(source) === 'inject',
|
|
57
|
+
);
|
|
58
|
+
if (!inject) return;
|
|
59
|
+
|
|
60
|
+
const className = node.name?.text ?? '(anonymous class)';
|
|
61
|
+
const report = (at: ts.Node, severity: Diagnostic['severity'], code: string, message: string) => {
|
|
62
|
+
const { line, character } = source.getLineAndCharacterOfPosition(at.getStart(source));
|
|
63
|
+
out.push({
|
|
64
|
+
file: source.fileName,
|
|
65
|
+
line: line + 1,
|
|
66
|
+
column: character + 1,
|
|
67
|
+
severity,
|
|
68
|
+
code,
|
|
69
|
+
message,
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const list = unwrap(inject.initializer);
|
|
74
|
+
if (!list || !ts.isArrayLiteralExpression(list)) {
|
|
75
|
+
report(
|
|
76
|
+
inject,
|
|
77
|
+
'warning',
|
|
78
|
+
'AVI002',
|
|
79
|
+
`${className}.inject is not an array literal and cannot be checked`,
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (list.elements.some(ts.isSpreadElement)) {
|
|
84
|
+
report(list, 'warning', 'AVI002', `${className}.inject uses a spread and cannot be checked`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ctor = node.members.find(ts.isConstructorDeclaration);
|
|
89
|
+
const params = ctor?.parameters ?? ts.factory.createNodeArray<ts.ParameterDeclaration>();
|
|
90
|
+
const required = params.filter(
|
|
91
|
+
(p) => !p.questionToken && !p.initializer && !p.dotDotDotToken,
|
|
92
|
+
).length;
|
|
93
|
+
if (list.elements.length < required) {
|
|
94
|
+
report(
|
|
95
|
+
list,
|
|
96
|
+
'error',
|
|
97
|
+
'AVI001',
|
|
98
|
+
`${className} needs ${String(required)} constructor arguments but inject lists ${String(list.elements.length)}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
params.forEach((param, i) => {
|
|
103
|
+
const element = list.elements[i];
|
|
104
|
+
if (!element || param.dotDotDotToken) return;
|
|
105
|
+
const provided = tokenType(element, checker);
|
|
106
|
+
if (!provided) {
|
|
107
|
+
report(
|
|
108
|
+
element,
|
|
109
|
+
'warning',
|
|
110
|
+
'AVI003',
|
|
111
|
+
`Cannot tell what ${element.getText(source)} resolves to`,
|
|
112
|
+
);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const expected = checker.getTypeAtLocation(param);
|
|
116
|
+
if (!checker.isTypeAssignableTo(provided, expected)) {
|
|
117
|
+
report(
|
|
118
|
+
element,
|
|
119
|
+
'error',
|
|
120
|
+
'AVI004',
|
|
121
|
+
`${className}.inject[${String(i)}] provides ${checker.typeToString(provided)}, but parameter "${param.name.getText(source)}" expects ${checker.typeToString(expected)}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function unwrap(expression: ts.Expression | undefined): ts.Expression | undefined {
|
|
128
|
+
let current = expression;
|
|
129
|
+
while (
|
|
130
|
+
current &&
|
|
131
|
+
(ts.isAsExpression(current) ||
|
|
132
|
+
ts.isSatisfiesExpression(current) ||
|
|
133
|
+
ts.isParenthesizedExpression(current) ||
|
|
134
|
+
ts.isTypeAssertionExpression(current))
|
|
135
|
+
) {
|
|
136
|
+
current = current.expression;
|
|
137
|
+
}
|
|
138
|
+
return current;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A class token resolves to its instance type; an `InjectionToken<T>` to `T`. */
|
|
142
|
+
function tokenType(element: ts.Expression, checker: ts.TypeChecker): ts.Type | undefined {
|
|
143
|
+
const type = checker.getTypeAtLocation(element);
|
|
144
|
+
const construct = type.getConstructSignatures()[0];
|
|
145
|
+
if (construct) return construct.getReturnType();
|
|
146
|
+
if (type.getSymbol()?.getName() === 'InjectionToken') {
|
|
147
|
+
return checker.getTypeArguments(type as ts.TypeReference)[0];
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { checkInjectLists, type Diagnostic } from './check.js';
|
|
3
|
+
import { writeManifest } from './manifest.js';
|
|
4
|
+
|
|
5
|
+
const USAGE = `Usage:
|
|
6
|
+
avi-compile check [--project tsconfig.json]
|
|
7
|
+
Type-check every static inject list against its constructor.
|
|
8
|
+
avi-compile manifest <entry.js> [--export AppModule] [--out avi.manifest.json]
|
|
9
|
+
Describe the app's modules, providers and routes without starting it.`;
|
|
10
|
+
|
|
11
|
+
export async function main(argv: readonly string[]): Promise<number> {
|
|
12
|
+
const [command, ...rest] = argv;
|
|
13
|
+
const { values, positionals } = parseArgs({
|
|
14
|
+
args: [...rest],
|
|
15
|
+
allowPositionals: true,
|
|
16
|
+
options: {
|
|
17
|
+
project: { type: 'string', default: 'tsconfig.json' },
|
|
18
|
+
export: { type: 'string', default: 'AppModule' },
|
|
19
|
+
out: { type: 'string', default: 'avi.manifest.json' },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (command === 'check') {
|
|
24
|
+
const diagnostics = checkInjectLists(values.project);
|
|
25
|
+
for (const d of diagnostics) console.log(format(d));
|
|
26
|
+
const errors = diagnostics.filter((d) => d.severity === 'error').length;
|
|
27
|
+
console.log(
|
|
28
|
+
`${String(errors)} error(s), ${String(diagnostics.length - errors)} warning(s) in static inject lists`,
|
|
29
|
+
);
|
|
30
|
+
return errors > 0 ? 1 : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (command === 'manifest' && positionals[0]) {
|
|
34
|
+
const description = await writeManifest({
|
|
35
|
+
entry: positionals[0],
|
|
36
|
+
exportName: values.export,
|
|
37
|
+
out: values.out,
|
|
38
|
+
});
|
|
39
|
+
console.log(
|
|
40
|
+
`Wrote ${values.out}: ${String(description.modules.length)} modules, ${String(description.routes.length)} routes`,
|
|
41
|
+
);
|
|
42
|
+
for (const controller of description.dynamic) {
|
|
43
|
+
console.log(`note: ${controller} creates its routes at runtime; they are not listed`);
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.error(USAGE);
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function format(d: Diagnostic): string {
|
|
53
|
+
return `${d.file}:${String(d.line)}:${String(d.column)} ${d.severity} ${d.code}: ${d.message}`;
|
|
54
|
+
}
|
package/src/index.ts
ADDED
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describeApp, type AppDescription, type ModuleRef } from '@avijs/core';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/** Serializes a description the same way every time, so manifests diff cleanly. */
|
|
7
|
+
export function serializeManifest(description: AppDescription): string {
|
|
8
|
+
return `${JSON.stringify(description, null, 2)}\n`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Imports the compiled entry module, describes the exported root module and writes the
|
|
13
|
+
* manifest. Importing runs the module's top-level code, but no provider is created.
|
|
14
|
+
*/
|
|
15
|
+
export async function writeManifest(options: {
|
|
16
|
+
entry: string;
|
|
17
|
+
exportName?: string;
|
|
18
|
+
out: string;
|
|
19
|
+
}): Promise<AppDescription> {
|
|
20
|
+
const exportName = options.exportName ?? 'AppModule';
|
|
21
|
+
const loaded = (await import(pathToFileURL(path.resolve(options.entry)).href)) as Record<
|
|
22
|
+
string,
|
|
23
|
+
unknown
|
|
24
|
+
>;
|
|
25
|
+
const root = loaded[exportName];
|
|
26
|
+
if (root === undefined) {
|
|
27
|
+
throw new Error(`${options.entry} has no export named ${exportName}`);
|
|
28
|
+
}
|
|
29
|
+
const description = describeApp(root as ModuleRef);
|
|
30
|
+
await writeFile(options.out, serializeManifest(description));
|
|
31
|
+
return description;
|
|
32
|
+
}
|