@opencrvs/create-countryconfig 2.0.1 → 2.1.0-beta

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 +293 -103
  2. package/package.json +5 -2
package/index.js CHANGED
@@ -11,25 +11,37 @@
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 { execSync } = require('child_process')
17
+ const readline = require('readline/promises')
18
+ const degit = require('degit').default
17
19
 
18
- const COUNTRYCONFIG_REPO_URL =
19
- 'https://github.com/opencrvs/opencrvs-countryconfig.git'
20
- const INFRASTRUCTURE_REPO_URL = 'https://github.com/opencrvs/infrastructure.git'
20
+ const INFRASTRUCTURE_REPOSITORY = 'opencrvs/infrastructure'
21
+ const CORE_REPOSITORY = 'opencrvs/opencrvs-core'
22
+ const COUNTRYCONFIG_TEMPLATE_REPOSITORY_SUBPATH =
23
+ 'packages/countryconfig-template'
24
+
25
+ const CORE_REPO_URL = 'https://github.com/' + CORE_REPOSITORY + '.git'
26
+ const INFRASTRUCTURE_REPO_URL =
27
+ 'https://github.com/' + INFRASTRUCTURE_REPOSITORY + '.git'
21
28
 
22
29
  const { version } = JSON.parse(
23
30
  fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8')
24
31
  )
25
32
 
33
+ function joinValues(values, separator) {
34
+ return values
35
+ .filter((value) => !!value)
36
+ .join(separator)
37
+ .trim()
38
+ }
39
+
26
40
  function tagExists(repoUrl, tag) {
27
41
  try {
28
42
  execSync(
29
43
  'git ls-remote --exit-code --tags ' + repoUrl + ' refs/tags/' + tag,
30
- {
31
- stdio: 'pipe'
32
- }
44
+ { stdio: 'pipe' }
33
45
  )
34
46
  return true
35
47
  } catch (err) {
@@ -41,9 +53,7 @@ function branchExists(repoUrl, branch) {
41
53
  try {
42
54
  execSync(
43
55
  'git ls-remote --exit-code --heads ' + repoUrl + ' refs/heads/' + branch,
44
- {
45
- stdio: 'pipe'
46
- }
56
+ { stdio: 'pipe' }
47
57
  )
48
58
  return true
49
59
  } catch (err) {
@@ -80,9 +90,8 @@ function listReleaseTags(repoUrl) {
80
90
  function getLatestCommonReleaseTag() {
81
91
  const infrastructureTags = new Set(listReleaseTags(INFRASTRUCTURE_REPO_URL))
82
92
  return (
83
- listReleaseTags(COUNTRYCONFIG_REPO_URL).find((tag) =>
84
- infrastructureTags.has(tag)
85
- ) || null
93
+ listReleaseTags(CORE_REPO_URL).find((tag) => infrastructureTags.has(tag)) ||
94
+ null
86
95
  )
87
96
  }
88
97
 
@@ -111,7 +120,7 @@ function resolveRef() {
111
120
  if (version.includes('-')) {
112
121
  const releaseBranch = 'release/' + version.split('-')[0]
113
122
  if (
114
- branchExists(COUNTRYCONFIG_REPO_URL, releaseBranch) &&
123
+ branchExists(CORE_REPO_URL, releaseBranch) &&
115
124
  branchExists(INFRASTRUCTURE_REPO_URL, releaseBranch)
116
125
  ) {
117
126
  return releaseBranch
@@ -121,7 +130,7 @@ function resolveRef() {
121
130
 
122
131
  const tag = 'v' + version
123
132
  if (
124
- tagExists(COUNTRYCONFIG_REPO_URL, tag) &&
133
+ tagExists(CORE_REPO_URL, tag) &&
125
134
  tagExists(INFRASTRUCTURE_REPO_URL, tag)
126
135
  ) {
127
136
  return tag
@@ -141,126 +150,307 @@ function resolveRef() {
141
150
  }
142
151
 
143
152
  console.error(
144
- '\nError: no matching release tag was found in both the country config and ' +
153
+ '\nError: no matching release tag was found in both the core and ' +
145
154
  'infrastructure repositories.'
146
155
  )
147
156
  process.exit(1)
148
157
  }
149
158
 
150
- function cloneRepository(repoUrl, ref, targetDir) {
151
- execSync(
152
- 'git clone --depth 1 --branch ' + ref + ' ' + repoUrl + ' ' + targetDir,
153
- { stdio: 'inherit' }
154
- )
159
+ /**
160
+ * Clones a repository from GitHub to a target directory.
161
+ *
162
+ * @param {*} param0 repository - The repository to clone (e.g., 'opencrvs/opencrvs-core').
163
+ * @param {*} param0 repositorySubPath - The subpath within the repository to clone (optional). Otherwise the entire repository will be cloned.
164
+ * @param {*} param0 branch - The branch to clone (optional). Defaults to the default branch if not specified.
165
+ *
166
+ * @param {*} targetDir - The target directory where the repository will be cloned.
167
+ */
168
+ async function cloneRepository(
169
+ { repository, repositorySubPath, branch },
170
+ targetDir
171
+ ) {
172
+ const repositoryPath = joinValues([repository, repositorySubPath], '/')
173
+ const fullPath = joinValues([repositoryPath, branch], '#')
174
+
175
+ console.log(`Cloning repository from ${fullPath} to ${targetDir}...`)
176
+
177
+ const emitter = degit(fullPath, {
178
+ mode: 'git'
179
+ })
180
+
181
+ await emitter.clone(targetDir)
182
+ console.log(`Copied files from ${fullPath} to ${targetDir} succesfully.`)
155
183
  }
156
184
 
157
- const projectName = process.argv[2]
185
+ function ensureTargetDirectoryDoesNotExist(directoryName) {
186
+ const targetDirectoryPath = path.resolve(process.cwd(), directoryName)
158
187
 
159
- if (!projectName) {
160
- console.error(
161
- 'Please specify a project name:\n\n npm create @opencrvs/countryconfig <project-name>\n'
162
- )
163
- process.exit(1)
188
+ if (fs.existsSync(targetDirectoryPath)) {
189
+ console.error(
190
+ 'Error: Directory already exists in path "' + targetDirectoryPath + '".'
191
+ )
192
+ process.exit(1)
193
+ }
164
194
  }
165
195
 
166
- const countryconfigDirName = projectName + '-countryconfig'
167
- const infrastructureDirName = projectName + '-infrastructure'
196
+ /**
197
+ * Asks whether to enable telemetry. Defaults to yes, and answers yes without
198
+ * prompting when not attached to a terminal (e.g. non-interactive scaffolding).
199
+ */
200
+ async function promptEnableTelemetry() {
201
+ if (!process.stdin.isTTY) {
202
+ return true
203
+ }
168
204
 
169
- const countryconfigTargetDir = path.resolve(process.cwd(), countryconfigDirName)
170
- const infrastructureTargetDir = path.resolve(
171
- process.cwd(),
172
- infrastructureDirName
173
- )
205
+ const rl = readline.createInterface({
206
+ input: process.stdin,
207
+ output: process.stdout
208
+ })
209
+ try {
210
+ const answer = (
211
+ await rl.question(
212
+ '\nEnable anonymous usage telemetry to help improve OpenCRVS? Only ' +
213
+ 'aggregate metrics are shared — no personal or protected data. [Y/n] '
214
+ )
215
+ )
216
+ .trim()
217
+ .toLowerCase()
218
+ return answer === '' || answer === 'y' || answer === 'yes'
219
+ } finally {
220
+ rl.close()
221
+ }
222
+ }
174
223
 
175
- if (fs.existsSync(countryconfigTargetDir)) {
176
- console.error(
177
- 'Error: Directory "' + countryconfigDirName + '" already exists.'
224
+ /**
225
+ * Prompts for a single line of input, re-asking until `validate` accepts the
226
+ * trimmed answer. `validate` returns an error message string when the answer is
227
+ * invalid, or a falsy value when it is accepted. Exits when not attached to a
228
+ * terminal, since a mandatory value cannot be gathered non-interactively.
229
+ */
230
+ async function promptRequired(question, validate) {
231
+ if (!process.stdin.isTTY) {
232
+ console.error(
233
+ '\nError: interactive input is required to set the organisation name and country code.'
234
+ )
235
+ process.exit(1)
236
+ }
237
+
238
+ const rl = readline.createInterface({
239
+ input: process.stdin,
240
+ output: process.stdout
241
+ })
242
+ try {
243
+ while (true) {
244
+ const answer = (await rl.question(question)).trim()
245
+ const error = validate(answer)
246
+ if (!error) {
247
+ return answer
248
+ }
249
+ console.error(error)
250
+ }
251
+ } finally {
252
+ rl.close()
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Prompts for the organisation name reported with telemetry. Mandatory.
258
+ */
259
+ function promptOrganisation() {
260
+ return promptRequired('\nOrganisation running this instance: ', (answer) =>
261
+ answer === '' ? 'Please enter an organisation name.' : undefined
178
262
  )
179
- process.exit(1)
180
263
  }
181
264
 
182
- if (fs.existsSync(infrastructureTargetDir)) {
183
- console.error(
184
- 'Error: Directory "' + infrastructureDirName + '" already exists.'
265
+ /**
266
+ * Prompts for the alpha-3 ISO country code reported with telemetry, re-asking
267
+ * until a valid three-letter code is given. Mandatory.
268
+ */
269
+ async function promptCountryCode() {
270
+ const answer = await promptRequired(
271
+ '\nAlpha-3 ISO country code of this instance (e.g. "GBR"): ',
272
+ (value) =>
273
+ /^[A-Za-z]{3}$/.test(value)
274
+ ? undefined
275
+ : 'Please enter a three-letter alpha-3 ISO country code (e.g. "GBR").'
185
276
  )
186
- process.exit(1)
277
+ return answer.toUpperCase()
187
278
  }
188
279
 
189
- const ref = resolveRef()
280
+ /**
281
+ * Flips the `TELEMETRY_ENABLED` env var default in the cloned country config's
282
+ * environment to `true`. The template ships it defaulting to `false`.
283
+ */
284
+ function enableTelemetryInEnvironment(targetPath) {
285
+ const environmentPath = path.join(targetPath, 'src', 'environment.ts')
286
+ if (!fs.existsSync(environmentPath)) {
287
+ console.warn(
288
+ '\nWarning: could not find src/environment.ts; telemetry default not changed.'
289
+ )
290
+ return
291
+ }
190
292
 
191
- console.log(
192
- '\nScaffolding OpenCRVS country config in ./' + countryconfigDirName + '...\n'
193
- )
293
+ const original = fs.readFileSync(environmentPath, 'utf-8')
294
+ const updated = original.replace(
295
+ /(TELEMETRY_ENABLED:\s*bool\(\{[\s\S]*?default:\s*)false/,
296
+ '$1true'
297
+ )
194
298
 
195
- try {
196
- cloneRepository(COUNTRYCONFIG_REPO_URL, ref, countryconfigDirName)
197
- } catch (err) {
198
- console.error('Failed to clone the country config repository:', err.message)
199
- process.exit(1)
299
+ if (updated === original) {
300
+ console.warn(
301
+ '\nWarning: could not update the TELEMETRY_ENABLED default in src/environment.ts.'
302
+ )
303
+ return
304
+ }
305
+
306
+ fs.writeFileSync(environmentPath, updated)
307
+ console.log('\nTelemetry enabled (TELEMETRY_ENABLED now defaults to true).')
200
308
  }
201
309
 
202
- try {
203
- fs.rmSync(path.join(countryconfigTargetDir, '.git'), {
204
- recursive: true,
205
- force: true
206
- })
207
- } catch (err) {
208
- console.error(
209
- 'Failed to remove .git directory from country config:',
210
- err.message
310
+ /**
311
+ * Replaces the string `default` of an envalid `str({ ... })` field in the
312
+ * cloned country config's environment. Returns the updated source, or the
313
+ * original source (with a warning) when the field could not be located.
314
+ */
315
+ function setEnvironmentStringDefault(source, key, value) {
316
+ const pattern = new RegExp(
317
+ `(${key}:\\s*str\\(\\{[\\s\\S]*?default:\\s*)'[^']*'`
211
318
  )
212
- process.exit(1)
319
+ // Escape for a single-quoted TS string literal, and use a function replacer
320
+ // so `$` in the value is not treated as a replacement pattern.
321
+ const literal = "'" + value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"
322
+ const updated = source.replace(pattern, (_, prefix) => prefix + literal)
323
+
324
+ if (updated === source) {
325
+ console.warn(
326
+ `\nWarning: could not update the ${key} default in src/environment.ts.`
327
+ )
328
+ }
329
+
330
+ return updated
331
+ }
332
+
333
+ /**
334
+ * Writes the given organisation name and alpha-3 country code as the defaults
335
+ * for the `ORGANISATION` and `COUNTRY_CODE` env vars in the cloned country
336
+ * config's environment.
337
+ */
338
+ function setTelemetryIdentityInEnvironment(
339
+ targetPath,
340
+ { organisation, countryCode }
341
+ ) {
342
+ const environmentPath = path.join(targetPath, 'src', 'environment.ts')
343
+ if (!fs.existsSync(environmentPath)) {
344
+ console.warn(
345
+ '\nWarning: could not find src/environment.ts; organisation and country code defaults not changed.'
346
+ )
347
+ return
348
+ }
349
+
350
+ let source = fs.readFileSync(environmentPath, 'utf-8')
351
+ source = setEnvironmentStringDefault(source, 'ORGANISATION', organisation)
352
+ source = setEnvironmentStringDefault(source, 'COUNTRY_CODE', countryCode)
353
+ fs.writeFileSync(environmentPath, source)
354
+ }
355
+
356
+ function updatePackageJsonName(targetPath, newName) {
357
+ console.log('\nUpdating package.json with project name: ' + newName + '\n')
358
+
359
+ const pkgPath = path.join(targetPath, 'package.json')
360
+ if (fs.existsSync(pkgPath)) {
361
+ try {
362
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
363
+ pkg.name = newName
364
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
365
+ } catch (err) {
366
+ console.error('\nFailed to update package.json:', err.message)
367
+ process.exit(1)
368
+ }
369
+ } else {
370
+ console.warn(
371
+ '\nWarning: No package.json found in the targetPath: ' +
372
+ targetPath +
373
+ '. Project name was not updated.'
374
+ )
375
+ }
213
376
  }
214
377
 
215
- const pkgPath = path.join(countryconfigTargetDir, 'package.json')
216
- if (fs.existsSync(pkgPath)) {
378
+ async function main() {
379
+ const projectName = process.argv[2]
380
+
381
+ if (!projectName) {
382
+ console.error(
383
+ 'Please specify a project name:\n\n npm create @opencrvs/countryconfig <project-name>\n'
384
+ )
385
+ process.exit(1)
386
+ }
387
+
388
+ const countryconfigDirName = projectName + '-countryconfig'
389
+ const countryconfigTargetPath = path.resolve(
390
+ process.cwd(),
391
+ countryconfigDirName
392
+ )
393
+ const infrastructureDirName = projectName + '-infrastructure'
394
+ const infrastructureTargetPath = path.resolve(
395
+ process.cwd(),
396
+ infrastructureDirName
397
+ )
398
+
399
+ ensureTargetDirectoryDoesNotExist(countryconfigDirName)
400
+ ensureTargetDirectoryDoesNotExist(infrastructureDirName)
401
+
402
+ const ref = resolveRef()
403
+
404
+ // Gather all answers up front so the operator isn't interrupted mid-clone.
405
+ const organisation = await promptOrganisation()
406
+ const countryCode = await promptCountryCode()
407
+ const telemetryEnabled = await promptEnableTelemetry()
408
+
217
409
  try {
218
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
219
- pkg.name = countryconfigDirName
220
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
410
+ await cloneRepository(
411
+ {
412
+ repository: CORE_REPOSITORY,
413
+ repositorySubPath: COUNTRYCONFIG_TEMPLATE_REPOSITORY_SUBPATH,
414
+ branch: ref
415
+ },
416
+ countryconfigTargetPath
417
+ )
221
418
  } catch (err) {
222
- console.error('Failed to update package.json:', err.message)
419
+ console.error('\nFailed to clone country config template:', err.message)
223
420
  process.exit(1)
224
421
  }
225
- } else {
226
- console.warn(
227
- 'Warning: No package.json found in the cloned country config repository. Project name was not updated.'
228
- )
229
- }
230
422
 
231
- console.log(
232
- '\nScaffolding OpenCRVS infrastructure in ./' +
233
- infrastructureDirName +
234
- '...\n'
235
- )
423
+ updatePackageJsonName(countryconfigTargetPath, countryconfigDirName)
236
424
 
237
- try {
238
- cloneRepository(INFRASTRUCTURE_REPO_URL, ref, infrastructureDirName)
239
- } catch (err) {
240
- console.error('Failed to clone the infrastructure repository:', err.message)
241
- process.exit(1)
242
- }
425
+ try {
426
+ await cloneRepository(
427
+ { repository: INFRASTRUCTURE_REPOSITORY, branch: ref },
428
+ infrastructureTargetPath
429
+ )
430
+ } catch (err) {
431
+ console.error('Failed to clone the infrastructure repository:', err.message)
432
+ process.exit(1)
433
+ }
243
434
 
244
- try {
245
- fs.rmSync(path.join(infrastructureTargetDir, '.git'), {
246
- recursive: true,
247
- force: true
435
+ setTelemetryIdentityInEnvironment(countryconfigTargetPath, {
436
+ organisation,
437
+ countryCode
248
438
  })
249
- } catch (err) {
250
- console.error(
251
- 'Failed to remove .git directory from infrastructure:',
252
- err.message
253
- )
254
- process.exit(1)
439
+
440
+ if (telemetryEnabled) {
441
+ enableTelemetryInEnvironment(countryconfigTargetPath)
442
+ }
443
+
444
+ console.log('\nDone! Your project has been set up in two directories:\n')
445
+ console.log(' ./' + countryconfigDirName + ' -- country configuration')
446
+ console.log(' ./' + infrastructureDirName + ' -- server infrastructure\n')
447
+ console.log('To get started with the country config:\n')
448
+ console.log(' cd ' + countryconfigDirName)
449
+ console.log(' git init')
450
+ console.log(' tilt up\n')
451
+ console.log('To get started with the infrastructure:\n')
452
+ console.log(' cd ' + infrastructureDirName)
453
+ console.log(' git init\n')
255
454
  }
256
455
 
257
- console.log('\nDone! Your project has been set up in two directories:\n')
258
- console.log(' ./' + countryconfigDirName + ' -- country configuration')
259
- console.log(' ./' + infrastructureDirName + ' -- server infrastructure\n')
260
- console.log('To get started with the country config:\n')
261
- console.log(' cd ' + countryconfigDirName)
262
- console.log(' git init')
263
- console.log(' npm install\n')
264
- console.log('To get started with the infrastructure:\n')
265
- console.log(' cd ' + infrastructureDirName)
266
- console.log(' git init\n')
456
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencrvs/create-countryconfig",
3
- "version": "2.0.1",
3
+ "version": "2.1.0-beta",
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
  }