@rsdk/cli.cmd.autodoc 6.0.0-next.7 → 6.0.0-next.8

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +382 -0
  2. package/TODO.MD +24 -0
  3. package/dist/commands/autodoc.cmd.js +8 -10
  4. package/dist/commands/autodoc.cmd.js.map +1 -1
  5. package/dist/commands/openapi.variation.js +1 -2
  6. package/dist/commands/openapi.variation.js.map +1 -1
  7. package/dist/generators/file-generator.interface.d.ts +1 -1
  8. package/dist/generators/openapi-json/config.schema.d.ts +16 -139
  9. package/dist/generators/openapi-json/config.schema.js +19 -128
  10. package/dist/generators/openapi-json/config.schema.js.map +1 -1
  11. package/dist/generators/openapi-json/openapi-json.generator.d.ts +4 -5
  12. package/dist/generators/openapi-json/openapi-json.generator.js +12 -109
  13. package/dist/generators/openapi-json/openapi-json.generator.js.map +1 -1
  14. package/jest.config.e2e.js +1 -0
  15. package/jest.config.js +1 -0
  16. package/jest.config.unit.js +1 -0
  17. package/package.json +15 -32
  18. package/src/base/app.loader.ts +39 -0
  19. package/src/base/document.file.ts +33 -0
  20. package/src/base/index.ts +2 -0
  21. package/src/commands/autodoc.cmd.ts +93 -0
  22. package/src/commands/openapi.variation.ts +172 -0
  23. package/src/generators/autodoc-md/autodoc-md.generator.ts +56 -0
  24. package/src/generators/autodoc-md/formatters.ts +9 -0
  25. package/src/generators/autodoc-md/fragments/document-fragment.abstract.ts +6 -0
  26. package/src/generators/autodoc-md/fragments/extractor/document.extractor.ts +73 -0
  27. package/src/generators/autodoc-md/fragments/implementations/additional-sources.fragment.ts +41 -0
  28. package/src/generators/autodoc-md/fragments/implementations/app-metadata.fragment.ts +18 -0
  29. package/src/generators/autodoc-md/fragments/implementations/autodoc.fragment.ts +30 -0
  30. package/src/generators/autodoc-md/fragments/implementations/config-section.fragment.ts +148 -0
  31. package/src/generators/autodoc-md/fragments/implementations/header.fragment.ts +18 -0
  32. package/src/generators/autodoc-md/fragments/implementations/plugin.fragment.ts +21 -0
  33. package/src/generators/autodoc-md/fragments/implementations/transports.fragment.ts +36 -0
  34. package/src/generators/autodoc-md/fragments/index.ts +9 -0
  35. package/src/generators/autodoc-md/index.ts +1 -0
  36. package/src/generators/file-generator.interface.ts +22 -0
  37. package/src/generators/index.ts +4 -0
  38. package/src/generators/openapi-json/config.schema.spec.ts +265 -0
  39. package/src/generators/openapi-json/config.schema.ts +37 -0
  40. package/src/generators/openapi-json/index.ts +2 -0
  41. package/src/generators/openapi-json/openapi-json.generator.ts +53 -0
  42. package/src/generators/rsdk-json/index.ts +1 -0
  43. package/src/generators/rsdk-json/rsdk-json.generator.ts +24 -0
  44. package/src/index.ts +1 -0
  45. package/tsconfig.build.json +12 -0
  46. package/tsconfig.json +7 -0
@@ -0,0 +1,39 @@
1
+ import { Assert } from '@rsdk/common';
2
+ import { Path } from '@rsdk/common.node';
3
+ import { PlatformApp } from '@rsdk/core';
4
+ import type { ILogger } from '@rsdk/logging';
5
+
6
+ export class AppLoader {
7
+ constructor(private readonly logger: ILogger) {}
8
+
9
+ async loadApp(
10
+ fullpath: string,
11
+ required: boolean,
12
+ ): Promise<PlatformApp | undefined> {
13
+ let app: PlatformApp | undefined;
14
+
15
+ try {
16
+ const [absolute] = Path.absolutize(fullpath);
17
+ const loaded = await import(absolute);
18
+
19
+ app = loaded.app as PlatformApp;
20
+ } catch (error) {
21
+ Assert.isError(error);
22
+ this.logger.warn(`module by path ${fullpath} not found`);
23
+
24
+ if (!required) {
25
+ this.logger.info(`found --if-present, finished with exit code 0`);
26
+ return undefined;
27
+ }
28
+
29
+ this.logger.error(`Error on import ${fullpath}`, error);
30
+ throw error;
31
+ }
32
+
33
+ if (app instanceof PlatformApp) {
34
+ return app;
35
+ }
36
+
37
+ throw new Error('app is not instance of PlatformApp');
38
+ }
39
+ }
@@ -0,0 +1,33 @@
1
+ import type { ILogger } from '@rsdk/logging';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ /**
7
+ * TODO: Может быть удобная абстракция для работы с файлами.
8
+ * Ещё в старых загашниках надо NetworkFile поискать
9
+ */
10
+ export type DocumentFile = {
11
+ name: string;
12
+ content: string;
13
+ };
14
+
15
+ export class DocumentWriter {
16
+ constructor(
17
+ private readonly logger: ILogger,
18
+ private readonly basepath: string,
19
+ ) {}
20
+
21
+ async write(file: DocumentFile): Promise<string> {
22
+ const exists = existsSync(this.basepath);
23
+ if (!exists) {
24
+ await mkdir(this.basepath, { recursive: true });
25
+ }
26
+ const fullpath = path.join(this.basepath, file.name);
27
+
28
+ this.logger.info(`writing file ${file.name}...`);
29
+ await writeFile(fullpath, file.content);
30
+ this.logger.info(`file ${file.name} created!`);
31
+ return fullpath;
32
+ }
33
+ }
@@ -0,0 +1,2 @@
1
+ export * from './app.loader';
2
+ export * from './document.file';
@@ -0,0 +1,93 @@
1
+ import { Command, IRunnable, Option, ValueOption } from '@rsdk/cli.common';
2
+ import { FsPathParser, Path } from '@rsdk/common.node';
3
+ import { ILogger, LoggerFactory } from '@rsdk/logging';
4
+ import { API_ZONE_INTERNAL } from '@rsdk/zones';
5
+
6
+ import { type DocumentFile, DocumentWriter } from '../base';
7
+ import { AppLoader } from '../base';
8
+ import {
9
+ MarkdownDocsGenerator,
10
+ OpenApiFileGenerator,
11
+ RsdkFileGenerator,
12
+ } from '../generators';
13
+
14
+ import { OpenApiCommand } from './openapi.variation';
15
+
16
+ @Command('autodoc', {
17
+ description: 'autodoc your app',
18
+ variations: [OpenApiCommand],
19
+ })
20
+ export class AutodocCommand implements IRunnable {
21
+ private readonly logger: ILogger;
22
+ private readonly loader: AppLoader;
23
+
24
+ constructor() {
25
+ this.logger = LoggerFactory.create(AutodocCommand);
26
+ this.loader = new AppLoader(this.logger);
27
+ }
28
+
29
+ async run(
30
+ @Option('verbose', { description: 'Verbose output' })
31
+ verbose: boolean,
32
+
33
+ @Option('if-present', { description: 'Not fail if not provided app file' })
34
+ ifPresent: boolean,
35
+
36
+ @Option('open-api', {
37
+ description: 'generate open api (internal routes are excluded)',
38
+ })
39
+ openApi: boolean,
40
+
41
+ @ValueOption('out', new FsPathParser('dir', { check: 'dirname' }), {
42
+ description: 'output directory',
43
+ defaultValue: 'autodoc',
44
+ alias: 'o',
45
+ })
46
+ outPath: string,
47
+
48
+ @ValueOption('app', new FsPathParser('file'), {
49
+ description: 'path to app.js file',
50
+ defaultValue: './dist/app.js',
51
+ alias: 'a',
52
+ })
53
+ appPath: string,
54
+ ): Promise<void> {
55
+ if (verbose) {
56
+ this.logger.info(`app path: ${appPath}`);
57
+ this.logger.info(`output directory path: ${outPath}`);
58
+ this.logger.info(`--if-present: ${ifPresent}`);
59
+ }
60
+
61
+ const app = await this.loader.loadApp(appPath, !ifPresent);
62
+ if (!app) {
63
+ return;
64
+ }
65
+
66
+ const documents: DocumentFile[] = [
67
+ await new RsdkFileGenerator(app).createFile('rsdk.json'),
68
+ await new MarkdownDocsGenerator(app).createFile('autodoc.md'),
69
+ ];
70
+
71
+ if (openApi) {
72
+ const generator = new OpenApiFileGenerator(app, {
73
+ excludeZones: [API_ZONE_INTERNAL],
74
+ });
75
+
76
+ const openApiDocument = await generator.createFile('openapi.json');
77
+
78
+ if (openApiDocument) {
79
+ documents.push(openApiDocument);
80
+ }
81
+ }
82
+
83
+ const [absolutePath] = Path.absolutize(outPath);
84
+
85
+ const writer = new DocumentWriter(this.logger, absolutePath);
86
+
87
+ await Promise.all(
88
+ documents.map(async (doc) => {
89
+ await writer.write(doc);
90
+ }),
91
+ );
92
+ }
93
+ }
@@ -0,0 +1,172 @@
1
+ import {
2
+ CommandVariation,
3
+ IRunnable,
4
+ Option,
5
+ ValueOption,
6
+ } from '@rsdk/cli.common';
7
+ import { ArrayParser, StringParser, text } from '@rsdk/common';
8
+ import { FsPathParser, Path, readObj } from '@rsdk/common.node';
9
+ import { ILogger, LoggerFactory } from '@rsdk/logging';
10
+ import { Value } from '@sinclair/typebox/value';
11
+
12
+ import { AppLoader } from '../base/app.loader';
13
+ import { DocumentWriter } from '../base/document.file';
14
+ import {
15
+ OpenApiConfigSchema,
16
+ OpenApiFileGenerator,
17
+ OpenApiGeneratorOptions,
18
+ } from '../generators';
19
+
20
+ interface OpenApiConfig extends Partial<OpenApiGeneratorOptions> {
21
+ out?: string;
22
+ app?: string;
23
+ }
24
+
25
+ @CommandVariation('autodoc:openapi', {
26
+ description: 'generate open api',
27
+ })
28
+ export class OpenApiCommand implements IRunnable {
29
+ private readonly logger: ILogger;
30
+ private readonly loader: AppLoader;
31
+
32
+ constructor() {
33
+ this.logger = LoggerFactory.create(OpenApiCommand);
34
+ this.loader = new AppLoader(this.logger);
35
+ }
36
+
37
+ async run(
38
+ @Option('verbose', { description: 'Verbose output', defaultValue: false })
39
+ verbose: boolean,
40
+
41
+ @Option('if-present', {
42
+ description: 'Not fail if not provided app file',
43
+ defaultValue: false,
44
+ })
45
+ ifPresent: boolean,
46
+
47
+ @ValueOption('out', new FsPathParser('file', { check: 'dirname' }), {
48
+ description: 'output file',
49
+ alias: 'o',
50
+ })
51
+ outPath?: string,
52
+
53
+ @ValueOption('app', new FsPathParser('file'), {
54
+ description: 'path to app.js file',
55
+ alias: 'a',
56
+ })
57
+ appPath?: string,
58
+
59
+ @ValueOption('conf', new FsPathParser('file'), {
60
+ description: text`
61
+ Path to configuration file (YAML or JSON) containing any of the command options.
62
+ Command line options take precedence over config file values.
63
+ `,
64
+ })
65
+ configPath?: string,
66
+
67
+ @ValueOption('include-zones', new ArrayParser(new StringParser()), {
68
+ description: text`
69
+ API zones to include. Only controllers with these zones will be included in the output.
70
+ Zones are specified as a comma-separated list.
71
+ `,
72
+ })
73
+ includeZones?: string[],
74
+
75
+ @ValueOption('exclude-zones', new ArrayParser(new StringParser()), {
76
+ description: text`
77
+ API zones to exclude. Only controllers without these zones will be included in the output.
78
+ Zones are specified as a comma-separated list.
79
+ `,
80
+ })
81
+ excludeZones?: string[],
82
+
83
+ @ValueOption('title', new StringParser(), {
84
+ description: text`
85
+ The title of OpenAPI document. Takes precedence over config file and app metadata.
86
+ `,
87
+ })
88
+ title?: string,
89
+
90
+ @ValueOption('version', new StringParser(), {
91
+ description: text`
92
+ The version of OpenAPI document. Takes precedence over config file and app metadata.
93
+ `,
94
+ })
95
+ version?: string,
96
+
97
+ @ValueOption('description', new StringParser(), {
98
+ description: text`
99
+ The description of OpenAPI document. Takes precedence over config file and app metadata.
100
+ `,
101
+ })
102
+ description?: string,
103
+ ): Promise<void> {
104
+ if (verbose) {
105
+ this.logger.info(`app path: ${appPath}`);
106
+ this.logger.info(`output file: ${outPath}`);
107
+ this.logger.info(`--if-present: ${ifPresent}`);
108
+ }
109
+
110
+ const config = await this.readConfig(configPath);
111
+
112
+ // Command line options take precedence over config file
113
+ const effectiveOutPath = outPath ?? config.out ?? 'openapi.json';
114
+ const effectiveAppPath = appPath ?? config.app ?? './dist/app.js';
115
+
116
+ if (verbose) {
117
+ this.logger.info(`effective out path: ${effectiveOutPath}`);
118
+ this.logger.info(`effective app path: ${effectiveAppPath}`);
119
+ }
120
+
121
+ const app = await this.loader.loadApp(effectiveAppPath, !ifPresent);
122
+ if (!app) {
123
+ return;
124
+ }
125
+
126
+ const [absolutePath] = Path.absolutize(effectiveOutPath);
127
+ const [dirname, filename] = Path.split(absolutePath);
128
+
129
+ const options: Partial<OpenApiGeneratorOptions> = {
130
+ ...config,
131
+ ...(includeZones && { includeZones }),
132
+ ...(excludeZones && { excludeZones }),
133
+ ...(title && { title }),
134
+ ...(description && { description }),
135
+ ...(version && { version }),
136
+ };
137
+
138
+ if (verbose) {
139
+ this.logger.info('override options', options);
140
+ }
141
+
142
+ const generator = new OpenApiFileGenerator(app, options);
143
+ const writer = new DocumentWriter(this.logger, dirname);
144
+
145
+ const document = await generator.createFile(filename);
146
+
147
+ await writer.write(document);
148
+ }
149
+
150
+ private async readConfig(
151
+ configPath: string | undefined,
152
+ ): Promise<Partial<OpenApiConfig>> {
153
+ if (!configPath) {
154
+ return {};
155
+ }
156
+
157
+ this.logger.info(`reading config file: ${configPath}`);
158
+
159
+ const [absolutePath] = Path.absolutize(configPath);
160
+ const config = await readObj(absolutePath);
161
+
162
+ if (!Value.Check(OpenApiConfigSchema, config)) {
163
+ const errors = [...Value.Errors(OpenApiConfigSchema, config)];
164
+
165
+ throw new Error(
166
+ `Invalid config file: ${configPath}\n${JSON.stringify(errors, null, 2)}`,
167
+ );
168
+ }
169
+
170
+ return config;
171
+ }
172
+ }
@@ -0,0 +1,56 @@
1
+ import type { DocumentNode } from '@rsdk/autodoc.protocol';
2
+ import { Composite } from '@rsdk/autodoc.protocol';
3
+ import type { PlatformApp } from '@rsdk/core';
4
+ import { LoggerFactory } from '@rsdk/logging';
5
+
6
+ import { FileGenerator } from '../file-generator.interface';
7
+
8
+ import type { DocumentFragment } from './fragments';
9
+ import {
10
+ AdditionalSourcesFragment,
11
+ AppMetadataFragment,
12
+ AutodocMetadataFragment,
13
+ ConfigSectionFragment,
14
+ HeaderFragment,
15
+ PluginFragment,
16
+ TransportsFragment,
17
+ } from './fragments';
18
+
19
+ export class MarkdownDocsGenerator extends FileGenerator {
20
+ constructor(private readonly app: PlatformApp) {
21
+ super(LoggerFactory.create(MarkdownDocsGenerator));
22
+ }
23
+
24
+ protected async getContent(): Promise<string> {
25
+ this.logger.info('Start generating documentation');
26
+
27
+ const structure = await this.getStructure();
28
+
29
+ const nodes = await Promise.all(
30
+ structure.map((item) => item.getDocumentNode()),
31
+ );
32
+
33
+ const metadata = await this.app.getMetadata();
34
+ const content = await new Composite(
35
+ nodes.filter((node): node is DocumentNode => node !== null),
36
+ 'Application: ' + metadata.name,
37
+ ).toString();
38
+
39
+ this.logger.info('Generating documentation finished');
40
+ return content;
41
+ }
42
+
43
+ private async getStructure(): Promise<DocumentFragment[]> {
44
+ const metadata = await this.app.getMetadata();
45
+
46
+ return [
47
+ new HeaderFragment(this.app),
48
+ new AppMetadataFragment(this.app),
49
+ new PluginFragment(this.app.platformAppOptions.plugins),
50
+ new TransportsFragment(this.app.platformAppOptions.transports),
51
+ new AdditionalSourcesFragment(metadata.config.sources),
52
+ new ConfigSectionFragment(metadata),
53
+ new AutodocMetadataFragment(this.app),
54
+ ];
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ export class MdFormatter {
2
+ static italic(str: string): string {
3
+ return `*${str}*`;
4
+ }
5
+
6
+ static bold(str: string): string {
7
+ return `**${str}**`;
8
+ }
9
+ }
@@ -0,0 +1,6 @@
1
+ import type { DocumentNode } from '@rsdk/autodoc.protocol';
2
+ import type { Promisable } from 'type-fest';
3
+
4
+ export abstract class DocumentFragment {
5
+ abstract getDocumentNode(): Promisable<DocumentNode[] | DocumentNode | null>;
6
+ }
@@ -0,0 +1,73 @@
1
+ import type {
2
+ Abstract,
3
+ ClassProvider,
4
+ DynamicModule,
5
+ ExistingProvider,
6
+ FactoryProvider,
7
+ Type,
8
+ ValueProvider,
9
+ } from '@nestjs/common';
10
+ import type { DocumentResolver } from '@rsdk/autodoc.protocol';
11
+ import { AutodocMetadata } from '@rsdk/autodoc.protocol';
12
+ import { NestAssert, RsdkMetadataProvider } from '@rsdk/metadata';
13
+ import { NestDefinitionIterator } from '@rsdk/nest-tools';
14
+ import type { Promisable } from 'type-fest';
15
+
16
+ type DocumentResolverMap = Map<string, DocumentResolver>;
17
+
18
+ export class DocumentExtractor {
19
+ constructor(readonly module: Promisable<DynamicModule | Type>) {}
20
+
21
+ async getDocumentResolverMap(): Promise<DocumentResolverMap> {
22
+ return this.recursiveFillDocumentMap();
23
+ }
24
+
25
+ /**
26
+ * Рекурсивно проходит по дереву объявлений неста и добавляет все AutodocResolver в `map`
27
+ */
28
+ private async recursiveFillDocumentMap(
29
+ autodocResolverMap: DocumentResolverMap = new Map(),
30
+ ): Promise<DocumentResolverMap> {
31
+ const nestDefinitionIterator = new NestDefinitionIterator(this.module);
32
+
33
+ for await (const nestDef of nestDefinitionIterator.iterate()) {
34
+ if (!NestAssert.isObjectOrCtor(nestDef)) {
35
+ continue;
36
+ }
37
+ for (const value of RsdkMetadataProvider.getMetadataSource(nestDef)) {
38
+ if (!NestAssert.isObjectOrCtor(value)) {
39
+ continue;
40
+ }
41
+ this.fillDocumentMap(value, autodocResolverMap);
42
+ }
43
+ }
44
+
45
+ return autodocResolverMap;
46
+ }
47
+
48
+ /**
49
+ * Извлекает autodoc-метаданные из definition и добавляет их в map
50
+ */
51
+ private fillDocumentMap(
52
+ definition:
53
+ | DynamicModule
54
+ | Type<any>
55
+ | ClassProvider<any>
56
+ | ValueProvider<any>
57
+ | FactoryProvider<any>
58
+ | ExistingProvider<any>
59
+ | Abstract<any>
60
+ // eslint-disable-next-line @typescript-eslint/ban-types
61
+ | Function,
62
+ extractorMap: DocumentResolverMap,
63
+ ): void {
64
+ const resolvers = AutodocMetadata.getResolvers(definition);
65
+
66
+ if (!resolvers) {
67
+ return;
68
+ }
69
+ for (const resolver of resolvers) {
70
+ extractorMap.set(resolver.scope, resolver.resolver);
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,41 @@
1
+ import { Composite } from '@rsdk/autodoc.protocol';
2
+ import type { SourceMetadata } from '@rsdk/core';
3
+
4
+ import { DocumentFragment } from '../document-fragment.abstract';
5
+
6
+ export class AdditionalSourcesFragment extends DocumentFragment {
7
+ constructor(private sources?: SourceMetadata[]) {
8
+ super();
9
+ }
10
+
11
+ getContent(): string {
12
+ if (!this.sources?.length) {
13
+ return '';
14
+ }
15
+ const fragments: string[] = [];
16
+
17
+ fragments.push('## Additional Sources', '\n');
18
+
19
+ for (const metadata of this.sources) {
20
+ fragments.push(
21
+ `### ${metadata.name}`,
22
+ `${metadata.name} ${metadata.description}`,
23
+ );
24
+ }
25
+ return fragments.join('\n');
26
+ }
27
+
28
+ getDocumentNode(): Composite | null {
29
+ if (!this.sources) {
30
+ return null;
31
+ }
32
+ const sourceNodes = this.sources.map((metadata) => {
33
+ return new Composite(
34
+ [`${metadata.name} ${metadata.description}`],
35
+ metadata.name,
36
+ );
37
+ });
38
+
39
+ return new Composite(sourceNodes, 'AdditionalSources');
40
+ }
41
+ }
@@ -0,0 +1,18 @@
1
+ import type { DocumentNode } from '@rsdk/autodoc.protocol';
2
+ import { Composite } from '@rsdk/autodoc.protocol';
3
+ import type { PlatformApp } from '@rsdk/core';
4
+
5
+ import type { DocumentFragment } from '../document-fragment.abstract';
6
+
7
+ export class AppMetadataFragment implements DocumentFragment {
8
+ constructor(private readonly app: PlatformApp) {}
9
+
10
+ async getDocumentNode(): Promise<DocumentNode[] | DocumentNode | null> {
11
+ const metadata = await this.app.getMetadata();
12
+
13
+ if (!metadata.description) {
14
+ return null;
15
+ }
16
+ return new Composite(metadata.description, 'Description');
17
+ }
18
+ }
@@ -0,0 +1,30 @@
1
+ import type { DocumentNode } from '@rsdk/autodoc.protocol';
2
+ import { Composite } from '@rsdk/autodoc.protocol';
3
+ import type { PlatformApp } from '@rsdk/core';
4
+
5
+ import type { DocumentFragment } from '../document-fragment.abstract';
6
+ import { DocumentExtractor } from '../extractor/document.extractor';
7
+
8
+ export class AutodocMetadataFragment implements DocumentFragment {
9
+ constructor(readonly app: PlatformApp) {}
10
+
11
+ async getDocumentNode(): Promise<DocumentNode> {
12
+ const root = this.app.context.getRoot();
13
+ const resolvers = await new DocumentExtractor(
14
+ root,
15
+ ).getDocumentResolverMap();
16
+
17
+ const nodes: DocumentNode[] = [];
18
+
19
+ const rsdkMetadataProvider =
20
+ await this.app.context.getRsdkMetadataProvider();
21
+
22
+ for (const resolver of resolvers.values()) {
23
+ const node = await resolver.getNode(rsdkMetadataProvider);
24
+
25
+ node && nodes.push(node);
26
+ }
27
+
28
+ return new Composite(nodes);
29
+ }
30
+ }