@thegetty/quire-cli 1.0.0-rc.34 → 1.0.0-rc.36

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 (159) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -0
  3. package/bin/cli.js +19 -1
  4. package/package.json +21 -8
  5. package/schemas/config.schema.json +194 -0
  6. package/schemas/figures.schema.json +56 -0
  7. package/schemas/layout.schema.json +5 -0
  8. package/schemas/objects.schema.json +62 -0
  9. package/schemas/publication.schema.json +140 -0
  10. package/schemas/references.schema.json +29 -0
  11. package/src/Command.js +26 -6
  12. package/src/Command.spec.js +99 -0
  13. package/src/commands/README.md +213 -122
  14. package/src/commands/build.js +46 -37
  15. package/src/commands/build.spec.js +113 -0
  16. package/src/commands/build.test.js +402 -0
  17. package/src/commands/clean.js +25 -19
  18. package/src/commands/clean.spec.js +108 -0
  19. package/src/commands/clean.test.js +260 -0
  20. package/src/commands/config.js +251 -0
  21. package/src/commands/config.spec.js +107 -0
  22. package/src/commands/config.test.js +715 -0
  23. package/src/commands/create.js +42 -14
  24. package/src/commands/create.spec.js +112 -0
  25. package/src/commands/create.test.js +415 -0
  26. package/src/commands/epub.js +59 -30
  27. package/src/commands/epub.spec.js +114 -0
  28. package/src/commands/epub.test.js +503 -0
  29. package/src/commands/index.js +9 -2
  30. package/src/commands/info.js +18 -24
  31. package/src/commands/info.spec.js +64 -0
  32. package/src/commands/info.test.js +415 -0
  33. package/src/commands/pdf.js +58 -84
  34. package/src/commands/pdf.spec.js +114 -0
  35. package/src/commands/pdf.test.js +464 -0
  36. package/src/commands/preview.js +26 -31
  37. package/src/commands/preview.spec.js +97 -0
  38. package/src/commands/preview.test.js +250 -0
  39. package/src/commands/use.js +56 -0
  40. package/src/commands/use.spec.js +61 -0
  41. package/src/commands/use.test.js +280 -0
  42. package/src/commands/validate.js +31 -19
  43. package/src/commands/validate.spec.js +82 -0
  44. package/src/commands/validate.test.js +234 -0
  45. package/src/commands/workflows.js +70 -0
  46. package/src/errors/build/build-failed-error.js +19 -0
  47. package/src/errors/build/config-field-missing-error.js +20 -0
  48. package/src/errors/build/config-file-not-found-error.js +20 -0
  49. package/src/errors/build/index.js +11 -0
  50. package/src/errors/index.js +48 -0
  51. package/src/errors/install/dependency-install-error.js +19 -0
  52. package/src/errors/install/directory-not-empty-error.js +25 -0
  53. package/src/errors/install/index.js +12 -0
  54. package/src/errors/install/invalid-path-error.js +31 -0
  55. package/src/errors/install/invalid-starter-error.js +27 -0
  56. package/src/errors/install/version-not-found-error.js +35 -0
  57. package/src/errors/output/epub-generation-error.js +19 -0
  58. package/src/errors/output/index.js +14 -0
  59. package/src/errors/output/invalid-epub-library-error.js +20 -0
  60. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  61. package/src/errors/output/missing-build-output-error.js +21 -0
  62. package/src/errors/output/pdf-generation-error.js +24 -0
  63. package/src/errors/output/tool-not-found-error.js +37 -0
  64. package/src/errors/project/index.js +10 -0
  65. package/src/errors/project/not-in-project-error.js +20 -0
  66. package/src/errors/project/project-create-error.js +21 -0
  67. package/src/errors/quire-error.js +27 -0
  68. package/src/errors/validation/validation-error.js +20 -11
  69. package/src/helpers/clean.js +1 -1
  70. package/src/helpers/docs-url.js +32 -0
  71. package/src/helpers/test-cwd.js +5 -6
  72. package/src/helpers/test-cwd.test.js +192 -0
  73. package/src/helpers/which.js +10 -4
  74. package/src/lib/11ty/README.md +135 -19
  75. package/src/lib/11ty/api.js +176 -93
  76. package/src/lib/11ty/cli.js +77 -35
  77. package/src/lib/11ty/index.js +64 -5
  78. package/src/lib/11ty/index.test.js +655 -0
  79. package/src/lib/README.md +275 -0
  80. package/src/lib/commander/index.js +100 -0
  81. package/src/lib/commander/index.test.js +86 -0
  82. package/src/lib/commander/options.js +195 -0
  83. package/src/lib/commander/options.test.js +109 -0
  84. package/src/lib/conf/README.md +84 -73
  85. package/src/lib/conf/config.js +5 -3
  86. package/src/lib/conf/config.test.js +281 -0
  87. package/src/lib/conf/defaults.js +44 -0
  88. package/src/lib/conf/format.js +60 -0
  89. package/src/lib/conf/format.test.js +106 -0
  90. package/src/lib/conf/helpers.js +91 -0
  91. package/src/lib/conf/helpers.test.js +136 -0
  92. package/src/lib/conf/index.js +22 -0
  93. package/src/lib/conf/schema.js +53 -8
  94. package/src/lib/epub/README.md +133 -2
  95. package/src/lib/epub/engines.js +46 -0
  96. package/src/lib/epub/epub.js +36 -11
  97. package/src/lib/epub/index.js +111 -21
  98. package/src/lib/epub/index.test.js +518 -0
  99. package/src/lib/epub/pandoc.js +33 -4
  100. package/src/lib/epub/pandoc.test.js +122 -0
  101. package/src/lib/epub/schema.js +21 -0
  102. package/src/lib/error/README.md +170 -0
  103. package/src/lib/error/handler.js +107 -0
  104. package/src/lib/git/README.md +151 -2
  105. package/src/lib/git/index.js +217 -13
  106. package/src/lib/git/index.spec.js +80 -0
  107. package/src/lib/git/index.test.js +453 -0
  108. package/src/lib/installer/index.js +309 -0
  109. package/src/lib/installer/index.spec.js +83 -0
  110. package/src/lib/installer/index.test.js +545 -0
  111. package/src/lib/logger/README.md +424 -0
  112. package/src/lib/logger/debug.js +90 -0
  113. package/src/lib/logger/debug.spec.js +130 -0
  114. package/src/lib/logger/index.js +228 -0
  115. package/src/lib/logger/index.spec.js +131 -0
  116. package/src/lib/logger/index.test.js +477 -0
  117. package/src/lib/npm/README.md +127 -0
  118. package/src/lib/npm/index.js +198 -0
  119. package/src/lib/npm/index.spec.js +60 -0
  120. package/src/lib/npm/index.test.js +355 -0
  121. package/src/lib/pdf/README.md +131 -0
  122. package/src/lib/pdf/engines.js +46 -0
  123. package/src/lib/pdf/index.js +124 -21
  124. package/src/lib/pdf/index.test.js +708 -0
  125. package/src/lib/pdf/paged.js +100 -52
  126. package/src/lib/pdf/paged.test.js +366 -0
  127. package/src/lib/pdf/prince.js +115 -37
  128. package/src/lib/pdf/prince.test.js +202 -0
  129. package/src/lib/pdf/schema.js +21 -0
  130. package/src/lib/pdf/split.js +61 -33
  131. package/src/lib/pdf/split.test.js +445 -0
  132. package/src/lib/process/manager.js +110 -0
  133. package/src/lib/process/manager.test.js +55 -0
  134. package/src/lib/project/build.js +143 -0
  135. package/src/lib/project/build.test.js +253 -0
  136. package/src/lib/project/config.js +48 -0
  137. package/src/lib/project/config.test.js +134 -0
  138. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  139. package/src/lib/project/detect.test.js +157 -0
  140. package/src/lib/project/index.js +40 -0
  141. package/src/lib/project/paths.js +224 -0
  142. package/src/lib/project/version.js +110 -0
  143. package/src/lib/project/version.test.js +350 -0
  144. package/src/lib/reporter/README.md +211 -2
  145. package/src/lib/reporter/index.js +512 -0
  146. package/src/lib/reporter/index.test.js +593 -0
  147. package/src/main.js +134 -47
  148. package/src/main.spec.js +61 -0
  149. package/src/main.test.js +347 -0
  150. package/src/validators/utils.js +2 -1
  151. package/src/commands/conf.js +0 -43
  152. package/src/commands/version.js +0 -43
  153. package/src/lib/11ty/paths.js +0 -103
  154. package/src/lib/i18n/README.md +0 -3
  155. package/src/lib/i18n/config.js +0 -53
  156. package/src/lib/i18n/index.js +0 -43
  157. package/src/lib/i18n/localeService.js +0 -58
  158. package/src/lib/quire/README.md +0 -19
  159. package/src/lib/quire/index.js +0 -350
@@ -1,6 +1,10 @@
1
1
  import Command from '#src/Command.js'
2
- import { api, cli, paths, projectRoot } from '#lib/11ty/index.js'
2
+ import { Option } from 'commander'
3
+ import { withOutputModes } from '#lib/commander/index.js'
4
+ import { api, cli } from '#lib/11ty/index.js'
5
+ import paths from '#lib/project/index.js'
3
6
  import { clean } from '#helpers/clean.js'
7
+ import reporter from '#lib/reporter/index.js'
4
8
  import testcwd from '#helpers/test-cwd.js'
5
9
 
6
10
  /**
@@ -12,56 +16,61 @@ import testcwd from '#helpers/test-cwd.js'
12
16
  * @extends {Command}
13
17
  */
14
18
  export default class BuildCommand extends Command {
15
- static definition = {
19
+ static definition = withOutputModes({
16
20
  name: 'build',
17
21
  description: 'Generate publication outputs',
18
- summary: 'run build',
19
- version: '1.0.0',
20
- args: [
21
- // [
22
- // '[formats...]', 'output formats',
23
- // {
24
- // choices: ['pdf', 'epub'],
25
- // }
26
- // ],
27
- ],
22
+ summary: 'generate HTML site files',
23
+ docsLink: 'quire-commands/#output-files',
24
+ helpText: `
25
+ Examples:
26
+ quire build Build the site
27
+ quire build --verbose Build with detailed progress
28
+ quire build --debug Build with debug output
29
+
30
+ Note: Run before "quire pdf" or "quire epub" commands.
31
+ `,
32
+ version: '1.1.0',
28
33
  options: [
29
34
  [ '-d', '--dry-run', 'run build without writing files' ],
30
- [ '-q', '--quiet', 'run build with no console messages' ],
31
- [ '-v', '--verbose', 'run build with verbose console messages' ],
32
- [
33
- '--11ty <module>', 'use the specified 11ty module', 'cli',
34
- // { choices: ['api', 'cli'], default: 'cli' }
35
- ],
36
- [ '--debug', 'run build with debug output to console' ],
35
+ [ '--dryrun', 'alias for --dry-run', { hidden: true, implies: { dryRun: true } } ],
36
+ // Use Option object syntax to configure this as a hidden option
37
+ new Option('--11ty <module>', 'use the specified 11ty module')
38
+ .choices(['api', 'cli']).default('api').hideHelp(),
37
39
  ],
38
- }
40
+ })
39
41
 
40
42
  constructor() {
41
43
  super(BuildCommand.definition)
42
44
  }
43
45
 
44
- action(options, command) {
45
- if (options.debug) {
46
- console.debug('[CLI] Command \'%s\' called with options %o', this.name(), options)
47
- }
46
+ async action(options, command) {
47
+ this.debug('called with options %O', options)
48
48
 
49
- if (options['11ty'] === 'cli') {
50
- console.debug('[CLI] running eleventy using lib/11ty cli')
51
- cli.build(options)
52
- } else {
53
- console.debug('[CLI] running eleventy using lib/11ty api')
54
- api.build(options)
49
+ // Configure reporter for this command
50
+ reporter.configure({ quiet: options.quiet, verbose: options.verbose })
51
+
52
+ reporter.start('Building site...', { showElapsed: true })
53
+
54
+ try {
55
+ if (options['11ty'] === 'api') {
56
+ this.debug('running eleventy using lib/11ty api')
57
+ await api.build(options)
58
+ } else {
59
+ this.debug('running eleventy using lib/11ty cli')
60
+ await cli.build(options)
61
+ }
62
+ reporter.succeed('Build complete')
63
+ } catch (error) {
64
+ reporter.fail('Build failed')
65
+ throw error
55
66
  }
56
67
  }
57
68
 
58
- preAction(command) {
59
- testcwd(command)
69
+ preAction(thisCommand, actionCommand) {
70
+ testcwd(thisCommand)
60
71
 
61
- const options = command.opts()
62
- if (options.debug) {
63
- console.debug('[CLI] Calling \'build\' command pre-action with options', options)
64
- }
65
- clean(projectRoot, paths, options)
72
+ const options = thisCommand.opts()
73
+ this.debug('pre-action with options %O', options)
74
+ clean(paths.getProjectRoot(), paths.toObject(), options)
66
75
  }
67
76
  }
@@ -0,0 +1,113 @@
1
+ import { Command, Option } from 'commander'
2
+ import program from '#src/main.js'
3
+ import test from 'ava'
4
+
5
+ /**
6
+ * Command Contract/Interface Tests
7
+ *
8
+ * Verifies the command's public API and Commander.js integration.
9
+ * @see docs/testing-commands.md
10
+ */
11
+
12
+ test.before((t) => {
13
+ // Get the registered command (from program.commands) once and share across all tests
14
+ t.context.command = program.commands.find((cmd) => cmd.name() === 'build')
15
+ })
16
+
17
+ test('command is registered in CLI program', (t) => {
18
+ const { command } = t.context
19
+
20
+ t.truthy(command, 'command "build" should be registered in program')
21
+ t.true(command instanceof Command, 'registered command should be Commander.js Command instance')
22
+ })
23
+
24
+ test('registered command has correct metadata', (t) => {
25
+ const { command } = t.context
26
+
27
+ t.is(command.name(), 'build')
28
+ t.truthy(command.description())
29
+ t.is(typeof command._actionHandler, 'function', 'command should have action handler')
30
+ })
31
+
32
+ test('registered command has no arguments', (t) => {
33
+ const { command } = t.context
34
+ const registeredArguments = command.registeredArguments
35
+
36
+ t.is(registeredArguments.length, 0, 'build command should have no arguments')
37
+ })
38
+
39
+ test('registered command has correct options', (t) => {
40
+ const { command } = t.context
41
+
42
+ // Get all options
43
+ const dryRunOption = command.options.find((opt) => opt.long === '--dry-run')
44
+ const dryrunAlias = command.options.find((opt) => opt.long === '--dryrun')
45
+ const quietOption = command.options.find((opt) => opt.long === '--quiet')
46
+ const verboseOption = command.options.find((opt) => opt.long === '--verbose')
47
+ const progressOption = command.options.find((opt) => opt.long === '--progress')
48
+ const eleventyOption = command.options.find((opt) => opt.long === '--11ty')
49
+ const debugOption = command.options.find((opt) => opt.long === '--debug')
50
+
51
+ // Verify all options exist
52
+ t.truthy(dryRunOption, '--dry-run option should exist')
53
+ t.truthy(dryrunAlias, '--dryrun alias should exist')
54
+ t.truthy(quietOption, '--quiet option should exist')
55
+ t.truthy(verboseOption, '--verbose option should exist')
56
+ t.truthy(progressOption, '--progress option should exist')
57
+ t.truthy(eleventyOption, '--11ty option should exist')
58
+ t.truthy(debugOption, '--debug option should exist')
59
+
60
+ // Verify they are Option instances
61
+ t.true(dryRunOption instanceof Option, '--dry-run should be Option instance')
62
+ t.true(dryrunAlias instanceof Option, '--dryrun should be Option instance')
63
+ t.true(quietOption instanceof Option, '--quiet should be Option instance')
64
+ t.true(verboseOption instanceof Option, '--verbose should be Option instance')
65
+ t.true(progressOption instanceof Option, '--progress should be Option instance')
66
+ t.true(eleventyOption instanceof Option, '--11ty should be Option instance')
67
+ t.true(debugOption instanceof Option, '--debug should be Option instance')
68
+
69
+ // Verify option properties
70
+ t.is(dryRunOption.long, '--dry-run')
71
+ t.is(dryRunOption.short, '-d')
72
+ t.truthy(dryRunOption.description)
73
+
74
+ // --dryrun is a hidden alias for --dry-run
75
+ t.is(dryrunAlias.long, '--dryrun')
76
+ t.true(dryrunAlias.hidden, '--dryrun should be hidden from help')
77
+
78
+ t.is(quietOption.long, '--quiet')
79
+ t.is(quietOption.short, '-q')
80
+ t.truthy(quietOption.description)
81
+
82
+ t.is(verboseOption.long, '--verbose')
83
+ t.is(verboseOption.short, '-v')
84
+ t.truthy(verboseOption.description)
85
+
86
+ // --progress is a hidden alias for --verbose
87
+ t.is(progressOption.long, '--progress')
88
+ t.truthy(progressOption.description)
89
+ t.true(progressOption.hidden, '--progress should be hidden from help')
90
+
91
+ t.is(eleventyOption.long, '--11ty')
92
+ t.truthy(eleventyOption.description)
93
+ t.true(eleventyOption.required, '--11ty should require a value')
94
+
95
+ t.is(debugOption.long, '--debug')
96
+ t.truthy(debugOption.description)
97
+ t.false(debugOption.required, '--debug should not require a value')
98
+ })
99
+
100
+ test('command options are accessible via public API', (t) => {
101
+ const { command } = t.context
102
+
103
+ // Test that options can be accessed the way Commander.js does
104
+ const optionNames = command.options.map((opt) => opt.long)
105
+
106
+ t.true(optionNames.includes('--dry-run'))
107
+ t.true(optionNames.includes('--dryrun'))
108
+ t.true(optionNames.includes('--quiet'))
109
+ t.true(optionNames.includes('--verbose'))
110
+ t.true(optionNames.includes('--progress'))
111
+ t.true(optionNames.includes('--11ty'))
112
+ t.true(optionNames.includes('--debug'))
113
+ })
@@ -0,0 +1,402 @@
1
+ import test from 'ava'
2
+ import { Volume, createFsFromVolume } from 'memfs'
3
+ import sinon from 'sinon'
4
+ import esmock from 'esmock'
5
+
6
+ test.beforeEach((t) => {
7
+ // Create sinon sandbox for mocking
8
+ t.context.sandbox = sinon.createSandbox()
9
+
10
+ // Create in-memory file system
11
+ t.context.vol = new Volume()
12
+ t.context.fs = createFsFromVolume(t.context.vol)
13
+
14
+ // Setup mock directory structure
15
+ t.context.vol.fromJSON({
16
+ '/project/content/_data/config.yaml': 'title: Test Project',
17
+ '/project/content/index.md': '# Quire Test Project',
18
+ '/project/package.json': JSON.stringify({ name: 'test-project' })
19
+ })
20
+
21
+ t.context.projectRoot = '/project'
22
+
23
+ // Create mock logger (no global console stubbing needed!)
24
+ t.context.mockLogger = {
25
+ info: t.context.sandbox.stub(),
26
+ error: t.context.sandbox.stub(),
27
+ debug: t.context.sandbox.stub(),
28
+ log: t.context.sandbox.stub(),
29
+ warn: t.context.sandbox.stub()
30
+ }
31
+
32
+ // Create mock reporter
33
+ t.context.mockReporter = {
34
+ configure: t.context.sandbox.stub().returnsThis(),
35
+ start: t.context.sandbox.stub().returnsThis(),
36
+ update: t.context.sandbox.stub().returnsThis(),
37
+ succeed: t.context.sandbox.stub().returnsThis(),
38
+ fail: t.context.sandbox.stub().returnsThis(),
39
+ stop: t.context.sandbox.stub().returnsThis(),
40
+ }
41
+ })
42
+
43
+ test.afterEach.always((t) => {
44
+ // Restore all mocks
45
+ t.context.sandbox.restore()
46
+
47
+ // Clear in-memory file system
48
+ t.context.vol.reset()
49
+ })
50
+
51
+ test('build command should call eleventy CLI with default options', async (t) => {
52
+ const { sandbox, fs, mockLogger, mockReporter } = t.context
53
+
54
+ // Mock eleventy CLI module
55
+ const mockEleventyCli = {
56
+ build: sandbox.stub().resolves({ exitCode: 0 })
57
+ }
58
+
59
+ // Mock eleventy API module
60
+ const mockEleventyApi = {
61
+ build: sandbox.stub().resolves()
62
+ }
63
+
64
+ // Mock clean helper
65
+ const mockClean = sandbox.stub()
66
+
67
+ // Mock testcwd helper
68
+ const mockTestcwd = sandbox.stub()
69
+
70
+ // Use esmock to replace imports
71
+ const BuildCommand = await esmock('./build.js', {
72
+ '#lib/11ty/index.js': {
73
+ api: mockEleventyApi,
74
+ cli: mockEleventyCli
75
+ },
76
+ '#lib/project/index.js': {
77
+ default: {
78
+ getProjectRoot: () => '/project',
79
+ toObject: () => ({ output: '_site' })
80
+ }
81
+ },
82
+ '#helpers/clean.js': {
83
+ clean: mockClean
84
+ },
85
+ '#helpers/test-cwd.js': {
86
+ default: mockTestcwd
87
+ },
88
+ '#lib/logger/index.js': {
89
+ logger: mockLogger
90
+ },
91
+ '#lib/reporter/index.js': {
92
+ default: mockReporter
93
+ },
94
+ 'fs-extra': fs
95
+ })
96
+
97
+ const command = new BuildCommand()
98
+ command.name = sandbox.stub().returns('build')
99
+ command.opts = sandbox.stub().returns({})
100
+
101
+ // Run preAction
102
+ command.preAction(command)
103
+
104
+ // Run action with default options (uses cli by default)
105
+ await command.action({ '11ty': 'cli' }, command)
106
+
107
+ t.true(mockTestcwd.called, 'testcwd should be called in preAction')
108
+ t.true(mockClean.called, 'clean should be called in preAction')
109
+ t.false(mockEleventyApi.build.called, 'eleventy API build should not be called')
110
+ t.true(mockEleventyCli.build.called, 'eleventy CLI build should be called')
111
+ t.true(mockReporter.start.called, 'reporter.start should be called')
112
+ t.true(mockReporter.succeed.called, 'reporter.succeed should be called')
113
+ })
114
+
115
+ test('build command should call eleventy API when 11ty option is "api"', async (t) => {
116
+ const { sandbox, fs, mockLogger, mockReporter } = t.context
117
+
118
+ // Mock eleventy CLI module
119
+ const mockEleventyCli = {
120
+ build: sandbox.stub().resolves({ exitCode: 0 })
121
+ }
122
+
123
+ // Mock eleventy API module
124
+ const mockEleventyApi = {
125
+ build: sandbox.stub().resolves()
126
+ }
127
+
128
+ // Mock clean helper
129
+ const mockClean = sandbox.stub()
130
+
131
+ // Mock testcwd helper
132
+ const mockTestcwd = sandbox.stub()
133
+
134
+ // Use esmock to replace imports
135
+ const BuildCommand = await esmock('./build.js', {
136
+ '#lib/11ty/index.js': {
137
+ cli: mockEleventyCli,
138
+ api: mockEleventyApi
139
+ },
140
+ '#lib/project/index.js': {
141
+ default: {
142
+ getProjectRoot: () => '/project',
143
+ toObject: () => ({ output: '_site' })
144
+ }
145
+ },
146
+ '#helpers/clean.js': {
147
+ clean: mockClean
148
+ },
149
+ '#helpers/test-cwd.js': {
150
+ default: mockTestcwd
151
+ },
152
+ '#lib/logger/index.js': {
153
+ logger: mockLogger
154
+ },
155
+ '#lib/reporter/index.js': {
156
+ default: mockReporter
157
+ },
158
+ 'fs-extra': fs
159
+ })
160
+
161
+ const command = new BuildCommand()
162
+ command.name = sandbox.stub().returns('build')
163
+
164
+ // Run action with api option
165
+ await command.action({ '11ty': 'api' }, command)
166
+
167
+ t.false(mockEleventyCli.build.called, 'eleventy CLI build should not be called')
168
+ t.true(mockEleventyApi.build.called, 'eleventy API build should be called')
169
+ })
170
+
171
+ test('build command should call clean with correct parameters in preAction', async (t) => {
172
+ const { sandbox, fs, mockLogger, mockReporter } = t.context
173
+
174
+ // Mock eleventy modules
175
+ const mockEleventyCli = {
176
+ build: sandbox.stub().resolves({ exitCode: 0 })
177
+ }
178
+
179
+ const mockEleventyApi = {
180
+ build: sandbox.stub().resolves()
181
+ }
182
+
183
+ // Mock clean helper
184
+ const mockClean = sandbox.stub()
185
+
186
+ // Mock testcwd helper
187
+ const mockTestcwd = sandbox.stub()
188
+
189
+ // Use esmock to replace imports
190
+ const BuildCommand = await esmock('./build.js', {
191
+ '#lib/11ty/index.js': {
192
+ cli: mockEleventyCli,
193
+ api: mockEleventyApi
194
+ },
195
+ '#lib/project/index.js': {
196
+ default: {
197
+ getProjectRoot: () => '/project',
198
+ toObject: () => ({ output: '_site' })
199
+ }
200
+ },
201
+ '#helpers/clean.js': {
202
+ clean: mockClean
203
+ },
204
+ '#helpers/test-cwd.js': {
205
+ default: mockTestcwd
206
+ },
207
+ '#lib/logger/index.js': {
208
+ logger: mockLogger
209
+ },
210
+ '#lib/reporter/index.js': {
211
+ default: mockReporter
212
+ },
213
+ 'fs-extra': fs
214
+ })
215
+
216
+ const command = new BuildCommand()
217
+ command.name = sandbox.stub().returns('build')
218
+ const options = { verbose: true }
219
+ command.opts = sandbox.stub().returns(options)
220
+
221
+ // Run preAction
222
+ command.preAction(command)
223
+
224
+ t.true(mockClean.called, 'clean should be called')
225
+ t.true(mockClean.calledWith('/project', { output: '_site' }, options), 'clean should be called with correct parameters')
226
+ })
227
+
228
+ test('build command should pass options to eleventy build', async (t) => {
229
+ const { sandbox, fs, mockLogger, mockReporter } = t.context
230
+
231
+ // Mock eleventy CLI module
232
+ const mockEleventyCli = {
233
+ build: sandbox.stub().resolves({ exitCode: 0 })
234
+ }
235
+
236
+ // Mock eleventy API module
237
+ const mockEleventyApi = {
238
+ build: sandbox.stub().resolves()
239
+ }
240
+
241
+ // Mock clean helper
242
+ const mockClean = sandbox.stub()
243
+
244
+ // Mock testcwd helper
245
+ const mockTestcwd = sandbox.stub()
246
+
247
+ // Use esmock to replace imports
248
+ const BuildCommand = await esmock('./build.js', {
249
+ '#lib/11ty/index.js': {
250
+ cli: mockEleventyCli,
251
+ api: mockEleventyApi
252
+ },
253
+ '#lib/project/index.js': {
254
+ default: {
255
+ getProjectRoot: () => '/project',
256
+ toObject: () => ({ output: '_site' })
257
+ }
258
+ },
259
+ '#helpers/clean.js': {
260
+ clean: mockClean
261
+ },
262
+ '#helpers/test-cwd.js': {
263
+ default: mockTestcwd
264
+ },
265
+ '#lib/logger/index.js': {
266
+ logger: mockLogger
267
+ },
268
+ '#lib/reporter/index.js': {
269
+ default: mockReporter
270
+ },
271
+ 'fs-extra': fs
272
+ })
273
+
274
+ const command = new BuildCommand()
275
+ command.name = sandbox.stub().returns('build')
276
+
277
+ const options = { '11ty': 'cli', verbose: true, quiet: false }
278
+
279
+ // Run action with options
280
+ await command.action(options, command)
281
+
282
+ t.true(mockEleventyCli.build.calledWith(options), 'eleventy CLI build should be called with options')
283
+ })
284
+
285
+ test('build command should call reporter.fail when build fails', async (t) => {
286
+ const { sandbox, fs, mockLogger, mockReporter } = t.context
287
+
288
+ const buildError = new Error('Build failed')
289
+
290
+ // Mock eleventy API module to throw error
291
+ const mockEleventyApi = {
292
+ build: sandbox.stub().rejects(buildError)
293
+ }
294
+
295
+ // Mock eleventy CLI module
296
+ const mockEleventyCli = {
297
+ build: sandbox.stub().resolves({ exitCode: 0 })
298
+ }
299
+
300
+ // Mock clean helper
301
+ const mockClean = sandbox.stub()
302
+
303
+ // Mock testcwd helper
304
+ const mockTestcwd = sandbox.stub()
305
+
306
+ // Use esmock to replace imports
307
+ const BuildCommand = await esmock('./build.js', {
308
+ '#lib/11ty/index.js': {
309
+ api: mockEleventyApi,
310
+ cli: mockEleventyCli
311
+ },
312
+ '#lib/project/index.js': {
313
+ default: {
314
+ getProjectRoot: () => '/project',
315
+ toObject: () => ({ output: '_site' })
316
+ }
317
+ },
318
+ '#helpers/clean.js': {
319
+ clean: mockClean
320
+ },
321
+ '#helpers/test-cwd.js': {
322
+ default: mockTestcwd
323
+ },
324
+ '#lib/logger/index.js': {
325
+ logger: mockLogger
326
+ },
327
+ '#lib/reporter/index.js': {
328
+ default: mockReporter
329
+ },
330
+ 'fs-extra': fs
331
+ })
332
+
333
+ const command = new BuildCommand()
334
+ command.name = sandbox.stub().returns('build')
335
+
336
+ // Run action and expect it to throw
337
+ await t.throwsAsync(() => command.action({ '11ty': 'api' }, command), { message: 'Build failed' })
338
+
339
+ t.true(mockReporter.start.called, 'reporter.start should be called')
340
+ t.true(mockReporter.fail.called, 'reporter.fail should be called on error')
341
+ t.false(mockReporter.succeed.called, 'reporter.succeed should not be called on error')
342
+ })
343
+
344
+ test('build command should configure reporter with quiet option', async (t) => {
345
+ const { sandbox, fs, mockLogger, mockReporter } = t.context
346
+
347
+ // Mock eleventy CLI module
348
+ const mockEleventyCli = {
349
+ build: sandbox.stub().resolves({ exitCode: 0 })
350
+ }
351
+
352
+ // Mock eleventy API module
353
+ const mockEleventyApi = {
354
+ build: sandbox.stub().resolves()
355
+ }
356
+
357
+ // Mock clean helper
358
+ const mockClean = sandbox.stub()
359
+
360
+ // Mock testcwd helper
361
+ const mockTestcwd = sandbox.stub()
362
+
363
+ // Use esmock to replace imports
364
+ const BuildCommand = await esmock('./build.js', {
365
+ '#lib/11ty/index.js': {
366
+ cli: mockEleventyCli,
367
+ api: mockEleventyApi
368
+ },
369
+ '#lib/project/index.js': {
370
+ default: {
371
+ getProjectRoot: () => '/project',
372
+ toObject: () => ({ output: '_site' })
373
+ }
374
+ },
375
+ '#helpers/clean.js': {
376
+ clean: mockClean
377
+ },
378
+ '#helpers/test-cwd.js': {
379
+ default: mockTestcwd
380
+ },
381
+ '#lib/logger/index.js': {
382
+ logger: mockLogger
383
+ },
384
+ '#lib/reporter/index.js': {
385
+ default: mockReporter
386
+ },
387
+ 'fs-extra': fs
388
+ })
389
+
390
+ const command = new BuildCommand()
391
+ command.name = sandbox.stub().returns('build')
392
+
393
+ const options = { '11ty': 'api', quiet: true }
394
+
395
+ // Run action with quiet option
396
+ await command.action(options, command)
397
+
398
+ t.true(
399
+ mockReporter.configure.calledWith(sinon.match({ quiet: true })),
400
+ 'reporter.configure should be called with quiet option'
401
+ )
402
+ })
@@ -1,6 +1,7 @@
1
1
  import Command from '#src/Command.js'
2
+ import { withOutputModes } from '#lib/commander/index.js'
2
3
  import { clean } from '#helpers/clean.js'
3
- import { paths, projectRoot } from '#lib/11ty/index.js'
4
+ import paths from '#lib/project/index.js'
4
5
  import testcwd from '#helpers/test-cwd.js'
5
6
 
6
7
  /**
@@ -15,39 +16,44 @@ import testcwd from '#helpers/test-cwd.js'
15
16
  * @extends {Command}
16
17
  */
17
18
  export default class CleanCommand extends Command {
18
- static definition = {
19
+ static definition = withOutputModes({
19
20
  name: 'clean',
20
21
  description: 'Remove build outputs',
21
- summary: 'remove build outputs',
22
+ summary: 'delete generated files',
23
+ docsLink: 'quire-commands/#output-files',
24
+ helpText: `
25
+ Examples:
26
+ quire clean Remove build outputs
27
+ quire clean --dry-run Preview files to be deleted
28
+ quire clean --verbose Clean with detailed progress
29
+ `,
22
30
  version: '1.0.0',
23
- args: [],
24
31
  options: [
25
- [ '-d', '--dry-run', 'show paths to be cleaned without deleting files' ],
26
- [ '-p', '--progress', 'display progress of removing files' ],
27
- [ '-v', '--verbose', 'run clean with verbose console messages' ],
28
- [ '--debug', 'run clean with debug output to console' ],
32
+ [ '-d, --dry-run', 'show paths to be cleaned without deleting files' ],
33
+ [ '--dryrun', 'alias for --dry-run', { hidden: true, implies: { dryRun: true } } ],
29
34
  ],
30
- }
35
+ })
31
36
 
32
37
  constructor() {
33
38
  super(CleanCommand.definition)
34
39
  }
35
40
 
36
41
  async action(options, command) {
37
- if (options.debug) {
38
- console.debug('[CLI] Command \'%s\' called with options %o', this.name(), options)
39
- }
42
+ this.debug('called with options %O', options)
40
43
 
41
- const deletedPaths = await clean(projectRoot, paths, options)
44
+ const deletedPaths = await clean(paths.getProjectRoot(), paths.toObject(), options)
42
45
 
43
- const message = deletedPaths && deletedPaths.length
44
- ? `the following files ${options.dryRun ? 'will be' : 'have been'} deleted:`
45
- : 'no files to delete'
46
+ if (!deletedPaths || !deletedPaths.length) {
47
+ this.logger.info('No files to delete')
48
+ return
49
+ }
46
50
 
47
- console.debug(`[CLI] ${message}\n${deletedPaths.join('\n')}`)
51
+ const verb = options.dryRun ? 'Will delete' : 'Deleted'
52
+ const listing = deletedPaths.map((p) => ` ${p}`).join('\n')
53
+ this.logger.info(`${verb} ${deletedPaths.length} file(s):\n${listing}`)
48
54
  }
49
55
 
50
- preAction(command) {
51
- testcwd(command)
56
+ preAction(thisCommand, actionCommand) {
57
+ testcwd(thisCommand)
52
58
  }
53
59
  }