poops 1.9.4 → 1.9.5

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 CHANGED
@@ -216,8 +216,8 @@ You can freely remove the properties that you don't need. For example, if you do
216
216
 
217
217
  Scripts are bundled with [esbuild](https://esbuild.github.io/). Supports `.js`, `.ts`, `.jsx`, and `.tsx` files out of the box — including React and other JSX frameworks. You can specify multiple scripts to bundle. Each script has the following properties:
218
218
 
219
- - `in` - the input path, can be a file path, an array of file paths, or a glob pattern (e.g. `"src/js/*.js"`). Globs must use `/` separators (even on Windows)
220
- - `out` - the output path, can be a directory or a file path. With multiple inputs it must be a directory — entry points from different directories nest their output under the common ancestor (esbuild's `outbase`)
219
+ - `in` - the input path, can be a file path, an array of file paths, or a glob pattern (e.g. `"src/js/*.js"`, `"src/elements/*/index.{js,mjs,cjs,jsx,ts,tsx}"`). Globs must use `/` separators (even on Windows)
220
+ - `out` - the output path, can be a directory or a file path. With multiple inputs it must be a directory — entry points from different directories nest their output under the common ancestor (esbuild's `outbase`). A **glob-matched** `index.*` is named after its directory instead, placed relative to the glob's static prefix: `"src/elements/*/index.js"` gives `<out>/accordion.js`, while `"src/*/accordion/index.js"` keeps the differing segment as `<out>/blocks/accordion.js`. A literal `in: "src/index.js"` keeps its own basename
221
221
  - `options` - the options for the bundler. You can apply most of the esbuild options that are not in conflict with Poops. See [esbuild's options](https://esbuild.github.io/api/#build-api) for more info.
222
222
 
223
223
  **Options:**
@@ -350,8 +350,8 @@ Poops does not need `react` or `react-dom` as its own dependency — they are re
350
350
 
351
351
  Styles are bundled with [Dart Sass](https://sass-lang.com/dart-sass). You can specify multiple styles to bundle. Each style has the following properties:
352
352
 
353
- - `in` - the input path, can be a file path, an array of file paths, or a glob pattern (e.g. `"src/scss/*.scss"`). Globs must use `/` separators (even on Windows) and skip Sass partials (`_*.scss`). Each matched file is compiled separately
354
- - `out` - the output path, can be a directory or a file path. With multiple inputs it must be a directory — each input compiles to `<out>/<basename>.css`, so inputs sharing a basename (e.g. `a/main.scss` and `b/main.scss`) will overwrite each other
353
+ - `in` - the input path, can be a file path, an array of file paths, or a glob pattern (e.g. `"src/scss/*.scss"`, `"src/elements/*/index.{scss,sass,css}"`). Globs must use `/` separators (even on Windows) and skip Sass partials (`_*.scss`). Each matched file is compiled separately
354
+ - `out` - the output path, can be a directory or a file path. With multiple inputs it must be a directory — each input compiles to `<out>/<basename>.css`, so inputs sharing a basename (e.g. `a/main.scss` and `b/main.scss`) will overwrite each other. A **glob-matched** `index.*` is named after its directory instead, placed relative to the glob's static prefix: `"src/elements/*/index.scss"` gives `<out>/accordion.css`, while `"src/*/accordion/index.scss"` keeps the differing segment as `<out>/blocks/accordion.css`. A literal `in: "src/scss/index.scss"` keeps its own basename
355
355
  - `options` - the options for the bundler.
356
356
 
357
357
  **Options:**
package/lib/copy.js CHANGED
@@ -34,7 +34,7 @@ export default class Copy {
34
34
 
35
35
  for (const inEntry of inEntries) {
36
36
  const hasGlobMagic = typeof hasMagic === 'function'
37
- ? hasMagic(inEntry)
37
+ ? hasMagic(inEntry, { magicalBraces: true })
38
38
  : ['*', '?', '[', ']', '{', '}', '(', ')', '!'].some((ch) => inEntry.includes(ch))
39
39
  let matches = []
40
40
  try {
package/lib/scripts.js CHANGED
@@ -5,6 +5,9 @@ import {
5
5
  pathExists,
6
6
  mkPath,
7
7
  pathForFile,
8
+ isIndexEntry,
9
+ globBase,
10
+ indexEntryOut,
8
11
  fillBannerTemplate,
9
12
  buildTime,
10
13
  fileSize
@@ -29,14 +32,21 @@ export default class Scripts {
29
32
  const entryPoints = []
30
33
  let missing = false
31
34
  for (const entry of configured) {
32
- if (hasMagic(entry)) {
35
+ if (hasMagic(entry, { magicalBraces: true })) {
33
36
  // Globs must use `/` even on Windows; sort for deterministic build order
34
37
  const matches = globSync(entry, { posix: true }).sort()
35
38
  if (!matches.length) {
36
39
  log({ tag: 'script', error: true, text: 'Entry does not exist:', link: entry })
37
40
  missing = true
38
41
  }
39
- entryPoints.push(...matches)
42
+ // A glob-matched `index.js` is named after its directory, relative to
43
+ // the glob's static prefix — otherwise every component collides on
44
+ // `index.js` or nests a level deep under esbuild's outbase. Literal
45
+ // entries keep their basename — see `isIndexEntry`.
46
+ const base = globBase(entry)
47
+ entryPoints.push(...matches.map(match => (
48
+ isIndexEntry(match) ? { in: match, out: indexEntryOut(match, base) } : match
49
+ )))
40
50
  } else if (!pathExists(entry)) {
41
51
  log({ tag: 'script', error: true, text: 'Entry does not exist:', link: entry })
42
52
  missing = true
@@ -78,6 +88,9 @@ export default class Scripts {
78
88
  log({ tag: 'error', text: 'Cannot output multiple script files to a single file. Please specify an output directory path instead.' })
79
89
  process.exit(1)
80
90
  }
91
+ // An explicit `outfile` names the output itself, so drop any `{ in, out }`
92
+ // wrapper the glob resolver added — esbuild rejects the pair.
93
+ opts.entryPoints = infilePath.map(entry => entry.in || entry)
81
94
  opts.outfile = outfilePath
82
95
  }
83
96
 
package/lib/styles.js CHANGED
@@ -5,6 +5,9 @@ import {
5
5
  mkPath,
6
6
  pathForFile,
7
7
  buildStyleOutputFilePath,
8
+ isIndexEntry,
9
+ globBase,
10
+ indexEntryOut,
8
11
  fillBannerTemplate,
9
12
  buildTime,
10
13
  fileSize
@@ -30,9 +33,10 @@ export default class Styles {
30
33
  // `in` may be an array of entry points and/or globs — same resolution as scripts
31
34
  const configured = Array.isArray(styleEntry.in) ? styleEntry.in : [styleEntry.in]
32
35
  const entryPoints = []
36
+ const indexEntries = new Map()
33
37
  let missing = false
34
38
  for (const entry of configured) {
35
- if (hasMagic(entry)) {
39
+ if (hasMagic(entry, { magicalBraces: true })) {
36
40
  // Globs must use `/` even on Windows; sort for deterministic build order.
37
41
  // Skip sass partials (_*.scss) — they are imports, not entry points.
38
42
  const matches = globSync(entry, { posix: true }).filter(match => !path.basename(match).startsWith('_')).sort()
@@ -40,6 +44,13 @@ export default class Styles {
40
44
  log({ tag: 'style', error: true, text: 'Entry does not exist:', link: entry })
41
45
  missing = true
42
46
  }
47
+ // A glob-matched `index.scss` is named after its directory, relative
48
+ // to the glob's static prefix — otherwise every component collides on
49
+ // `index.css`. Literal entries keep their basename — see `isIndexEntry`.
50
+ const base = globBase(entry)
51
+ for (const match of matches) {
52
+ if (isIndexEntry(match)) indexEntries.set(match, indexEntryOut(match, base))
53
+ }
43
54
  entryPoints.push(...matches)
44
55
  } else if (!pathExists(entry)) {
45
56
  log({ tag: 'style', error: true, text: 'Entry does not exist:', link: entry })
@@ -54,7 +65,11 @@ export default class Styles {
54
65
  process.exit(1)
55
66
  }
56
67
  for (const entryPoint of entryPoints) {
57
- await this.compileEntry(entryPoint, styleEntry.out, styleEntry.options)
68
+ // Resolve the index rename here, where `out` is still known to be a dir
69
+ const out = indexEntries.has(entryPoint) && !pathForFile(styleEntry.out)
70
+ ? path.join(styleEntry.out, `${indexEntries.get(entryPoint)}.css`)
71
+ : styleEntry.out
72
+ await this.compileEntry(entryPoint, out, styleEntry.options)
58
73
  }
59
74
  }
60
75
  }
@@ -34,10 +34,38 @@ export function insertMinSuffix(filePath) {
34
34
  return path.join(path.dirname(filePath), `${name}.min${ext}`)
35
35
  }
36
36
 
37
+ // `index` is a directory entry point — name the output after the directory, so
38
+ // `src/elements/accordion/index.scss` outputs `accordion.css`. Only applied to
39
+ // glob matches: a literal `in: 'src/index.js'` keeps its own basename, since
40
+ // renaming it to `src.js` would silently move an explicitly named output.
41
+ export function isIndexEntry(inputPath) {
42
+ const { dir, name } = path.parse(inputPath)
43
+ return name === 'index' && !!dir && dir !== '.' && dir !== '..'
44
+ }
45
+
46
+ // The static prefix of a glob — everything before its first magic segment.
47
+ // `src/elements/*/index.ts` → `src/elements`, `src/*/accordion/index.ts` → `src`.
48
+ // Derived from the pattern alone, so it doesn't shift with what happened to match.
49
+ export function globBase(pattern) {
50
+ const parts = toPosix(pattern).split('/')
51
+ const magicAt = parts.findIndex(part => hasMagic(part, { magicalBraces: true }))
52
+ return parts.slice(0, magicAt === -1 ? -1 : magicAt).join('/')
53
+ }
54
+
55
+ // Extension-less output path for a glob-matched `index.*`: the directory name,
56
+ // keeping whatever nesting the glob's static prefix doesn't already account for.
57
+ // A glob spanning several groups (`src/*/accordion/index.ts`) therefore stays
58
+ // apart as `blocks/accordion` and `elements/accordion` instead of colliding.
59
+ export function indexEntryOut(inputPath, base) {
60
+ const dir = path.posix.dirname(toPosix(inputPath))
61
+ const rel = base ? path.posix.relative(base, dir) : dir
62
+ return rel || path.posix.basename(dir)
63
+ }
64
+
37
65
  export function buildStyleOutputFilePath(inputPath, outputPath) {
38
66
  if (pathForFile(outputPath)) return outputPath
39
67
  const { name } = path.parse(inputPath)
40
- return path.join(path.join(outputPath, `${name}.css`))
68
+ return path.join(outputPath, `${name}.css`)
41
69
  }
42
70
 
43
71
  export function fillBannerTemplate(template, packagesPath) {
@@ -185,7 +213,7 @@ export function pathContainsPathSegment(filePath, segment) {
185
213
  // Watcher paths arrive with native separators, config segments with `/`
186
214
  filePath = toPosix(filePath)
187
215
  segment = stripDirNavSegments(segment)
188
- if (hasMagic(segment)) {
216
+ if (hasMagic(segment, { magicalBraces: true })) {
189
217
  segment = convertGlobToRegex(segment)
190
218
  if (!segment) return false
191
219
  return segment.test(filePath)
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.9.4",
4
+ "version": "1.9.5",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "poops.js",