@thegetty/quire-cli 1.0.0-rc.7 → 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 (45) hide show
  1. package/CHANGELOG.md +187 -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/README.md +8 -8
  9. package/src/commands/conf.js +43 -0
  10. package/src/commands/create.js +13 -9
  11. package/src/commands/epub.js +1 -2
  12. package/src/commands/info.js +130 -0
  13. package/src/commands/pdf.js +60 -10
  14. package/src/commands/validate.js +64 -0
  15. package/src/errors/validation/validation-error.js +13 -0
  16. package/src/errors/validation/yaml-duplicate-error.js +11 -0
  17. package/src/errors/validation/yaml-parse-error.js +11 -0
  18. package/src/errors/validation/yaml-validation-error.js +11 -0
  19. package/src/helpers/clean.js +5 -0
  20. package/src/helpers/is-empty.js +4 -0
  21. package/src/helpers/is-quire.js +4 -0
  22. package/src/helpers/os-utils.js +4 -0
  23. package/src/helpers/test-cwd.js +4 -0
  24. package/src/helpers/which.js +4 -0
  25. package/src/lib/11ty/api.js +1 -0
  26. package/src/lib/11ty/cli.js +29 -6
  27. package/src/lib/conf/README.md +104 -0
  28. package/src/lib/conf/config.js +35 -0
  29. package/src/lib/conf/defaults.js +37 -0
  30. package/src/lib/conf/migrations.js +11 -0
  31. package/src/lib/conf/schema.js +30 -0
  32. package/src/lib/pdf/README.md +11 -4
  33. package/src/lib/pdf/index.js +4 -4
  34. package/src/lib/pdf/paged.js +88 -9
  35. package/src/lib/pdf/pagedPlugin.js +49 -0
  36. package/src/lib/pdf/prince.js +79 -4
  37. package/src/lib/pdf/princePlugin.js +35 -0
  38. package/src/lib/pdf/split.js +61 -0
  39. package/src/lib/quire/index.js +79 -143
  40. package/src/main.js +21 -6
  41. package/src/packageConfig.js +15 -0
  42. package/src/validators/utils.js +98 -0
  43. package/src/validators/validate-yaml.js +38 -0
  44. package/src/lib/config/README.md +0 -3
  45. package/src/lib/config/index.js +0 -1
@@ -3,9 +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'
8
+ import git from '#lib/git/index.js'
9
+ import packageConfig from '#src/packageConfig.js'
9
10
  import path from 'node:path'
10
11
  import semver from 'semver'
11
12
 
@@ -15,17 +16,19 @@ const __dirname = path.dirname(__filename)
15
16
  // Version install path is relative to process working directory
16
17
  const INSTALL_PATH = path.join('src', 'lib', 'quire', 'versions')
17
18
  const PACKAGE_NAME = '@thegetty/quire-11ty'
18
- const VERSION_FILE = '.quire'
19
+
20
+ const QUIRE_VERSION = config.get('quireVersion')
21
+ const VERSION_FILE = config.get('versionFile')
19
22
 
20
23
  /**
21
- * Return an absolute path to an installed `quire-11ty` version
24
+ * Return an absolute path to an installed quire-11ty version
22
25
  *
23
- * @return {String} path to installed `quire-11ty` version
26
+ * @return {String} path to installed quire-11ty version
24
27
  */
25
- function getPath(version='latest') {
26
- const absolutePath = path.relative('/', `${INSTALL_PATH}/${version}`)
28
+ function getPath(version=QUIRE_VERSION) {
29
+ const absolutePath = path.relative('/', path.join(INSTALL_PATH, version))
27
30
  if (!fs.existsSync(absolutePath)) {
28
- console.error(`[CLI:quire] \`quire-11ty@${version}\` is not installed`)
31
+ console.error(`[CLI:quire] quire-11ty@${version} is not installed`)
29
32
  return null
30
33
  }
31
34
  console.debug(`[CLI:quire] %s`, absolutePath)
@@ -33,7 +36,7 @@ function getPath(version='latest') {
33
36
  }
34
37
 
35
38
  /**
36
- * Read the required `quire-11ty` version for the project `.quire` file
39
+ * Read the required quire-11ty version for the quire version file
37
40
  *
38
41
  * @param {String} projectPath Absolute system path to the project root
39
42
  *
@@ -48,25 +51,19 @@ function getVersion(projectPath) {
48
51
  }
49
52
 
50
53
  /**
51
- * Read the required `quire-11ty` version from starter `package.json` `peerDependencies`
54
+ * Read required quire-11ty and starter versions from starter peerDependencies
52
55
  *
53
56
  * @param {String} projectPath Absolute system path to the project root
54
57
  *
55
- * @return {String} version Quire-11ty semantic version with caret or other
56
- * comparators trimmed off the beginning
57
- *
58
- * @TODO refactor `latest()` function to programmatically return a specific
59
- * version of `@thegetty/quire-11ty` from a semantic version string
60
- * (i.e `^1.0.0-pre-release.0` => `1.0.0-pre-release.2`) so this string-trimming
61
- * 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
62
61
  */
63
- async function getVersionFromStarter(projectPath) {
64
- const packageConfig = fs.readFileSync(path.join(projectPath, 'package.json'), { encoding:'utf8' })
65
- const { peerDependencies } = JSON.parse(packageConfig)
66
- const version = peerDependencies[PACKAGE_NAME]
67
- return version === 'latest'
68
- ? await latest()
69
- : version.substr(version.search(/\d/))
62
+ async function getVersionsFromStarter(projectPath) {
63
+ const projectPackageConfig = fs.readFileSync(path.join(projectPath, 'package.json'), { encoding:'utf8' })
64
+ const { peerDependencies, version: starterVersion } = JSON.parse(projectPackageConfig)
65
+ const quire11tyVersion = peerDependencies[PACKAGE_NAME]
66
+ return { quire11tyVersion, starterVersion }
70
67
  }
71
68
 
72
69
  /**
@@ -93,11 +90,9 @@ async function initStarter (starter, projectPath, options) {
93
90
  return
94
91
  }
95
92
 
96
- starter = starter || 'https://github.com/thegetty/quire-starter-default'
97
-
98
93
  console.debug('[CLI:quire] init-starter',
99
- `\n project root: "${projectPath}"`,
100
- `\n starter: "${starter}"`
94
+ `\n project: ${path.join(__dirname, projectPath)}`,
95
+ `\n starter: ${starter}`
101
96
  )
102
97
 
103
98
  /**
@@ -110,14 +105,24 @@ async function initStarter (starter, projectPath, options) {
110
105
  .catch((error) => console.error('[CLI:error] ', error))
111
106
 
112
107
  /**
113
- * 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.
114
110
  *
115
- * A version specified in `options.quireVersion` overrides the version in starter
116
- * project `package.json`.
111
+ * Uses 'latest' to get the latest semantic version compatible with version ranges
117
112
  */
118
- const quireVersion = options.quireVersion || await getVersionFromStarter(projectPath)
113
+ const { quire11tyVersion, starterVersion } = await getVersionsFromStarter(projectPath)
114
+ const quireVersion = await latest(options.quireVersion || quire11tyVersion)
119
115
  setVersion(projectPath, quireVersion)
120
116
 
117
+ /**
118
+ * Write quire-11ty, quire-cli, starter versions to the version file
119
+ */
120
+ const versionInfo = {
121
+ cli: packageConfig.version,
122
+ starter: `${starter}@${starterVersion}`,
123
+ }
124
+ fs.writeFileSync(path.join(projectPath, VERSION_FILE), JSON.stringify(versionInfo))
125
+
121
126
  // Re-initialize project directory as a new git repository
122
127
  await fs.remove(path.join(projectPath, '.git'))
123
128
 
@@ -137,57 +142,9 @@ async function initStarter (starter, projectPath, options) {
137
142
  */
138
143
  const projectFiles = fs.readdirSync(projectPath)
139
144
  await git.init().add(projectFiles).commit('Initial Commit')
140
-
141
145
  return quireVersion
142
146
  }
143
147
 
144
- /**
145
- * Install `quire-11ty`, default to 'latest' version
146
- *
147
- * @TODO refactor this to be callable by the installInProject method
148
- *
149
- * @param {Object} options options passed from `quire new` command
150
- * @return {Promise}
151
- */
152
- async function install(options = {}) {
153
- const version = options.quireVersion || 'latest'
154
- console.debug(`[CLI:quire] installing quire-11ty@${version}`)
155
- const absoluteInstallPath = path.join(__dirname, 'versions')
156
- fs.ensureDirSync(absoluteInstallPath)
157
- /**
158
- * `Destination` is relative to `node_modules` of the working-directory
159
- * so we have included a relative path to parent directory in order to
160
- * install versions to a different local path.
161
- * @see https://github.com/scott-lin/install-npm-version
162
- */
163
- const installOptions = {
164
- Destination: path.join('..', version),
165
- Debug: false,
166
- Overwrite: options.force || options.overwrite || false,
167
- Verbosity: options.debug ? 'Debug' : 'Silent',
168
- WorkingDirectory: absoluteInstallPath
169
- }
170
- await inv.Install(`${PACKAGE_NAME}@${version}`, installOptions)
171
-
172
- // delete empty `node_modules` directory that `install-npm-version` creates
173
- const invNodeModulesDir = path.join(absoluteInstallPath, 'node_modules')
174
- if (fs.existsSync(invNodeModulesDir)) fs.rmdir(invNodeModulesDir)
175
-
176
- symlinkLatest()
177
-
178
- console.debug('[CLI:quire] installing dev dependencies')
179
- /**
180
- * Manually install necessary dev dependencies to run 11ty;
181
- * these must be `devDependencies` so that they are not bundled into
182
- * the final `_site` package when running `quire build`
183
- */
184
- const currentWorkingDirectory = cwd()
185
- const versionDir = path.join(absoluteInstallPath, version)
186
- chdir(versionDir)
187
- await execaCommand('npm cache clean --force')
188
- await execaCommand('npm install --save-dev')
189
- }
190
-
191
148
  /**
192
149
  * Install `quire-11ty` directly into a quire project
193
150
  *
@@ -198,9 +155,10 @@ async function install(options = {}) {
198
155
  * @param {Object} options options passed from `quire new` command
199
156
  * @return {Promise}
200
157
  */
201
- async function installInProject(projectPath, options = {}) {
202
- const { quirePath, quireVersion } = options
203
- 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
+
204
162
  console.debug(`[CLI:quire] installing ${quire11tyPackage} into ${projectPath}`)
205
163
 
206
164
  /**
@@ -215,27 +173,24 @@ async function installInProject(projectPath, options = {}) {
215
173
  .catch((error) => console.error('[CLI:error] ', error))
216
174
 
217
175
  const temp11tyDirectory = '.temp'
218
- /**
219
- * `Destination` is relative to `node_modules` of the working-directory
220
- * so we have included a relative path to parent directory in order to
221
- * install versions to a different local path.
222
- * @see https://github.com/scott-lin/install-npm-version
223
- */
224
- const installOptions = {
225
- Destination: path.join('..', temp11tyDirectory),
226
- Debug: false,
227
- Overwrite: options.force || options.overwrite || false,
228
- Verbosity: options.debug ? 'Debug' : 'Silent',
229
- WorkingDirectory: projectPath
230
- }
231
- 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}`)
184
+
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/`)
232
188
 
233
- // delete empty `node_modules` directory that `install-npm-version` creates
234
- const invNodeModulesDir = path.join(projectPath, 'node_modules')
235
- if (fs.existsSync(invNodeModulesDir)) fs.rmdir(invNodeModulesDir)
189
+ fs.rmSync(tarballPath)
190
+ }
236
191
 
237
- // Copy all files installed in `.temp` to projectPath
238
- fs.copySync(path.join(projectPath, '.temp'), projectPath)
192
+ // Copy `.temp` to projectPath
193
+ fs.cpSync(tempDir, projectPath, {recursive: true})
239
194
 
240
195
  console.debug('[CLI:quire] installing dev dependencies into quire project')
241
196
  /**
@@ -243,17 +198,16 @@ async function installInProject(projectPath, options = {}) {
243
198
  * these must be `devDependencies` so that they are not bundled into
244
199
  * the final `_site` package when running `quire build`
245
200
  */
246
- await execaCommand('npm cache clean --force', { cwd: projectPath })
247
201
  try {
248
202
  await execaCommand('npm install --save-dev', { cwd: projectPath })
249
203
  } catch(error) {
250
204
  console.warn(`[CLI:error]`, error)
251
- fs.removeSync(projectPath)
205
+ fs.rmSync(projectPath, {recursive: true})
252
206
  return
253
207
  }
254
208
 
255
209
  const eleventyFilesToCommit = fs
256
- .readdirSync(path.join(projectPath, temp11tyDirectory))
210
+ .readdirSync(tempDir)
257
211
  .filter((filePath) => filePath !== 'node_modules')
258
212
 
259
213
  eleventyFilesToCommit.push('package-lock.json')
@@ -265,30 +219,32 @@ async function installInProject(projectPath, options = {}) {
265
219
  await git.add(eleventyFilesToCommit).commit('Adds `@thegetty/quire-11ty` files')
266
220
 
267
221
  // remove temporary 11ty install directory
268
- fs.removeSync(path.join(projectPath, temp11tyDirectory))
222
+ fs.rmSync(path.join(projectPath, temp11tyDirectory), {recursive: true})
269
223
  }
270
224
 
271
225
  /**
272
226
  * Retrieve latest published version of the `quire-11ty` package
273
-
274
- * @todo refactor to programmatically return a specific version
275
- * of `@thegetty/quire-11ty` from a semantic version string
276
- * (i.e `^1.0.0-pre-release.0` => `1.0.0-pre-release.2`)
277
- * so that the latest function may be used like:
278
- * await latest('^1.0.0-pre-release.0') => '1.0.0-pre-release.2'
279
- *
280
- * Nota bene: `npm view [<@scope>/]<name>[@<version>] version`
281
- * @see https://docs.npmjs.com/cli/v7/commands/npm-view
282
- * returns a list of versions that satisfy the `<version>` range specifier,
283
- * piping this to execa `stdout` we get only the last line of output.
284
- * @todo use [`parse-columns`](https://github.com/sindresorhus/parse-columns)
285
- * to parse the column formated list of versions returned by `npm view`
286
- *
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
+ *
287
231
  * @return {String} `quire-11ty@latest` semantic version string
288
232
  */
289
- async function latest() {
290
- const { stdout: quireVersion } =
291
- 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
+ }
292
248
  return quireVersion
293
249
  }
294
250
 
@@ -325,11 +281,6 @@ async function remove(version) {
325
281
  * @param {String} version a version identifier or distribution tag
326
282
  */
327
283
  function setVersion(projectPath, version) {
328
- if (!version) {
329
- console.error('[CLI] no version specified')
330
- return
331
- }
332
- fs.writeFileSync(path.join(projectPath, VERSION_FILE), version)
333
284
  const projectName = path.basename(projectPath)
334
285
  console.info(`${projectName} set to use quire-11ty@${version}`)
335
286
  }
@@ -378,19 +329,6 @@ function symlinkLatest() {
378
329
  return fs.symlinkSync(target, source, type)
379
330
  }
380
331
 
381
- /**
382
- * Tests if a `quire-11ty` version is already installed
383
- * and installs the version if it is not already installed.
384
- *
385
- * @param {String} version `quire-11ty` semantic version
386
- */
387
- function testVersion(version) {
388
- version ||= getVersion()
389
- if (!versions.includes(version)) {
390
- install(version)
391
- }
392
- }
393
-
394
332
  /**
395
333
  * Get an array of published `quire-11ty` package versions
396
334
  *
@@ -404,12 +342,10 @@ export const quire = {
404
342
  getPath,
405
343
  getVersion,
406
344
  initStarter,
407
- install,
408
345
  installInProject,
409
346
  latest,
410
347
  list,
411
348
  remove,
412
349
  setVersion,
413
- testVersion,
414
350
  versions,
415
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'