poops 1.2.3 → 1.3.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.
- package/README.md +395 -70
- package/lib/copy.js +5 -3
- package/lib/images.js +57 -0
- package/lib/markup/collections.js +39 -17
- package/lib/markup/engines/liquid.js +34 -33
- package/lib/markup/engines/nunjucks.js +44 -36
- package/lib/markup/helpers.js +164 -37
- package/lib/markup/image-cache.js +114 -0
- package/lib/markup/indexer.js +158 -2
- package/lib/markup/renderer.js +60 -0
- package/lib/markups.js +199 -49
- package/lib/postcss.js +16 -30
- package/lib/reactor.js +11 -29
- package/lib/scripts.js +37 -34
- package/lib/styles.js +17 -31
- package/lib/utils/helpers.js +20 -16
- package/lib/utils/log.js +15 -1
- package/lib/utils/minify.js +41 -0
- package/package.json +7 -3
- package/poops.js +120 -32
package/lib/styles.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { transform } from 'esbuild'
|
|
2
1
|
import fs from 'node:fs'
|
|
3
2
|
import {
|
|
4
3
|
pathExists,
|
|
5
4
|
mkPath,
|
|
6
|
-
insertMinSuffix,
|
|
7
5
|
buildStyleOutputFilePath,
|
|
8
6
|
fillBannerTemplate,
|
|
9
7
|
buildTime,
|
|
10
8
|
fileSize
|
|
11
9
|
} from './utils/helpers.js'
|
|
10
|
+
import minifyToFile from './utils/minify.js'
|
|
12
11
|
import path from 'node:path'
|
|
13
12
|
import * as sass from 'sass'
|
|
14
13
|
import log from './utils/log.js'
|
|
@@ -25,10 +24,13 @@ export default class Styles {
|
|
|
25
24
|
if (!this.config.styles) return
|
|
26
25
|
this.config.styles = Array.isArray(this.config.styles) ? this.config.styles : [this.config.styles]
|
|
27
26
|
for (const styleEntry of this.config.styles) {
|
|
28
|
-
if (styleEntry.in
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
if (!styleEntry.in || !styleEntry.out) continue
|
|
28
|
+
if (!pathExists(styleEntry.in)) {
|
|
29
|
+
log({ tag: 'style', error: true, text: 'Entry does not exist:', link: styleEntry.in })
|
|
30
|
+
continue
|
|
31
31
|
}
|
|
32
|
+
mkPath(styleEntry.out)
|
|
33
|
+
await this.compileEntry(styleEntry.in, styleEntry.out, styleEntry.options)
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -55,7 +57,7 @@ export default class Styles {
|
|
|
55
57
|
opts.sourceMapIncludeSources = options.sourcemap
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
outfilePath = buildStyleOutputFilePath(infilePath, outfilePath
|
|
60
|
+
outfilePath = buildStyleOutputFilePath(infilePath, outfilePath)
|
|
59
61
|
|
|
60
62
|
const stylesStart = performance.now()
|
|
61
63
|
let compiledSass
|
|
@@ -79,30 +81,14 @@ export default class Styles {
|
|
|
79
81
|
log({ tag: 'style', text: 'Compiled:', link: `${outfilePath}.map` })
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (this.banner) minified.code = this.banner + '\n' + minified.code
|
|
93
|
-
fs.writeFileSync(minPath, minified.code)
|
|
94
|
-
const stylesMinEnd = performance.now()
|
|
95
|
-
log({ tag: 'style', text: 'Compiled:', link: minPath, size: fileSize(minPath), time: buildTime(stylesMinStart, stylesMinEnd) })
|
|
96
|
-
} catch (err) {
|
|
97
|
-
log({ tag: 'style', error: true, text: 'Failed compiling:', link: minPath })
|
|
98
|
-
console.error(err)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (options.justMinified) {
|
|
102
|
-
fs.unlinkSync(outfilePath)
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
if (pathExists(minPath)) fs.unlinkSync(minPath)
|
|
106
|
-
}
|
|
84
|
+
await minifyToFile({
|
|
85
|
+
outfilePath,
|
|
86
|
+
loader: 'css',
|
|
87
|
+
code: compiledSass.css,
|
|
88
|
+
banner: this.banner,
|
|
89
|
+
tag: 'style',
|
|
90
|
+
options,
|
|
91
|
+
startTime: options.justMinified ? stylesStart : undefined
|
|
92
|
+
})
|
|
107
93
|
}
|
|
108
94
|
}
|
package/lib/utils/helpers.js
CHANGED
|
@@ -4,6 +4,10 @@ import path from 'node:path'
|
|
|
4
4
|
import yaml from 'yaml'
|
|
5
5
|
import { convertGlobToRegex } from 'book-of-spells'
|
|
6
6
|
|
|
7
|
+
export function toPosix(filePath) {
|
|
8
|
+
return path.sep === '/' ? filePath : filePath.split(path.sep).join('/')
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export function pathExists() {
|
|
8
12
|
return fs.existsSync(path.join(...arguments))
|
|
9
13
|
}
|
|
@@ -36,13 +40,6 @@ export function buildStyleOutputFilePath(inputPath, outputPath) {
|
|
|
36
40
|
return path.join(path.join(outputPath, `${name}.css`))
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
export function buildScriptOutputFilePath(inputPath, outputPath) {
|
|
40
|
-
if (pathForFile(outputPath)) return outputPath
|
|
41
|
-
const { name, ext } = path.parse(inputPath)
|
|
42
|
-
const outExt = /\.(tsx?|jsx)$/i.test(ext) ? '.js' : ext
|
|
43
|
-
return path.join(outputPath, `${name}${outExt}`)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
43
|
export function fillBannerTemplate(template, packagesPath) {
|
|
47
44
|
packagesPath = packagesPath || process.cwd()
|
|
48
45
|
const packagesFilePath = path.join(packagesPath, 'package.json')
|
|
@@ -63,13 +60,14 @@ export function fillBannerTemplate(template, packagesPath) {
|
|
|
63
60
|
|
|
64
61
|
export function buildTime(start, end) {
|
|
65
62
|
const time = Math.round(end - start)
|
|
66
|
-
|
|
67
|
-
const seconds = Math.floor(time / 1000)
|
|
63
|
+
const minutes = Math.floor(time / 60000)
|
|
64
|
+
const seconds = Math.floor(time / 1000) % 60
|
|
68
65
|
const ms = time % 1000
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
const parts = []
|
|
67
|
+
if (minutes) parts.push(`${minutes}m`)
|
|
68
|
+
if (seconds) parts.push(`${seconds}s`)
|
|
69
|
+
if (ms) parts.push(`${ms}ms`)
|
|
70
|
+
return parts.join(' ') || '0ms'
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
export function fileSize(filePath) {
|
|
@@ -77,8 +75,12 @@ export function fileSize(filePath) {
|
|
|
77
75
|
const fileSizeInBytes = stats.size
|
|
78
76
|
if (fileSizeInBytes < 1000) return `${fileSizeInBytes}B`
|
|
79
77
|
if (fileSizeInBytes < 1000 * 1000) return `${(fileSizeInBytes / 1000).toFixed(0)}KB`
|
|
80
|
-
if (fileSizeInBytes < 1000 * 1000 * 1000)
|
|
81
|
-
|
|
78
|
+
if (fileSizeInBytes < 1000 * 1000 * 1000) {
|
|
79
|
+
const kb = Math.floor((fileSizeInBytes % (1000 * 1000)) / 1000)
|
|
80
|
+
return `${Math.floor(fileSizeInBytes / 1000 / 1000)}MB${kb ? ` ${kb}KB` : ''}`
|
|
81
|
+
}
|
|
82
|
+
const mb = Math.floor((fileSizeInBytes % (1000 * 1000 * 1000)) / 1000 / 1000)
|
|
83
|
+
return `${Math.floor(fileSizeInBytes / 1000 / 1000 / 1000)}GB${mb ? ` ${mb}MB` : ''}`
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
export function readJsonFile(filePath) {
|
|
@@ -146,10 +148,12 @@ export function copyDirectory(src, dest) {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
export function stripDirNavSegments(filePath) {
|
|
149
|
-
return path.normalize(filePath).replace(/(\.\.\/|\.\/|\/\.\.|\.\.\\|\.\\|\\\.\.)/g, '')
|
|
151
|
+
return toPosix(path.normalize(filePath)).replace(/(\.\.\/|\.\/|\/\.\.|\.\.\\|\.\\|\\\.\.)/g, '')
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
export function pathContainsPathSegment(filePath, segment) {
|
|
155
|
+
// Watcher paths arrive with native separators, config segments with `/`
|
|
156
|
+
filePath = toPosix(filePath)
|
|
153
157
|
segment = stripDirNavSegments(segment)
|
|
154
158
|
if (hasMagic(segment)) {
|
|
155
159
|
segment = convertGlobToRegex(segment)
|
package/lib/utils/log.js
CHANGED
|
@@ -8,6 +8,7 @@ const TAG_COLORS = {
|
|
|
8
8
|
style: 'magentaBright',
|
|
9
9
|
markup: 'cyanBright',
|
|
10
10
|
copy: 'green',
|
|
11
|
+
image: 'greenBright',
|
|
11
12
|
info: 'blue',
|
|
12
13
|
error: 'redBright'
|
|
13
14
|
}
|
|
@@ -18,18 +19,31 @@ const DEFAULT_COLOR = 'white'
|
|
|
18
19
|
* @param {Object} options
|
|
19
20
|
* @param {string} options.tag - Tag label (e.g. 'script', 'style', 'markup', 'reactor', 'copy', 'info')
|
|
20
21
|
* @param {boolean} [options.error] - Whether this is an error message
|
|
22
|
+
* @param {boolean} [options.warn] - Whether this is a warning (does not fail the build)
|
|
21
23
|
* @param {string} options.text - Main message text
|
|
22
24
|
* @param {string} [options.link] - File path or URL to display underlined
|
|
23
25
|
* @param {string} [options.size] - File size string
|
|
24
26
|
* @param {string} [options.time] - Build time string
|
|
25
27
|
*/
|
|
26
|
-
|
|
28
|
+
let errorLogged = false
|
|
29
|
+
|
|
30
|
+
// Build mode exits non-zero if any module reported an error, even ones
|
|
31
|
+
// swallowed internally (log-and-continue). log() is the single point
|
|
32
|
+
// every module error passes through, so the flag lives here.
|
|
33
|
+
export function hasLoggedErrors() {
|
|
34
|
+
return errorLogged
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function log({ tag, error, warn, text, link, size, time }) {
|
|
27
38
|
if (tag === 'error') error = true
|
|
39
|
+
if (error) errorLogged = true
|
|
28
40
|
const color = TAG_COLORS[tag] || DEFAULT_COLOR
|
|
29
41
|
let msg = pstyle.paint(`{${color}.bold|[${tag}]}`)
|
|
30
42
|
|
|
31
43
|
if (error) {
|
|
32
44
|
msg += pstyle.paint(' {redBright.bold|[error]}')
|
|
45
|
+
} else if (warn) {
|
|
46
|
+
msg += pstyle.paint(' {yellowBright.bold|[warn]}')
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
msg += pstyle.paint(` {dim|${text}}`)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { transform } from 'esbuild'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { pathExists, insertMinSuffix, fileSize, buildTime } from './helpers.js'
|
|
4
|
+
import log from './log.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared post-compile minify step: writes `<name>.min<ext>` next to
|
|
8
|
+
* `outfilePath`, deletes the unminified file when `options.justMinified`,
|
|
9
|
+
* or deletes a stale `.min` file when minify is off.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} params
|
|
12
|
+
* @param {string} params.outfilePath - Compiled (unminified) output file
|
|
13
|
+
* @param {'js'|'css'} params.loader - esbuild transform loader
|
|
14
|
+
* @param {string} [params.code] - Source to minify; read from outfilePath when omitted
|
|
15
|
+
* @param {string} [params.banner] - Banner prepended to the minified code
|
|
16
|
+
* @param {string} params.tag - Log tag
|
|
17
|
+
* @param {Object} [params.options] - Entry options (`minify`, `justMinified`)
|
|
18
|
+
* @param {number} [params.startTime] - Timing start override (justMinified logs include compile time)
|
|
19
|
+
*/
|
|
20
|
+
export default async function minifyToFile({ outfilePath, loader, code, banner, tag, options = {}, startTime }) {
|
|
21
|
+
const minPath = insertMinSuffix(outfilePath)
|
|
22
|
+
|
|
23
|
+
if (!options.minify) {
|
|
24
|
+
if (pathExists(minPath)) fs.unlinkSync(minPath)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const start = startTime ?? performance.now()
|
|
30
|
+
if (code == null) code = fs.readFileSync(outfilePath, 'utf-8')
|
|
31
|
+
const minified = await transform(code, { loader, minify: true })
|
|
32
|
+
if (banner) minified.code = banner + '\n' + minified.code
|
|
33
|
+
fs.writeFileSync(minPath, minified.code)
|
|
34
|
+
log({ tag, text: 'Compiled:', link: minPath, size: fileSize(minPath), time: buildTime(start, performance.now()) })
|
|
35
|
+
} catch (err) {
|
|
36
|
+
log({ tag, error: true, text: 'Failed compiling:', link: minPath })
|
|
37
|
+
console.error(err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.justMinified) fs.unlinkSync(outfilePath)
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poops",
|
|
3
3
|
"description": "Straightforward, no-bullshit bundler for the web.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "poops.js",
|
|
@@ -34,16 +34,20 @@
|
|
|
34
34
|
"static-site-generator"
|
|
35
35
|
],
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"postcss": "^8.0.0"
|
|
37
|
+
"postcss": "^8.0.0",
|
|
38
|
+
"poops-images": ">=1.2.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependenciesMeta": {
|
|
40
41
|
"postcss": {
|
|
41
42
|
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"poops-images": {
|
|
45
|
+
"optional": true
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"scripts": {
|
|
45
49
|
"build": "node ./poops.js -b",
|
|
46
|
-
"lint": "eslint
|
|
50
|
+
"lint": "eslint .",
|
|
47
51
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest"
|
|
48
52
|
},
|
|
49
53
|
"dependencies": {
|
package/poops.js
CHANGED
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
import chokidar from 'chokidar'
|
|
4
4
|
import connect from 'connect'
|
|
5
5
|
import Copy from './lib/copy.js'
|
|
6
|
-
import { pathExists, doesFileBelongToPath } from './lib/utils/helpers.js'
|
|
6
|
+
import { pathExists, doesFileBelongToPath, pathContainsPathSegment } from './lib/utils/helpers.js'
|
|
7
7
|
import http from 'node:http'
|
|
8
8
|
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()
|
|
@@ -82,16 +85,27 @@ function setupLiveReloadServer(config) {
|
|
|
82
85
|
function setupWatchers(config, modules) {
|
|
83
86
|
if (!config.watch) return
|
|
84
87
|
|
|
85
|
-
// TODO: think about watching the updates of the config file itself, we can reload the config and recompile everything.
|
|
86
|
-
// 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
88
|
// awaitWriteFinish: wait for saves to finish writing before recompiling, so a
|
|
88
89
|
// mid-write (truncated/partial) file is never read. Fixes intermittent broken
|
|
89
|
-
// builds on editor save.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
// builds on editor save. Bump thresholds if slow disks flake.
|
|
91
|
+
// Shared by 'change' and 'add': editors with atomic saves (rename-write)
|
|
92
|
+
// fire unlink+add instead of change, so both events must trigger the same
|
|
93
|
+
// rebuilds. 'add' also covers genuinely new files (e.g. a new markup page).
|
|
94
|
+
// Rebuild branches shared by change/add/deletion: a deletion needs the same
|
|
95
|
+
// rebuilds (a deleted-but-still-imported file must surface the error).
|
|
96
|
+
// Scripts/styles outputs can land inside a watched dir (e.g. a Shopify
|
|
97
|
+
// theme's assets/): the compiler's own write must not retrigger that
|
|
98
|
+
// compiler, or watch loops forever. Zones are the dirs the compilers write
|
|
99
|
+
// into; only their own extensions are skipped there, so e.g. a hand-edited
|
|
100
|
+
// .liquid asset in the same dir still rebuilds markup.
|
|
101
|
+
const outputZones = [config.scripts, config.styles].flat()
|
|
102
|
+
.filter((entry) => entry && entry.out)
|
|
103
|
+
.map((entry) => (path.extname(entry.out) ? path.dirname(entry.out) : entry.out))
|
|
104
|
+
.filter((zone) => zone && zone !== '.')
|
|
105
|
+
const isBuildOutput = (file) => outputZones.some((zone) => pathContainsPathSegment(file, zone))
|
|
106
|
+
|
|
107
|
+
const rebuild = (file) => {
|
|
108
|
+
if (/(\.m?jsx?|\.tsx?)$/i.test(file) && !isBuildOutput(file)) {
|
|
95
109
|
modules.scripts.compile().catch(err => console.error(err))
|
|
96
110
|
|
|
97
111
|
if (modules.reactor.belongsToReactor(file)) {
|
|
@@ -103,33 +117,86 @@ function setupWatchers(config, modules) {
|
|
|
103
117
|
}).catch(err => console.error(err))
|
|
104
118
|
}
|
|
105
119
|
}
|
|
106
|
-
if (/(\.sass|\.scss|\.css)$/i.test(file)) {
|
|
120
|
+
if (/(\.sass|\.scss|\.css)$/i.test(file) && !isBuildOutput(file)) {
|
|
107
121
|
modules.styles.compile().then(() => modules.postcss.compile()).catch(err => console.error(err))
|
|
108
122
|
}
|
|
109
123
|
if (/(\.html|\.xml|\.rss|\.atom|\.njk|\.liquid|\.md)$/i.test(file)) {
|
|
110
124
|
modules.markups.compile().then(() => modules.postcss.compile()).catch(err => console.error(err))
|
|
111
125
|
}
|
|
112
126
|
|
|
113
|
-
// TODO: We can actually reload the page only if the data file from data has changed.
|
|
114
127
|
if (/(\.json|\.ya?ml)$/i.test(file)) {
|
|
115
128
|
modules.markups.reloadDataFiles().then(() => modules.markups.compile()).catch(err => console.error(err))
|
|
116
129
|
}
|
|
130
|
+
}
|
|
117
131
|
|
|
132
|
+
// Source image extensions handled by poops-images. The doesFileBelongToPath
|
|
133
|
+
// guard (against config.images.in) also breaks the feedback loop: generated
|
|
134
|
+
// variants land in the `out` dir, never in `in`, so they don't retrigger.
|
|
135
|
+
const imageExtRe = /(\.jpe?g|\.png|\.tiff?|\.webp|\.heic|\.heif|\.svg|\.gif)$/i
|
|
136
|
+
const belongsToImages = (file) => imageExtRe.test(file) && doesFileBelongToPath(file, config.images)
|
|
137
|
+
|
|
138
|
+
const compileChanged = (file) => {
|
|
139
|
+
rebuild(file)
|
|
140
|
+
if (belongsToImages(file)) {
|
|
141
|
+
modules.images.compile()
|
|
142
|
+
.then(() => modules.markups.compile())
|
|
143
|
+
.then(() => modules.postcss.compile())
|
|
144
|
+
.catch(err => console.error(err))
|
|
145
|
+
}
|
|
118
146
|
doesFileBelongToPath(file, config.copy) && modules.copy.execute().catch(err => console.error(err))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Atomic-save editors (rename-write) fire unlink+add for every save, so an
|
|
150
|
+
// unlink only counts as a real deletion if no add for the same path follows
|
|
151
|
+
// within the settle window. Must outlive the 150ms awaitWriteFinish delay
|
|
152
|
+
// on 'add'. Fixed 300ms; make configurable if slow disks flake.
|
|
153
|
+
const UNLINK_SETTLE_MS = 300
|
|
154
|
+
const pendingUnlinks = new Map()
|
|
155
|
+
|
|
156
|
+
const scheduleUnlink = (target, handler) => {
|
|
157
|
+
clearTimeout(pendingUnlinks.get(target))
|
|
158
|
+
pendingUnlinks.set(target, setTimeout(() => {
|
|
159
|
+
pendingUnlinks.delete(target)
|
|
160
|
+
handler(target)
|
|
161
|
+
}, UNLINK_SETTLE_MS))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const handleDeleted = (file) => {
|
|
165
|
+
modules.markups.removeOutput(file)
|
|
166
|
+
if (belongsToImages(file)) {
|
|
167
|
+
// Deleted source: drop its variants + cache entry, then recompile markup
|
|
168
|
+
// so galleries/srcsets reading the image cache no longer reference it.
|
|
169
|
+
modules.images.remove(file)
|
|
170
|
+
.then(() => modules.markups.compile())
|
|
171
|
+
.then(() => modules.postcss.compile())
|
|
172
|
+
.catch(err => console.error(err))
|
|
122
173
|
}
|
|
174
|
+
rebuild(file)
|
|
123
175
|
modules.copy.unlink(file, doesFileBelongToPath(file, config.copy))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
modules.markups.
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handleDeletedDir = (dirPath) => {
|
|
179
|
+
modules.markups.removeOutput(dirPath)
|
|
180
|
+
if (doesFileBelongToPath(dirPath, config.markup)) {
|
|
181
|
+
modules.markups.compile().then(() => modules.postcss.compile()).catch(err => console.error(err))
|
|
130
182
|
}
|
|
131
|
-
|
|
132
|
-
}
|
|
183
|
+
modules.copy.unlink(dirPath, doesFileBelongToPath(dirPath, config.copy))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
chokidar.watch(config.watch, {
|
|
187
|
+
ignoreInitial: true,
|
|
188
|
+
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
|
|
189
|
+
}).on('change', compileChanged)
|
|
190
|
+
.on('add', (file) => {
|
|
191
|
+
const pending = pendingUnlinks.get(file)
|
|
192
|
+
if (pending) {
|
|
193
|
+
clearTimeout(pending)
|
|
194
|
+
pendingUnlinks.delete(file)
|
|
195
|
+
}
|
|
196
|
+
compileChanged(file)
|
|
197
|
+
})
|
|
198
|
+
.on('unlink', (file) => scheduleUnlink(file, handleDeleted))
|
|
199
|
+
.on('unlinkDir', (dirPath) => scheduleUnlink(dirPath, handleDeletedDir))
|
|
133
200
|
}
|
|
134
201
|
|
|
135
202
|
// Main function 💩
|
|
@@ -138,22 +205,32 @@ async function poops() {
|
|
|
138
205
|
const postcss = new PostCSS(config)
|
|
139
206
|
const reactor = new Reactor(config)
|
|
140
207
|
const scripts = new Scripts(config)
|
|
208
|
+
const images = new Images(config)
|
|
141
209
|
const markups = new Markups(config)
|
|
142
210
|
const copy = new Copy(config)
|
|
143
211
|
|
|
144
|
-
|
|
145
|
-
|
|
212
|
+
// Thrown errors are caught so one failing step doesn't stop the rest;
|
|
213
|
+
// `failed` + hasLoggedErrors() (module-internal, swallowed errors) decide
|
|
214
|
+
// the build exit code, so a broken compile can't ship as a green build.
|
|
215
|
+
let failed = false
|
|
216
|
+
const step = async(task) => {
|
|
217
|
+
try { await task() } catch (err) { failed = true; console.error(err) }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await step(() => styles.compile())
|
|
221
|
+
await step(() => reactor.compile())
|
|
146
222
|
config.reactorData = reactor.getRendered()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
223
|
+
await step(() => scripts.compile())
|
|
224
|
+
await step(() => images.compile()) // before markups: engines read the poops-images cache
|
|
225
|
+
await step(() => markups.compile())
|
|
226
|
+
await step(() => postcss.compile())
|
|
227
|
+
await step(() => copy.execute())
|
|
151
228
|
|
|
152
229
|
if (build || (!config.watch && !config.livereload && !config.serve)) {
|
|
153
|
-
process.exit(0)
|
|
230
|
+
process.exit(failed || hasLoggedErrors() ? 1 : 0)
|
|
154
231
|
}
|
|
155
232
|
|
|
156
|
-
setupWatchers(config, { styles, postcss, reactor, scripts, markups, copy })
|
|
233
|
+
setupWatchers(config, { styles, postcss, reactor, scripts, images, markups, copy })
|
|
157
234
|
}
|
|
158
235
|
|
|
159
236
|
// CLI Header
|
|
@@ -191,6 +268,12 @@ if (overrideBaseURL && config.markup) {
|
|
|
191
268
|
config.markup.baseURL = overrideBaseURL
|
|
192
269
|
}
|
|
193
270
|
|
|
271
|
+
// poops-images resolves custom handlers/composites relative to the config file;
|
|
272
|
+
// without this it would default to cwd. Matches poops-images' own loadConfig.
|
|
273
|
+
if (config.images && typeof config.images === 'object' && config.images.configDir === undefined) {
|
|
274
|
+
config.images.configDir = path.dirname(configPath)
|
|
275
|
+
}
|
|
276
|
+
|
|
194
277
|
async function getAvailablePort(port, max) {
|
|
195
278
|
while (port < max) {
|
|
196
279
|
const status = await portscanner.checkPortStatus(port, 'localhost')
|
|
@@ -250,6 +333,11 @@ async function startServer() {
|
|
|
250
333
|
// Start the webserver
|
|
251
334
|
if (!build && config.serve) {
|
|
252
335
|
startServer()
|
|
336
|
+
} else if (!build && config.livereload) {
|
|
337
|
+
// livereload without serve: still needs the livereload server
|
|
338
|
+
resolveLiveReloadPort(config)
|
|
339
|
+
.then(poops)
|
|
340
|
+
.then(() => setupLiveReloadServer(config))
|
|
253
341
|
} else {
|
|
254
342
|
poops()
|
|
255
343
|
}
|