poops 1.2.2 → 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/README.md +263 -58
- package/lib/copy.js +4 -2
- package/lib/images.js +57 -0
- package/lib/markup/collections.js +38 -14
- package/lib/markup/engines/liquid.js +17 -28
- package/lib/markup/engines/nunjucks.js +22 -34
- package/lib/markup/helpers.js +180 -37
- package/lib/markup/image-cache.js +114 -0
- package/lib/markup/indexer.js +6 -2
- package/lib/markups.js +56 -11
- 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 +114 -23
package/lib/markups.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { pathExists, pathIsDirectory, readDataFile, mkDir, buildTime } from './utils/helpers.js'
|
|
2
|
-
import { replaceOutExtensions, getRelativePathPrefix, getPageUrl, getPageUrlRelativeToOutput, parseFrontMatter, clearFrontMatterCache } from './markup/helpers.js'
|
|
1
|
+
import { pathExists, pathIsDirectory, readDataFile, mkDir, buildTime, toPosix } from './utils/helpers.js'
|
|
2
|
+
import { replaceOutExtensions, getRelativePathPrefix, getPageUrl, getPageUrlRelativeToOutput, parseFrontMatter, clearFrontMatterCache, wordcount } from './markup/helpers.js'
|
|
3
3
|
import { collectionAutoDiscovery, getCollectionDataBasedOnConfig, buildCollectionPaginationData, generateCollectionPaginationPages } from './markup/collections.js'
|
|
4
4
|
import { generateIndexFiles } from './markup/indexer.js'
|
|
5
5
|
import NunjucksEngine from './markup/engines/nunjucks.js'
|
|
@@ -36,7 +36,6 @@ export default class Markups {
|
|
|
36
36
|
this.searchIndexConfig = moduleConfig.options.searchIndex || moduleConfig.searchIndex
|
|
37
37
|
this.sitemapConfig = moduleConfig.options.sitemap || moduleConfig.sitemap
|
|
38
38
|
this.baseURL = moduleConfig.baseURL || moduleConfig.options.baseURL || null
|
|
39
|
-
this.dataFiles = []
|
|
40
39
|
|
|
41
40
|
// Instantiate engine
|
|
42
41
|
const EngineClass = ENGINES[engineName]
|
|
@@ -71,8 +70,8 @@ export default class Markups {
|
|
|
71
70
|
}
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
this.loadDataFiles(
|
|
73
|
+
this.dataConfig = moduleConfig.data || moduleConfig.options.data
|
|
74
|
+
this.loadDataFiles(this.dataConfig)
|
|
76
75
|
|
|
77
76
|
if (!moduleConfig.out) {
|
|
78
77
|
moduleConfig.out = this.markupOut
|
|
@@ -92,7 +91,7 @@ export default class Markups {
|
|
|
92
91
|
for (const file of files) {
|
|
93
92
|
const fullPath = path.join(process.cwd(), dataDir, file)
|
|
94
93
|
if (pathIsDirectory(fullPath)) {
|
|
95
|
-
const dirFiles = globSync(path.join(fullPath, '**/*.+(json|yml|yaml)'))
|
|
94
|
+
const dirFiles = globSync(toPosix(path.join(fullPath, '**/*.+(json|yml|yaml)')))
|
|
96
95
|
for (const f of dirFiles) {
|
|
97
96
|
resolved.push(path.relative(path.join(process.cwd(), dataDir), f))
|
|
98
97
|
}
|
|
@@ -101,25 +100,59 @@ export default class Markups {
|
|
|
101
100
|
}
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
const loadedKeys = new Set()
|
|
106
104
|
for (const dataFile of resolved) {
|
|
107
105
|
try {
|
|
108
106
|
const data = readDataFile(path.join(process.cwd(), dataDir, dataFile))
|
|
109
107
|
const globalKeyName = path.basename(dataFile, path.extname(dataFile)).replace(/[.\-\s]/g, '_')
|
|
110
108
|
this.engine.setGlobal(globalKeyName, data)
|
|
109
|
+
loadedKeys.add(globalKeyName)
|
|
111
110
|
} catch (err) {
|
|
112
111
|
log({ tag: 'error', text: 'Data file not found:', link: dataFile })
|
|
113
112
|
continue
|
|
114
113
|
}
|
|
115
114
|
}
|
|
115
|
+
|
|
116
|
+
// A deleted data file must also drop its global, or stale data renders forever
|
|
117
|
+
if (this.dataGlobalKeys) {
|
|
118
|
+
for (const key of this.dataGlobalKeys) {
|
|
119
|
+
if (!loadedKeys.has(key)) this.engine.removeGlobal(key)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.dataGlobalKeys = loadedKeys
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
reloadDataFiles() {
|
|
119
|
-
this.loadDataFiles(this.
|
|
126
|
+
this.loadDataFiles(this.dataConfig)
|
|
120
127
|
return Promise.resolve()
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
// Maps a deleted source path to its build output and removes it.
|
|
131
|
+
// compile() only globs existing sources, so it can never clean up
|
|
132
|
+
// after a deletion — this is the only place stale output gets removed.
|
|
133
|
+
removeOutput(sourcePath) {
|
|
134
|
+
if (!this.engine) return
|
|
135
|
+
const markupIn = path.join(process.cwd(), this.markupIn)
|
|
136
|
+
const rel = path.relative(markupIn, path.join(process.cwd(), sourcePath))
|
|
137
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return
|
|
138
|
+
|
|
139
|
+
const mapped = path.join(process.cwd(), this.markupOut, rel)
|
|
140
|
+
|
|
141
|
+
// rel === '' with a directory output would be the whole out dir —
|
|
142
|
+
// never remove that, it also holds css/js from other modules
|
|
143
|
+
if (rel !== '' && fs.existsSync(mapped) && fs.statSync(mapped).isDirectory()) {
|
|
144
|
+
fs.rmSync(mapped, { recursive: true })
|
|
145
|
+
log({ tag: this.logTag, text: 'Removed:', link: path.relative(process.cwd(), mapped) })
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const outFile = replaceOutExtensions(mapped)
|
|
150
|
+
if (fs.existsSync(outFile) && !fs.statSync(outFile).isDirectory()) {
|
|
151
|
+
fs.unlinkSync(outFile)
|
|
152
|
+
log({ tag: this.logTag, text: 'Removed:', link: path.relative(process.cwd(), outFile) })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
123
156
|
generateMarkupGlobPattern(excludes) {
|
|
124
157
|
let markupDefaultExcludes = ['node_modules', '.git', '.svn', '.hg']
|
|
125
158
|
|
|
@@ -152,6 +185,7 @@ export default class Markups {
|
|
|
152
185
|
try {
|
|
153
186
|
const frontMatterResult = parseFrontMatter(templateName)
|
|
154
187
|
context.page = frontMatterResult.frontMatter
|
|
188
|
+
context.page.wordcount = wordcount(frontMatterResult.content)
|
|
155
189
|
} catch (err) {
|
|
156
190
|
log({ tag: 'error', text: 'Failed parsing front matter:', link: templateName })
|
|
157
191
|
console.error(err)
|
|
@@ -171,9 +205,10 @@ export default class Markups {
|
|
|
171
205
|
|
|
172
206
|
async compileDirectory(markupIn, collectionData, pageEntries) {
|
|
173
207
|
const markupStart = performance.now()
|
|
208
|
+
// glob patterns must use `/` — on Windows `\` is an escape character
|
|
174
209
|
const markupFiles = [
|
|
175
|
-
...globSync(path.join(markupIn, this.generateMarkupGlobPattern(this.includePaths))),
|
|
176
|
-
...globSync(path.join(markupIn, `*.+(${this.engine.markupExtensions})`))
|
|
210
|
+
...globSync(toPosix(path.join(markupIn, this.generateMarkupGlobPattern(this.includePaths)))),
|
|
211
|
+
...globSync(toPosix(path.join(markupIn, `*.+(${this.engine.markupExtensions})`)))
|
|
177
212
|
]
|
|
178
213
|
const compilePromises = []
|
|
179
214
|
const indexableExtensions = this.engine.indexableExtensions
|
|
@@ -301,6 +336,16 @@ export default class Markups {
|
|
|
301
336
|
if (!moduleConfig || !moduleConfig.in) return
|
|
302
337
|
|
|
303
338
|
if (this.config.reactorData) {
|
|
339
|
+
// A removed reactor component must also drop its injected global,
|
|
340
|
+
// same staleness rule as data files in loadDataFiles()
|
|
341
|
+
const reactorKeys = new Set(Object.keys(this.config.reactorData))
|
|
342
|
+
if (this.reactorGlobalKeys) {
|
|
343
|
+
for (const key of this.reactorGlobalKeys) {
|
|
344
|
+
if (!reactorKeys.has(key)) this.engine.removeGlobal(key)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
this.reactorGlobalKeys = reactorKeys
|
|
348
|
+
|
|
304
349
|
for (const [name, html] of Object.entries(this.config.reactorData)) {
|
|
305
350
|
this.engine.setGlobal(name, html)
|
|
306
351
|
}
|
package/lib/postcss.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 log from './utils/log.js'
|
|
13
12
|
|
|
14
13
|
let postcss
|
|
@@ -65,10 +64,13 @@ export default class PostCSS {
|
|
|
65
64
|
|
|
66
65
|
this.config.postcss = Array.isArray(this.config.postcss) ? this.config.postcss : [this.config.postcss]
|
|
67
66
|
for (const entry of this.config.postcss) {
|
|
68
|
-
if (entry.in
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
if (!entry.in || !entry.out) continue
|
|
68
|
+
if (!pathExists(entry.in)) {
|
|
69
|
+
log({ tag: 'postcss', error: true, text: 'Entry does not exist:', link: entry.in })
|
|
70
|
+
continue
|
|
71
71
|
}
|
|
72
|
+
mkPath(entry.out)
|
|
73
|
+
await this.compileEntry(entry.in, entry.out, entry.options)
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -98,30 +100,14 @@ export default class PostCSS {
|
|
|
98
100
|
const end = performance.now()
|
|
99
101
|
if (!options.justMinified) log({ tag: 'postcss', text: 'Compiled:', link: outfilePath, size: fileSize(outfilePath), time: buildTime(start, end) })
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (this.banner) minified.code = this.banner + '\n' + minified.code
|
|
112
|
-
fs.writeFileSync(minPath, minified.code)
|
|
113
|
-
const minEnd = performance.now()
|
|
114
|
-
log({ tag: 'postcss', text: 'Compiled:', link: minPath, size: fileSize(minPath), time: buildTime(minStart, minEnd) })
|
|
115
|
-
} catch (err) {
|
|
116
|
-
log({ tag: 'postcss', error: true, text: 'Failed compiling:', link: minPath })
|
|
117
|
-
console.error(err)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (options.justMinified) {
|
|
121
|
-
fs.unlinkSync(outfilePath)
|
|
122
|
-
}
|
|
123
|
-
} else {
|
|
124
|
-
if (pathExists(minPath)) fs.unlinkSync(minPath)
|
|
125
|
-
}
|
|
103
|
+
await minifyToFile({
|
|
104
|
+
outfilePath,
|
|
105
|
+
loader: 'css',
|
|
106
|
+
code: css,
|
|
107
|
+
banner: this.banner,
|
|
108
|
+
tag: 'postcss',
|
|
109
|
+
options,
|
|
110
|
+
startTime: options.justMinified ? start : undefined
|
|
111
|
+
})
|
|
126
112
|
}
|
|
127
113
|
}
|
package/lib/reactor.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { build
|
|
1
|
+
import { build } from 'esbuild'
|
|
2
2
|
import { deepMerge } from 'book-of-spells'
|
|
3
3
|
import {
|
|
4
4
|
pathExists,
|
|
5
5
|
mkPath,
|
|
6
|
-
insertMinSuffix,
|
|
7
6
|
pathContainsPathSegment,
|
|
8
7
|
fillBannerTemplate,
|
|
9
8
|
buildTime,
|
|
10
9
|
fileSize
|
|
11
10
|
} from './utils/helpers.js'
|
|
11
|
+
import minifyToFile from './utils/minify.js'
|
|
12
12
|
import fs from 'node:fs'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import { createRequire } from 'node:module'
|
|
@@ -29,9 +29,12 @@ export default class Reactor {
|
|
|
29
29
|
this.rendered = {}
|
|
30
30
|
|
|
31
31
|
for (const entry of this.config.reactor) {
|
|
32
|
-
if (entry.component
|
|
33
|
-
|
|
32
|
+
if (!entry.component || !entry.inject) continue
|
|
33
|
+
if (!pathExists(entry.component)) {
|
|
34
|
+
log({ tag: 'reactor', error: true, text: 'Component does not exist:', link: entry.component })
|
|
35
|
+
continue
|
|
34
36
|
}
|
|
37
|
+
await this.compileEntry(entry)
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
this.renderedChanged = JSON.stringify(this.rendered) !== JSON.stringify(prevRendered)
|
|
@@ -83,6 +86,9 @@ export const html = renderToString(React.createElement(Component));
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// --- Step 2: Bundle the client entry for the browser ---
|
|
89
|
+
if (client && out && !pathExists(client)) {
|
|
90
|
+
log({ tag: 'reactor', error: true, text: 'Entry does not exist:', link: client })
|
|
91
|
+
}
|
|
86
92
|
if (client && out && pathExists(client)) {
|
|
87
93
|
mkPath(out)
|
|
88
94
|
|
|
@@ -129,31 +135,7 @@ export const html = renderToString(React.createElement(Component));
|
|
|
129
135
|
if (!options.justMinified) log({ tag: 'reactor', text: 'Compiled:', link: out, size: fileSize(out), time: buildTime(esbuildStart, esbuildEnd) })
|
|
130
136
|
if (options.sourcemap) log({ tag: 'reactor', text: 'Compiled:', link: `${out}.map` })
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
const minPath = insertMinSuffix(out)
|
|
134
|
-
try {
|
|
135
|
-
const terserStart = performance.now()
|
|
136
|
-
const minifyResult = await transform(fs.readFileSync(out, 'utf-8'), {
|
|
137
|
-
minify: true,
|
|
138
|
-
loader: 'js'
|
|
139
|
-
})
|
|
140
|
-
const terserEnd = performance.now()
|
|
141
|
-
|
|
142
|
-
if (this.banner) minifyResult.code = this.banner + '\n' + minifyResult.code
|
|
143
|
-
fs.writeFileSync(minPath, minifyResult.code)
|
|
144
|
-
log({ tag: 'reactor', text: 'Compiled:', link: minPath, size: fileSize(minPath), time: buildTime(terserStart, terserEnd) })
|
|
145
|
-
} catch (err) {
|
|
146
|
-
log({ tag: 'reactor', error: true, text: 'Failed compiling:', link: minPath })
|
|
147
|
-
console.error(err)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (options.justMinified) {
|
|
151
|
-
fs.unlinkSync(out)
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
const minPath = insertMinSuffix(out)
|
|
155
|
-
if (pathExists(minPath)) fs.unlinkSync(minPath)
|
|
156
|
-
}
|
|
138
|
+
await minifyToFile({ outfilePath: out, loader: 'js', banner: this.banner, tag: 'reactor', options })
|
|
157
139
|
}
|
|
158
140
|
}
|
|
159
141
|
|
package/lib/scripts.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { build
|
|
1
|
+
import { build } from 'esbuild'
|
|
2
|
+
import { globSync, hasMagic } from 'glob'
|
|
2
3
|
import { deepMerge } from 'book-of-spells'
|
|
3
4
|
import {
|
|
4
5
|
pathExists,
|
|
5
6
|
mkPath,
|
|
6
7
|
pathForFile,
|
|
7
|
-
insertMinSuffix,
|
|
8
|
-
buildScriptOutputFilePath,
|
|
9
8
|
fillBannerTemplate,
|
|
10
9
|
buildTime,
|
|
11
10
|
fileSize
|
|
12
11
|
} from './utils/helpers.js'
|
|
13
|
-
import
|
|
12
|
+
import minifyToFile from './utils/minify.js'
|
|
14
13
|
import log from './utils/log.js'
|
|
15
14
|
|
|
16
15
|
export default class Scripts {
|
|
@@ -24,10 +23,30 @@ export default class Scripts {
|
|
|
24
23
|
this.config.scripts = Array.isArray(this.config.scripts) ? this.config.scripts : [this.config.scripts]
|
|
25
24
|
|
|
26
25
|
for (const scriptEntry of this.config.scripts) {
|
|
27
|
-
if (scriptEntry.in
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if (!scriptEntry.in || !scriptEntry.out) continue
|
|
27
|
+
// `in` may be an array of entry points — pathExists on an array throws
|
|
28
|
+
const configured = Array.isArray(scriptEntry.in) ? scriptEntry.in : [scriptEntry.in]
|
|
29
|
+
const entryPoints = []
|
|
30
|
+
let missing = false
|
|
31
|
+
for (const entry of configured) {
|
|
32
|
+
if (hasMagic(entry)) {
|
|
33
|
+
// Globs must use `/` even on Windows; sort for deterministic build order
|
|
34
|
+
const matches = globSync(entry, { posix: true }).sort()
|
|
35
|
+
if (!matches.length) {
|
|
36
|
+
log({ tag: 'script', error: true, text: 'Entry does not exist:', link: entry })
|
|
37
|
+
missing = true
|
|
38
|
+
}
|
|
39
|
+
entryPoints.push(...matches)
|
|
40
|
+
} else if (!pathExists(entry)) {
|
|
41
|
+
log({ tag: 'script', error: true, text: 'Entry does not exist:', link: entry })
|
|
42
|
+
missing = true
|
|
43
|
+
} else {
|
|
44
|
+
entryPoints.push(entry)
|
|
45
|
+
}
|
|
30
46
|
}
|
|
47
|
+
if (missing) continue
|
|
48
|
+
mkPath(scriptEntry.out)
|
|
49
|
+
await this.compileEntry(entryPoints, scriptEntry.out, scriptEntry.options)
|
|
31
50
|
}
|
|
32
51
|
}
|
|
33
52
|
|
|
@@ -73,9 +92,16 @@ export default class Scripts {
|
|
|
73
92
|
|
|
74
93
|
deepMerge(opts, optionsClone) // ability to pass other esbuild options `node_modules/esbuild/lib/main.d.ts`
|
|
75
94
|
|
|
95
|
+
// Multi-dir entry points nest output under their common ancestor (esbuild's
|
|
96
|
+
// outbase), so output paths can't be derived from basenames — read them
|
|
97
|
+
// from the metafile instead. Keys are relative to absWorkingDir.
|
|
98
|
+
opts.metafile = true
|
|
99
|
+
opts.absWorkingDir = process.cwd()
|
|
100
|
+
|
|
76
101
|
const esbuildStart = performance.now()
|
|
102
|
+
let result
|
|
77
103
|
try {
|
|
78
|
-
await build(opts)
|
|
104
|
+
result = await build(opts)
|
|
79
105
|
} catch (err) {
|
|
80
106
|
log({ tag, error: true, text: 'Failed compiling:', link: outfilePath })
|
|
81
107
|
console.error(err)
|
|
@@ -83,36 +109,13 @@ export default class Scripts {
|
|
|
83
109
|
}
|
|
84
110
|
const esbuildEnd = performance.now()
|
|
85
111
|
|
|
86
|
-
for (const
|
|
87
|
-
|
|
88
|
-
const minPath = insertMinSuffix(newOutFilePath)
|
|
112
|
+
for (const [newOutFilePath, output] of Object.entries(result.metafile.outputs)) {
|
|
113
|
+
if (!output.entryPoint) continue // sourcemaps, chunks
|
|
89
114
|
|
|
90
115
|
if (!options.justMinified) log({ tag, text: 'Compiled:', link: newOutFilePath, size: fileSize(newOutFilePath), time: buildTime(esbuildStart, esbuildEnd) })
|
|
91
116
|
if (options.sourcemap) log({ tag, text: 'Compiled:', link: `${newOutFilePath}.map` })
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const terserStart = performance.now()
|
|
96
|
-
const minifyResult = await transform(fs.readFileSync(newOutFilePath, 'utf-8'), {
|
|
97
|
-
minify: true,
|
|
98
|
-
loader: 'js'
|
|
99
|
-
})
|
|
100
|
-
const terserEnd = performance.now()
|
|
101
|
-
|
|
102
|
-
if (this.banner) minifyResult.code = this.banner + '\n' + minifyResult.code
|
|
103
|
-
fs.writeFileSync(minPath, minifyResult.code)
|
|
104
|
-
log({ tag, text: 'Compiled:', link: minPath, size: fileSize(minPath), time: buildTime(terserStart, terserEnd) })
|
|
105
|
-
} catch (err) {
|
|
106
|
-
log({ tag, error: true, text: 'Failed compiling:', link: minPath })
|
|
107
|
-
console.error(err)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (options.justMinified) {
|
|
111
|
-
fs.unlinkSync(newOutFilePath)
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
if (pathExists(minPath)) fs.unlinkSync(minPath)
|
|
115
|
-
}
|
|
118
|
+
await minifyToFile({ outfilePath: newOutFilePath, loader: 'js', banner: this.banner, tag, options })
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
}
|
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.2.
|
|
4
|
+
"version": "1.2.4",
|
|
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": {
|