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/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
- const data = moduleConfig.data || moduleConfig.options.data
75
- this.loadDataFiles(data)
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
- if (!this.dataFiles.length) this.dataFiles = resolved
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.dataFiles)
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 && entry.out && pathExists(entry.in)) {
69
- mkPath(entry.out)
70
- await this.compileEntry(entry.in, entry.out, entry.options)
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
- const minPath = insertMinSuffix(outfilePath)
102
- if (options.minify) {
103
- try {
104
- const minStart = !options.justMinified ? performance.now() : start
105
-
106
- const minified = await transform(css, {
107
- loader: 'css',
108
- minify: true
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, transform } from 'esbuild'
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 && entry.inject && pathExists(entry.component)) {
33
- await this.compileEntry(entry)
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
- if (options.minify) {
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, transform } from 'esbuild'
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 fs from 'node:fs'
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 && scriptEntry.out && pathExists(scriptEntry.in)) {
28
- mkPath(scriptEntry.out)
29
- await this.compileEntry(scriptEntry.in, scriptEntry.out, scriptEntry.options)
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 entry of infilePath) {
87
- const newOutFilePath = buildScriptOutputFilePath(entry, outfilePath)
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
- if (options.minify) {
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 && styleEntry.out && pathExists(styleEntry.in)) {
29
- mkPath(styleEntry.out)
30
- await this.compileEntry(styleEntry.in, styleEntry.out, styleEntry.options)
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, options)
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
- const minPath = insertMinSuffix(outfilePath)
83
- if (options.minify) {
84
- try {
85
- const stylesMinStart = !options.justMinified ? performance.now() : stylesStart
86
-
87
- const minified = await transform(compiledSass.css, {
88
- loader: 'css',
89
- minify: true
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
  }
@@ -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
- if (time < 1000) return `${time}ms`
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
- if (time < 60 * 1000) return `${seconds}s ${ms}ms`
70
- const minutes = Math.floor(seconds / 60)
71
- const remainingSeconds = seconds % 60
72
- return `${minutes}m ${remainingSeconds}s ${ms}ms`
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) return `${Math.floor(fileSizeInBytes / 1000 / 1000)}MB ${Math.floor((fileSizeInBytes % (1000 * 1000)) / 1000)}KB`
81
- return fileSizeInBytes
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
- export default function log({ tag, error, text, link, size, time }) {
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.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 ./poops.js",
50
+ "lint": "eslint .",
47
51
  "test": "NODE_OPTIONS='--experimental-vm-modules' jest"
48
52
  },
49
53
  "dependencies": {