poops 1.2.3 → 1.2.4

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.
package/poops.js CHANGED
@@ -9,11 +9,12 @@ import os from 'node:os'
9
9
  import fs from 'node:fs'
10
10
  import livereload from 'livereload'
11
11
  import Markups from './lib/markups.js'
12
+ import Images from './lib/images.js'
12
13
  import path from 'node:path'
13
14
  import serveStatic from 'serve-static'
14
15
  import Reactor from './lib/reactor.js'
15
16
  import Scripts from './lib/scripts.js'
16
- import log, { styledLog } from './lib/utils/log.js'
17
+ import log, { styledLog, hasLoggedErrors } from './lib/utils/log.js'
17
18
  import Styles from './lib/styles.js'
18
19
  import PostCSS from './lib/postcss.js'
19
20
  import Argoyle from 'argoyle'
@@ -72,7 +73,9 @@ function setupLiveReloadServer(config) {
72
73
 
73
74
  const liveReloadServer = livereload.createServer({
74
75
  exclusions: [...new Set(liveReloadExcludes)],
75
- port: config.livereload_port
76
+ port: config.livereload_port,
77
+ exts: config.livereload.exts, // replaces the default watched extensions
78
+ extraExts: config.livereload.extraExts // adds to the defaults
76
79
  })
77
80
  styledLog(`🔃 {dim}LiveReload :{/} ${liveReloadServer.config.port}`)
78
81
  console.log()
@@ -86,11 +89,13 @@ function setupWatchers(config, modules) {
86
89
  // TODO: ability to automatically create a watch list of directories if watch is set to true. The list will be generated from the `in` property of each task.
87
90
  // awaitWriteFinish: wait for saves to finish writing before recompiling, so a
88
91
  // mid-write (truncated/partial) file is never read. Fixes intermittent broken
89
- // builds on editor save. ponytail: default thresholds are fine; bump if slow disks flake.
90
- chokidar.watch(config.watch, {
91
- ignoreInitial: true,
92
- awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
93
- }).on('change', (file) => {
92
+ // builds on editor save. Bump thresholds if slow disks flake.
93
+ // Shared by 'change' and 'add': editors with atomic saves (rename-write)
94
+ // fire unlink+add instead of change, so both events must trigger the same
95
+ // rebuilds. 'add' also covers genuinely new files (e.g. a new markup page).
96
+ // Rebuild branches shared by change/add/deletion: a deletion needs the same
97
+ // rebuilds (a deleted-but-still-imported file must surface the error).
98
+ const rebuild = (file) => {
94
99
  if (/(\.m?jsx?|\.tsx?)$/i.test(file)) {
95
100
  modules.scripts.compile().catch(err => console.error(err))
96
101
 
@@ -110,26 +115,79 @@ function setupWatchers(config, modules) {
110
115
  modules.markups.compile().then(() => modules.postcss.compile()).catch(err => console.error(err))
111
116
  }
112
117
 
113
- // TODO: We can actually reload the page only if the data file from data has changed.
114
118
  if (/(\.json|\.ya?ml)$/i.test(file)) {
115
119
  modules.markups.reloadDataFiles().then(() => modules.markups.compile()).catch(err => console.error(err))
116
120
  }
121
+ }
117
122
 
123
+ // Source image extensions handled by poops-images. The doesFileBelongToPath
124
+ // guard (against config.images.in) also breaks the feedback loop: generated
125
+ // variants land in the `out` dir, never in `in`, so they don't retrigger.
126
+ const imageExtRe = /(\.jpe?g|\.png|\.tiff?|\.webp|\.heic|\.heif|\.svg|\.gif)$/i
127
+ const belongsToImages = (file) => imageExtRe.test(file) && doesFileBelongToPath(file, config.images)
128
+
129
+ const compileChanged = (file) => {
130
+ rebuild(file)
131
+ if (belongsToImages(file)) {
132
+ modules.images.compile()
133
+ .then(() => modules.markups.compile())
134
+ .then(() => modules.postcss.compile())
135
+ .catch(err => console.error(err))
136
+ }
118
137
  doesFileBelongToPath(file, config.copy) && modules.copy.execute().catch(err => console.error(err))
119
- }).on('unlink', (file) => {
120
- if (/(\.html|\.xml|\.rss|\.atom|\.njk|\.liquid|\.md)$/i.test(file)) {
121
- modules.markups.compile().catch(err => console.error(err))
138
+ }
139
+
140
+ // Atomic-save editors (rename-write) fire unlink+add for every save, so an
141
+ // unlink only counts as a real deletion if no add for the same path follows
142
+ // within the settle window. Must outlive the 150ms awaitWriteFinish delay
143
+ // on 'add'. Fixed 300ms; make configurable if slow disks flake.
144
+ const UNLINK_SETTLE_MS = 300
145
+ const pendingUnlinks = new Map()
146
+
147
+ const scheduleUnlink = (target, handler) => {
148
+ clearTimeout(pendingUnlinks.get(target))
149
+ pendingUnlinks.set(target, setTimeout(() => {
150
+ pendingUnlinks.delete(target)
151
+ handler(target)
152
+ }, UNLINK_SETTLE_MS))
153
+ }
154
+
155
+ const handleDeleted = (file) => {
156
+ modules.markups.removeOutput(file)
157
+ if (belongsToImages(file)) {
158
+ // Deleted source: drop its variants + cache entry, then recompile markup
159
+ // so galleries/srcsets reading the image cache no longer reference it.
160
+ modules.images.remove(file)
161
+ .then(() => modules.markups.compile())
162
+ .then(() => modules.postcss.compile())
163
+ .catch(err => console.error(err))
122
164
  }
165
+ rebuild(file)
123
166
  modules.copy.unlink(file, doesFileBelongToPath(file, config.copy))
124
- }).on('unlinkDir', (dirPath) => {
125
- doesFileBelongToPath(dirPath, config.markup) && modules.markups.compile().catch(err => console.error(err))
126
- modules.copy.unlink(dirPath, doesFileBelongToPath(dirPath, config.copy))
127
- }).on('add', (file) => {
128
- if (/(\.json|\.ya?ml)$/i.test(file)) {
129
- modules.markups.reloadDataFiles().then(() => modules.markups.compile()).catch(err => console.error(err))
167
+ }
168
+
169
+ const handleDeletedDir = (dirPath) => {
170
+ modules.markups.removeOutput(dirPath)
171
+ if (doesFileBelongToPath(dirPath, config.markup)) {
172
+ modules.markups.compile().then(() => modules.postcss.compile()).catch(err => console.error(err))
130
173
  }
131
- doesFileBelongToPath(file, config.copy) && modules.copy.execute().catch(err => console.error(err))
132
- })
174
+ modules.copy.unlink(dirPath, doesFileBelongToPath(dirPath, config.copy))
175
+ }
176
+
177
+ chokidar.watch(config.watch, {
178
+ ignoreInitial: true,
179
+ awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
180
+ }).on('change', compileChanged)
181
+ .on('add', (file) => {
182
+ const pending = pendingUnlinks.get(file)
183
+ if (pending) {
184
+ clearTimeout(pending)
185
+ pendingUnlinks.delete(file)
186
+ }
187
+ compileChanged(file)
188
+ })
189
+ .on('unlink', (file) => scheduleUnlink(file, handleDeleted))
190
+ .on('unlinkDir', (dirPath) => scheduleUnlink(dirPath, handleDeletedDir))
133
191
  }
134
192
 
135
193
  // Main function 💩
@@ -138,22 +196,32 @@ async function poops() {
138
196
  const postcss = new PostCSS(config)
139
197
  const reactor = new Reactor(config)
140
198
  const scripts = new Scripts(config)
199
+ const images = new Images(config)
141
200
  const markups = new Markups(config)
142
201
  const copy = new Copy(config)
143
202
 
144
- try { await styles.compile() } catch (err) { console.error(err) }
145
- try { await reactor.compile() } catch (err) { console.error(err) }
203
+ // Thrown errors are caught so one failing step doesn't stop the rest;
204
+ // `failed` + hasLoggedErrors() (module-internal, swallowed errors) decide
205
+ // the build exit code, so a broken compile can't ship as a green build.
206
+ let failed = false
207
+ const step = async(task) => {
208
+ try { await task() } catch (err) { failed = true; console.error(err) }
209
+ }
210
+
211
+ await step(() => styles.compile())
212
+ await step(() => reactor.compile())
146
213
  config.reactorData = reactor.getRendered()
147
- try { await scripts.compile() } catch (err) { console.error(err) }
148
- try { await markups.compile() } catch (err) { console.error(err) }
149
- try { await postcss.compile() } catch (err) { console.error(err) }
150
- try { await copy.execute() } catch (err) { console.error(err) }
214
+ await step(() => scripts.compile())
215
+ await step(() => images.compile()) // before markups: engines read the poops-images cache
216
+ await step(() => markups.compile())
217
+ await step(() => postcss.compile())
218
+ await step(() => copy.execute())
151
219
 
152
220
  if (build || (!config.watch && !config.livereload && !config.serve)) {
153
- process.exit(0)
221
+ process.exit(failed || hasLoggedErrors() ? 1 : 0)
154
222
  }
155
223
 
156
- setupWatchers(config, { styles, postcss, reactor, scripts, markups, copy })
224
+ setupWatchers(config, { styles, postcss, reactor, scripts, images, markups, copy })
157
225
  }
158
226
 
159
227
  // CLI Header
@@ -191,6 +259,12 @@ if (overrideBaseURL && config.markup) {
191
259
  config.markup.baseURL = overrideBaseURL
192
260
  }
193
261
 
262
+ // poops-images resolves custom handlers/composites relative to the config file;
263
+ // without this it would default to cwd. Matches poops-images' own loadConfig.
264
+ if (config.images && typeof config.images === 'object' && config.images.configDir === undefined) {
265
+ config.images.configDir = path.dirname(configPath)
266
+ }
267
+
194
268
  async function getAvailablePort(port, max) {
195
269
  while (port < max) {
196
270
  const status = await portscanner.checkPortStatus(port, 'localhost')
@@ -250,6 +324,11 @@ async function startServer() {
250
324
  // Start the webserver
251
325
  if (!build && config.serve) {
252
326
  startServer()
327
+ } else if (!build && config.livereload) {
328
+ // livereload without serve: still needs the livereload server
329
+ resolveLiveReloadPort(config)
330
+ .then(poops)
331
+ .then(() => setupLiveReloadServer(config))
253
332
  } else {
254
333
  poops()
255
334
  }