@ackee/create-node-app 1.0.1 → 2.0.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.
Files changed (149) hide show
  1. package/AUTHORS +2 -1
  2. package/README.md +26 -18
  3. package/docs/development.md +42 -0
  4. package/lib/Bootstrap.js +106 -65
  5. package/lib/Bootstrap.js.map +1 -1
  6. package/lib/Builder.js +111 -0
  7. package/lib/Builder.js.map +1 -0
  8. package/lib/Files.js +21 -0
  9. package/lib/Files.js.map +1 -0
  10. package/lib/Logger.js +26 -8
  11. package/lib/Logger.js.map +1 -1
  12. package/lib/Mergers/ConfigMerger.js +22 -0
  13. package/lib/Mergers/ConfigMerger.js.map +1 -0
  14. package/lib/Mergers/ContainerMerger.js +172 -0
  15. package/lib/Mergers/ContainerMerger.js.map +1 -0
  16. package/lib/Mergers/EnvJsoncMerger.js +20 -0
  17. package/lib/Mergers/EnvJsoncMerger.js.map +1 -0
  18. package/lib/Mergers/Merger.js +36 -0
  19. package/lib/Mergers/Merger.js.map +1 -0
  20. package/lib/Mergers/PackageJsonMerger.js +36 -0
  21. package/lib/Mergers/PackageJsonMerger.js.map +1 -0
  22. package/lib/Npm.js +40 -12
  23. package/lib/Npm.js.map +1 -1
  24. package/lib/PackageJson.js +4 -4
  25. package/lib/PackageJson.js.map +1 -1
  26. package/lib/StarterLoader.js +86 -0
  27. package/lib/StarterLoader.js.map +1 -0
  28. package/package.json +8 -5
  29. package/src/Bootstrap.ts +123 -82
  30. package/src/Builder.ts +172 -0
  31. package/src/Files.ts +22 -0
  32. package/src/Logger.ts +26 -7
  33. package/src/Mergers/ConfigMerger.ts +28 -0
  34. package/src/Mergers/ContainerMerger.ts +241 -0
  35. package/src/Mergers/EnvJsoncMerger.ts +24 -0
  36. package/src/Mergers/Merger.ts +51 -0
  37. package/src/Mergers/PackageJsonMerger.ts +45 -0
  38. package/src/Npm.ts +60 -15
  39. package/src/PackageJson.ts +6 -4
  40. package/src/Starter.ts +2 -2
  41. package/src/StarterLoader.ts +148 -0
  42. package/starter/{cloudrun → _base}/.env.jsonc +1 -5
  43. package/starter/{cloudrun → _base}/.eslintrc.cjs +3 -2
  44. package/starter/_base/README.md +53 -0
  45. package/starter/_base/package.json +45 -0
  46. package/starter/{cloudrun → _base}/src/adapters/pino.logger.ts +1 -1
  47. package/starter/_base/src/config.ts +16 -0
  48. package/starter/{cloudrun-graphql → _base}/src/container.ts +3 -1
  49. package/starter/_base/src/index.ts +14 -0
  50. package/starter/{cloudrun → _base}/src/view/cli/README.md +2 -6
  51. package/starter/api/graphql/.env.jsonc +8 -0
  52. package/starter/{cloudrun-graphql → api/graphql}/.eslintrc.cjs +4 -5
  53. package/starter/api/graphql/node-app.jsonc +6 -0
  54. package/starter/api/graphql/package.json +36 -0
  55. package/starter/{cloudrun-graphql → api/graphql}/src/config.ts +0 -4
  56. package/starter/api/graphql/src/index.ts +11 -0
  57. package/starter/{cloudrun-graphql → api/graphql}/src/test/helloWorld.test.ts +14 -3
  58. package/starter/api/graphql/src/view/graphql/context-factory.ts +13 -0
  59. package/starter/{cloudrun-graphql → api/graphql}/src/view/server.ts +16 -6
  60. package/starter/api/rest/.env.jsonc +6 -0
  61. package/starter/api/rest/.eslintrc.cjs +8 -0
  62. package/starter/api/rest/node-app.jsonc +6 -0
  63. package/starter/api/rest/package.json +25 -0
  64. package/starter/{cloudrun → api/rest}/src/config.ts +0 -5
  65. package/starter/api/rest/src/container.ts +13 -0
  66. package/starter/{cloudrun → api/rest}/src/index.ts +4 -4
  67. package/starter/{cloudrun → api/rest}/src/test/health-check.test.ts +3 -5
  68. package/starter/{cloudrun → api/rest}/src/test/util/openapi-test.util.ts +3 -3
  69. package/starter/{cloudrun → api/rest}/src/view/rest/middleware/error-handler.ts +1 -1
  70. package/starter/{cloudrun → api/rest}/src/view/rest/routes.ts +1 -1
  71. package/starter/{cloudrun → api/rest}/src/view/rest/util/openapi.util.ts +19 -19
  72. package/starter/{cloudrun → api/rest}/src/view/server.ts +6 -4
  73. package/starter/infra/postgresql-knex/.env.jsonc +5 -0
  74. package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose.yml +1 -1
  75. package/starter/infra/postgresql-knex/knexfile.ts +16 -0
  76. package/starter/infra/postgresql-knex/node-app.jsonc +6 -0
  77. package/starter/infra/postgresql-knex/package.json +13 -0
  78. package/starter/infra/postgresql-knex/src/adapters/knex.database.test.ts +21 -0
  79. package/starter/infra/postgresql-knex/src/adapters/knex.database.ts +14 -0
  80. package/starter/infra/postgresql-knex/src/adapters/repositories/migration.repository.ts +24 -0
  81. package/starter/infra/postgresql-knex/src/config.ts +14 -0
  82. package/starter/infra/postgresql-knex/src/container.ts +23 -0
  83. package/starter/infra/postgresql-knex/src/db/migration.template.ts +4 -0
  84. package/starter/infra/postgresql-knex/src/db/migrations/.gitkeep +0 -0
  85. package/starter/infra/postgresql-knex/src/db/seed.template.ts +3 -0
  86. package/starter/infra/postgresql-knex/src/db/seeds/.gitkeep +0 -0
  87. package/starter/infra/postgresql-knex/src/domain/ports/database.d.ts +4 -0
  88. package/starter/infra/postgresql-knex/src/domain/ports/repositories/migration.repository.d.ts +9 -0
  89. package/starter/infra/postgresql-knex/src/test/setup.ts +16 -0
  90. package/starter/{shared → pipeline/cloudrun-gitlab}/.gitlab-ci.yml +15 -6
  91. package/starter/pipeline/cloudrun-gitlab/node-app.jsonc +6 -0
  92. package/tsconfig.tsbuildinfo +1 -1
  93. package/lib/Toolbelt.js +0 -102
  94. package/lib/Toolbelt.js.map +0 -1
  95. package/lib/cloudrun/CloudRunStarter.js +0 -127
  96. package/lib/cloudrun/CloudRunStarter.js.map +0 -1
  97. package/lib/cloudrun-graphql/GraphQLStarter.js +0 -118
  98. package/lib/cloudrun-graphql/GraphQLStarter.js.map +0 -1
  99. package/src/Toolbelt.ts +0 -132
  100. package/src/cloudrun/CloudRunStarter.ts +0 -182
  101. package/src/cloudrun-graphql/GraphQLStarter.ts +0 -182
  102. package/starter/cloudrun/README.md +0 -69
  103. package/starter/cloudrun/src/container.ts +0 -18
  104. package/starter/cloudrun/src/context.ts +0 -39
  105. package/starter/cloudrun/src/domain/errors/codes.ts +0 -9
  106. package/starter/cloudrun/src/domain/errors/errors.ts +0 -25
  107. package/starter/cloudrun/src/domain/ports/logger.d.ts +0 -21
  108. package/starter/cloudrun-graphql/.env.jsonc +0 -12
  109. package/starter/cloudrun-graphql/README.md +0 -53
  110. package/starter/cloudrun-graphql/src/adapters/pino.logger.ts +0 -44
  111. package/starter/cloudrun-graphql/src/index.ts +0 -11
  112. package/starter/shared/.gitignore_ +0 -5
  113. package/starter/shared/ci-branch-config/common.env +0 -7
  114. package/starter/shared/ci-branch-config/development.env +0 -7
  115. package/starter/shared/ci-branch-config/master.env +0 -7
  116. package/starter/shared/ci-branch-config/stage.env +0 -7
  117. package/starter/shared/docker-compose/docker-compose.override.yml +0 -5
  118. package/starter/shared/jest.config.js +0 -12
  119. /package/starter/{shared → _base}/.dockerignore +0 -0
  120. /package/starter/{cloudrun → _base}/.eslint.tsconfig.json +0 -0
  121. /package/starter/{shared → _base}/.mocha-junit-config.json +0 -0
  122. /package/starter/{shared → _base}/.mocharc.json +0 -0
  123. /package/starter/{shared → _base}/.nvmrc +0 -0
  124. /package/starter/{shared → _base}/Dockerfile +0 -0
  125. /package/starter/{shared → _base}/prettier.config.cjs +0 -0
  126. /package/starter/{cloudrun-graphql → _base}/src/context.ts +0 -0
  127. /package/starter/{cloudrun-graphql → _base}/src/domain/errors/codes.ts +0 -0
  128. /package/starter/{cloudrun-graphql → _base}/src/domain/errors/errors.ts +0 -0
  129. /package/starter/{cloudrun-graphql → _base}/src/domain/ports/logger.d.ts +0 -0
  130. /package/starter/{shared → _base}/src/test/setup.ts +0 -0
  131. /package/starter/{cloudrun → _base}/src/view/cli/cli.ts +0 -0
  132. /package/starter/{shared → _base}/tsconfig.json +0 -0
  133. /package/starter/{cloudrun-graphql → api/graphql}/.eslint.tsconfig.json +0 -0
  134. /package/starter/{cloudrun-graphql → api/graphql}/codegen.yml +0 -0
  135. /package/starter/{cloudrun-graphql → api/graphql}/src/view/controller.ts +0 -0
  136. /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/resolvers/greeting.resolver.ts +0 -0
  137. /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/resolvers.ts +0 -0
  138. /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/schema/schema.graphql +0 -0
  139. /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/schema.ts +0 -0
  140. /package/starter/{cloudrun → api/rest}/src/domain/health-check.service.ts +0 -0
  141. /package/starter/{cloudrun → api/rest}/src/view/cli/openapi/generate.ts +0 -0
  142. /package/starter/{cloudrun/src/view/rest/controller → api/rest/src/view/rest/controllers}/health-check.controller.ts +0 -0
  143. /package/starter/{cloudrun → api/rest}/src/view/rest/middleware/context-middleware.ts +0 -0
  144. /package/starter/{cloudrun → api/rest}/src/view/rest/middleware/request-logger.ts +0 -0
  145. /package/starter/{cloudrun → api/rest}/src/view/rest/request.d.ts +0 -0
  146. /package/starter/{cloudrun → api/rest}/src/view/rest/spec/openapi.yml +0 -0
  147. /package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose-entrypoint.sh +0 -0
  148. /package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose.ci.yml +0 -0
  149. /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 { logger } from './Logger.js'
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
- constructor(settings?: { dir?: Path }) {
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.info(`> npm ${args.join(' ')}`)
12
- const result = this.dir
13
- ? childProcess.spawnSync('npm', args, {
58
+ this.logger.debug(`> npm ${args.join(' ')}`)
59
+ const options: childProcess.SpawnOptions = this.dir
60
+ ? {
14
61
  cwd: this.dir,
15
- })
16
- : childProcess.spawnSync('npm', args)
17
- if ((result?.status ?? 0) > 0) {
18
- logger.info(
19
- `Failed npm command: npm ${args.join(' ')}. ${String(result.output)}`
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
  }
@@ -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
- constructor(npm: Npm) {
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.info(`> package.json updated ${JSON.stringify(partialWith)}`)
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
@@ -1,7 +1,7 @@
1
- import { Toolbelt } from './Toolbelt.js'
1
+ import { Builder } from './Builder.js'
2
2
 
3
3
  export interface Starter {
4
4
  readonly name: string
5
- setToolbelt(toolbelt: Toolbelt): Starter
5
+ setToolbelt(toolbelt: Builder): Starter
6
6
  install(): void
7
7
  }
@@ -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', 'src/openapi', 'docs'],
5
+ ignorePatterns: ['dist', 'docs', 'knexfile.ts'],
5
6
  parserOptions: {
6
7
  project: '.eslint.tsconfig.json',
7
8
  },
8
- }
9
+ }