@thegetty/quire-cli 1.0.0-rc.9 → 1.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 (44) hide show
  1. package/CHANGELOG.md +174 -7
  2. package/README.md +15 -0
  3. package/bin/cli.js +13 -7
  4. package/package.json +19 -14
  5. package/patches/README.md +19 -0
  6. package/patches/install-npm-version+1.0.9.patch +12119 -0
  7. package/src/Command.js +14 -0
  8. package/src/commands/conf.js +43 -0
  9. package/src/commands/create.js +13 -9
  10. package/src/commands/epub.js +1 -2
  11. package/src/commands/info.js +15 -10
  12. package/src/commands/pdf.js +60 -10
  13. package/src/commands/validate.js +64 -0
  14. package/src/errors/validation/validation-error.js +13 -0
  15. package/src/errors/validation/yaml-duplicate-error.js +11 -0
  16. package/src/errors/validation/yaml-parse-error.js +11 -0
  17. package/src/errors/validation/yaml-validation-error.js +11 -0
  18. package/src/helpers/clean.js +5 -0
  19. package/src/helpers/is-empty.js +4 -0
  20. package/src/helpers/is-quire.js +4 -0
  21. package/src/helpers/os-utils.js +4 -0
  22. package/src/helpers/test-cwd.js +4 -0
  23. package/src/helpers/which.js +4 -0
  24. package/src/lib/11ty/api.js +1 -0
  25. package/src/lib/11ty/cli.js +29 -6
  26. package/src/lib/conf/README.md +104 -0
  27. package/src/lib/conf/config.js +35 -0
  28. package/src/lib/conf/defaults.js +37 -0
  29. package/src/lib/conf/migrations.js +11 -0
  30. package/src/lib/conf/schema.js +30 -0
  31. package/src/lib/pdf/README.md +11 -4
  32. package/src/lib/pdf/index.js +4 -4
  33. package/src/lib/pdf/paged.js +88 -9
  34. package/src/lib/pdf/pagedPlugin.js +49 -0
  35. package/src/lib/pdf/prince.js +79 -4
  36. package/src/lib/pdf/princePlugin.js +35 -0
  37. package/src/lib/pdf/split.js +61 -0
  38. package/src/lib/quire/index.js +67 -144
  39. package/src/main.js +21 -6
  40. package/src/packageConfig.js +15 -0
  41. package/src/validators/utils.js +98 -0
  42. package/src/validators/validate-yaml.js +38 -0
  43. package/src/lib/config/README.md +0 -3
  44. package/src/lib/config/index.js +0 -1
@@ -3,10 +3,10 @@ import { chdir, cwd } from 'node:process'
3
3
  import { execa, execaCommand } from 'execa'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { isEmpty } from '#helpers/is-empty.js'
6
+ import config from '#lib/conf/config.js'
6
7
  import fs from 'fs-extra'
7
- import git from '#src/lib/git/index.js'
8
- import inv from 'install-npm-version'
9
- import packageConfig from '#root/package.json' assert { type: 'json' }
8
+ import git from '#lib/git/index.js'
9
+ import packageConfig from '#src/packageConfig.js'
10
10
  import path from 'node:path'
11
11
  import semver from 'semver'
12
12
 
@@ -16,17 +16,19 @@ const __dirname = path.dirname(__filename)
16
16
  // Version install path is relative to process working directory
17
17
  const INSTALL_PATH = path.join('src', 'lib', 'quire', 'versions')
18
18
  const PACKAGE_NAME = '@thegetty/quire-11ty'
19
- const VERSION_FILE = '.quire'
19
+
20
+ const QUIRE_VERSION = config.get('quireVersion')
21
+ const VERSION_FILE = config.get('versionFile')
20
22
 
21
23
  /**
22
- * Return an absolute path to an installed `quire-11ty` version
24
+ * Return an absolute path to an installed quire-11ty version
23
25
  *
24
- * @return {String} path to installed `quire-11ty` version
26
+ * @return {String} path to installed quire-11ty version
25
27
  */
26
- function getPath(version='latest') {
27
- const absolutePath = path.relative('/', `${INSTALL_PATH}/${version}`)
28
+ function getPath(version=QUIRE_VERSION) {
29
+ const absolutePath = path.relative('/', path.join(INSTALL_PATH, version))
28
30
  if (!fs.existsSync(absolutePath)) {
29
- console.error(`[CLI:quire] \`quire-11ty@${version}\` is not installed`)
31
+ console.error(`[CLI:quire] quire-11ty@${version} is not installed`)
30
32
  return null
31
33
  }
32
34
  console.debug(`[CLI:quire] %s`, absolutePath)
@@ -34,7 +36,7 @@ function getPath(version='latest') {
34
36
  }
35
37
 
36
38
  /**
37
- * Read the required `quire-11ty` version for the project `.quire` file
39
+ * Read the required quire-11ty version for the quire version file
38
40
  *
39
41
  * @param {String} projectPath Absolute system path to the project root
40
42
  *
@@ -49,25 +51,18 @@ function getVersion(projectPath) {
49
51
  }
50
52
 
51
53
  /**
52
- * Read the required `quire-11ty` version from starter `package.json` `peerDependencies`
54
+ * Read required quire-11ty and starter versions from starter peerDependencies
53
55
  *
54
56
  * @param {String} projectPath Absolute system path to the project root
55
57
  *
56
- * @return {String} version Quire-11ty semantic version with caret or other
57
- * comparators trimmed off the beginning
58
- *
59
- * @TODO refactor `latest()` function to programmatically return a specific
60
- * version of `@thegetty/quire-11ty` from a semantic version string
61
- * (i.e `^1.0.0-pre-release.0` => `1.0.0-pre-release.2`) so this string-trimming
62
- * logic can be removed
58
+ * @return {Object}
59
+ * @property {String} quire11tyVersion Latest compatible Quire-11ty semantic version
60
+ * @property {String} starterVersion Starter project version defined in the starter package.json
63
61
  */
64
62
  async function getVersionsFromStarter(projectPath) {
65
63
  const projectPackageConfig = fs.readFileSync(path.join(projectPath, 'package.json'), { encoding:'utf8' })
66
64
  const { peerDependencies, version: starterVersion } = JSON.parse(projectPackageConfig)
67
- const quire11ty = peerDependencies[PACKAGE_NAME]
68
- const quire11tyVersion = quire11ty === 'latest'
69
- ? await latest()
70
- : quire11ty.substr(quire11ty.search(/\d/))
65
+ const quire11tyVersion = peerDependencies[PACKAGE_NAME]
71
66
  return { quire11tyVersion, starterVersion }
72
67
  }
73
68
 
@@ -95,11 +90,9 @@ async function initStarter (starter, projectPath, options) {
95
90
  return
96
91
  }
97
92
 
98
- starter = starter || 'https://github.com/thegetty/quire-starter-default'
99
-
100
93
  console.debug('[CLI:quire] init-starter',
101
- `\n project root: "${projectPath}"`,
102
- `\n starter: "${starter}"`
94
+ `\n project: ${path.join(__dirname, projectPath)}`,
95
+ `\n starter: ${starter}`
103
96
  )
104
97
 
105
98
  /**
@@ -112,20 +105,19 @@ async function initStarter (starter, projectPath, options) {
112
105
  .catch((error) => console.error('[CLI:error] ', error))
113
106
 
114
107
  /**
115
- * Determine `quire-11ty` version required by the starter project.
108
+ * Determine the quire-11ty version to use in the new project,
109
+ * from the quireVersion option or as required by the starter project.
116
110
  *
117
- * A version specified in `options.quireVersion` overrides the version in starter
118
- * project `package.json`.
111
+ * Uses 'latest' to get the latest semantic version compatible with version ranges
119
112
  */
120
113
  const { quire11tyVersion, starterVersion } = await getVersionsFromStarter(projectPath)
121
- const quireVersion = options.quireVersion || quire11tyVersion
122
-
114
+ const quireVersion = await latest(options.quireVersion || quire11tyVersion)
123
115
  setVersion(projectPath, quireVersion)
124
116
 
125
117
  /**
126
- * Write quire-11ty, cli, starter name and version to VERSION_FILE
118
+ * Write quire-11ty, quire-cli, starter versions to the version file
127
119
  */
128
- const versionInfo = {
120
+ const versionInfo = {
129
121
  cli: packageConfig.version,
130
122
  starter: `${starter}@${starterVersion}`,
131
123
  }
@@ -150,57 +142,9 @@ async function initStarter (starter, projectPath, options) {
150
142
  */
151
143
  const projectFiles = fs.readdirSync(projectPath)
152
144
  await git.init().add(projectFiles).commit('Initial Commit')
153
-
154
145
  return quireVersion
155
146
  }
156
147
 
157
- /**
158
- * Install `quire-11ty`, default to 'latest' version
159
- *
160
- * @TODO refactor this to be callable by the installInProject method
161
- *
162
- * @param {Object} options options passed from `quire new` command
163
- * @return {Promise}
164
- */
165
- async function install(options = {}) {
166
- const version = options.quireVersion || 'latest'
167
- console.debug(`[CLI:quire] installing quire-11ty@${version}`)
168
- const absoluteInstallPath = path.join(__dirname, 'versions')
169
- fs.ensureDirSync(absoluteInstallPath)
170
- /**
171
- * `Destination` is relative to `node_modules` of the working-directory
172
- * so we have included a relative path to parent directory in order to
173
- * install versions to a different local path.
174
- * @see https://github.com/scott-lin/install-npm-version
175
- */
176
- const installOptions = {
177
- Destination: path.join('..', version),
178
- Debug: false,
179
- Overwrite: options.force || options.overwrite || false,
180
- Verbosity: options.debug ? 'Debug' : 'Silent',
181
- WorkingDirectory: absoluteInstallPath
182
- }
183
- await inv.Install(`${PACKAGE_NAME}@${version}`, installOptions)
184
-
185
- // delete empty `node_modules` directory that `install-npm-version` creates
186
- const invNodeModulesDir = path.join(absoluteInstallPath, 'node_modules')
187
- if (fs.existsSync(invNodeModulesDir)) fs.rmdir(invNodeModulesDir)
188
-
189
- symlinkLatest()
190
-
191
- console.debug('[CLI:quire] installing dev dependencies')
192
- /**
193
- * Manually install necessary dev dependencies to run 11ty;
194
- * these must be `devDependencies` so that they are not bundled into
195
- * the final `_site` package when running `quire build`
196
- */
197
- const currentWorkingDirectory = cwd()
198
- const versionDir = path.join(absoluteInstallPath, version)
199
- chdir(versionDir)
200
- await execaCommand('npm cache clean --force')
201
- await execaCommand('npm install --save-dev')
202
- }
203
-
204
148
  /**
205
149
  * Install `quire-11ty` directly into a quire project
206
150
  *
@@ -211,9 +155,10 @@ async function install(options = {}) {
211
155
  * @param {Object} options options passed from `quire new` command
212
156
  * @return {Promise}
213
157
  */
214
- async function installInProject(projectPath, options = {}) {
215
- const { quirePath, quireVersion } = options
216
- const quire11tyPackage = fs.existsSync(quirePath) ? quirePath : `${PACKAGE_NAME}@${quireVersion}`
158
+ async function installInProject(projectPath, quireVersion, options = {}) {
159
+ const { quirePath } = options
160
+ const quire11tyPackage = `${PACKAGE_NAME}@${quireVersion}`
161
+
217
162
  console.debug(`[CLI:quire] installing ${quire11tyPackage} into ${projectPath}`)
218
163
 
219
164
  /**
@@ -228,27 +173,24 @@ async function installInProject(projectPath, options = {}) {
228
173
  .catch((error) => console.error('[CLI:error] ', error))
229
174
 
230
175
  const temp11tyDirectory = '.temp'
231
- /**
232
- * `Destination` is relative to `node_modules` of the working-directory
233
- * so we have included a relative path to parent directory in order to
234
- * install versions to a different local path.
235
- * @see https://github.com/scott-lin/install-npm-version
236
- */
237
- const installOptions = {
238
- Destination: path.join('..', temp11tyDirectory),
239
- Debug: false,
240
- Overwrite: options.force || options.overwrite || false,
241
- Verbosity: options.debug ? 'Debug' : 'Silent',
242
- WorkingDirectory: projectPath
243
- }
244
- await inv.Install(quire11tyPackage, installOptions)
176
+ const tempDir = path.join(projectPath,temp11tyDirectory)
177
+ fs.mkdirSync(tempDir)
178
+
179
+ // Copy if passed a path and it exists, otherwise attempt to download the tarball for this pathspec
180
+ if (fs.existsSync(quirePath)) {
181
+ fs.cpSync(quirePath, tempDir, {recursive: true})
182
+ } else {
183
+ await execaCommand(`npm pack ${ options.debug ? '--debug' : '--quiet' } --pack-destination ${tempDir} ${quire11tyPackage}`)
245
184
 
246
- // delete empty `node_modules` directory that `install-npm-version` creates
247
- const invNodeModulesDir = path.join(projectPath, 'node_modules')
248
- if (fs.existsSync(invNodeModulesDir)) fs.rmdir(invNodeModulesDir)
185
+ // Extract only the package dir from the tar bar and strip it from the extracted path
186
+ const tarballPath = path.join(tempDir, `thegetty-quire-11ty-${quireVersion}.tgz`)
187
+ await execaCommand(`tar -xzf ${tarballPath} -C ${tempDir} --strip-components=1 package/`)
249
188
 
250
- // Copy all files installed in `.temp` to projectPath
251
- fs.copySync(path.join(projectPath, '.temp'), projectPath)
189
+ fs.rmSync(tarballPath)
190
+ }
191
+
192
+ // Copy `.temp` to projectPath
193
+ fs.cpSync(tempDir, projectPath, {recursive: true})
252
194
 
253
195
  console.debug('[CLI:quire] installing dev dependencies into quire project')
254
196
  /**
@@ -256,17 +198,16 @@ async function installInProject(projectPath, options = {}) {
256
198
  * these must be `devDependencies` so that they are not bundled into
257
199
  * the final `_site` package when running `quire build`
258
200
  */
259
- await execaCommand('npm cache clean --force', { cwd: projectPath })
260
201
  try {
261
202
  await execaCommand('npm install --save-dev', { cwd: projectPath })
262
203
  } catch(error) {
263
204
  console.warn(`[CLI:error]`, error)
264
- fs.removeSync(projectPath)
205
+ fs.rmSync(projectPath, {recursive: true})
265
206
  return
266
207
  }
267
208
 
268
209
  const eleventyFilesToCommit = fs
269
- .readdirSync(path.join(projectPath, temp11tyDirectory))
210
+ .readdirSync(tempDir)
270
211
  .filter((filePath) => filePath !== 'node_modules')
271
212
 
272
213
  eleventyFilesToCommit.push('package-lock.json')
@@ -278,30 +219,32 @@ async function installInProject(projectPath, options = {}) {
278
219
  await git.add(eleventyFilesToCommit).commit('Adds `@thegetty/quire-11ty` files')
279
220
 
280
221
  // remove temporary 11ty install directory
281
- fs.removeSync(path.join(projectPath, temp11tyDirectory))
222
+ fs.rmSync(path.join(projectPath, temp11tyDirectory), {recursive: true})
282
223
  }
283
224
 
284
225
  /**
285
226
  * Retrieve latest published version of the `quire-11ty` package
286
-
287
- * @todo refactor to programmatically return a specific version
288
- * of `@thegetty/quire-11ty` from a semantic version string
289
- * (i.e `^1.0.0-pre-release.0` => `1.0.0-pre-release.2`)
290
- * so that the latest function may be used like:
291
- * await latest('^1.0.0-pre-release.0') => '1.0.0-pre-release.2'
292
- *
293
- * Nota bene: `npm view [<@scope>/]<name>[@<version>] version`
294
- * @see https://docs.npmjs.com/cli/v7/commands/npm-view
295
- * returns a list of versions that satisfy the `<version>` range specifier,
296
- * piping this to execa `stdout` we get only the last line of output.
297
- * @todo use [`parse-columns`](https://github.com/sindresorhus/parse-columns)
298
- * to parse the column formated list of versions returned by `npm view`
299
- *
227
+ * or the latest compatible version with the provided semantic version string
228
+ *
229
+ * @param {String} version A semantic version string, i.e `^1.0.0-pre-release.0`
230
+ *
300
231
  * @return {String} `quire-11ty@latest` semantic version string
301
232
  */
302
- async function latest() {
303
- const { stdout: quireVersion } =
304
- await execa('npm', ['view', PACKAGE_NAME, 'version'])
233
+ async function latest(version) {
234
+ let quireVersion;
235
+ if (!version || version === 'latest') {
236
+ const { stdout } =
237
+ await execa('npm', ['view', PACKAGE_NAME, 'version'])
238
+ quireVersion = stdout
239
+ } else {
240
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}`)
241
+ const json = await response.json()
242
+ const versions = Object.keys(json.versions)
243
+ quireVersion = semver.maxSatisfying(versions, version)
244
+ }
245
+ if (!quireVersion) {
246
+ throw new Error(`[CLI:quire] Sorry, we couldn't find a version of quire-11ty compatible with the version "${version}". You can set the quire-11ty version in the starter project's package.json or specify a version when running \`quire new\` with the \`--quire-version\` flag. You can run \`npm view @thegetty/quire-11ty versions\` to view all versions.`)
247
+ }
305
248
  return quireVersion
306
249
  }
307
250
 
@@ -338,11 +281,6 @@ async function remove(version) {
338
281
  * @param {String} version a version identifier or distribution tag
339
282
  */
340
283
  function setVersion(projectPath, version) {
341
- if (!version) {
342
- console.error('[CLI] no version specified')
343
- return
344
- }
345
-
346
284
  const projectName = path.basename(projectPath)
347
285
  console.info(`${projectName} set to use quire-11ty@${version}`)
348
286
  }
@@ -391,19 +329,6 @@ function symlinkLatest() {
391
329
  return fs.symlinkSync(target, source, type)
392
330
  }
393
331
 
394
- /**
395
- * Tests if a `quire-11ty` version is already installed
396
- * and installs the version if it is not already installed.
397
- *
398
- * @param {String} version `quire-11ty` semantic version
399
- */
400
- function testVersion(version) {
401
- version ||= getVersion()
402
- if (!versions.includes(version)) {
403
- install(version)
404
- }
405
- }
406
-
407
332
  /**
408
333
  * Get an array of published `quire-11ty` package versions
409
334
  *
@@ -417,12 +342,10 @@ export const quire = {
417
342
  getPath,
418
343
  getVersion,
419
344
  initStarter,
420
- install,
421
345
  installInProject,
422
346
  latest,
423
347
  list,
424
348
  remove,
425
349
  setVersion,
426
- testVersion,
427
350
  versions,
428
351
  }
package/src/main.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Argument, Command, Option } from 'commander'
2
- import commands from './commands/index.js'
3
- import packageConfig from '../package.json' assert { type: 'json' }
2
+ import commands from '#src/commands/index.js'
3
+ import config from '#lib/conf/config.js'
4
+ import packageConfig from '#src/packageConfig.js'
5
+
6
+ const { version } = packageConfig
4
7
 
5
8
  /**
6
9
  * Quire CLI implements the command pattern.
@@ -12,9 +15,9 @@ import packageConfig from '../package.json' assert { type: 'json' }
12
15
  const program = new Command()
13
16
 
14
17
  program
15
- .name('quire-cli')
18
+ .name('quire')
16
19
  .description('Quire command-line interface')
17
- .version(packageConfig.version, '-v, --version', 'output quire version number')
20
+ .version(version, '-v, --version', 'output quire version number')
18
21
  .configureHelp({
19
22
  helpWidth: 80,
20
23
  sortOptions: false,
@@ -23,9 +26,12 @@ program
23
26
 
24
27
  /**
25
28
  * Register each command as a subcommand of this program
29
+ *
30
+ * @todo refactor command definition to allow for per-command custom help text
31
+ * @see https://github.com/tj/commander.js?tab=readme-ov-file#automated-help
26
32
  */
27
33
  commands.forEach((command) => {
28
- const { action, aliases, args, description, name, options } = command
34
+ const { action, alias, aliases, args, description, name, options } = command
29
35
 
30
36
  const subCommand = program
31
37
  .command(name)
@@ -33,8 +39,12 @@ commands.forEach((command) => {
33
39
  .addHelpCommand()
34
40
  .showHelpAfterError()
35
41
 
42
+ if (alias instanceof String) {
43
+ subCommand.alias(alias)
44
+ }
45
+
36
46
  if (Array.isArray(aliases)) {
37
- aliases.forEach((alias) => subCommand.alias(alias))
47
+ subCommand.aliases(aliases)
38
48
  }
39
49
 
40
50
  /**
@@ -103,6 +113,11 @@ commands.forEach((command) => {
103
113
 
104
114
  // subCommand.action((args) => action.apply(command, args))
105
115
  subCommand.action(action)
116
+
117
+ /**
118
+ * Inject the CLI configuration into commands
119
+ */
120
+ subCommand.config = config
106
121
  })
107
122
 
108
123
  /**
@@ -0,0 +1,15 @@
1
+ import { dirname } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { readPackageUpSync } from 'read-package-up'
4
+
5
+ const __filename = fileURLToPath(import.meta.url)
6
+ const __dirname = dirname(__filename)
7
+
8
+ /**
9
+ * Nota bene: current working directory will be that from which cli commands
10
+ * are being, readPackageUpSync is passed the directory of this module as the
11
+ * starting path to search for the quire-cli package config file.
12
+ */
13
+ const { packageJson } = readPackageUpSync({ cwd: __dirname, normalize: true })
14
+
15
+ export default packageJson
@@ -0,0 +1,98 @@
1
+ import YamlDuplicateIdError from '../errors/validation/yaml-duplicate-error.js'
2
+ import { fileURLToPath } from 'url'
3
+ import fs from 'node:fs'
4
+ import path from 'path'
5
+ import { projectRoot } from '#lib/11ty/index.js'
6
+
7
+ const IMAGE_KEYS = new Set(['src', 'image', 'logo'])
8
+
9
+ export function getSchemaForDocument(file) {
10
+ const schemaName = path.basename(file, path.extname(file))
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
+ const schemaPath = path.join(__dirname,'..','..','schemas', `${schemaName}.schema.json`)
13
+
14
+ try {
15
+ return JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
16
+ } catch (error) {
17
+ console.warn(`Warning: No schema found for document ${schemaName} at path: ${schemaPath}.`)
18
+ return null
19
+ }
20
+ }
21
+
22
+ // Recursive helper to check image paths in nested figure list structures
23
+ function collectImagePaths(node, paths=[]) {
24
+ if(!node || typeof node !== 'object') return
25
+
26
+ for (const key in node) {
27
+ const value = node[key]
28
+ if (IMAGE_KEYS.has(key) && typeof value === 'string') {
29
+ paths.push(value)
30
+ }
31
+ collectImagePaths(value, paths)
32
+ }
33
+
34
+ return paths
35
+ }
36
+
37
+ function validateImage(label, src) {
38
+ if(!src) return
39
+
40
+ let assetPath = ''
41
+ if(src.endsWith('.html')) {
42
+ assetPath = path.join(projectRoot, 'content', '_assets', src)
43
+ } else {
44
+ assetPath = path.join(projectRoot, 'content', '_assets', 'images', src)
45
+ }
46
+
47
+ if(!fs.existsSync(assetPath)) {
48
+ console.warn(`Warning: ${label} source not found at path: ${assetPath}`)
49
+ }
50
+ }
51
+
52
+ export function validateImagePaths(doc) {
53
+ validateImage('Cover image', doc?.epub?.defaultCoverImage)
54
+ validateImage('Promo image', doc?.promo_image)
55
+
56
+ for (const figure of doc?.figure_list || []) {
57
+ let paths = []
58
+ const imagePaths = collectImagePaths(figure, paths)
59
+ for (const imgPath of imagePaths) {
60
+ validateImage(`Figure id ${figure.id}`, imgPath)
61
+ }
62
+ }
63
+
64
+ for (const publisher of doc?.publisher || []) {
65
+ validateImage('Logo', publisher.logo)
66
+ }
67
+
68
+ for (const contributor of doc?.contributor || []) {
69
+ validateImage('Contributor', contributor.image)
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Lifted from packages/11ty/_plugins/globalData
75
+ * Throws an error if data contains duplicate ids
76
+ * @param {Object|Array} data
77
+ */
78
+ export const checkForDuplicateIds = function (data, file) {
79
+ if (!data) return
80
+
81
+ if (Array.isArray(data)) {
82
+ if (data.every((item) => Object.hasOwn(item, 'id'))) {
83
+ const duplicates = data.filter((a, index) => {
84
+ return index !== data.findIndex((b) => b.id === a.id)
85
+ })
86
+ if (duplicates.length) {
87
+ const ids = duplicates.map(({ id }) => id)
88
+ throw new YamlDuplicateIdError(file, `Error in ${file}: Duplicate IDs found: ${ids.join(', ')}`)
89
+ }
90
+ }
91
+ }
92
+
93
+ if (typeof data === 'object') {
94
+ Object.keys(data).forEach((key) => {
95
+ checkForDuplicateIds(data[key], file)
96
+ })
97
+ }
98
+ }
@@ -0,0 +1,38 @@
1
+ import Ajv from 'ajv'
2
+ import addFormats from 'ajv-formats'
3
+ import { validateImagePaths ,getSchemaForDocument, checkForDuplicateIds } from './utils.js'
4
+ import YamlValidationError from '../errors/validation/yaml-validation-error.js'
5
+ import fs from 'fs'
6
+ import yaml from 'js-yaml'
7
+
8
+ export default function yamlValidation(file) {
9
+
10
+ const fileContent = fs.readFileSync(file, 'utf8')
11
+
12
+ let doc
13
+ try {
14
+ doc = yaml.load(fileContent)
15
+ } catch (error) {
16
+ const message = `Error in ${file}: ${error.reason} at line ${error.mark.line} column ${error.mark.column}`
17
+ throw new YamlValidationError(file, `${message}`)
18
+ }
19
+
20
+ const schema = getSchemaForDocument(file)
21
+ if(!schema){ return }
22
+
23
+ const ajv = new Ajv({allErrors:true})
24
+ addFormats(ajv)
25
+ const validate = ajv.compile(schema)
26
+ const valid = validate(doc)
27
+ if(!valid) {
28
+ const messages = validate.errors
29
+ .map(err => `${err.instancePath || '(root)'} ${err.message}`)
30
+ .join('\n')
31
+
32
+ const fullMessage = `Error in ${file}:\n${messages}`
33
+ throw new YamlValidationError(file, fullMessage)
34
+ }
35
+
36
+ validateImagePaths(doc)
37
+ checkForDuplicateIds(doc, file)
38
+ }
@@ -1,3 +0,0 @@
1
- ## CLI Configuration Manager
2
-
3
- This `quire-cli/lib/config` module orchestrates creation of new projects and management of existing project configuration.
@@ -1 +0,0 @@
1
- import semver from 'semver'