@opencrvs/create-countryconfig 2.1.0-rc.a434ca3 → 2.1.0-rc.a4ec5a4

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 (2) hide show
  1. package/index.js +284 -97
  2. package/package.json +5 -2
package/index.js CHANGED
@@ -11,128 +11,315 @@
11
11
  * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
12
12
  */
13
13
 
14
- const { execSync } = require('child_process')
15
14
  const path = require('path')
16
15
  const fs = require('fs')
16
+ const readline = require('readline/promises')
17
+ const degit = require('degit').default
17
18
 
18
- const INFRASTRUCTURE_REPO_URL = 'https://github.com/opencrvs/infrastructure.git'
19
- const COUNTRYCONFIG_TEMPLATE_PATH = path.resolve(
20
- __dirname,
21
- '../../countryconfig-template'
22
- )
19
+ const INFRASTRUCTURE_REPOSITORY = 'opencrvs/infrastructure'
20
+ const CORE_REPOSITORY = 'opencrvs/opencrvs-core'
21
+ const COUNTRYCONFIG_TEMPLATE_REPOSITORY_SUBPATH =
22
+ 'packages/countryconfig-template'
23
23
 
24
- const projectName = process.argv[2]
24
+ function joinValues(values, separator) {
25
+ return values
26
+ .filter((value) => !!value)
27
+ .join(separator)
28
+ .trim()
29
+ }
30
+
31
+ /**
32
+ * Clones a repository from GitHub to a target directory.
33
+ *
34
+ * @param {*} param0 repository - The repository to clone (e.g., 'opencrvs/opencrvs-core').
35
+ * @param {*} param0 repositorySubPath - The subpath within the repository to clone (optional). Otherwise the entire repository will be cloned.
36
+ * @param {*} param0 branch - The branch to clone (optional). Defaults to the default branch if not specified.
37
+ *
38
+ * @param {*} targetDir - The target directory where the repository will be cloned.
39
+ */
40
+ async function cloneRepository(
41
+ { repository, repositorySubPath, branch },
42
+ targetDir
43
+ ) {
44
+ const repositoryPath = joinValues([repository, repositorySubPath], '/')
45
+ const fullPath = joinValues([repositoryPath, branch], '#')
46
+
47
+ console.log(`Cloning repository from ${fullPath} to ${targetDir}...`)
48
+
49
+ const emitter = degit(fullPath, {
50
+ mode: 'git'
51
+ })
52
+
53
+ await emitter.clone(targetDir)
54
+ console.log(`Copied files from ${fullPath} to ${targetDir} succesfully.`)
55
+ }
56
+
57
+ function ensureTargetDirectoryDoesNotExist(directoryName) {
58
+ const targetDirectoryPath = path.resolve(process.cwd(), directoryName)
59
+
60
+ if (fs.existsSync(targetDirectoryPath)) {
61
+ console.error(
62
+ 'Error: Directory already exists in path "' + targetDirectoryPath + '".'
63
+ )
64
+ process.exit(1)
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Asks whether to enable telemetry. Defaults to yes, and answers yes without
70
+ * prompting when not attached to a terminal (e.g. non-interactive scaffolding).
71
+ */
72
+ async function promptEnableTelemetry() {
73
+ if (!process.stdin.isTTY) {
74
+ return true
75
+ }
76
+
77
+ const rl = readline.createInterface({
78
+ input: process.stdin,
79
+ output: process.stdout
80
+ })
81
+ try {
82
+ const answer = (
83
+ await rl.question(
84
+ '\nEnable anonymous usage telemetry to help improve OpenCRVS? Only ' +
85
+ 'aggregate metrics are shared — no personal or protected data. [Y/n] '
86
+ )
87
+ )
88
+ .trim()
89
+ .toLowerCase()
90
+ return answer === '' || answer === 'y' || answer === 'yes'
91
+ } finally {
92
+ rl.close()
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Prompts for a single line of input, re-asking until `validate` accepts the
98
+ * trimmed answer. `validate` returns an error message string when the answer is
99
+ * invalid, or a falsy value when it is accepted. Exits when not attached to a
100
+ * terminal, since a mandatory value cannot be gathered non-interactively.
101
+ */
102
+ async function promptRequired(question, validate) {
103
+ if (!process.stdin.isTTY) {
104
+ console.error(
105
+ '\nError: interactive input is required to set the organisation name and country code.'
106
+ )
107
+ process.exit(1)
108
+ }
25
109
 
26
- if (!projectName) {
27
- console.error(
28
- 'Please specify a project name:\n\n npm create @opencrvs/countryconfig <project-name>\n'
110
+ const rl = readline.createInterface({
111
+ input: process.stdin,
112
+ output: process.stdout
113
+ })
114
+ try {
115
+ while (true) {
116
+ const answer = (await rl.question(question)).trim()
117
+ const error = validate(answer)
118
+ if (!error) {
119
+ return answer
120
+ }
121
+ console.error(error)
122
+ }
123
+ } finally {
124
+ rl.close()
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Prompts for the organisation name reported with telemetry. Mandatory.
130
+ */
131
+ function promptOrganisation() {
132
+ return promptRequired('\nOrganisation running this instance: ', (answer) =>
133
+ answer === '' ? 'Please enter an organisation name.' : undefined
29
134
  )
30
- process.exit(1)
31
135
  }
32
136
 
33
- const countryconfigDirName = projectName + '-countryconfig'
34
- const infrastructureDirName = projectName + '-infrastructure'
137
+ /**
138
+ * Prompts for the alpha-3 ISO country code reported with telemetry, re-asking
139
+ * until a valid three-letter code is given. Mandatory.
140
+ */
141
+ async function promptCountryCode() {
142
+ const answer = await promptRequired(
143
+ '\nAlpha-3 ISO country code of this instance (e.g. "GBR"): ',
144
+ (value) =>
145
+ /^[A-Za-z]{3}$/.test(value)
146
+ ? undefined
147
+ : 'Please enter a three-letter alpha-3 ISO country code (e.g. "GBR").'
148
+ )
149
+ return answer.toUpperCase()
150
+ }
35
151
 
36
- const countryconfigTargetDir = path.resolve(process.cwd(), countryconfigDirName)
37
- const infrastructureTargetDir = path.resolve(
38
- process.cwd(),
39
- infrastructureDirName
40
- )
152
+ /**
153
+ * Flips the `TELEMETRY_ENABLED` env var default in the cloned country config's
154
+ * environment to `true`. The template ships it defaulting to `false`.
155
+ */
156
+ function enableTelemetryInEnvironment(targetPath) {
157
+ const environmentPath = path.join(targetPath, 'src', 'environment.ts')
158
+ if (!fs.existsSync(environmentPath)) {
159
+ console.warn(
160
+ '\nWarning: could not find src/environment.ts; telemetry default not changed.'
161
+ )
162
+ return
163
+ }
41
164
 
42
- if (fs.existsSync(countryconfigTargetDir)) {
43
- console.error(
44
- 'Error: Directory "' + countryconfigDirName + '" already exists.'
165
+ const original = fs.readFileSync(environmentPath, 'utf-8')
166
+ const updated = original.replace(
167
+ /(TELEMETRY_ENABLED:\s*bool\(\{[\s\S]*?default:\s*)false/,
168
+ '$1true'
45
169
  )
46
- process.exit(1)
170
+
171
+ if (updated === original) {
172
+ console.warn(
173
+ '\nWarning: could not update the TELEMETRY_ENABLED default in src/environment.ts.'
174
+ )
175
+ return
176
+ }
177
+
178
+ fs.writeFileSync(environmentPath, updated)
179
+ console.log('\nTelemetry enabled (TELEMETRY_ENABLED now defaults to true).')
47
180
  }
48
181
 
49
- if (fs.existsSync(infrastructureTargetDir)) {
50
- console.error(
51
- 'Error: Directory "' + infrastructureDirName + '" already exists.'
182
+ /**
183
+ * Replaces the string `default` of an envalid `str({ ... })` field in the
184
+ * cloned country config's environment. Returns the updated source, or the
185
+ * original source (with a warning) when the field could not be located.
186
+ */
187
+ function setEnvironmentStringDefault(source, key, value) {
188
+ const pattern = new RegExp(
189
+ `(${key}:\\s*str\\(\\{[\\s\\S]*?default:\\s*)'[^']*'`
52
190
  )
53
- process.exit(1)
191
+ // Escape for a single-quoted TS string literal, and use a function replacer
192
+ // so `$` in the value is not treated as a replacement pattern.
193
+ const literal = "'" + value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"
194
+ const updated = source.replace(pattern, (_, prefix) => prefix + literal)
195
+
196
+ if (updated === source) {
197
+ console.warn(
198
+ `\nWarning: could not update the ${key} default in src/environment.ts.`
199
+ )
200
+ }
201
+
202
+ return updated
54
203
  }
55
204
 
56
- console.log(
57
- '\nCopying OpenCRVS country config template to ./' +
58
- countryconfigDirName +
59
- '...\n'
60
- )
61
-
62
- console.log(
63
- 'cp -r ' + COUNTRYCONFIG_TEMPLATE_PATH + ' ' + countryconfigTargetDir
64
- )
65
- try {
66
- execSync(
67
- 'cp -r ' + COUNTRYCONFIG_TEMPLATE_PATH + ' ' + countryconfigTargetDir,
68
- {
69
- stdio: 'inherit'
205
+ /**
206
+ * Writes the given organisation name and alpha-3 country code as the defaults
207
+ * for the `ORGANISATION` and `COUNTRY_CODE` env vars in the cloned country
208
+ * config's environment.
209
+ */
210
+ function setTelemetryIdentityInEnvironment(
211
+ targetPath,
212
+ { organisation, countryCode }
213
+ ) {
214
+ const environmentPath = path.join(targetPath, 'src', 'environment.ts')
215
+ if (!fs.existsSync(environmentPath)) {
216
+ console.warn(
217
+ '\nWarning: could not find src/environment.ts; organisation and country code defaults not changed.'
218
+ )
219
+ return
220
+ }
221
+
222
+ let source = fs.readFileSync(environmentPath, 'utf-8')
223
+ source = setEnvironmentStringDefault(source, 'ORGANISATION', organisation)
224
+ source = setEnvironmentStringDefault(source, 'COUNTRY_CODE', countryCode)
225
+ fs.writeFileSync(environmentPath, source)
226
+ }
227
+
228
+ function updatePackageJsonName(targetPath, newName) {
229
+ console.log('\nUpdating package.json with project name: ' + newName + '\n')
230
+
231
+ const pkgPath = path.join(targetPath, 'package.json')
232
+ if (fs.existsSync(pkgPath)) {
233
+ try {
234
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
235
+ pkg.name = newName
236
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
237
+ } catch (err) {
238
+ console.error('\nFailed to update package.json:', err.message)
239
+ process.exit(1)
70
240
  }
71
- )
72
- console.log('\nCopying template completed successfully.\n')
73
- } catch (err) {
74
- console.error('\nFailed to copy country config template:', err.message)
75
- process.exit(1)
241
+ } else {
242
+ console.warn(
243
+ '\nWarning: No package.json found in the targetPath: ' +
244
+ targetPath +
245
+ '. Project name was not updated.'
246
+ )
247
+ }
76
248
  }
77
249
 
78
- console.log(
79
- '\nUpdating package.json with project name: ' + countryconfigDirName + '\n'
80
- )
81
- const pkgPath = path.join(countryconfigTargetDir, 'package.json')
82
- if (fs.existsSync(pkgPath)) {
250
+ async function main() {
251
+ const projectName = process.argv[2]
252
+
253
+ if (!projectName) {
254
+ console.error(
255
+ 'Please specify a project name:\n\n npm create @opencrvs/countryconfig <project-name>\n'
256
+ )
257
+ process.exit(1)
258
+ }
259
+
260
+ const countryconfigDirName = projectName + '-countryconfig'
261
+ const countryconfigTargetPath = path.resolve(
262
+ process.cwd(),
263
+ countryconfigDirName
264
+ )
265
+ const infrastructureDirName = projectName + '-infrastructure'
266
+ const infrastructureTargetPath = path.resolve(
267
+ process.cwd(),
268
+ infrastructureDirName
269
+ )
270
+
271
+ ensureTargetDirectoryDoesNotExist(countryconfigDirName)
272
+ ensureTargetDirectoryDoesNotExist(infrastructureDirName)
273
+
274
+ // Gather all answers up front so the operator isn't interrupted mid-clone.
275
+ const organisation = await promptOrganisation()
276
+ const countryCode = await promptCountryCode()
277
+ const telemetryEnabled = await promptEnableTelemetry()
278
+
83
279
  try {
84
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
85
- pkg.name = countryconfigDirName
86
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
280
+ await cloneRepository(
281
+ {
282
+ repository: CORE_REPOSITORY,
283
+ repositorySubPath: COUNTRYCONFIG_TEMPLATE_REPOSITORY_SUBPATH
284
+ },
285
+ countryconfigTargetPath
286
+ )
87
287
  } catch (err) {
88
- console.error('\nFailed to update package.json:', err.message)
288
+ console.error('\nFailed to clone country config template:', err.message)
89
289
  process.exit(1)
90
290
  }
91
- } else {
92
- console.warn(
93
- '\nWarning: No package.json found in the cloned country config repository. Project name was not updated.'
94
- )
95
- }
96
291
 
97
- console.log(
98
- '\nCloning OpenCRVS infrastructure repository from remote to ./' +
99
- infrastructureDirName +
100
- '...\n'
101
- )
102
-
103
- try {
104
- execSync(
105
- 'git clone --depth 1 ' +
106
- INFRASTRUCTURE_REPO_URL +
107
- ' ' +
108
- infrastructureDirName,
109
- { stdio: 'inherit' }
110
- )
111
- } catch (err) {
112
- console.error('Failed to clone the infrastructure repository:', err.message)
113
- process.exit(1)
114
- }
292
+ updatePackageJsonName(countryconfigTargetPath, countryconfigDirName)
115
293
 
116
- try {
117
- fs.rmSync(path.join(infrastructureTargetDir, '.git'), {
118
- recursive: true,
119
- force: true
294
+ try {
295
+ await cloneRepository(
296
+ { repository: INFRASTRUCTURE_REPOSITORY },
297
+ infrastructureTargetPath
298
+ )
299
+ } catch (err) {
300
+ console.error('Failed to clone the infrastructure repository:', err.message)
301
+ process.exit(1)
302
+ }
303
+
304
+ setTelemetryIdentityInEnvironment(countryconfigTargetPath, {
305
+ organisation,
306
+ countryCode
120
307
  })
121
- } catch (err) {
122
- console.error(
123
- '\nFailed to remove .git directory from infrastructure:',
124
- err.message
125
- )
126
- process.exit(1)
308
+
309
+ if (telemetryEnabled) {
310
+ enableTelemetryInEnvironment(countryconfigTargetPath)
311
+ }
312
+
313
+ console.log('\nDone! Your project has been set up in two directories:\n')
314
+ console.log(' ./' + countryconfigDirName + ' -- country configuration')
315
+ console.log(' ./' + infrastructureDirName + ' -- server infrastructure\n')
316
+ console.log('To get started with the country config:\n')
317
+ console.log(' cd ' + countryconfigDirName)
318
+ console.log(' git init')
319
+ console.log(' tilt up\n')
320
+ console.log('To get started with the infrastructure:\n')
321
+ console.log(' cd ' + infrastructureDirName)
322
+ console.log(' git init\n')
127
323
  }
128
324
 
129
- console.log('\nDone! Your project has been set up in two directories:\n')
130
- console.log(' ./' + countryconfigDirName + ' -- country configuration')
131
- console.log(' ./' + infrastructureDirName + ' -- server infrastructure\n')
132
- console.log('To get started with the country config:\n')
133
- console.log(' cd ' + countryconfigDirName)
134
- console.log(' git init')
135
- console.log(' tilt up\n')
136
- console.log('To get started with the infrastructure:\n')
137
- console.log(' cd ' + infrastructureDirName)
138
- console.log(' git init\n')
325
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencrvs/create-countryconfig",
3
- "version": "2.1.0-rc.a434ca3",
3
+ "version": "2.1.0-rc.a4ec5a4",
4
4
  "description": "Scaffold a new OpenCRVS country configuration",
5
5
  "bin": {
6
6
  "create-countryconfig": "./index.js"
@@ -17,5 +17,8 @@
17
17
  "countryconfig",
18
18
  "create"
19
19
  ],
20
- "license": "MPL-2.0"
20
+ "license": "MPL-2.0",
21
+ "dependencies": {
22
+ "degit": "^3.6.5"
23
+ }
21
24
  }