@ackee/create-node-app 1.0.1 → 2.0.2
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/AUTHORS +2 -1
- package/README.md +27 -19
- package/docs/development.md +42 -0
- package/lib/Bootstrap.js +106 -65
- package/lib/Bootstrap.js.map +1 -1
- package/lib/Builder.js +111 -0
- package/lib/Builder.js.map +1 -0
- package/lib/Files.js +21 -0
- package/lib/Files.js.map +1 -0
- package/lib/Logger.js +26 -8
- package/lib/Logger.js.map +1 -1
- package/lib/Mergers/ConfigMerger.js +22 -0
- package/lib/Mergers/ConfigMerger.js.map +1 -0
- package/lib/Mergers/ContainerMerger.js +172 -0
- package/lib/Mergers/ContainerMerger.js.map +1 -0
- package/lib/Mergers/EnvJsoncMerger.js +20 -0
- package/lib/Mergers/EnvJsoncMerger.js.map +1 -0
- package/lib/Mergers/Merger.js +36 -0
- package/lib/Mergers/Merger.js.map +1 -0
- package/lib/Mergers/PackageJsonMerger.js +36 -0
- package/lib/Mergers/PackageJsonMerger.js.map +1 -0
- package/lib/Npm.js +40 -12
- package/lib/Npm.js.map +1 -1
- package/lib/PackageJson.js +4 -4
- package/lib/PackageJson.js.map +1 -1
- package/lib/StarterLoader.js +86 -0
- package/lib/StarterLoader.js.map +1 -0
- package/package.json +10 -7
- package/src/Bootstrap.ts +123 -82
- package/src/Builder.ts +172 -0
- package/src/Files.ts +22 -0
- package/src/Logger.ts +26 -7
- package/src/Mergers/ConfigMerger.ts +28 -0
- package/src/Mergers/ContainerMerger.ts +241 -0
- package/src/Mergers/EnvJsoncMerger.ts +24 -0
- package/src/Mergers/Merger.ts +51 -0
- package/src/Mergers/PackageJsonMerger.ts +45 -0
- package/src/Npm.ts +60 -15
- package/src/PackageJson.ts +6 -4
- package/src/Starter.ts +2 -2
- package/src/StarterLoader.ts +148 -0
- package/starter/{cloudrun → _base}/.env.jsonc +1 -5
- package/starter/{cloudrun → _base}/.eslintrc.cjs +3 -2
- package/starter/_base/README.md +53 -0
- package/starter/_base/package.json +45 -0
- package/starter/{cloudrun → _base}/src/adapters/pino.logger.ts +1 -1
- package/starter/_base/src/config.ts +16 -0
- package/starter/{cloudrun-graphql → _base}/src/container.ts +3 -1
- package/starter/_base/src/index.ts +14 -0
- package/starter/{cloudrun → _base}/src/view/cli/README.md +2 -6
- package/starter/api/graphql/.env.jsonc +8 -0
- package/starter/{cloudrun-graphql → api/graphql}/.eslintrc.cjs +4 -5
- package/starter/api/graphql/node-app.jsonc +6 -0
- package/starter/api/graphql/package.json +36 -0
- package/starter/{cloudrun-graphql → api/graphql}/src/config.ts +0 -4
- package/starter/api/graphql/src/index.ts +11 -0
- package/starter/{cloudrun-graphql → api/graphql}/src/test/helloWorld.test.ts +14 -3
- package/starter/api/graphql/src/view/graphql/context-factory.ts +13 -0
- package/starter/{cloudrun-graphql → api/graphql}/src/view/server.ts +16 -6
- package/starter/api/rest/.env.jsonc +6 -0
- package/starter/api/rest/.eslintrc.cjs +8 -0
- package/starter/api/rest/node-app.jsonc +6 -0
- package/starter/api/rest/package.json +25 -0
- package/starter/{cloudrun → api/rest}/src/config.ts +0 -5
- package/starter/api/rest/src/container.ts +13 -0
- package/starter/{cloudrun → api/rest}/src/index.ts +4 -4
- package/starter/{cloudrun → api/rest}/src/test/health-check.test.ts +3 -5
- package/starter/{cloudrun → api/rest}/src/test/util/openapi-test.util.ts +3 -3
- package/starter/{cloudrun → api/rest}/src/view/rest/middleware/error-handler.ts +1 -1
- package/starter/{cloudrun → api/rest}/src/view/rest/routes.ts +1 -1
- package/starter/{cloudrun → api/rest}/src/view/rest/util/openapi.util.ts +22 -19
- package/starter/{cloudrun → api/rest}/src/view/server.ts +6 -4
- package/starter/infra/postgresql-knex/.env.jsonc +5 -0
- package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose.yml +1 -1
- package/starter/infra/postgresql-knex/knexfile.ts +16 -0
- package/starter/infra/postgresql-knex/node-app.jsonc +6 -0
- package/starter/infra/postgresql-knex/package.json +13 -0
- package/starter/infra/postgresql-knex/src/adapters/knex.database.test.ts +21 -0
- package/starter/infra/postgresql-knex/src/adapters/knex.database.ts +14 -0
- package/starter/infra/postgresql-knex/src/adapters/repositories/migration.repository.ts +24 -0
- package/starter/infra/postgresql-knex/src/config.ts +14 -0
- package/starter/infra/postgresql-knex/src/container.ts +23 -0
- package/starter/infra/postgresql-knex/src/db/migration.template.ts +4 -0
- package/starter/infra/postgresql-knex/src/db/migrations/.gitkeep +0 -0
- package/starter/infra/postgresql-knex/src/db/seed.template.ts +3 -0
- package/starter/infra/postgresql-knex/src/db/seeds/.gitkeep +0 -0
- package/starter/infra/postgresql-knex/src/domain/ports/database.d.ts +4 -0
- package/starter/infra/postgresql-knex/src/domain/ports/repositories/migration.repository.d.ts +9 -0
- package/starter/infra/postgresql-knex/src/test/setup.ts +16 -0
- package/starter/{shared → pipeline/cloudrun-gitlab}/.gitlab-ci.yml +15 -6
- package/starter/pipeline/cloudrun-gitlab/node-app.jsonc +6 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/Toolbelt.js +0 -102
- package/lib/Toolbelt.js.map +0 -1
- package/lib/cloudrun/CloudRunStarter.js +0 -127
- package/lib/cloudrun/CloudRunStarter.js.map +0 -1
- package/lib/cloudrun-graphql/GraphQLStarter.js +0 -118
- package/lib/cloudrun-graphql/GraphQLStarter.js.map +0 -1
- package/src/Toolbelt.ts +0 -132
- package/src/cloudrun/CloudRunStarter.ts +0 -182
- package/src/cloudrun-graphql/GraphQLStarter.ts +0 -182
- package/starter/cloudrun/README.md +0 -69
- package/starter/cloudrun/src/container.ts +0 -18
- package/starter/cloudrun/src/context.ts +0 -39
- package/starter/cloudrun/src/domain/errors/codes.ts +0 -9
- package/starter/cloudrun/src/domain/errors/errors.ts +0 -25
- package/starter/cloudrun/src/domain/ports/logger.d.ts +0 -21
- package/starter/cloudrun-graphql/.env.jsonc +0 -12
- package/starter/cloudrun-graphql/README.md +0 -53
- package/starter/cloudrun-graphql/src/adapters/pino.logger.ts +0 -44
- package/starter/cloudrun-graphql/src/index.ts +0 -11
- package/starter/shared/.gitignore_ +0 -5
- package/starter/shared/ci-branch-config/common.env +0 -7
- package/starter/shared/ci-branch-config/development.env +0 -7
- package/starter/shared/ci-branch-config/master.env +0 -7
- package/starter/shared/ci-branch-config/stage.env +0 -7
- package/starter/shared/docker-compose/docker-compose.override.yml +0 -5
- package/starter/shared/jest.config.js +0 -12
- /package/starter/{shared → _base}/.dockerignore +0 -0
- /package/starter/{cloudrun → _base}/.eslint.tsconfig.json +0 -0
- /package/starter/{shared → _base}/.mocha-junit-config.json +0 -0
- /package/starter/{shared → _base}/.mocharc.json +0 -0
- /package/starter/{shared → _base}/.nvmrc +0 -0
- /package/starter/{shared → _base}/Dockerfile +0 -0
- /package/starter/{shared → _base}/prettier.config.cjs +0 -0
- /package/starter/{cloudrun-graphql → _base}/src/context.ts +0 -0
- /package/starter/{cloudrun-graphql → _base}/src/domain/errors/codes.ts +0 -0
- /package/starter/{cloudrun-graphql → _base}/src/domain/errors/errors.ts +0 -0
- /package/starter/{cloudrun-graphql → _base}/src/domain/ports/logger.d.ts +0 -0
- /package/starter/{shared → _base}/src/test/setup.ts +0 -0
- /package/starter/{cloudrun → _base}/src/view/cli/cli.ts +0 -0
- /package/starter/{shared → _base}/tsconfig.json +0 -0
- /package/starter/{cloudrun-graphql → api/graphql}/.eslint.tsconfig.json +0 -0
- /package/starter/{cloudrun-graphql → api/graphql}/codegen.yml +0 -0
- /package/starter/{cloudrun-graphql → api/graphql}/src/view/controller.ts +0 -0
- /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/resolvers/greeting.resolver.ts +0 -0
- /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/resolvers.ts +0 -0
- /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/schema/schema.graphql +0 -0
- /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/schema.ts +0 -0
- /package/starter/{cloudrun → api/rest}/src/domain/health-check.service.ts +0 -0
- /package/starter/{cloudrun → api/rest}/src/view/cli/openapi/generate.ts +0 -0
- /package/starter/{cloudrun/src/view/rest/controller → api/rest/src/view/rest/controllers}/health-check.controller.ts +0 -0
- /package/starter/{cloudrun → api/rest}/src/view/rest/middleware/context-middleware.ts +0 -0
- /package/starter/{cloudrun → api/rest}/src/view/rest/middleware/request-logger.ts +0 -0
- /package/starter/{cloudrun → api/rest}/src/view/rest/request.d.ts +0 -0
- /package/starter/{cloudrun → api/rest}/src/view/rest/spec/openapi.yml +0 -0
- /package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose-entrypoint.sh +0 -0
- /package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose.ci.yml +0 -0
- /package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose.local.yml +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { Merger } from './Merger.js'
|
|
2
|
+
import * as ts from 'typescript'
|
|
3
|
+
import { Files } from '../Files.js'
|
|
4
|
+
|
|
5
|
+
export class ContainerMerger extends Merger {
|
|
6
|
+
async merge(originDir: string): Promise<string> {
|
|
7
|
+
const content = await this.getWhichExistsOrNull(originDir)
|
|
8
|
+
if (content) {
|
|
9
|
+
return content
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { originPath, destPath } = this.getPaths(originDir)
|
|
13
|
+
|
|
14
|
+
const [originContainer, destContainer] = await Promise.all([
|
|
15
|
+
Files.readUtf8File(originPath),
|
|
16
|
+
Files.readUtf8File(destPath),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
const originAst = this.parseFile(originContainer)
|
|
20
|
+
const destAst = this.parseFile(destContainer)
|
|
21
|
+
|
|
22
|
+
const mergedImports = this.mergeImports(originAst, destAst)
|
|
23
|
+
const mergedInterface = this.mergeInterface(originAst, destAst)
|
|
24
|
+
const mergedFunction = this.mergeFunction(originAst, destAst)
|
|
25
|
+
|
|
26
|
+
return `${mergedImports}\n\n${mergedInterface}\n\nexport type ContainerFactory = () => Promise<Container>\n\n${mergedFunction}\n`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected parseFile(content: string): ts.SourceFile {
|
|
30
|
+
return ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected mergeImports(
|
|
34
|
+
originAst: ts.SourceFile,
|
|
35
|
+
destAst: ts.SourceFile
|
|
36
|
+
): string {
|
|
37
|
+
const imports: string[] = []
|
|
38
|
+
|
|
39
|
+
const addImports = (node: ts.Node) => {
|
|
40
|
+
if (!ts.isImportDeclaration(node)) {
|
|
41
|
+
node.forEachChild(addImports)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const importText = node.getText()
|
|
46
|
+
if (!imports.includes(importText)) {
|
|
47
|
+
imports.push(importText)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
node.forEachChild(addImports)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
addImports(originAst)
|
|
54
|
+
addImports(destAst)
|
|
55
|
+
|
|
56
|
+
return imports.join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected mergeInterface(
|
|
60
|
+
originAst: ts.SourceFile,
|
|
61
|
+
destAst: ts.SourceFile
|
|
62
|
+
): string {
|
|
63
|
+
const properties: string[] = []
|
|
64
|
+
|
|
65
|
+
const extractProperties = (node: ts.Node) => {
|
|
66
|
+
if (!ts.isInterfaceDeclaration(node) || node.name.text !== 'Container') {
|
|
67
|
+
node.forEachChild(extractProperties)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
node.members.forEach(member => {
|
|
72
|
+
if (!ts.isPropertySignature(member)) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const propText = member.getText().trim()
|
|
77
|
+
if (!properties.includes(propText)) {
|
|
78
|
+
properties.push(propText)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
node.forEachChild(extractProperties)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
extractProperties(originAst)
|
|
86
|
+
extractProperties(destAst)
|
|
87
|
+
|
|
88
|
+
return `export interface Container {\n ${properties.join('\n ')}\n}`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
protected mergeFunction(
|
|
92
|
+
originAst: ts.SourceFile,
|
|
93
|
+
destAst: ts.SourceFile
|
|
94
|
+
): string {
|
|
95
|
+
const originFunctionBody = this.extractFunctionBody(originAst)
|
|
96
|
+
const destFunctionBody = this.extractFunctionBody(destAst)
|
|
97
|
+
|
|
98
|
+
const allFunctionContent = [
|
|
99
|
+
...originFunctionBody.content,
|
|
100
|
+
...destFunctionBody.content,
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
const mergedReturnProps = this.mergeReturnProperties(
|
|
104
|
+
originFunctionBody.returnProps,
|
|
105
|
+
destFunctionBody.returnProps
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
let body = ''
|
|
109
|
+
if (allFunctionContent.length > 0) {
|
|
110
|
+
body += allFunctionContent.join('\n') + '\n\n'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
body += ` return {\n ${mergedReturnProps.join(',\n ')}\n }`
|
|
114
|
+
|
|
115
|
+
return `export const createContainer = async (): Promise<Container> => {\n${body}\n}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected mergeReturnProperties(
|
|
119
|
+
originProps: ts.ObjectLiteralElementLike[],
|
|
120
|
+
destProps: ts.ObjectLiteralElementLike[]
|
|
121
|
+
): string[] {
|
|
122
|
+
const mergedProps = new Map<string, ts.ObjectLiteralElementLike>()
|
|
123
|
+
|
|
124
|
+
originProps.forEach(prop => {
|
|
125
|
+
const propName = this.getPropertyName(prop)
|
|
126
|
+
if (propName) {
|
|
127
|
+
mergedProps.set(propName, prop)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
destProps.forEach(prop => {
|
|
132
|
+
const propName = this.getPropertyName(prop)
|
|
133
|
+
if (propName) {
|
|
134
|
+
mergedProps.set(propName, prop)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return Array.from(mergedProps.values()).map(prop => {
|
|
139
|
+
const propText = prop.getText()
|
|
140
|
+
if (
|
|
141
|
+
ts.isPropertyAssignment(prop) &&
|
|
142
|
+
ts.isObjectLiteralExpression(prop.initializer)
|
|
143
|
+
) {
|
|
144
|
+
return this.formatNestedObject(propText)
|
|
145
|
+
}
|
|
146
|
+
return propText
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected getPropertyName(prop: ts.ObjectLiteralElementLike): string | null {
|
|
151
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
152
|
+
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
|
|
153
|
+
return prop.name.text
|
|
154
|
+
}
|
|
155
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
156
|
+
return prop.name.text
|
|
157
|
+
}
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected formatNestedObject(propText: string): string {
|
|
162
|
+
const lines = propText.split('\n')
|
|
163
|
+
return lines
|
|
164
|
+
.map((line, index) => {
|
|
165
|
+
if (index === 0) {
|
|
166
|
+
return line
|
|
167
|
+
}
|
|
168
|
+
return ' ' + line
|
|
169
|
+
})
|
|
170
|
+
.join('\n')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
protected extractFunctionBody(ast: ts.SourceFile): {
|
|
174
|
+
content: string[]
|
|
175
|
+
returnProps: ts.ObjectLiteralElementLike[]
|
|
176
|
+
} {
|
|
177
|
+
const content: string[] = []
|
|
178
|
+
const returnProps: ts.ObjectLiteralElementLike[] = []
|
|
179
|
+
|
|
180
|
+
const visit = (node: ts.Node) => {
|
|
181
|
+
if (!this.isCreateContainer(node)) {
|
|
182
|
+
ts.forEachChild(node, visit)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!node.initializer || !ts.isArrowFunction(node.initializer)) {
|
|
187
|
+
ts.forEachChild(node, visit)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const body = node.initializer.body
|
|
192
|
+
if (!ts.isBlock(body)) {
|
|
193
|
+
ts.forEachChild(node, visit)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
body.statements.forEach(statement => {
|
|
198
|
+
if (this.isContentStatement(statement)) {
|
|
199
|
+
content.push(statement.getText().trim())
|
|
200
|
+
} else if (this.isReturnWithObject(statement)) {
|
|
201
|
+
// Extract the actual property nodes, not just their names
|
|
202
|
+
statement.expression.properties.forEach(prop => {
|
|
203
|
+
returnProps.push(prop)
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
ts.forEachChild(node, visit)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
visit(ast)
|
|
212
|
+
return { content, returnProps }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
protected isCreateContainer(node: ts.Node): node is ts.VariableDeclaration {
|
|
216
|
+
return (
|
|
217
|
+
ts.isVariableDeclaration(node) &&
|
|
218
|
+
node.name.getText() === 'createContainer'
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
protected isContentStatement(statement: ts.Statement): boolean {
|
|
223
|
+
return (
|
|
224
|
+
ts.isVariableStatement(statement) ||
|
|
225
|
+
(ts.isExpressionStatement(statement) &&
|
|
226
|
+
ts.isCallExpression(statement.expression))
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
protected isReturnWithObject(
|
|
231
|
+
statement: ts.Statement
|
|
232
|
+
): statement is ts.ReturnStatement & {
|
|
233
|
+
expression: ts.ObjectLiteralExpression
|
|
234
|
+
} {
|
|
235
|
+
return (
|
|
236
|
+
ts.isReturnStatement(statement) &&
|
|
237
|
+
!!statement.expression &&
|
|
238
|
+
ts.isObjectLiteralExpression(statement.expression)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Merger } from './Merger.js'
|
|
2
|
+
import { Files } from '../Files.js'
|
|
3
|
+
|
|
4
|
+
export class EnvJsoncMerger extends Merger {
|
|
5
|
+
async merge(originDir: string): Promise<string> {
|
|
6
|
+
const content = await this.getWhichExistsOrNull(originDir)
|
|
7
|
+
if (content) {
|
|
8
|
+
return content
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { originPath, destPath } = this.getPaths(originDir)
|
|
12
|
+
|
|
13
|
+
const [destEnvConfig, originEnvConfig] = await Promise.all([
|
|
14
|
+
Files.readUtf8File(destPath),
|
|
15
|
+
Files.readUtf8File(originPath),
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
const originWithoutOpenBracket = originEnvConfig.replace('{\n', '')
|
|
19
|
+
|
|
20
|
+
return destEnvConfig
|
|
21
|
+
.replace(',\n}\n', '\n}\n')
|
|
22
|
+
.replace('\n}\n', `,\n${originWithoutOpenBracket}`)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { Files } from '../Files.js'
|
|
3
|
+
|
|
4
|
+
export abstract class Merger {
|
|
5
|
+
abstract merge(originDir: string): Promise<string>
|
|
6
|
+
|
|
7
|
+
protected destPath: string
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
protected readonly destDir: string,
|
|
11
|
+
protected readonly pathToFile: string
|
|
12
|
+
) {
|
|
13
|
+
this.destPath = path.join(this.destDir, this.pathToFile)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public getDestPath() {
|
|
17
|
+
return this.destPath
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected getPaths(originDir: string) {
|
|
21
|
+
return {
|
|
22
|
+
originPath: path.join(originDir, this.pathToFile),
|
|
23
|
+
destPath: this.getDestPath(),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected async getWhichExistsOrNull(
|
|
28
|
+
originDir: string
|
|
29
|
+
): Promise<string | null> {
|
|
30
|
+
const { originPath, destPath } = this.getPaths(originDir)
|
|
31
|
+
|
|
32
|
+
const [originExists, destExists] = await Promise.all([
|
|
33
|
+
Files.exists(originPath),
|
|
34
|
+
Files.exists(destPath),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
if (!originExists && destExists) {
|
|
38
|
+
return Files.readUtf8File(destPath)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!destExists && originExists) {
|
|
42
|
+
return Files.readUtf8File(originPath)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!originExists && !destExists) {
|
|
46
|
+
throw new Error(`No file found to merge: ${this.pathToFile}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Merger } from './Merger.js'
|
|
2
|
+
import { Files } from '../Files.js'
|
|
3
|
+
|
|
4
|
+
export class PackageJsonMerger extends Merger {
|
|
5
|
+
constructor(
|
|
6
|
+
private readonly projectName: string,
|
|
7
|
+
destDir: string,
|
|
8
|
+
pathToFile: string
|
|
9
|
+
) {
|
|
10
|
+
super(destDir, pathToFile)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async merge(originDir: string): Promise<string> {
|
|
14
|
+
const content = await this.getWhichExistsOrNull(originDir)
|
|
15
|
+
if (content) {
|
|
16
|
+
return content
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { originPath, destPath } = this.getPaths(originDir)
|
|
20
|
+
|
|
21
|
+
const [destPckgJson, starterPckgJson] = await Promise.all([
|
|
22
|
+
Files.readUtf8File(destPath),
|
|
23
|
+
Files.readUtf8File(originPath),
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
const destPckgJsonObj = JSON.parse(destPckgJson)
|
|
27
|
+
const starterPckgJsonObj = JSON.parse(starterPckgJson)
|
|
28
|
+
|
|
29
|
+
destPckgJsonObj.name = this.projectName
|
|
30
|
+
destPckgJsonObj.scripts = {
|
|
31
|
+
...destPckgJsonObj.scripts,
|
|
32
|
+
...starterPckgJsonObj.scripts,
|
|
33
|
+
}
|
|
34
|
+
destPckgJsonObj.dependencies = {
|
|
35
|
+
...destPckgJsonObj.dependencies,
|
|
36
|
+
...starterPckgJsonObj.dependencies,
|
|
37
|
+
}
|
|
38
|
+
destPckgJsonObj.devDependencies = {
|
|
39
|
+
...destPckgJsonObj.devDependencies,
|
|
40
|
+
...starterPckgJsonObj.devDependencies,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return JSON.stringify(destPckgJsonObj, null, 2)
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/Npm.ts
CHANGED
|
@@ -1,27 +1,72 @@
|
|
|
1
1
|
import * as childProcess from 'child_process'
|
|
2
|
-
import {
|
|
2
|
+
import { Logger } from './Logger.js'
|
|
3
3
|
import { Path } from './types.js'
|
|
4
4
|
|
|
5
|
+
export class NpmError extends Error {
|
|
6
|
+
constructor(
|
|
7
|
+
message: string,
|
|
8
|
+
public readonly code: number | null
|
|
9
|
+
) {
|
|
10
|
+
super(message)
|
|
11
|
+
this.name = 'NpmError'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
export class Npm {
|
|
16
|
+
protected readonly logger: Logger
|
|
6
17
|
public readonly dir: Path
|
|
7
|
-
|
|
18
|
+
|
|
19
|
+
constructor(settings?: { dir?: Path; logger?: Logger }) {
|
|
20
|
+
this.logger = settings?.logger ?? new Logger()
|
|
8
21
|
this.dir = settings?.dir as Path
|
|
9
22
|
}
|
|
23
|
+
|
|
24
|
+
protected spawn(
|
|
25
|
+
cmd: string,
|
|
26
|
+
args: ReadonlyArray<string>,
|
|
27
|
+
options: childProcess.SpawnOptions = {}
|
|
28
|
+
) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const cp = childProcess.spawn(cmd, args, options)
|
|
31
|
+
const error: string[] = []
|
|
32
|
+
const stdout: string[] = []
|
|
33
|
+
|
|
34
|
+
cp.stdout?.on('data', data => {
|
|
35
|
+
stdout.push(data.toString())
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
cp.on('error', e => {
|
|
39
|
+
error.push(e.toString())
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
cp.on('close', code => {
|
|
43
|
+
if (error.length || (code !== null && code > 0)) {
|
|
44
|
+
reject(
|
|
45
|
+
new NpmError(
|
|
46
|
+
error.length ? error.join('') : stdout.join(''),
|
|
47
|
+
code ?? null
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
} else {
|
|
51
|
+
resolve(undefined)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
10
57
|
public run(args: string[]) {
|
|
11
|
-
logger.
|
|
12
|
-
const
|
|
13
|
-
?
|
|
58
|
+
this.logger.debug(`> npm ${args.join(' ')}`)
|
|
59
|
+
const options: childProcess.SpawnOptions = this.dir
|
|
60
|
+
? {
|
|
14
61
|
cwd: this.dir,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
}
|
|
62
|
+
stdio: this.logger.enableDebug ? 'inherit' : 'pipe',
|
|
63
|
+
}
|
|
64
|
+
: { stdio: this.logger.enableDebug ? 'inherit' : 'pipe' }
|
|
65
|
+
|
|
66
|
+
return this.spawn('npm', args, options)
|
|
22
67
|
}
|
|
23
68
|
public init() {
|
|
24
|
-
this.run(['init', '--yes'])
|
|
69
|
+
return this.run(['init', '--yes'])
|
|
25
70
|
}
|
|
26
71
|
|
|
27
72
|
public i(module?: string) {
|
|
@@ -29,10 +74,10 @@ export class Npm {
|
|
|
29
74
|
return this.run(['i'])
|
|
30
75
|
}
|
|
31
76
|
const args = ['i', module]
|
|
32
|
-
this.run(args)
|
|
77
|
+
return this.run(args)
|
|
33
78
|
}
|
|
34
79
|
public iDev(module: string) {
|
|
35
80
|
const args = ['i', '-D', module]
|
|
36
|
-
this.run(args)
|
|
81
|
+
return this.run(args)
|
|
37
82
|
}
|
|
38
83
|
}
|
package/src/PackageJson.ts
CHANGED
|
@@ -2,19 +2,21 @@ import * as path from 'path'
|
|
|
2
2
|
import * as fs from 'fs'
|
|
3
3
|
import * as lodash from 'lodash-es'
|
|
4
4
|
import { Npm } from './Npm.js'
|
|
5
|
-
import { logger } from './Logger.js'
|
|
6
5
|
import { Path } from './types.js'
|
|
6
|
+
import { Logger } from './Logger.js'
|
|
7
7
|
|
|
8
8
|
export class PackageJson {
|
|
9
9
|
public readonly path: Path
|
|
10
10
|
protected npm: Npm
|
|
11
|
-
|
|
11
|
+
protected logger: Logger
|
|
12
|
+
constructor(npm: Npm, logger: Logger) {
|
|
12
13
|
let packagejsonPath = './package.json' as Path
|
|
13
14
|
if (npm.dir) {
|
|
14
15
|
packagejsonPath = path.normalize(`${npm.dir}/${packagejsonPath}`) as Path
|
|
15
16
|
}
|
|
16
17
|
this.path = packagejsonPath
|
|
17
18
|
this.npm = npm
|
|
19
|
+
this.logger = logger
|
|
18
20
|
}
|
|
19
21
|
public setType(type: 'module' | 'commonjs') {
|
|
20
22
|
this.mergeWith({
|
|
@@ -25,7 +27,7 @@ export class PackageJson {
|
|
|
25
27
|
return JSON.parse(fs.readFileSync(this.path, 'utf-8'))
|
|
26
28
|
}
|
|
27
29
|
public runScript(name: string) {
|
|
28
|
-
this.npm.run(['run', name])
|
|
30
|
+
return this.npm.run(['run', name])
|
|
29
31
|
}
|
|
30
32
|
public addNpmScript(name: string, command: string) {
|
|
31
33
|
this.mergeWith({
|
|
@@ -37,7 +39,7 @@ export class PackageJson {
|
|
|
37
39
|
// Updated package json using merge with given object
|
|
38
40
|
public mergeWith(partialWith: any) {
|
|
39
41
|
const json = lodash.merge(this.toJSON(), partialWith)
|
|
40
|
-
logger.
|
|
42
|
+
this.logger.debug(`> package.json updated ${JSON.stringify(partialWith)}`)
|
|
41
43
|
fs.writeFileSync(
|
|
42
44
|
path.join(this.path),
|
|
43
45
|
JSON.stringify(json, null, 2),
|
package/src/Starter.ts
CHANGED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import glob from 'fast-glob'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { Files } from './Files.js'
|
|
5
|
+
|
|
6
|
+
export interface StarterConfig {
|
|
7
|
+
module: string
|
|
8
|
+
name: string
|
|
9
|
+
id: string
|
|
10
|
+
prebuild?: string[]
|
|
11
|
+
replace?: string[]
|
|
12
|
+
merge?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LoadedStarter {
|
|
16
|
+
name: string
|
|
17
|
+
config: StarterConfig
|
|
18
|
+
|
|
19
|
+
path: string
|
|
20
|
+
configPath: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StarterModule {
|
|
24
|
+
name: string
|
|
25
|
+
starters: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class StarterLoader {
|
|
29
|
+
private static readonly starterPath: string = path.normalize(
|
|
30
|
+
path.join(import.meta.dirname, '..', 'starter')
|
|
31
|
+
)
|
|
32
|
+
private readonly starters: Map<string, LoadedStarter> = new Map()
|
|
33
|
+
private readonly modules: StarterModule[] = []
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
const configFiles = glob.sync(
|
|
37
|
+
`${StarterLoader.starterPath}/**/node-app.jsonc`
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
for (const configFile of configFiles) {
|
|
41
|
+
const config = StarterLoader.validateConfig(
|
|
42
|
+
configFile,
|
|
43
|
+
JSON.parse(fs.readFileSync(configFile, 'utf8'))
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const original = this.starters.get(config.id)
|
|
47
|
+
if (original) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Duplicate starter: ${config.name}\n` +
|
|
50
|
+
`> Starter 1: ${original.path}\n` +
|
|
51
|
+
`> Starter 2: ${path.dirname(configFile)}`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.starters.set(config.id, {
|
|
56
|
+
name: config.name,
|
|
57
|
+
config,
|
|
58
|
+
path: path.dirname(configFile),
|
|
59
|
+
configPath: configFile,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const module = this.modules.find(module => module.name === config.module)
|
|
63
|
+
if (module) {
|
|
64
|
+
module.starters.push(config.id)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
this.modules.push({
|
|
68
|
+
name: config.module,
|
|
69
|
+
starters: [config.id],
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.modules.sort((a, b) => a.name.localeCompare(b.name))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getOptions(): StarterModule[] {
|
|
77
|
+
return this.modules
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getStarter(id: string): LoadedStarter {
|
|
81
|
+
const starter = this.starters.get(id)
|
|
82
|
+
if (!starter) {
|
|
83
|
+
throw new Error(`Starter ${id} not found`)
|
|
84
|
+
}
|
|
85
|
+
return starter
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private static validateConfig(
|
|
89
|
+
configPath: string,
|
|
90
|
+
config: any
|
|
91
|
+
): StarterConfig {
|
|
92
|
+
if (!config.module) {
|
|
93
|
+
throw new Error(`Invalid config at ${configPath}: module key is required`)
|
|
94
|
+
}
|
|
95
|
+
if (!config.name) {
|
|
96
|
+
throw new Error(`Invalid config at ${configPath}: name key is required`)
|
|
97
|
+
}
|
|
98
|
+
if (!config.id) {
|
|
99
|
+
throw new Error(`Invalid config at ${configPath}: id key is required`)
|
|
100
|
+
}
|
|
101
|
+
if (!StarterLoader.isValidOptionalStringArray(config.prebuild)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Invalid config at ${configPath}: "prebuild" must be array of npm script names or empty`
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!StarterLoader.isValidOptionalStringArray(config.replace)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Invalid config at ${configPath}: "replace" must be array of files where strings should be replaced`
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
if (config.replace) {
|
|
113
|
+
const configDir = path.dirname(configPath)
|
|
114
|
+
|
|
115
|
+
const invalidReplace = config.replace.find((replace: string) => {
|
|
116
|
+
const replacePath = path.join(configDir, replace)
|
|
117
|
+
return (
|
|
118
|
+
!fs.existsSync(replacePath) ||
|
|
119
|
+
!Files.isInSameTree(configDir, path.join(configDir, replacePath))
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (invalidReplace) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Invalid config at ${configPath}: "replace" must be array of files in the project directory, got: ${invalidReplace}`
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const invalidKeys = Object.keys(config).filter(
|
|
131
|
+
key => !['module', 'name', 'prebuild', 'replace', 'id'].includes(key)
|
|
132
|
+
)
|
|
133
|
+
if (invalidKeys.length > 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Invalid config at ${configPath}: Unknown key(s): ${invalidKeys.join(', ')}`
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
return config
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
protected static isValidOptionalStringArray(array: any): array is string[] {
|
|
142
|
+
return (
|
|
143
|
+
!array ||
|
|
144
|
+
(Array.isArray(array) &&
|
|
145
|
+
array.every((item: any) => typeof item === 'string'))
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -2,9 +2,5 @@
|
|
|
2
2
|
// Logging level, see https://github.com/pinojs/pino/blob/master/docs/api.md#logger-level
|
|
3
3
|
"LOGGER_DEFAULT_LEVEL": "debug",
|
|
4
4
|
// Enable/disable logging object multiline formatted logging https://github.com/pinojs/pino/blob/master/docs/api.md#prettyprint-boolean--object
|
|
5
|
-
"LOGGER_PRETTY": false
|
|
6
|
-
// API server listening port.
|
|
7
|
-
"SERVER_PORT": 3000,
|
|
8
|
-
// Boolean to remove sensitive info from http error responses
|
|
9
|
-
"ENABLE_PRODUCTION_HTTP_ERROR_RESPONSES": false
|
|
5
|
+
"LOGGER_PRETTY": false
|
|
10
6
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
|
|
1
2
|
module.exports = {
|
|
2
3
|
...require('@ackee/styleguide-backend-config/eslint'),
|
|
3
4
|
root: true,
|
|
4
|
-
ignorePatterns: ['dist', '
|
|
5
|
+
ignorePatterns: ['dist', 'docs', 'knexfile.ts'],
|
|
5
6
|
parserOptions: {
|
|
6
7
|
project: '.eslint.tsconfig.json',
|
|
7
8
|
},
|
|
8
|
-
}
|
|
9
|
+
}
|