@rvoh/psychic 2.0.4 → 2.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/dist/cjs/src/bin/index.js +5 -9
- package/dist/cjs/src/cli/helpers/ASTBuilder.js +175 -0
- package/dist/cjs/src/cli/helpers/ASTPsychicTypesBuilder.js +59 -0
- package/dist/esm/src/bin/index.js +5 -9
- package/dist/esm/src/cli/helpers/ASTBuilder.js +175 -0
- package/dist/esm/src/cli/helpers/ASTPsychicTypesBuilder.js +59 -0
- package/dist/types/src/bin/index.d.ts +1 -1
- package/dist/types/src/cli/helpers/ASTBuilder.d.ts +89 -0
- package/dist/types/src/cli/helpers/ASTPsychicTypesBuilder.d.ts +28 -0
- package/package.json +4 -4
- package/dist/cjs/src/cli/helpers/TypesBuilder.js +0 -23
- package/dist/esm/src/cli/helpers/TypesBuilder.js +0 -23
- package/dist/types/src/cli/helpers/TypesBuilder.d.ts +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliFileWriter, DreamBin, DreamCLI } from '@rvoh/dream/system';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import ASTPsychicTypesBuilder from '../cli/helpers/ASTPsychicTypesBuilder.js';
|
|
5
5
|
import generateController from '../generate/controller.js';
|
|
6
6
|
import generateResource from '../generate/resource.js';
|
|
7
7
|
import isObject from '../helpers/isObject.js';
|
|
@@ -54,11 +54,10 @@ export default class PsychicBin {
|
|
|
54
54
|
await CliFileWriter.revert();
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
DreamCLI.logger.logEndProgress();
|
|
57
|
+
static async syncTypes() {
|
|
58
|
+
await DreamCLI.logger.logProgress(`syncing types/psychic.ts...`, async () => {
|
|
59
|
+
await new ASTPsychicTypesBuilder().build();
|
|
60
|
+
});
|
|
62
61
|
}
|
|
63
62
|
static async syncOpenapiTypescriptFiles() {
|
|
64
63
|
DreamCLI.logger.logStartProgress(`syncing openapi types...`);
|
|
@@ -117,8 +116,5 @@ export default class PsychicBin {
|
|
|
117
116
|
output = { ...output, ...res };
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
|
-
if (Object.keys(output).length) {
|
|
121
|
-
await PsychicBin.syncTypes(output);
|
|
122
|
-
}
|
|
123
119
|
}
|
|
124
120
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { DreamApp } from '@rvoh/dream';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
const f = ts.factory;
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
*
|
|
8
|
+
* This is a base class, which is inherited by the ASTSchemaBuilder,
|
|
9
|
+
* the ASTKyselyCodegenEnhancer, and the ASTGlobalSchemaBuilder,
|
|
10
|
+
* each of which is responsible for building up the output of the various
|
|
11
|
+
* type files consumed by dream internally.
|
|
12
|
+
*
|
|
13
|
+
* This base class is just a container for common methods used by all
|
|
14
|
+
* classes.
|
|
15
|
+
*/
|
|
16
|
+
export default class ASTBuilder {
|
|
17
|
+
/**
|
|
18
|
+
* @internal
|
|
19
|
+
*
|
|
20
|
+
* builds a new line, useful for injecting new lines into AST statements
|
|
21
|
+
*/
|
|
22
|
+
newLine() {
|
|
23
|
+
return f.createIdentifier('\n');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @internal
|
|
27
|
+
*
|
|
28
|
+
* given an interface declaration, it will extrace the relevant property statement
|
|
29
|
+
* by the given property name.
|
|
30
|
+
*/
|
|
31
|
+
getPropertyFromInterface(interfaceNode, propertyName) {
|
|
32
|
+
for (const member of interfaceNode.members) {
|
|
33
|
+
if (ts.isPropertySignature(member)) {
|
|
34
|
+
if (ts.isIdentifier(member.name) && member.name.text === propertyName) {
|
|
35
|
+
return member;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @internal
|
|
43
|
+
*
|
|
44
|
+
* returns an array of string type literals which were extracted from
|
|
45
|
+
* either a type or type union, depending on what is provided
|
|
46
|
+
* for the typeAlias. this allows you to safely and easily collect
|
|
47
|
+
* an array of types given an alias
|
|
48
|
+
*/
|
|
49
|
+
extractStringLiteralTypeNodesFromTypeOrUnion(typeAlias) {
|
|
50
|
+
const literals = [];
|
|
51
|
+
if (ts.isUnionTypeNode(typeAlias.type)) {
|
|
52
|
+
typeAlias.type.types.forEach(typeNode => {
|
|
53
|
+
if (ts.isLiteralTypeNode(typeNode) && ts.isStringLiteral(typeNode.literal)) {
|
|
54
|
+
literals.push(typeNode);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else if (ts.isLiteralTypeNode(typeAlias.type) && ts.isStringLiteral(typeAlias.type.literal)) {
|
|
59
|
+
literals.push(typeAlias.type);
|
|
60
|
+
}
|
|
61
|
+
return literals;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @internal
|
|
65
|
+
*
|
|
66
|
+
* returns an array of type literals which were extracted from
|
|
67
|
+
* either a type or type union, depending on what is provided
|
|
68
|
+
* for the typeAlias. this allows you to safely and easily collect
|
|
69
|
+
* an array of types given an alias
|
|
70
|
+
*/
|
|
71
|
+
extractTypeNodesFromTypeOrUnion(typeAlias) {
|
|
72
|
+
const literals = [];
|
|
73
|
+
if (typeAlias.type && ts.isUnionTypeNode(typeAlias.type)) {
|
|
74
|
+
typeAlias.type.types.forEach(typeNode => {
|
|
75
|
+
literals.push(typeNode);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (typeAlias.type) {
|
|
79
|
+
literals.push(typeAlias.type);
|
|
80
|
+
}
|
|
81
|
+
return literals;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* @internal
|
|
85
|
+
*
|
|
86
|
+
* returns the provided node iff
|
|
87
|
+
* a.) the node is an exported type alias
|
|
88
|
+
* b.) the exported name matches the provided name (or else there was no name provided)
|
|
89
|
+
*
|
|
90
|
+
* otherwise, returns null
|
|
91
|
+
*/
|
|
92
|
+
exportedTypeAliasOrNull(node, exportName) {
|
|
93
|
+
if (ts.isTypeAliasDeclaration(node) &&
|
|
94
|
+
node?.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
95
|
+
(!exportName ? true : node.name.text === exportName))
|
|
96
|
+
return node;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* @internal
|
|
101
|
+
*
|
|
102
|
+
* returns the provided node iff
|
|
103
|
+
* a.) the node is an exported interface
|
|
104
|
+
* b.) the exported name matches the provided name (or else there was no name provided)
|
|
105
|
+
*
|
|
106
|
+
* otherwise, returns null
|
|
107
|
+
*/
|
|
108
|
+
exportedInterfaceOrNull(node, exportName) {
|
|
109
|
+
if (ts.isInterfaceDeclaration(node) &&
|
|
110
|
+
node?.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
111
|
+
(!exportName ? true : node.name.text === exportName))
|
|
112
|
+
return node;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* @internal
|
|
117
|
+
*
|
|
118
|
+
* returns the path to the dream.globals.ts file
|
|
119
|
+
*/
|
|
120
|
+
psychicTypesPath() {
|
|
121
|
+
const dreamApp = DreamApp.getOrFail();
|
|
122
|
+
return path.join(dreamApp.projectRoot, dreamApp.paths.types, 'psychic.ts');
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* @internal
|
|
126
|
+
*
|
|
127
|
+
* safely runs prettier against the provided output. If prettier
|
|
128
|
+
* is not installed, then the original output is returned
|
|
129
|
+
*/
|
|
130
|
+
async prettier(output) {
|
|
131
|
+
try {
|
|
132
|
+
// dynamically, safely bring in prettier.
|
|
133
|
+
// ini the event that it fails, we will return the
|
|
134
|
+
// original output, unformatted, since prettier
|
|
135
|
+
// is technically not a real dependency of dream,
|
|
136
|
+
// though psychic and dream apps are provisioned
|
|
137
|
+
// with prettier by default, so this should usually work
|
|
138
|
+
const prettier = (await import('prettier')).default;
|
|
139
|
+
const results = await prettier.format(output, {
|
|
140
|
+
parser: 'typescript',
|
|
141
|
+
semi: false,
|
|
142
|
+
singleQuote: true,
|
|
143
|
+
tabWidth: 2,
|
|
144
|
+
lineWidth: 80,
|
|
145
|
+
});
|
|
146
|
+
return typeof results === 'string' ? results : output;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// intentional noop, we don't want to raise if prettier
|
|
150
|
+
// fails, since it is possible for the end user to not
|
|
151
|
+
// want to use prettier, and it is not a required peer
|
|
152
|
+
// dependency of dream
|
|
153
|
+
return output;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* @internal
|
|
158
|
+
*
|
|
159
|
+
* given a type node, it will send back the first found generic
|
|
160
|
+
* provided to that type.
|
|
161
|
+
*/
|
|
162
|
+
getFirstGenericType(node) {
|
|
163
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
164
|
+
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
165
|
+
return node.typeArguments[0];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (ts.isCallExpression(node)) {
|
|
169
|
+
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
170
|
+
return node.typeArguments[0];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CliFileWriter, DreamCLI } from '@rvoh/dream/system';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import PsychicApp from '../../psychic-app/index.js';
|
|
4
|
+
import ASTBuilder from './ASTBuilder.js';
|
|
5
|
+
const f = ts.factory;
|
|
6
|
+
/**
|
|
7
|
+
* Responsible for building dream globals, which can be found at
|
|
8
|
+
* types/dream.globals.ts.
|
|
9
|
+
*
|
|
10
|
+
* This class leverages internal AST building mechanisms built into
|
|
11
|
+
* typescript to manually build up object literals and interfaces
|
|
12
|
+
* for our app to consume.
|
|
13
|
+
*/
|
|
14
|
+
export default class ASTPsychicTypesBuilder extends ASTBuilder {
|
|
15
|
+
async build() {
|
|
16
|
+
const logger = DreamCLI.logger;
|
|
17
|
+
const sourceFile = ts.createSourceFile('', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
18
|
+
await logger.logProgress('[psychic] building psychic types', async () => {
|
|
19
|
+
const output = await this.prettier(this.printStatements(this.buildPsychicTypes(), sourceFile));
|
|
20
|
+
await CliFileWriter.write(this.psychicTypesPath(), output);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @internal
|
|
25
|
+
*
|
|
26
|
+
* builds up the `export const psychicTypes = ...` statement within the types/psychic.ts
|
|
27
|
+
* file. It does this by leveraging low-level AST utils built into typescript
|
|
28
|
+
* to manually build up an object literal, cast it as a const, and write it to
|
|
29
|
+
* an exported variable.
|
|
30
|
+
*/
|
|
31
|
+
buildPsychicTypes() {
|
|
32
|
+
const psychicApp = PsychicApp.getOrFail();
|
|
33
|
+
const psychicTypesObjectLiteral = f.createObjectLiteralExpression([
|
|
34
|
+
f.createPropertyAssignment(f.createIdentifier('openapiNames'), f.createArrayLiteralExpression(Object.keys(psychicApp.openapi).map(key => f.createStringLiteral(key)))),
|
|
35
|
+
], true);
|
|
36
|
+
// add "as const" to the end of the schema object we
|
|
37
|
+
// have built before returning it
|
|
38
|
+
const constAssertion = f.createAsExpression(psychicTypesObjectLiteral, f.createKeywordTypeNode(ts.SyntaxKind.ConstKeyword));
|
|
39
|
+
const psychicTypesObjectLiteralConst = f.createVariableStatement(undefined, f.createVariableDeclarationList([
|
|
40
|
+
f.createVariableDeclaration(f.createIdentifier('psychicTypes'), undefined, undefined, constAssertion),
|
|
41
|
+
], ts.NodeFlags.Const));
|
|
42
|
+
const defaultExportIdentifier = f.createIdentifier('psychicTypes');
|
|
43
|
+
const exportDefaultStatement = f.createExportDefault(defaultExportIdentifier);
|
|
44
|
+
return [psychicTypesObjectLiteralConst, this.newLine(), exportDefaultStatement];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @internal
|
|
48
|
+
*
|
|
49
|
+
* writes the compiled statements to string.
|
|
50
|
+
*
|
|
51
|
+
*/
|
|
52
|
+
printStatements(statements, sourceFile) {
|
|
53
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, omitTrailingSemicolon: true });
|
|
54
|
+
const result = printer.printList(ts.ListFormat.SourceFileStatements, f.createNodeArray(statements), sourceFile);
|
|
55
|
+
// TODO: add autogenerate disclaimer
|
|
56
|
+
return `\
|
|
57
|
+
${result}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliFileWriter, DreamBin, DreamCLI } from '@rvoh/dream/system';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import ASTPsychicTypesBuilder from '../cli/helpers/ASTPsychicTypesBuilder.js';
|
|
5
5
|
import generateController from '../generate/controller.js';
|
|
6
6
|
import generateResource from '../generate/resource.js';
|
|
7
7
|
import isObject from '../helpers/isObject.js';
|
|
@@ -54,11 +54,10 @@ export default class PsychicBin {
|
|
|
54
54
|
await CliFileWriter.revert();
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
DreamCLI.logger.logEndProgress();
|
|
57
|
+
static async syncTypes() {
|
|
58
|
+
await DreamCLI.logger.logProgress(`syncing types/psychic.ts...`, async () => {
|
|
59
|
+
await new ASTPsychicTypesBuilder().build();
|
|
60
|
+
});
|
|
62
61
|
}
|
|
63
62
|
static async syncOpenapiTypescriptFiles() {
|
|
64
63
|
DreamCLI.logger.logStartProgress(`syncing openapi types...`);
|
|
@@ -117,8 +116,5 @@ export default class PsychicBin {
|
|
|
117
116
|
output = { ...output, ...res };
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
|
-
if (Object.keys(output).length) {
|
|
121
|
-
await PsychicBin.syncTypes(output);
|
|
122
|
-
}
|
|
123
119
|
}
|
|
124
120
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { DreamApp } from '@rvoh/dream';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
const f = ts.factory;
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
*
|
|
8
|
+
* This is a base class, which is inherited by the ASTSchemaBuilder,
|
|
9
|
+
* the ASTKyselyCodegenEnhancer, and the ASTGlobalSchemaBuilder,
|
|
10
|
+
* each of which is responsible for building up the output of the various
|
|
11
|
+
* type files consumed by dream internally.
|
|
12
|
+
*
|
|
13
|
+
* This base class is just a container for common methods used by all
|
|
14
|
+
* classes.
|
|
15
|
+
*/
|
|
16
|
+
export default class ASTBuilder {
|
|
17
|
+
/**
|
|
18
|
+
* @internal
|
|
19
|
+
*
|
|
20
|
+
* builds a new line, useful for injecting new lines into AST statements
|
|
21
|
+
*/
|
|
22
|
+
newLine() {
|
|
23
|
+
return f.createIdentifier('\n');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @internal
|
|
27
|
+
*
|
|
28
|
+
* given an interface declaration, it will extrace the relevant property statement
|
|
29
|
+
* by the given property name.
|
|
30
|
+
*/
|
|
31
|
+
getPropertyFromInterface(interfaceNode, propertyName) {
|
|
32
|
+
for (const member of interfaceNode.members) {
|
|
33
|
+
if (ts.isPropertySignature(member)) {
|
|
34
|
+
if (ts.isIdentifier(member.name) && member.name.text === propertyName) {
|
|
35
|
+
return member;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @internal
|
|
43
|
+
*
|
|
44
|
+
* returns an array of string type literals which were extracted from
|
|
45
|
+
* either a type or type union, depending on what is provided
|
|
46
|
+
* for the typeAlias. this allows you to safely and easily collect
|
|
47
|
+
* an array of types given an alias
|
|
48
|
+
*/
|
|
49
|
+
extractStringLiteralTypeNodesFromTypeOrUnion(typeAlias) {
|
|
50
|
+
const literals = [];
|
|
51
|
+
if (ts.isUnionTypeNode(typeAlias.type)) {
|
|
52
|
+
typeAlias.type.types.forEach(typeNode => {
|
|
53
|
+
if (ts.isLiteralTypeNode(typeNode) && ts.isStringLiteral(typeNode.literal)) {
|
|
54
|
+
literals.push(typeNode);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else if (ts.isLiteralTypeNode(typeAlias.type) && ts.isStringLiteral(typeAlias.type.literal)) {
|
|
59
|
+
literals.push(typeAlias.type);
|
|
60
|
+
}
|
|
61
|
+
return literals;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @internal
|
|
65
|
+
*
|
|
66
|
+
* returns an array of type literals which were extracted from
|
|
67
|
+
* either a type or type union, depending on what is provided
|
|
68
|
+
* for the typeAlias. this allows you to safely and easily collect
|
|
69
|
+
* an array of types given an alias
|
|
70
|
+
*/
|
|
71
|
+
extractTypeNodesFromTypeOrUnion(typeAlias) {
|
|
72
|
+
const literals = [];
|
|
73
|
+
if (typeAlias.type && ts.isUnionTypeNode(typeAlias.type)) {
|
|
74
|
+
typeAlias.type.types.forEach(typeNode => {
|
|
75
|
+
literals.push(typeNode);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (typeAlias.type) {
|
|
79
|
+
literals.push(typeAlias.type);
|
|
80
|
+
}
|
|
81
|
+
return literals;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* @internal
|
|
85
|
+
*
|
|
86
|
+
* returns the provided node iff
|
|
87
|
+
* a.) the node is an exported type alias
|
|
88
|
+
* b.) the exported name matches the provided name (or else there was no name provided)
|
|
89
|
+
*
|
|
90
|
+
* otherwise, returns null
|
|
91
|
+
*/
|
|
92
|
+
exportedTypeAliasOrNull(node, exportName) {
|
|
93
|
+
if (ts.isTypeAliasDeclaration(node) &&
|
|
94
|
+
node?.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
95
|
+
(!exportName ? true : node.name.text === exportName))
|
|
96
|
+
return node;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* @internal
|
|
101
|
+
*
|
|
102
|
+
* returns the provided node iff
|
|
103
|
+
* a.) the node is an exported interface
|
|
104
|
+
* b.) the exported name matches the provided name (or else there was no name provided)
|
|
105
|
+
*
|
|
106
|
+
* otherwise, returns null
|
|
107
|
+
*/
|
|
108
|
+
exportedInterfaceOrNull(node, exportName) {
|
|
109
|
+
if (ts.isInterfaceDeclaration(node) &&
|
|
110
|
+
node?.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
111
|
+
(!exportName ? true : node.name.text === exportName))
|
|
112
|
+
return node;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* @internal
|
|
117
|
+
*
|
|
118
|
+
* returns the path to the dream.globals.ts file
|
|
119
|
+
*/
|
|
120
|
+
psychicTypesPath() {
|
|
121
|
+
const dreamApp = DreamApp.getOrFail();
|
|
122
|
+
return path.join(dreamApp.projectRoot, dreamApp.paths.types, 'psychic.ts');
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* @internal
|
|
126
|
+
*
|
|
127
|
+
* safely runs prettier against the provided output. If prettier
|
|
128
|
+
* is not installed, then the original output is returned
|
|
129
|
+
*/
|
|
130
|
+
async prettier(output) {
|
|
131
|
+
try {
|
|
132
|
+
// dynamically, safely bring in prettier.
|
|
133
|
+
// ini the event that it fails, we will return the
|
|
134
|
+
// original output, unformatted, since prettier
|
|
135
|
+
// is technically not a real dependency of dream,
|
|
136
|
+
// though psychic and dream apps are provisioned
|
|
137
|
+
// with prettier by default, so this should usually work
|
|
138
|
+
const prettier = (await import('prettier')).default;
|
|
139
|
+
const results = await prettier.format(output, {
|
|
140
|
+
parser: 'typescript',
|
|
141
|
+
semi: false,
|
|
142
|
+
singleQuote: true,
|
|
143
|
+
tabWidth: 2,
|
|
144
|
+
lineWidth: 80,
|
|
145
|
+
});
|
|
146
|
+
return typeof results === 'string' ? results : output;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// intentional noop, we don't want to raise if prettier
|
|
150
|
+
// fails, since it is possible for the end user to not
|
|
151
|
+
// want to use prettier, and it is not a required peer
|
|
152
|
+
// dependency of dream
|
|
153
|
+
return output;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* @internal
|
|
158
|
+
*
|
|
159
|
+
* given a type node, it will send back the first found generic
|
|
160
|
+
* provided to that type.
|
|
161
|
+
*/
|
|
162
|
+
getFirstGenericType(node) {
|
|
163
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
164
|
+
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
165
|
+
return node.typeArguments[0];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (ts.isCallExpression(node)) {
|
|
169
|
+
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
170
|
+
return node.typeArguments[0];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CliFileWriter, DreamCLI } from '@rvoh/dream/system';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import PsychicApp from '../../psychic-app/index.js';
|
|
4
|
+
import ASTBuilder from './ASTBuilder.js';
|
|
5
|
+
const f = ts.factory;
|
|
6
|
+
/**
|
|
7
|
+
* Responsible for building dream globals, which can be found at
|
|
8
|
+
* types/dream.globals.ts.
|
|
9
|
+
*
|
|
10
|
+
* This class leverages internal AST building mechanisms built into
|
|
11
|
+
* typescript to manually build up object literals and interfaces
|
|
12
|
+
* for our app to consume.
|
|
13
|
+
*/
|
|
14
|
+
export default class ASTPsychicTypesBuilder extends ASTBuilder {
|
|
15
|
+
async build() {
|
|
16
|
+
const logger = DreamCLI.logger;
|
|
17
|
+
const sourceFile = ts.createSourceFile('', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
18
|
+
await logger.logProgress('[psychic] building psychic types', async () => {
|
|
19
|
+
const output = await this.prettier(this.printStatements(this.buildPsychicTypes(), sourceFile));
|
|
20
|
+
await CliFileWriter.write(this.psychicTypesPath(), output);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @internal
|
|
25
|
+
*
|
|
26
|
+
* builds up the `export const psychicTypes = ...` statement within the types/psychic.ts
|
|
27
|
+
* file. It does this by leveraging low-level AST utils built into typescript
|
|
28
|
+
* to manually build up an object literal, cast it as a const, and write it to
|
|
29
|
+
* an exported variable.
|
|
30
|
+
*/
|
|
31
|
+
buildPsychicTypes() {
|
|
32
|
+
const psychicApp = PsychicApp.getOrFail();
|
|
33
|
+
const psychicTypesObjectLiteral = f.createObjectLiteralExpression([
|
|
34
|
+
f.createPropertyAssignment(f.createIdentifier('openapiNames'), f.createArrayLiteralExpression(Object.keys(psychicApp.openapi).map(key => f.createStringLiteral(key)))),
|
|
35
|
+
], true);
|
|
36
|
+
// add "as const" to the end of the schema object we
|
|
37
|
+
// have built before returning it
|
|
38
|
+
const constAssertion = f.createAsExpression(psychicTypesObjectLiteral, f.createKeywordTypeNode(ts.SyntaxKind.ConstKeyword));
|
|
39
|
+
const psychicTypesObjectLiteralConst = f.createVariableStatement(undefined, f.createVariableDeclarationList([
|
|
40
|
+
f.createVariableDeclaration(f.createIdentifier('psychicTypes'), undefined, undefined, constAssertion),
|
|
41
|
+
], ts.NodeFlags.Const));
|
|
42
|
+
const defaultExportIdentifier = f.createIdentifier('psychicTypes');
|
|
43
|
+
const exportDefaultStatement = f.createExportDefault(defaultExportIdentifier);
|
|
44
|
+
return [psychicTypesObjectLiteralConst, this.newLine(), exportDefaultStatement];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @internal
|
|
48
|
+
*
|
|
49
|
+
* writes the compiled statements to string.
|
|
50
|
+
*
|
|
51
|
+
*/
|
|
52
|
+
printStatements(statements, sourceFile) {
|
|
53
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, omitTrailingSemicolon: true });
|
|
54
|
+
const result = printer.printList(ts.ListFormat.SourceFileStatements, f.createNodeArray(statements), sourceFile);
|
|
55
|
+
// TODO: add autogenerate disclaimer
|
|
56
|
+
return `\
|
|
57
|
+
${result}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -13,7 +13,7 @@ export default class PsychicBin {
|
|
|
13
13
|
schemaOnly?: boolean;
|
|
14
14
|
}): Promise<void>;
|
|
15
15
|
static postSync(): Promise<void>;
|
|
16
|
-
static syncTypes(
|
|
16
|
+
static syncTypes(): Promise<void>;
|
|
17
17
|
static syncOpenapiTypescriptFiles(): Promise<void>;
|
|
18
18
|
static syncOpenapiJson(): Promise<void>;
|
|
19
19
|
static syncRoutes(): Promise<void>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* This is a base class, which is inherited by the ASTSchemaBuilder,
|
|
6
|
+
* the ASTKyselyCodegenEnhancer, and the ASTGlobalSchemaBuilder,
|
|
7
|
+
* each of which is responsible for building up the output of the various
|
|
8
|
+
* type files consumed by dream internally.
|
|
9
|
+
*
|
|
10
|
+
* This base class is just a container for common methods used by all
|
|
11
|
+
* classes.
|
|
12
|
+
*/
|
|
13
|
+
export default class ASTBuilder {
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*
|
|
17
|
+
* builds a new line, useful for injecting new lines into AST statements
|
|
18
|
+
*/
|
|
19
|
+
protected newLine(): ts.Identifier;
|
|
20
|
+
/**
|
|
21
|
+
* @internal
|
|
22
|
+
*
|
|
23
|
+
* given an interface declaration, it will extrace the relevant property statement
|
|
24
|
+
* by the given property name.
|
|
25
|
+
*/
|
|
26
|
+
protected getPropertyFromInterface(interfaceNode: ts.InterfaceDeclaration, propertyName: string): ts.PropertySignature | null;
|
|
27
|
+
/**
|
|
28
|
+
* @internal
|
|
29
|
+
*
|
|
30
|
+
* returns an array of string type literals which were extracted from
|
|
31
|
+
* either a type or type union, depending on what is provided
|
|
32
|
+
* for the typeAlias. this allows you to safely and easily collect
|
|
33
|
+
* an array of types given an alias
|
|
34
|
+
*/
|
|
35
|
+
protected extractStringLiteralTypeNodesFromTypeOrUnion(typeAlias: ts.TypeAliasDeclaration): (ts.LiteralTypeNode & {
|
|
36
|
+
literal: {
|
|
37
|
+
text: string;
|
|
38
|
+
};
|
|
39
|
+
})[];
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
*
|
|
43
|
+
* returns an array of type literals which were extracted from
|
|
44
|
+
* either a type or type union, depending on what is provided
|
|
45
|
+
* for the typeAlias. this allows you to safely and easily collect
|
|
46
|
+
* an array of types given an alias
|
|
47
|
+
*/
|
|
48
|
+
protected extractTypeNodesFromTypeOrUnion(typeAlias: ts.TypeAliasDeclaration | ts.PropertySignature): ts.TypeNode[];
|
|
49
|
+
/**
|
|
50
|
+
* @internal
|
|
51
|
+
*
|
|
52
|
+
* returns the provided node iff
|
|
53
|
+
* a.) the node is an exported type alias
|
|
54
|
+
* b.) the exported name matches the provided name (or else there was no name provided)
|
|
55
|
+
*
|
|
56
|
+
* otherwise, returns null
|
|
57
|
+
*/
|
|
58
|
+
protected exportedTypeAliasOrNull(node: ts.Node, exportName?: string): ts.TypeAliasDeclaration | null;
|
|
59
|
+
/**
|
|
60
|
+
* @internal
|
|
61
|
+
*
|
|
62
|
+
* returns the provided node iff
|
|
63
|
+
* a.) the node is an exported interface
|
|
64
|
+
* b.) the exported name matches the provided name (or else there was no name provided)
|
|
65
|
+
*
|
|
66
|
+
* otherwise, returns null
|
|
67
|
+
*/
|
|
68
|
+
protected exportedInterfaceOrNull(node: ts.Node, exportName?: string): ts.InterfaceDeclaration | null;
|
|
69
|
+
/**
|
|
70
|
+
* @internal
|
|
71
|
+
*
|
|
72
|
+
* returns the path to the dream.globals.ts file
|
|
73
|
+
*/
|
|
74
|
+
protected psychicTypesPath(): string;
|
|
75
|
+
/**
|
|
76
|
+
* @internal
|
|
77
|
+
*
|
|
78
|
+
* safely runs prettier against the provided output. If prettier
|
|
79
|
+
* is not installed, then the original output is returned
|
|
80
|
+
*/
|
|
81
|
+
protected prettier(output: string): Promise<string>;
|
|
82
|
+
/**
|
|
83
|
+
* @internal
|
|
84
|
+
*
|
|
85
|
+
* given a type node, it will send back the first found generic
|
|
86
|
+
* provided to that type.
|
|
87
|
+
*/
|
|
88
|
+
protected getFirstGenericType(node: ts.Node): ts.TypeNode | null;
|
|
89
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ASTBuilder from './ASTBuilder.js';
|
|
2
|
+
/**
|
|
3
|
+
* Responsible for building dream globals, which can be found at
|
|
4
|
+
* types/dream.globals.ts.
|
|
5
|
+
*
|
|
6
|
+
* This class leverages internal AST building mechanisms built into
|
|
7
|
+
* typescript to manually build up object literals and interfaces
|
|
8
|
+
* for our app to consume.
|
|
9
|
+
*/
|
|
10
|
+
export default class ASTPsychicTypesBuilder extends ASTBuilder {
|
|
11
|
+
build(): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* @internal
|
|
14
|
+
*
|
|
15
|
+
* builds up the `export const psychicTypes = ...` statement within the types/psychic.ts
|
|
16
|
+
* file. It does this by leveraging low-level AST utils built into typescript
|
|
17
|
+
* to manually build up an object literal, cast it as a const, and write it to
|
|
18
|
+
* an exported variable.
|
|
19
|
+
*/
|
|
20
|
+
private buildPsychicTypes;
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
*
|
|
24
|
+
* writes the compiled statements to string.
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
private printStatements;
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@rvoh/psychic",
|
|
4
4
|
"description": "Typescript web framework",
|
|
5
|
-
"version": "2.0
|
|
5
|
+
"version": "2.1.0",
|
|
6
6
|
"author": "RVOHealth",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"@rvoh/dream": "^2.0.3",
|
|
82
82
|
"@types/express": "^5.0.1",
|
|
83
83
|
"commander": "^12.1.0",
|
|
84
|
-
"express": "^5.1
|
|
84
|
+
"express": "^5.2.1",
|
|
85
85
|
"openapi-typescript": "^7.8.0"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"@rvoh/dream": "^2.0.3",
|
|
91
91
|
"@rvoh/dream-spec-helpers": "^2.0.0",
|
|
92
92
|
"@rvoh/psychic-spec-helpers": "^2.0.0",
|
|
93
|
-
"@types/express": "^5.0.
|
|
93
|
+
"@types/express": "^5.0.6",
|
|
94
94
|
"@types/express-session": "^1.18.2",
|
|
95
95
|
"@types/node": "^22.17.1",
|
|
96
96
|
"@types/passport": "^0",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"@types/supertest": "^6.0.3",
|
|
100
100
|
"@typescript/analyze-trace": "^0.10.1",
|
|
101
101
|
"eslint": "^9.19.0",
|
|
102
|
-
"express": "^5.1
|
|
102
|
+
"express": "^5.2.1",
|
|
103
103
|
"express-session": "^1.18.2",
|
|
104
104
|
"jsdom": "^26.1.0",
|
|
105
105
|
"kysely": "^0.28.5",
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { DreamApp } from '@rvoh/dream';
|
|
2
|
-
import { CliFileWriter } from '@rvoh/dream/system';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import PsychicApp from '../../psychic-app/index.js';
|
|
5
|
-
export default class TypesBuilder {
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
static async sync(customTypes = undefined) {
|
|
8
|
-
const dreamApp = DreamApp.getOrFail();
|
|
9
|
-
const schemaPath = path.join(dreamApp.projectRoot, dreamApp.paths.types, 'psychic.ts');
|
|
10
|
-
await CliFileWriter.write(schemaPath, this.build(customTypes));
|
|
11
|
-
}
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
-
static build(customTypes = undefined) {
|
|
14
|
-
const psychicApp = PsychicApp.getOrFail();
|
|
15
|
-
const output = {
|
|
16
|
-
openapiNames: Object.keys(psychicApp.openapi),
|
|
17
|
-
...(customTypes || {}),
|
|
18
|
-
};
|
|
19
|
-
return `const psychicTypes = ${JSON.stringify(output, null, 2)} as const
|
|
20
|
-
|
|
21
|
-
export default psychicTypes`;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { DreamApp } from '@rvoh/dream';
|
|
2
|
-
import { CliFileWriter } from '@rvoh/dream/system';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import PsychicApp from '../../psychic-app/index.js';
|
|
5
|
-
export default class TypesBuilder {
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
static async sync(customTypes = undefined) {
|
|
8
|
-
const dreamApp = DreamApp.getOrFail();
|
|
9
|
-
const schemaPath = path.join(dreamApp.projectRoot, dreamApp.paths.types, 'psychic.ts');
|
|
10
|
-
await CliFileWriter.write(schemaPath, this.build(customTypes));
|
|
11
|
-
}
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
-
static build(customTypes = undefined) {
|
|
14
|
-
const psychicApp = PsychicApp.getOrFail();
|
|
15
|
-
const output = {
|
|
16
|
-
openapiNames: Object.keys(psychicApp.openapi),
|
|
17
|
-
...(customTypes || {}),
|
|
18
|
-
};
|
|
19
|
-
return `const psychicTypes = ${JSON.stringify(output, null, 2)} as const
|
|
20
|
-
|
|
21
|
-
export default psychicTypes`;
|
|
22
|
-
}
|
|
23
|
-
}
|