@thegetty/quire-cli 1.0.0-rc.35 → 1.0.0-rc.37

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/patches/README.md +19 -0
  6. package/patches/install-npm-version+1.0.9.patch +12119 -0
  7. package/schemas/objects.schema.json +12 -38
  8. package/schemas/publication.schema.json +0 -22
  9. package/schemas/references.schema.json +0 -1
  10. package/src/Command.js +26 -6
  11. package/src/Command.spec.js +99 -0
  12. package/src/commands/README.md +213 -122
  13. package/src/commands/build.js +37 -36
  14. package/src/commands/build.spec.js +113 -0
  15. package/src/commands/build.test.js +397 -0
  16. package/src/commands/clean.js +25 -19
  17. package/src/commands/clean.spec.js +108 -0
  18. package/src/commands/clean.test.js +260 -0
  19. package/src/commands/config.js +251 -0
  20. package/src/commands/config.spec.js +107 -0
  21. package/src/commands/config.test.js +715 -0
  22. package/src/commands/create.js +42 -14
  23. package/src/commands/create.spec.js +112 -0
  24. package/src/commands/create.test.js +415 -0
  25. package/src/commands/epub.js +59 -30
  26. package/src/commands/epub.spec.js +114 -0
  27. package/src/commands/epub.test.js +503 -0
  28. package/src/commands/index.js +9 -2
  29. package/src/commands/info.js +18 -24
  30. package/src/commands/info.spec.js +64 -0
  31. package/src/commands/info.test.js +415 -0
  32. package/src/commands/pdf.js +58 -84
  33. package/src/commands/pdf.spec.js +114 -0
  34. package/src/commands/pdf.test.js +464 -0
  35. package/src/commands/preview.js +32 -31
  36. package/src/commands/preview.spec.js +97 -0
  37. package/src/commands/preview.test.js +250 -0
  38. package/src/commands/use.js +56 -0
  39. package/src/commands/use.spec.js +61 -0
  40. package/src/commands/use.test.js +280 -0
  41. package/src/commands/validate.js +31 -19
  42. package/src/commands/validate.spec.js +82 -0
  43. package/src/commands/validate.test.js +234 -0
  44. package/src/commands/workflows.js +70 -0
  45. package/src/errors/build/build-failed-error.js +19 -0
  46. package/src/errors/build/config-field-missing-error.js +20 -0
  47. package/src/errors/build/config-file-not-found-error.js +20 -0
  48. package/src/errors/build/index.js +11 -0
  49. package/src/errors/index.js +48 -0
  50. package/src/errors/install/dependency-install-error.js +19 -0
  51. package/src/errors/install/directory-not-empty-error.js +25 -0
  52. package/src/errors/install/index.js +12 -0
  53. package/src/errors/install/invalid-path-error.js +31 -0
  54. package/src/errors/install/invalid-starter-error.js +27 -0
  55. package/src/errors/install/version-not-found-error.js +35 -0
  56. package/src/errors/output/epub-generation-error.js +19 -0
  57. package/src/errors/output/index.js +14 -0
  58. package/src/errors/output/invalid-epub-library-error.js +20 -0
  59. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  60. package/src/errors/output/missing-build-output-error.js +21 -0
  61. package/src/errors/output/pdf-generation-error.js +24 -0
  62. package/src/errors/output/tool-not-found-error.js +37 -0
  63. package/src/errors/project/index.js +10 -0
  64. package/src/errors/project/not-in-project-error.js +20 -0
  65. package/src/errors/project/project-create-error.js +21 -0
  66. package/src/errors/quire-error.js +27 -0
  67. package/src/errors/validation/validation-error.js +20 -11
  68. package/src/helpers/clean.js +1 -1
  69. package/src/helpers/docs-url.js +32 -0
  70. package/src/helpers/test-cwd.js +5 -6
  71. package/src/helpers/test-cwd.test.js +192 -0
  72. package/src/helpers/which.js +10 -4
  73. package/src/lib/11ty/README.md +135 -19
  74. package/src/lib/11ty/api.js +196 -94
  75. package/src/lib/11ty/cli.js +91 -37
  76. package/src/lib/11ty/index.js +64 -5
  77. package/src/lib/11ty/index.test.js +655 -0
  78. package/src/lib/README.md +275 -0
  79. package/src/lib/commander/index.js +100 -0
  80. package/src/lib/commander/index.test.js +86 -0
  81. package/src/lib/commander/options.js +195 -0
  82. package/src/lib/commander/options.test.js +109 -0
  83. package/src/lib/conf/README.md +84 -73
  84. package/src/lib/conf/config.js +5 -3
  85. package/src/lib/conf/config.test.js +281 -0
  86. package/src/lib/conf/defaults.js +44 -0
  87. package/src/lib/conf/format.js +60 -0
  88. package/src/lib/conf/format.test.js +106 -0
  89. package/src/lib/conf/helpers.js +91 -0
  90. package/src/lib/conf/helpers.test.js +136 -0
  91. package/src/lib/conf/index.js +22 -0
  92. package/src/lib/conf/schema.js +53 -8
  93. package/src/lib/epub/README.md +133 -2
  94. package/src/lib/epub/engines.js +46 -0
  95. package/src/lib/epub/epub.js +36 -11
  96. package/src/lib/epub/index.js +111 -21
  97. package/src/lib/epub/index.test.js +518 -0
  98. package/src/lib/epub/pandoc.js +33 -4
  99. package/src/lib/epub/pandoc.test.js +122 -0
  100. package/src/lib/epub/schema.js +21 -0
  101. package/src/lib/error/README.md +170 -0
  102. package/src/lib/error/handler.js +107 -0
  103. package/src/lib/git/README.md +151 -2
  104. package/src/lib/git/index.js +217 -13
  105. package/src/lib/git/index.spec.js +80 -0
  106. package/src/lib/git/index.test.js +453 -0
  107. package/src/lib/installer/index.js +309 -0
  108. package/src/lib/installer/index.spec.js +83 -0
  109. package/src/lib/installer/index.test.js +545 -0
  110. package/src/lib/logger/README.md +424 -0
  111. package/src/lib/logger/debug.js +132 -0
  112. package/src/lib/logger/debug.spec.js +130 -0
  113. package/src/lib/logger/debug.test.js +128 -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,133 +1,235 @@
1
+ /**
2
+ * Quire11ty façade for Eleventy integration
3
+ *
4
+ * Provides a unified interface for Eleventy operations with path resolution.
5
+ * Follows the singleton pattern like lib/npm and lib/git for consistency
6
+ * and easy mocking in tests.
7
+ *
8
+ * @example Production usage
9
+ * import eleventy from '#lib/11ty/index.js'
10
+ * await eleventy.build({ debug: true })
11
+ * const outputDir = eleventy.paths.getOutputDir()
12
+ *
13
+ * @example Path-only usage
14
+ * import { paths } from '#lib/11ty/index.js'
15
+ * const projectRoot = paths.getProjectRoot()
16
+ *
17
+ * @example Test mocking
18
+ * const mockEleventy = {
19
+ * build: sandbox.stub().resolves(),
20
+ * serve: sandbox.stub().resolves(),
21
+ * paths: {
22
+ * getProjectRoot: () => '/project',
23
+ * getOutputDir: () => '_site'
24
+ * }
25
+ * }
26
+ * const MyCommand = await esmock('./mycommand.js', {
27
+ * '#lib/11ty/index.js': { default: mockEleventy }
28
+ * })
29
+ *
30
+ * @see https://www.11ty.dev/docs/programmatic/
31
+ * @module lib/11ty
32
+ */
1
33
  import { dynamicImport } from '#helpers/os-utils.js'
2
- import { pathToFileURL } from 'node:url'
3
34
  import path from 'node:path'
4
- import paths, { eleventyRoot, projectRoot } from './paths.js'
35
+ import paths from '#lib/project/index.js'
36
+ import reporter from '#lib/reporter/index.js'
37
+ import createDebug from '#debug'
38
+
39
+ const debug = createDebug('lib:11ty:api')
5
40
 
6
41
  /**
7
- * A factory function to configure an instance of Eleventy
8
- * @see https://www.11ty.dev/docs/config/#configuration-options
42
+ * Configure environment variables required for Eleventy.
43
+ *
44
+ * These must be set before the Eleventy instance is created and the
45
+ * `.eleventy.js` configuration file is parsed.
9
46
  *
10
- * @param {Object} options Eleventy configuration options
11
- * @return {Eleventy} A configured instance of Eleventy
47
+ * @see https://github.com/11ty/eleventy/issues/2655
48
+ *
49
+ * @param {Object} options
50
+ * @param {'production'|'development'} options.mode - Build mode
51
+ * @param {boolean} options.debug - Enable Eleventy debug output
12
52
  */
13
- const factory = async (options = {}) => {
14
- const { config, input, output } = paths
15
-
16
- if (options.debug) {
17
- console.debug('[CLI:11ty] projectRoot %s\n%o', projectRoot, paths)
53
+ const configureEleventyEnv = ({ mode = 'production', debug = false } = {}) => {
54
+ // Path configuration for decoupling quire-11ty from project input directory
55
+ process.env.ELEVENTY_DATA = paths.getDataDir()
56
+ process.env.ELEVENTY_INCLUDES = paths.getIncludesDir()
57
+ process.env.ELEVENTY_LAYOUTS = paths.getLayoutsDir()
58
+
59
+ // Build mode
60
+ process.env.ELEVENTY_ENV = mode
61
+
62
+ // Debug output
63
+ if (debug) {
64
+ process.env.DEBUG = 'Eleventy*'
18
65
  }
66
+ }
19
67
 
20
- /**
21
- * Dynamically import the correct version of Eleventy
22
- */
23
- const modulePath = path.join(eleventyRoot, 'node_modules', '@11ty', 'eleventy', 'src', 'Eleventy.js')
24
- const { default: Eleventy } = await dynamicImport(modulePath)
68
+ /**
69
+ * Create an instance of Eleventy configured for Quire projects
70
+ *
71
+ * @param {Object} options - Eleventy configuration options
72
+ * @param {boolean} [options.debug=false] - Enable debug output
73
+ * @param {boolean} [options.quiet=false] - Suppress output
74
+ * @param {string} [options.config] - Custom config path override
75
+ * @param {'build'|'serve'|'watch'} [options.runMode='build'] - Eleventy run mode
76
+ * @returns {Promise<Eleventy>} Configured Eleventy instance
77
+ */
78
+ const createEleventyInstance = async (options = {}) => {
79
+ const config = paths.getConfigPath()
80
+ const eleventyRoot = paths.getEleventyRoot()
81
+ const input = paths.getInputDir()
82
+ const output = paths.getOutputDir()
83
+ const projectRoot = paths.getProjectRoot()
25
84
 
26
- /**
27
- * Set Eleventy passthrough copy options
28
- * @see https://www.11ty.dev/docs/copy/#advanced-options
29
- * @see https://github.com/timkendrick/recursive-copy
30
- */
31
- const copyOptions = {
32
- debug: options.debug || false
33
- }
85
+ debug('projectRoot: %s', projectRoot)
86
+ debug('paths: %O', paths.toObject())
34
87
 
35
- /**
36
- * Set environment variables for paths relative to eleventy `input` dir,
37
- * to allow `quire-11ty` to be decouple from the project input directory.
38
- * Nota bene: environment variables read into the eleventy configuration
39
- * file _must_ be set before the eleventy configuration file is parsed.
40
- * @see https://github.com/11ty/eleventy/issues/2655
41
- */
42
- process.env.ELEVENTY_DATA = paths.data
43
- process.env.ELEVENTY_INCLUDES = paths.includes
44
- process.env.ELEVENTY_LAYOUTS = paths.layouts
88
+ // Dynamically import the correct version of Eleventy
89
+ const modulePath = path.join(eleventyRoot, 'node_modules', '@11ty', 'eleventy', 'src', 'Eleventy.js')
90
+ const { default: Eleventy } = await dynamicImport(modulePath)
45
91
 
46
- /**
47
- * Get an instance of the runtime of eleventy.
48
- * @see https://github.com/11ty/eleventy/blob/src/Eleventy.js
49
- *
50
- * @param {String} input Path from which to read content templates
51
- * @param {String} output Path where rendered files will be written
52
- * @param {Object} options Options are merged with the Eleventy UserConfig
53
- * @param {Object} config An eleventy configurtion object
54
- *
55
- * @returns {module:11ty/eleventy/Eleventy~Eleventy}
56
- */
92
+ // Create Eleventy instance
93
+ // @see https://github.com/11ty/eleventy/blob/src/Eleventy.js
57
94
  const eleventy = new Eleventy(input, output, {
58
95
  config: (eleventyConfig) => {
59
- /**
60
- * Override addPassthroughCopy to use _absolute_ system paths.
61
- * @see https://www.11ty.dev/docs/copy/#passthrough-file-copy
62
- * Nota bene: Eleventy addPassthroughCopy assumes paths are _relative_
63
- * to the `config` file however the quire-cli separates 11ty from the
64
- * project directory (`input`) and needs to use absolute system paths.
65
- */
66
- const addPassthroughCopy = eleventyConfig.addPassthroughCopy.bind(eleventyConfig)
67
- eleventyConfig.addPassthroughCopy = (entry) => {
68
- if (typeof entry === 'string') {
69
- const filePath = path.resolve(entry) //path.join(projectRoot, file)
70
- // console.debug('[11ty:API] passthrough copy %s', filePath)
71
- return addPassthroughCopy(filePath, copyOptions)
72
- } else {
73
- // console.debug('[11ty:API] passthrough copy %o', entry)
74
- entry = Object.fromEntries(
75
- Object.entries(entry).map(([ src, dest ]) => {
76
- return [ path.join(eleventyRoot, src), path.resolve(dest) ]
77
- })
78
- )
79
- // console.debug('[11ty:API] passthrough copy %o', entry)
80
- return addPassthroughCopy(entry, copyOptions)
81
- }
82
- }
83
-
84
- /**
85
- * Event callback when a build completes
86
- * @see https://www.11ty.dev/docs/events/#eleventy.after
87
- */
96
+ // Event callback when a build completes
97
+ // @see https://www.11ty.dev/docs/events/#eleventy.after
88
98
  eleventyConfig.on('eleventy.after', async () => {
89
- console.debug('[11ty:API] build complete')
99
+ debug('build complete')
90
100
  })
91
101
 
92
102
  return eleventyConfig
93
103
  },
94
104
  configPath: options.config || config,
95
105
  quietMode: options.quiet || false,
106
+ runMode: options.runMode || 'build',
96
107
  })
97
108
 
98
109
  return eleventy
99
110
  }
100
111
 
101
112
  /**
102
- * A wrapper module for using the Eleventy programmatic API
103
- * @see https://www.11ty.dev/docs/programmatic/
104
- * @todo read paths from the Quire project configuration
113
+ * Quire11ty façade class
114
+ *
115
+ * Provides abstracted Eleventy operations with unified logging,
116
+ * error handling, and path resolution. Stores active Eleventy
117
+ * instance for cleanup registration with ProcessManager.
105
118
  */
106
- export default {
107
- build: async (options = {}) => {
108
- process.cwd(projectRoot)
119
+ class Quire11ty {
120
+ /**
121
+ * Create Quire11ty instance
122
+ * @param {Object} pathsInstance - Paths instance for path resolution
123
+ */
124
+ constructor(pathsInstance) {
125
+ this.paths = pathsInstance
126
+ /**
127
+ * Active Eleventy instance (for API mode)
128
+ * @type {Eleventy|null}
129
+ */
130
+ this.activeInstance = null
131
+ }
132
+
133
+ /**
134
+ * Check if an Eleventy process is currently active
135
+ * @returns {boolean}
136
+ */
137
+ isActive() {
138
+ return this.activeInstance !== null
139
+ }
140
+
141
+ /**
142
+ * Gracefully close any active Eleventy process
143
+ *
144
+ * Calls eleventy.close() to stop the dev server and file watchers.
145
+ * This method is registered with ProcessManager for signal handling.
146
+ *
147
+ * @returns {Promise<void>}
148
+ */
149
+ async close() {
150
+ if (this.activeInstance) {
151
+ debug('shutting down Eleventy')
152
+ try {
153
+ await this.activeInstance.close()
154
+ } catch (error) {
155
+ // Ignore errors during shutdown (may already be closing)
156
+ debug('close error (may be expected): %s', error.message)
157
+ }
158
+ this.activeInstance = null
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Run Eleventy production build
164
+ *
165
+ * @param {Object} options - Build options
166
+ * @param {boolean} [options.debug=false] - Enable debug output
167
+ * @param {boolean} [options.dryRun=false] - Perform dry run without writing files
168
+ * @param {boolean} [options.quiet=false] - Suppress output
169
+ * @returns {Promise<void>}
170
+ */
171
+ async build(options = {}) {
172
+ const projectRoot = this.paths.getProjectRoot()
173
+ process.chdir(projectRoot)
174
+
175
+ configureEleventyEnv({ mode: 'production', debug: options.debug })
176
+
177
+ reporter.start('Building site...', { showElapsed: true })
109
178
 
110
- console.info('[CLI:11ty] running eleventy build')
179
+ const eleventy = await createEleventyInstance(options)
111
180
 
112
- process.env.ELEVENTY_ENV = 'production'
113
- if (options.debug) process.env.DEBUG = 'Eleventy*'
181
+ eleventy.setDryRun(options.dryRun)
114
182
 
115
- const eleventy = await factory(options)
183
+ try {
184
+ await eleventy.write()
185
+ reporter.succeed('Build complete')
186
+ } catch (error) {
187
+ reporter.fail('Build failed')
188
+ throw error
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Run Eleventy development server
194
+ *
195
+ * @param {Object} options - Serve options
196
+ * @param {boolean} [options.debug=false] - Enable debug output
197
+ * @param {number} [options.port] - Server port
198
+ * @param {boolean} [options.quiet=false] - Suppress output
199
+ * @returns {Promise<void>}
200
+ */
201
+ async serve(options = {}) {
202
+ const projectRoot = this.paths.getProjectRoot()
203
+ process.chdir(projectRoot)
204
+
205
+ configureEleventyEnv({ mode: 'development', debug: options.debug })
116
206
 
117
- eleventy.setDryRun(options.dryrun)
207
+ reporter.start('Starting development server...')
118
208
 
119
- await eleventy.write()
120
- },
121
- serve: async (options = {}) => {
122
- process.cwd(projectRoot)
209
+ const eleventy =
210
+ await createEleventyInstance({ ...options, runMode: 'serve' })
123
211
 
124
- console.info('[CLI:11ty] running development server')
212
+ // Store reference for lifecycle management (graceful shutdown)
213
+ this.activeInstance = eleventy
125
214
 
126
- process.env.ELEVENTY_ENV = 'development'
127
- if (options.debug) process.env.DEBUG = 'Eleventy*'
215
+ // Initialize Eleventy before serving (required for eleventyServe)
216
+ await eleventy.init()
128
217
 
129
- const eleventy = await factory(options)
218
+ // Register a ready callback to resolve the spinner when the server is listening
219
+ // @see https://www.11ty.dev/docs/dev-server/#options
220
+ eleventy.eleventyServe.config.serverOptions = {
221
+ ...eleventy.eleventyServe.config.serverOptions,
222
+ ready: (server) => {
223
+ const url = server.getServerUrl('localhost')
224
+ reporter.succeed(`Server running at ${url}`)
225
+ if (options.open) {
226
+ import('open').then(({ default: open }) => open(url))
227
+ }
228
+ },
229
+ }
130
230
 
131
231
  await eleventy.serve(options.port)
132
232
  }
133
233
  }
234
+
235
+ export { Quire11ty, createEleventyInstance, configureEleventyEnv }
@@ -1,7 +1,35 @@
1
1
  import { execa } from 'execa'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
- import paths, { eleventyRoot, projectRoot } from './paths.js'
4
+ import paths from '#lib/project/index.js'
5
+ import processManager from '#lib/process/manager.js'
6
+ import reporter from '#lib/reporter/index.js'
7
+ import { BuildFailedError } from '#src/errors/index.js'
8
+ import createDebug from '#debug'
9
+
10
+ const debug = createDebug('lib:11ty')
11
+
12
+ /**
13
+ * Spawn a cancellable subprocess with output piped to stdout
14
+ *
15
+ * @param {string[]} command - Command arguments for node
16
+ * @param {Object} options - Spawn options
17
+ * @param {string} options.cwd - Working directory
18
+ * @param {Object} options.env - Environment variables
19
+ * @returns {import('execa').ExecaChildProcess}
20
+ */
21
+ const spawn = (command, { cwd, env }) => {
22
+ const subprocess = execa('node', command, {
23
+ all: true,
24
+ cwd,
25
+ env,
26
+ execPath: process.execPath,
27
+ cancelSignal: processManager.signal,
28
+ gracefulCancel: true,
29
+ })
30
+ subprocess.all.pipe(process.stdout)
31
+ return subprocess
32
+ }
5
33
 
6
34
  /**
7
35
  * A factory function to configure an Eleventy CLI command
@@ -10,20 +38,21 @@ import paths, { eleventyRoot, projectRoot } from './paths.js'
10
38
  * @return {Array} Eleventy CLI options
11
39
  */
12
40
  const factory = (options = {}) => {
13
- const { config, input, output } = paths
41
+ const config = paths.getConfigPath()
42
+ const eleventyRoot = paths.getEleventyRoot()
43
+ const input = paths.getInputDir()
44
+ const output = paths.getOutputDir()
45
+ const projectRoot = paths.getProjectRoot()
14
46
 
15
- if (options.debug) {
16
- console.debug('[CLI:11ty] projectRoot %s\n%o', projectRoot, paths)
17
- }
47
+ debug('projectRoot: %s', projectRoot)
48
+ debug('paths: %O', paths.toObject())
18
49
 
19
50
  /**
20
51
  * Use the version of Eleventy installed to `lib/quire/versions`
21
- *
22
- * NB: in eleventy v3 the CLI extension (".cjs") is load-bearing but v2 is simply ".js"
23
- *
52
+ * Nota bene: in eleventy v3 the CLI extension (".cjs") is load-bearing but v2 is simply ".js"
24
53
  */
25
54
  const eleventyModuleDir = path.join(eleventyRoot, 'node_modules', '@11ty', 'eleventy')
26
- const packagePath = path.join(eleventyModuleDir,'package.json')
55
+ const packagePath = path.join(eleventyModuleDir,'package.json')
27
56
 
28
57
  const pack = JSON.parse(fs.readFileSync(packagePath))
29
58
 
@@ -59,14 +88,14 @@ const factory = (options = {}) => {
59
88
  * @see https://github.com/11ty/eleventy/issues/2655
60
89
  */
61
90
  const env = {
62
- ELEVENTY_DATA: paths.data,
63
- ELEVENTY_INCLUDES: paths.includes,
64
- ELEVENTY_LAYOUTS: paths.layouts,
91
+ ELEVENTY_DATA: paths.getDataDir(),
92
+ ELEVENTY_INCLUDES: paths.getIncludesDir(),
93
+ ELEVENTY_LAYOUTS: paths.getLayoutsDir(),
65
94
  }
66
95
 
67
96
  if (options.debug) env.DEBUG = 'Eleventy*'
68
97
 
69
- return { command, env }
98
+ return { command, env, projectRoot }
70
99
  }
71
100
 
72
101
  /**
@@ -74,45 +103,70 @@ const factory = (options = {}) => {
74
103
  * @see https://www.11ty.dev/docs/usage/#command-line-usage
75
104
  */
76
105
  export default {
106
+ /**
107
+ * Run Eleventy build via CLI
108
+ * @param {Object} options - Build options
109
+ */
77
110
  build: async (options = {}) => {
78
- console.info('[CLI:11ty] running eleventy build')
79
-
80
- const { command, env } = factory(options)
111
+ const { command, env, projectRoot } = factory(options)
81
112
 
82
113
  if (options.dryRun) command.push('--dryrun')
83
114
 
84
115
  env.ELEVENTY_ENV = 'production'
85
116
 
86
- const build = execa('node', command, {
87
- all: true,
88
- cwd: projectRoot,
89
- env,
90
- execPath: process.execPath
91
- })
92
- build.all.pipe(process.stdout)
93
- await build
94
-
95
- if (build.exitCode !== 0) {
96
- process.exit(build.exitCode)
117
+ reporter.start('Building site...', { showElapsed: true })
118
+
119
+ try {
120
+ const build = spawn(command, { cwd: projectRoot, env })
121
+ await build
122
+
123
+ if (build.exitCode !== 0) {
124
+ throw new BuildFailedError(`Eleventy exited with code ${build.exitCode}`)
125
+ }
126
+ reporter.succeed('Build complete')
127
+ } catch (error) {
128
+ if (error.isCanceled) {
129
+ debug('build cancelled')
130
+ return
131
+ }
132
+ reporter.fail('Build failed')
133
+ throw error
97
134
  }
98
135
  },
99
136
 
137
+ /**
138
+ * Run Eleventy development server via CLI
139
+ * @param {Object} options - Serve options
140
+ */
100
141
  serve: async (options = {}) => {
101
- console.info(`[CLI:11ty] running eleventy serve`)
102
-
103
- const { command, env } = factory(options)
142
+ const { command, env, projectRoot } = factory(options)
104
143
 
105
144
  command.push('--serve')
106
145
 
146
+ const port = options.port || 8080
107
147
  if (options.port) command.push(`--port=${options.port}`)
108
148
 
109
149
  env.ELEVENTY_ENV = 'development'
110
150
 
111
- await execa('node', command, {
112
- all: true,
113
- cwd: projectRoot,
114
- env,
115
- execPath: process.execPath
116
- }).all.pipe(process.stdout)
117
- }
151
+ reporter.start('Starting development server...')
152
+
153
+ // Resolve the spinner before the subprocess takes over stdout
154
+ const url = `http://localhost:${port}`
155
+ reporter.succeed(`Server running at ${url}`)
156
+
157
+ if (options.open) {
158
+ const { default: open } = await import('open')
159
+ open(url)
160
+ }
161
+
162
+ try {
163
+ await spawn(command, { cwd: projectRoot, env })
164
+ } catch (error) {
165
+ if (error.isCanceled) {
166
+ debug('server stopped')
167
+ return
168
+ }
169
+ throw error
170
+ }
171
+ },
118
172
  }
@@ -1,11 +1,70 @@
1
- import cli from './cli.js'
2
- import api from './api.js'
3
- import paths, { eleventyRoot, projectRoot } from './paths.js'
1
+ /**
2
+ * Quire Eleventy integration module
3
+ *
4
+ * Provides a unified façade for Eleventy operations following the singleton
5
+ * pattern established by lib/npm and lib/git. Integrates with the central
6
+ * ProcessManager for graceful shutdown on process signals.
7
+ *
8
+ * @example Recommended usage
9
+ * import eleventy from '#lib/11ty/index.js'
10
+ * await eleventy.build({ debug: true })
11
+ * const outputDir = eleventy.paths.getOutputDir()
12
+ *
13
+ * @example Path-only usage (for consumers that only need paths)
14
+ * import { paths } from '#lib/11ty/index.js'
15
+ * const projectRoot = paths.getProjectRoot()
16
+ *
17
+ * @example Legacy usage (deprecated)
18
+ * import { api, cli, paths } from '#lib/11ty/index.js'
19
+ *
20
+ * @module lib/11ty
21
+ */
22
+ import { Quire11ty } from './api.js'
23
+ import cliModule from './cli.js'
24
+ import paths, { Paths } from '#lib/project/index.js'
25
+ import processManager from '#lib/process/manager.js'
4
26
 
27
+ /**
28
+ * Quire11ty singleton instance
29
+ * @type {Quire11ty}
30
+ */
31
+ const eleventy = new Quire11ty(paths)
32
+
33
+ /**
34
+ * CLI wrapper - delegates to cli.js which imports processManager directly
35
+ */
36
+ const cli = {
37
+ build: async (options) => cliModule.build(options),
38
+ serve: async (options) => cliModule.serve(options),
39
+ }
40
+
41
+ /**
42
+ * Legacy API export for backwards compatibility
43
+ *
44
+ * Wraps the singleton methods to register cleanup handlers with
45
+ * ProcessManager for graceful shutdown support.
46
+ *
47
+ * @deprecated Use default export instead: `import eleventy from '#lib/11ty/index.js'`
48
+ */
49
+ const api = {
50
+ build: async (options) => eleventy.build(options),
51
+ serve: async (options) => {
52
+ processManager.onShutdown('eleventy', () => eleventy.close())
53
+ try {
54
+ return await eleventy.serve(options)
55
+ } finally {
56
+ processManager.onShutdownComplete('eleventy')
57
+ }
58
+ },
59
+ }
60
+
61
+ // Default export: unified façade
62
+ export default eleventy
63
+
64
+ // Named exports for backwards compatibility and path-only consumers
5
65
  export {
6
66
  api,
7
67
  cli,
8
- eleventyRoot,
9
68
  paths,
10
- projectRoot,
69
+ Paths,
11
70
  }