@aponiajs/cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/index.d.mts +33 -2
- package/dist/index.mjs +623 -15
- package/package.json +11 -1
- package/src/arguments.ts +226 -12
- package/src/component-names.ts +42 -0
- package/src/index.ts +45 -1
- package/src/module-registration.ts +115 -0
- package/src/schematic-generator.ts +624 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aponiajs/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Bun-native project generator and command-line interface for Aponia.",
|
|
5
5
|
"homepage": "https://github.com/aponiajs/aponiajs#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -35,5 +35,15 @@
|
|
|
35
35
|
"test": "bun test",
|
|
36
36
|
"check": "bunx --bun vp check",
|
|
37
37
|
"prepublishOnly": "bun run build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"change-case": "^5.4.4",
|
|
41
|
+
"fast-glob": "^3.3.3",
|
|
42
|
+
"inflection": "^3.0.2",
|
|
43
|
+
"ts-morph": "^28.0.0",
|
|
44
|
+
"yargs-parser": "^22.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/yargs-parser": "^21.0.3"
|
|
38
48
|
}
|
|
39
49
|
}
|
package/src/arguments.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { pascalCase } from "change-case";
|
|
2
|
+
import parseCliArguments from "yargs-parser";
|
|
3
|
+
|
|
1
4
|
export interface NewCommandOptions {
|
|
2
5
|
readonly command: "new";
|
|
3
6
|
readonly name: string;
|
|
@@ -5,11 +8,104 @@ export interface NewCommandOptions {
|
|
|
5
8
|
readonly skipInstall: boolean;
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
export const generateSchematics = [
|
|
12
|
+
"app",
|
|
13
|
+
"library",
|
|
14
|
+
"class",
|
|
15
|
+
"controller",
|
|
16
|
+
"decorator",
|
|
17
|
+
"filter",
|
|
18
|
+
"gateway",
|
|
19
|
+
"guard",
|
|
20
|
+
"interface",
|
|
21
|
+
"interceptor",
|
|
22
|
+
"middleware",
|
|
23
|
+
"module",
|
|
24
|
+
"pipe",
|
|
25
|
+
"provider",
|
|
26
|
+
"resolver",
|
|
27
|
+
"resource",
|
|
28
|
+
"service",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type GenerateSchematic = (typeof generateSchematics)[number];
|
|
32
|
+
|
|
33
|
+
export type ResourceTransport =
|
|
34
|
+
| "rest"
|
|
35
|
+
| "graphql-code-first"
|
|
36
|
+
| "graphql-schema-first"
|
|
37
|
+
| "microservice"
|
|
38
|
+
| "ws";
|
|
39
|
+
|
|
40
|
+
export interface GenerateCommandOptions {
|
|
41
|
+
readonly command: "generate";
|
|
42
|
+
readonly schematic: GenerateSchematic;
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly dryRun: boolean;
|
|
45
|
+
readonly flat?: boolean;
|
|
46
|
+
readonly spec?: boolean;
|
|
47
|
+
readonly skipImport: boolean;
|
|
48
|
+
readonly path?: string;
|
|
49
|
+
readonly module?: string;
|
|
50
|
+
readonly project?: string;
|
|
51
|
+
readonly crud: boolean;
|
|
52
|
+
readonly type: ResourceTransport;
|
|
53
|
+
}
|
|
54
|
+
|
|
8
55
|
export type CliCommand =
|
|
9
56
|
| NewCommandOptions
|
|
57
|
+
| GenerateCommandOptions
|
|
10
58
|
| { readonly command: "help" }
|
|
11
59
|
| { readonly command: "version" };
|
|
12
60
|
|
|
61
|
+
const schematicAliases: Readonly<Record<string, GenerateSchematic>> = {
|
|
62
|
+
app: "app",
|
|
63
|
+
application: "app",
|
|
64
|
+
lib: "library",
|
|
65
|
+
library: "library",
|
|
66
|
+
cl: "class",
|
|
67
|
+
class: "class",
|
|
68
|
+
co: "controller",
|
|
69
|
+
controller: "controller",
|
|
70
|
+
route: "controller",
|
|
71
|
+
router: "controller",
|
|
72
|
+
routes: "controller",
|
|
73
|
+
d: "decorator",
|
|
74
|
+
decorator: "decorator",
|
|
75
|
+
f: "filter",
|
|
76
|
+
filter: "filter",
|
|
77
|
+
ga: "gateway",
|
|
78
|
+
gateway: "gateway",
|
|
79
|
+
gu: "guard",
|
|
80
|
+
guard: "guard",
|
|
81
|
+
itf: "interface",
|
|
82
|
+
interface: "interface",
|
|
83
|
+
itc: "interceptor",
|
|
84
|
+
interceptor: "interceptor",
|
|
85
|
+
mi: "middleware",
|
|
86
|
+
middleware: "middleware",
|
|
87
|
+
mo: "module",
|
|
88
|
+
module: "module",
|
|
89
|
+
pi: "pipe",
|
|
90
|
+
pipe: "pipe",
|
|
91
|
+
pr: "provider",
|
|
92
|
+
provider: "provider",
|
|
93
|
+
r: "resolver",
|
|
94
|
+
resolver: "resolver",
|
|
95
|
+
res: "resource",
|
|
96
|
+
resource: "resource",
|
|
97
|
+
s: "service",
|
|
98
|
+
service: "service",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const resourceTransports = new Set<ResourceTransport>([
|
|
102
|
+
"rest",
|
|
103
|
+
"graphql-code-first",
|
|
104
|
+
"graphql-schema-first",
|
|
105
|
+
"microservice",
|
|
106
|
+
"ws",
|
|
107
|
+
]);
|
|
108
|
+
|
|
13
109
|
export function parseArguments(arguments_: readonly string[]): CliCommand {
|
|
14
110
|
const [command = "help", ...rest] = arguments_;
|
|
15
111
|
|
|
@@ -21,27 +117,145 @@ export function parseArguments(arguments_: readonly string[]): CliCommand {
|
|
|
21
117
|
return { command: "version" };
|
|
22
118
|
}
|
|
23
119
|
|
|
24
|
-
if (command
|
|
25
|
-
|
|
120
|
+
if (command === "new" || command === "n") {
|
|
121
|
+
return parseNewCommand(rest);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (command === "generate" || command === "g") {
|
|
125
|
+
return parseGenerateCommand(rest);
|
|
26
126
|
}
|
|
27
127
|
|
|
28
|
-
|
|
128
|
+
throw new Error(`Unknown command "${command}".`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseNewCommand(arguments_: readonly string[]): NewCommandOptions {
|
|
132
|
+
const parsed = parseOptions(arguments_);
|
|
133
|
+
const [name, ...extraPositionals] = parsed._;
|
|
29
134
|
if (!name) {
|
|
30
135
|
throw new Error("Project name is required.");
|
|
31
136
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const unknownOption = rest.find(
|
|
35
|
-
(argument) => argument.startsWith("-") && !supportedOptions.has(argument),
|
|
36
|
-
);
|
|
37
|
-
if (unknownOption) {
|
|
38
|
-
throw new Error(`Unknown option "${unknownOption}".`);
|
|
137
|
+
if (extraPositionals.length > 0) {
|
|
138
|
+
throw new Error(`Unexpected argument "${extraPositionals[0]}".`);
|
|
39
139
|
}
|
|
40
140
|
|
|
141
|
+
assertKnownOptions(parsed, ["dry-run", "skip-install"]);
|
|
142
|
+
|
|
41
143
|
return {
|
|
42
144
|
command: "new",
|
|
43
145
|
name,
|
|
44
|
-
dryRun:
|
|
45
|
-
skipInstall:
|
|
146
|
+
dryRun: readBooleanOption(parsed, "dry-run", false),
|
|
147
|
+
skipInstall: readBooleanOption(parsed, "skip-install", false),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseGenerateCommand(arguments_: readonly string[]): GenerateCommandOptions {
|
|
152
|
+
const parsed = parseOptions(arguments_);
|
|
153
|
+
const [schematicName, name, ...extraPositionals] = parsed._;
|
|
154
|
+
if (!schematicName) {
|
|
155
|
+
throw new Error("Schematic name is required.");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const schematic = schematicAliases[schematicName];
|
|
159
|
+
if (!schematic) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Unknown schematic "${schematicName}". Available schematics: ${generateSchematics.join(", ")}.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (!name) {
|
|
165
|
+
throw new Error(`${pascalCase(schematic)} name is required.`);
|
|
166
|
+
}
|
|
167
|
+
if (extraPositionals.length > 0) {
|
|
168
|
+
throw new Error(`Unexpected argument "${extraPositionals[0]}".`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
assertKnownOptions(parsed, [
|
|
172
|
+
"crud",
|
|
173
|
+
"dry-run",
|
|
174
|
+
"flat",
|
|
175
|
+
"module",
|
|
176
|
+
"path",
|
|
177
|
+
"project",
|
|
178
|
+
"skip-import",
|
|
179
|
+
"spec",
|
|
180
|
+
"type",
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const type = readStringOption(parsed, "type") ?? "rest";
|
|
184
|
+
if (!resourceTransports.has(type as ResourceTransport)) {
|
|
185
|
+
throw new Error(`Unknown resource transport "${type}".`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
command: "generate",
|
|
190
|
+
schematic,
|
|
191
|
+
name,
|
|
192
|
+
dryRun: readBooleanOption(parsed, "dry-run", false),
|
|
193
|
+
flat: readOptionalBooleanOption(parsed, "flat"),
|
|
194
|
+
spec: readOptionalBooleanOption(parsed, "spec"),
|
|
195
|
+
skipImport: readBooleanOption(parsed, "skip-import", false),
|
|
196
|
+
path: readStringOption(parsed, "path"),
|
|
197
|
+
module: readStringOption(parsed, "module"),
|
|
198
|
+
project: readStringOption(parsed, "project"),
|
|
199
|
+
crud: readBooleanOption(parsed, "crud", true),
|
|
200
|
+
type: type as ResourceTransport,
|
|
46
201
|
};
|
|
47
202
|
}
|
|
203
|
+
|
|
204
|
+
interface ParsedOptions extends Readonly<Record<string, unknown>> {
|
|
205
|
+
readonly _: readonly string[];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseOptions(arguments_: readonly string[]): ParsedOptions {
|
|
209
|
+
return parseCliArguments([...arguments_], {
|
|
210
|
+
alias: {
|
|
211
|
+
"dry-run": ["d"],
|
|
212
|
+
project: ["p"],
|
|
213
|
+
"skip-install": ["s"],
|
|
214
|
+
},
|
|
215
|
+
boolean: ["crud", "dry-run", "flat", "skip-import", "skip-install", "spec"],
|
|
216
|
+
string: ["module", "path", "project", "type"],
|
|
217
|
+
configuration: {
|
|
218
|
+
"camel-case-expansion": false,
|
|
219
|
+
"dot-notation": false,
|
|
220
|
+
"duplicate-arguments-array": false,
|
|
221
|
+
"parse-numbers": false,
|
|
222
|
+
"parse-positional-numbers": false,
|
|
223
|
+
"short-option-groups": false,
|
|
224
|
+
"strip-aliased": true,
|
|
225
|
+
},
|
|
226
|
+
}) as ParsedOptions;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function assertKnownOptions(options: ParsedOptions, supportedOptions: readonly string[]): void {
|
|
230
|
+
const supported = new Set(supportedOptions);
|
|
231
|
+
const unknown = Object.keys(options).find((option) => option !== "_" && !supported.has(option));
|
|
232
|
+
if (unknown) {
|
|
233
|
+
throw new Error(`Unknown option "--${unknown}".`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readBooleanOption(options: ParsedOptions, name: string, fallback: boolean): boolean {
|
|
238
|
+
return readOptionalBooleanOption(options, name) ?? fallback;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function readOptionalBooleanOption(options: ParsedOptions, name: string): boolean | undefined {
|
|
242
|
+
const value = options[name];
|
|
243
|
+
if (value === undefined) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
if (typeof value !== "boolean") {
|
|
247
|
+
throw new Error(`Option "--${name}" does not accept a value.`);
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function readStringOption(options: ParsedOptions, name: string): string | undefined {
|
|
253
|
+
const value = options[name];
|
|
254
|
+
if (value === undefined) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
if (typeof value !== "string") {
|
|
258
|
+
throw new Error(`Option "--${name}" requires a value.`);
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { camelCase, kebabCase, pascalCase } from "change-case";
|
|
2
|
+
import { singularize } from "inflection";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface ComponentNames {
|
|
6
|
+
readonly fileName: string;
|
|
7
|
+
readonly className: string;
|
|
8
|
+
readonly propertyName: string;
|
|
9
|
+
readonly singularFileName: string;
|
|
10
|
+
readonly singularClassName: string;
|
|
11
|
+
readonly routePath: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createComponentNames(input: string): ComponentNames {
|
|
15
|
+
const segments = normalizeNameSegments(input);
|
|
16
|
+
const fileName = segments.at(-1)!;
|
|
17
|
+
const singularFileName = singularize(fileName);
|
|
18
|
+
return {
|
|
19
|
+
fileName,
|
|
20
|
+
className: pascalCase(fileName),
|
|
21
|
+
propertyName: camelCase(fileName),
|
|
22
|
+
singularFileName,
|
|
23
|
+
singularClassName: pascalCase(singularFileName),
|
|
24
|
+
routePath: fileName,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeNameSegments(input: string): string[] {
|
|
29
|
+
if (isAbsolute(input) || input.includes("..")) {
|
|
30
|
+
throw new Error("Generated names must be relative and cannot contain parent traversal.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const segments = input
|
|
34
|
+
.replaceAll("\\", "/")
|
|
35
|
+
.split("/")
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map((segment) => kebabCase(segment, { locale: false }));
|
|
38
|
+
if (segments.length === 0 || segments.some((segment) => !/^[a-z][a-z0-9-]*$/.test(segment))) {
|
|
39
|
+
throw new Error("Generated names must contain letters and use kebab-case paths.");
|
|
40
|
+
}
|
|
41
|
+
return segments;
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import cliManifest from "../package.json" with { type: "json" };
|
|
2
2
|
import { parseArguments } from "./arguments.ts";
|
|
3
3
|
import { generateProject } from "./project-generator.ts";
|
|
4
|
+
import { generateSchematic } from "./schematic-generator.ts";
|
|
4
5
|
|
|
5
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
generateSchematics,
|
|
8
|
+
parseArguments,
|
|
9
|
+
type CliCommand,
|
|
10
|
+
type GenerateCommandOptions,
|
|
11
|
+
type GenerateSchematic,
|
|
12
|
+
type ResourceTransport,
|
|
13
|
+
} from "./arguments.ts";
|
|
6
14
|
export {
|
|
7
15
|
generateProject,
|
|
8
16
|
type GenerateProjectOptions,
|
|
9
17
|
type GenerateProjectResult,
|
|
10
18
|
} from "./project-generator.ts";
|
|
19
|
+
export {
|
|
20
|
+
generateSchematic,
|
|
21
|
+
type GenerateSchematicOptions,
|
|
22
|
+
type GenerateSchematicResult,
|
|
23
|
+
type SchematicChange,
|
|
24
|
+
} from "./schematic-generator.ts";
|
|
11
25
|
|
|
12
26
|
export async function runCli(arguments_: readonly string[]): Promise<number> {
|
|
13
27
|
try {
|
|
@@ -23,6 +37,14 @@ export async function runCli(arguments_: readonly string[]): Promise<number> {
|
|
|
23
37
|
return 0;
|
|
24
38
|
}
|
|
25
39
|
|
|
40
|
+
if (command.command === "generate") {
|
|
41
|
+
const result = await generateSchematic(command);
|
|
42
|
+
for (const change of result.changes) {
|
|
43
|
+
console.log(`${change.kind} ${change.path}`);
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
const result = await generateProject(command);
|
|
27
49
|
if (result.dryRun) {
|
|
28
50
|
console.log(`CREATE ${result.projectDirectory}`);
|
|
@@ -47,10 +69,32 @@ const helpText = `Aponia CLI
|
|
|
47
69
|
Usage:
|
|
48
70
|
aponia new <name> [options]
|
|
49
71
|
aponia n <name> [options]
|
|
72
|
+
aponia generate <schematic> <name> [options]
|
|
73
|
+
aponia g <schematic> <name> [options]
|
|
50
74
|
|
|
51
75
|
Options:
|
|
52
76
|
-d, --dry-run Report files without writing them
|
|
53
77
|
-s, --skip-install Generate without running bun install
|
|
78
|
+
--flat Generate without a schematic directory
|
|
79
|
+
--no-flat Generate inside a schematic directory
|
|
80
|
+
--spec Generate spec files
|
|
81
|
+
--no-spec Skip spec files
|
|
82
|
+
--skip-import Skip declaring module registration
|
|
83
|
+
--module <name> Select the declaring module
|
|
84
|
+
--path <path> Override the configured source root
|
|
85
|
+
-p, --project Select a configured project
|
|
86
|
+
--type <type> Select a resource transport
|
|
87
|
+
--crud Generate CRUD entry points (default)
|
|
88
|
+
--no-crud Generate a resource without CRUD entry points
|
|
54
89
|
-h, --help Show command help
|
|
55
90
|
-v, --version Show CLI version
|
|
91
|
+
|
|
92
|
+
Schematics:
|
|
93
|
+
app, library (lib), class (cl), controller (co), decorator (d),
|
|
94
|
+
filter (f), gateway (ga), guard (gu), interface (itf),
|
|
95
|
+
interceptor (itc), middleware (mi), module (mo), pipe (pi),
|
|
96
|
+
provider (pr), resolver (r), resource (res), service (s)
|
|
97
|
+
|
|
98
|
+
Controller aliases:
|
|
99
|
+
router, routers, route
|
|
56
100
|
`;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IndentationText,
|
|
3
|
+
Node,
|
|
4
|
+
Project,
|
|
5
|
+
QuoteKind,
|
|
6
|
+
SyntaxKind,
|
|
7
|
+
type ObjectLiteralExpression,
|
|
8
|
+
type SourceFile,
|
|
9
|
+
} from "ts-morph";
|
|
10
|
+
|
|
11
|
+
export type ModuleRegistrationKind = "controllers" | "imports" | "providers";
|
|
12
|
+
|
|
13
|
+
export function registerInModule(
|
|
14
|
+
source: string,
|
|
15
|
+
registrationKind: ModuleRegistrationKind,
|
|
16
|
+
symbol: string,
|
|
17
|
+
importPath: string,
|
|
18
|
+
): string {
|
|
19
|
+
const project = new Project({
|
|
20
|
+
useInMemoryFileSystem: true,
|
|
21
|
+
manipulationSettings: {
|
|
22
|
+
indentationText: IndentationText.TwoSpaces,
|
|
23
|
+
quoteKind: QuoteKind.Double,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const sourceFile = project.createSourceFile("module.ts", source);
|
|
27
|
+
const moduleMetadata = findModuleMetadata(sourceFile);
|
|
28
|
+
if (!moduleMetadata) {
|
|
29
|
+
throw new Error("The declaring module does not contain a @Module() metadata object.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isAlreadyRegistered(sourceFile, registrationKind, symbol, importPath)) {
|
|
33
|
+
return source;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
addImport(sourceFile, symbol, importPath);
|
|
37
|
+
addRegistration(moduleMetadata, registrationKind, symbol);
|
|
38
|
+
return sourceFile.getFullText();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findModuleMetadata(sourceFile: SourceFile): ObjectLiteralExpression | undefined {
|
|
42
|
+
const moduleCall = sourceFile
|
|
43
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
44
|
+
.find((callExpression) => callExpression.getExpression().getText() === "Module");
|
|
45
|
+
const metadata = moduleCall?.getArguments()[0];
|
|
46
|
+
return Node.isObjectLiteralExpression(metadata) ? metadata : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isAlreadyRegistered(
|
|
50
|
+
sourceFile: SourceFile,
|
|
51
|
+
registrationKind: ModuleRegistrationKind,
|
|
52
|
+
symbol: string,
|
|
53
|
+
importPath: string,
|
|
54
|
+
): boolean {
|
|
55
|
+
const hasImport = sourceFile
|
|
56
|
+
.getImportDeclarations()
|
|
57
|
+
.some(
|
|
58
|
+
(declaration) =>
|
|
59
|
+
declaration.getModuleSpecifierValue() === importPath &&
|
|
60
|
+
declaration.getNamedImports().some((namedImport) => namedImport.getName() === symbol),
|
|
61
|
+
);
|
|
62
|
+
if (!hasImport) return false;
|
|
63
|
+
|
|
64
|
+
const metadata = findModuleMetadata(sourceFile);
|
|
65
|
+
const registration = metadata?.getProperty(registrationKind);
|
|
66
|
+
if (!Node.isPropertyAssignment(registration)) return false;
|
|
67
|
+
const initializer = registration.getInitializer();
|
|
68
|
+
return (
|
|
69
|
+
Node.isArrayLiteralExpression(initializer) &&
|
|
70
|
+
initializer.getElements().some((element) => element.getText() === symbol)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function addImport(sourceFile: SourceFile, symbol: string, importPath: string): void {
|
|
75
|
+
const existingImport = sourceFile
|
|
76
|
+
.getImportDeclarations()
|
|
77
|
+
.find((declaration) => declaration.getModuleSpecifierValue() === importPath);
|
|
78
|
+
if (existingImport) {
|
|
79
|
+
if (!existingImport.getNamedImports().some((namedImport) => namedImport.getName() === symbol)) {
|
|
80
|
+
existingImport.addNamedImport(symbol);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
sourceFile.addImportDeclaration({
|
|
86
|
+
namedImports: [symbol],
|
|
87
|
+
moduleSpecifier: importPath,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function addRegistration(
|
|
92
|
+
metadata: ObjectLiteralExpression,
|
|
93
|
+
registrationKind: ModuleRegistrationKind,
|
|
94
|
+
symbol: string,
|
|
95
|
+
): void {
|
|
96
|
+
const registration = metadata.getProperty(registrationKind);
|
|
97
|
+
if (!registration) {
|
|
98
|
+
metadata.addPropertyAssignment({
|
|
99
|
+
name: registrationKind,
|
|
100
|
+
initializer: `[${symbol}]`,
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!Node.isPropertyAssignment(registration)) {
|
|
106
|
+
throw new Error(`Module metadata "${registrationKind}" must be a property assignment.`);
|
|
107
|
+
}
|
|
108
|
+
const initializer = registration.getInitializer();
|
|
109
|
+
if (!Node.isArrayLiteralExpression(initializer)) {
|
|
110
|
+
throw new Error(`Module metadata "${registrationKind}" must be an array.`);
|
|
111
|
+
}
|
|
112
|
+
if (!initializer.getElements().some((element) => element.getText() === symbol)) {
|
|
113
|
+
initializer.addElement(symbol);
|
|
114
|
+
}
|
|
115
|
+
}
|