@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 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
- # Temporary Holding Version
1
+ # @avijs/compiler
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './cli.js';
3
+ process.exitCode = await main(process.argv.slice(2));
4
+ //# sourceMappingURL=bin.js.map
@@ -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"}
@@ -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,2 @@
1
+ export declare function main(argv: readonly string[]): Promise<number>;
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -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
@@ -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"}
@@ -0,0 +1,5 @@
1
+ export { checkInjectLists } from './check.js';
2
+ export type { Diagnostic } from './check.js';
3
+ export { main } from './cli.js';
4
+ export { serializeManifest, writeManifest } from './manifest.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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,4 @@
1
+ export { checkInjectLists } from './check.js';
2
+ export { main } from './cli.js';
3
+ export { serializeManifest, writeManifest } from './manifest.js';
4
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './cli.js';
3
+
4
+ process.exitCode = await main(process.argv.slice(2));
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
@@ -0,0 +1,4 @@
1
+ export { checkInjectLists } from './check.js';
2
+ export type { Diagnostic } from './check.js';
3
+ export { main } from './cli.js';
4
+ export { serializeManifest, writeManifest } from './manifest.js';
@@ -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
+ }