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/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.3",
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": {