poops 1.3.0 → 1.4.1
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 +79 -30
- package/lib/markup/engines/liquid.js +1 -1
- package/lib/markup/engines/nunjucks.js +1 -1
- package/lib/markup/renderer.js +8 -0
- package/lib/markups.js +1 -0
- package/lib/styles.js +31 -5
- package/lib/utils/helpers.js +30 -0
- package/package.json +3 -1
- package/poops.js +4 -2
package/README.md
CHANGED
|
@@ -24,6 +24,37 @@ Intuitive with a minimal learning curve and minimal docs, utilizing the most eff
|
|
|
24
24
|
|
|
25
25
|
It uses a simple config file where you define your input and output paths and it poops out your bundled files. Simple as that.
|
|
26
26
|
|
|
27
|
+
## Table of Contents
|
|
28
|
+
|
|
29
|
+
- [Features](#features)
|
|
30
|
+
- [Quick Start](#quick-start)
|
|
31
|
+
- [Configuration](#configuration)
|
|
32
|
+
- [Scripts](#scripts)
|
|
33
|
+
- [JSX/TSX (React) Example](#jsxtsx-react-example)
|
|
34
|
+
- [Reactor (React Pre-rendering)](#reactor-react-pre-rendering)
|
|
35
|
+
- [Styles](#styles)
|
|
36
|
+
- [Design Tokens](#design-tokens)
|
|
37
|
+
- [PostCSS (optional)](#postcss-optional)
|
|
38
|
+
- [Tailwind CSS Example](#tailwind-css-example)
|
|
39
|
+
- [Markups](#markups)
|
|
40
|
+
- [Nunjucks vs Liquid](#nunjucks-vs-liquid)
|
|
41
|
+
- [Custom Engines](#custom-engines)
|
|
42
|
+
- [Collections & Pagination](#collections--pagination)
|
|
43
|
+
- [Custom Tags](#custom-tags)
|
|
44
|
+
- [image](#image)
|
|
45
|
+
- [googleFonts](#googlefonts)
|
|
46
|
+
- [highlight](#highlight)
|
|
47
|
+
- [Custom Filters](#custom-filters)
|
|
48
|
+
- [Search Index, Sitemap & Navigation](#search-index-sitemap--navigation)
|
|
49
|
+
- [Images (optional)](#images-optional)
|
|
50
|
+
- [Copy](#copy)
|
|
51
|
+
- [Banner (optional)](#banner-optional)
|
|
52
|
+
- [Local Server (optional)](#local-server-optional)
|
|
53
|
+
- [Live Reload (optional)](#live-reload-optional)
|
|
54
|
+
- [Watch (optional)](#watch-optional)
|
|
55
|
+
- [Include Paths (optional)](#include-paths-optional)
|
|
56
|
+
- [Why?](#why)
|
|
57
|
+
|
|
27
58
|
## Features
|
|
28
59
|
|
|
29
60
|
- Bundles SCSS/SASS to CSS
|
|
@@ -174,8 +205,8 @@ You can freely remove the properties that you don't need. For example, if you do
|
|
|
174
205
|
|
|
175
206
|
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:
|
|
176
207
|
|
|
177
|
-
- `in` - the input path, can be an array of file paths,
|
|
178
|
-
- `out` - the output path, can be a directory or a file path
|
|
208
|
+
- `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)
|
|
209
|
+
- `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`)
|
|
179
210
|
- `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.
|
|
180
211
|
|
|
181
212
|
**Options:**
|
|
@@ -308,8 +339,8 @@ Poops does not need `react` or `react-dom` as its own dependency — they are re
|
|
|
308
339
|
|
|
309
340
|
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:
|
|
310
341
|
|
|
311
|
-
- `in` - the input path,
|
|
312
|
-
- `out` - the output path, can be a directory or a file path
|
|
342
|
+
- `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
|
|
343
|
+
- `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
|
|
313
344
|
- `options` - the options for the bundler.
|
|
314
345
|
|
|
315
346
|
**Options:**
|
|
@@ -465,37 +496,34 @@ You can also pass options to plugins using the tuple form:
|
|
|
465
496
|
|
|
466
497
|
#### Tailwind CSS Example
|
|
467
498
|
|
|
468
|
-
|
|
499
|
+
Install the deps, then use a config like this:
|
|
469
500
|
|
|
470
501
|
```bash
|
|
471
502
|
npm i -D postcss @tailwindcss/postcss tailwindcss
|
|
472
|
-
node poops.js -c example-tailwind/poops.json
|
|
473
503
|
```
|
|
474
504
|
|
|
475
|
-
The example config (`example-tailwind/poops.json`):
|
|
476
|
-
|
|
477
505
|
```json
|
|
478
506
|
{
|
|
479
507
|
"postcss": {
|
|
480
|
-
"in": "
|
|
481
|
-
"out": "
|
|
508
|
+
"in": "src/css/main.css",
|
|
509
|
+
"out": "dist/css/main.css",
|
|
482
510
|
"options": {
|
|
483
511
|
"plugins": ["@tailwindcss/postcss"],
|
|
484
512
|
"minify": true
|
|
485
513
|
}
|
|
486
514
|
},
|
|
487
515
|
"markup": {
|
|
488
|
-
"in": "
|
|
489
|
-
"out": "
|
|
516
|
+
"in": "src/markup",
|
|
517
|
+
"out": "dist",
|
|
490
518
|
"site": {
|
|
491
519
|
"title": "Poops + Tailwind",
|
|
492
520
|
"description": "A Tailwind CSS example for Poops"
|
|
493
521
|
},
|
|
494
522
|
"includePaths": ["_layouts", "_partials"]
|
|
495
523
|
},
|
|
496
|
-
"serve": { "port":
|
|
524
|
+
"serve": { "port": 4040, "base": "/dist" },
|
|
497
525
|
"livereload": true,
|
|
498
|
-
"watch": ["
|
|
526
|
+
"watch": ["src"]
|
|
499
527
|
}
|
|
500
528
|
```
|
|
501
529
|
|
|
@@ -592,16 +620,26 @@ An engine class implements this contract (see [`lib/markup/engines/`](lib/markup
|
|
|
592
620
|
|
|
593
621
|
```js
|
|
594
622
|
export default class MyEngine {
|
|
595
|
-
constructor(templatesDir, includePaths, options) {}
|
|
596
|
-
get fileExtension() {
|
|
597
|
-
|
|
598
|
-
|
|
623
|
+
constructor(templatesDir, includePaths, options) {} // options: { autoescape }
|
|
624
|
+
get fileExtension() {
|
|
625
|
+
return ".liquid";
|
|
626
|
+
} // native template extension
|
|
627
|
+
get indexableExtensions() {
|
|
628
|
+
return new Set([".html"]);
|
|
629
|
+
} // extensions eligible for search index/nav
|
|
630
|
+
get markupExtensions() {
|
|
631
|
+
return "html|liquid|md";
|
|
632
|
+
} // glob alternation of processed extensions
|
|
599
633
|
registerFilters({ timeDateFormat, markupOut }) {}
|
|
600
634
|
registerTags(getOutputDir) {}
|
|
601
635
|
setGlobal(key, value) {}
|
|
602
636
|
removeGlobal(key) {}
|
|
603
|
-
async render(templatePath, context) {
|
|
604
|
-
|
|
637
|
+
async render(templatePath, context) {
|
|
638
|
+
return "html";
|
|
639
|
+
} // templatePath is an absolute file path
|
|
640
|
+
async renderString(source, context) {
|
|
641
|
+
return "html";
|
|
642
|
+
}
|
|
605
643
|
}
|
|
606
644
|
```
|
|
607
645
|
|
|
@@ -610,12 +648,12 @@ Optionally, an engine may implement `replaceOutExtensions(outputPath)` to contro
|
|
|
610
648
|
The easiest starting point is extending a built-in engine — deep imports are intentionally supported for this:
|
|
611
649
|
|
|
612
650
|
```js
|
|
613
|
-
import LiquidEngine from
|
|
651
|
+
import LiquidEngine from "poops/lib/markup/engines/liquid.js";
|
|
614
652
|
|
|
615
653
|
export default class MyEngine extends LiquidEngine {
|
|
616
654
|
registerFilters(opts) {
|
|
617
|
-
super.registerFilters(opts)
|
|
618
|
-
this.engine.registerFilter(
|
|
655
|
+
super.registerFilters(opts);
|
|
656
|
+
this.engine.registerFilter("shout", (str) => String(str).toUpperCase());
|
|
619
657
|
}
|
|
620
658
|
}
|
|
621
659
|
```
|
|
@@ -858,7 +896,7 @@ All filters are available in both engines. The only syntax difference is how arg
|
|
|
858
896
|
|
|
859
897
|
- `jsonify` — serializes a value to JSON. Usage: `{{ myObject | jsonify }}`
|
|
860
898
|
|
|
861
|
-
- `markdown` — renders a markdown string to HTML. Usage: `{{ "**bold**" | markdown }}`
|
|
899
|
+
- `markdown` — renders a markdown string to HTML with GitHub-style emoji shortcodes (e.g. `:rocket:` → 🚀). Usage: `{{ "**bold** :rocket:" | markdown }}`
|
|
862
900
|
|
|
863
901
|
- `date` — formats a date string. Uses [dayjs](https://day.js.org/) format tokens. A default format can be set via the `timeDateFormat` config option.
|
|
864
902
|
- Nunjucks: `{{ "2024-01-15" | date("MMMM D, YYYY") }}`
|
|
@@ -1088,12 +1126,23 @@ The string shorthand sets the output filename. For docs sites, use the object fo
|
|
|
1088
1126
|
|
|
1089
1127
|
```json
|
|
1090
1128
|
[
|
|
1091
|
-
{
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1129
|
+
{
|
|
1130
|
+
"title": "Guide",
|
|
1131
|
+
"url": "guide",
|
|
1132
|
+
"order": 1,
|
|
1133
|
+
"children": [
|
|
1134
|
+
{
|
|
1135
|
+
"title": "Getting Started",
|
|
1136
|
+
"url": "guide/getting-started",
|
|
1137
|
+
"order": 1
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
"title": "Advanced",
|
|
1141
|
+
"url": "guide/advanced",
|
|
1142
|
+
"children": [{ "title": "Config", "url": "guide/advanced/config" }]
|
|
1143
|
+
}
|
|
1144
|
+
]
|
|
1145
|
+
}
|
|
1097
1146
|
]
|
|
1098
1147
|
```
|
|
1099
1148
|
|
|
@@ -45,7 +45,7 @@ export default class LiquidEngine {
|
|
|
45
45
|
const engine = this.engine
|
|
46
46
|
engine.registerFilter('slugify', (str) => slugify(str))
|
|
47
47
|
engine.registerFilter('jsonify', (obj) => JSON.stringify(obj))
|
|
48
|
-
engine.registerFilter('markdown', (str) => marked.parse(str))
|
|
48
|
+
engine.registerFilter('markdown', (str) => marked.parse(String(str || '')))
|
|
49
49
|
engine.registerFilter('toc', (html) => renderToc(String(html || '')))
|
|
50
50
|
engine.registerFilter('date', (str, template) => {
|
|
51
51
|
const fmt = template || timeDateFormat
|
|
@@ -79,7 +79,7 @@ export default class NunjucksEngine {
|
|
|
79
79
|
const env = this.env
|
|
80
80
|
env.addFilter('slugify', slugify)
|
|
81
81
|
env.addFilter('jsonify', (obj) => JSON.stringify(obj))
|
|
82
|
-
env.addFilter('markdown', (str) => marked.parse(str))
|
|
82
|
+
env.addFilter('markdown', (str) => marked.parse(String(str || '')))
|
|
83
83
|
env.addFilter('toc', (html) => {
|
|
84
84
|
const toc = renderToc(String(html || ''))
|
|
85
85
|
// plain '' (falsy) when there are no headings, so `{% if x | toc %}` can
|
package/lib/markup/renderer.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Marked } from 'marked'
|
|
2
|
+
import { markedGithubEmoji } from 'marked-github-emoji'
|
|
3
|
+
import { markedGithubAlerts } from 'marked-github-alerts'
|
|
2
4
|
import { slugify } from 'book-of-spells'
|
|
3
5
|
import { highlightRenderer, highlightCode } from './highlight.js'
|
|
4
6
|
import { decodeTemplateEntities } from './helpers.js'
|
|
@@ -50,6 +52,12 @@ export const markdownRenderer = {
|
|
|
50
52
|
|
|
51
53
|
// One shared instance so both engines render markdown identically.
|
|
52
54
|
export const marked = new Marked({ renderer: markdownRenderer })
|
|
55
|
+
marked.use(markedGithubEmoji())
|
|
56
|
+
marked.use(markedGithubAlerts({
|
|
57
|
+
alerts: {
|
|
58
|
+
info: { title: 'Info', icon: 'info' }
|
|
59
|
+
}
|
|
60
|
+
}))
|
|
53
61
|
|
|
54
62
|
// Renders a markdown page source for the template engines. Entity decoding
|
|
55
63
|
// (which un-escapes inside {{ }} / {% %} so template args parse) must skip
|
package/lib/markups.js
CHANGED
|
@@ -223,6 +223,7 @@ export default class Markups {
|
|
|
223
223
|
try {
|
|
224
224
|
const frontMatterResult = parseFrontMatter(templateName)
|
|
225
225
|
context.page = frontMatterResult.frontMatter
|
|
226
|
+
context.page.content = frontMatterResult.content
|
|
226
227
|
context.page.wordcount = wordcount(frontMatterResult.content)
|
|
227
228
|
} catch (err) {
|
|
228
229
|
log({ tag: 'error', text: 'Failed parsing front matter:', link: templateName })
|
package/lib/styles.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import { globSync, hasMagic } from 'glob'
|
|
2
3
|
import {
|
|
3
4
|
pathExists,
|
|
4
5
|
mkPath,
|
|
6
|
+
pathForFile,
|
|
5
7
|
buildStyleOutputFilePath,
|
|
6
8
|
fillBannerTemplate,
|
|
7
9
|
buildTime,
|
|
@@ -25,12 +27,35 @@ export default class Styles {
|
|
|
25
27
|
this.config.styles = Array.isArray(this.config.styles) ? this.config.styles : [this.config.styles]
|
|
26
28
|
for (const styleEntry of this.config.styles) {
|
|
27
29
|
if (!styleEntry.in || !styleEntry.out) continue
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// `in` may be an array of entry points and/or globs — same resolution as scripts
|
|
31
|
+
const configured = Array.isArray(styleEntry.in) ? styleEntry.in : [styleEntry.in]
|
|
32
|
+
const entryPoints = []
|
|
33
|
+
let missing = false
|
|
34
|
+
for (const entry of configured) {
|
|
35
|
+
if (hasMagic(entry)) {
|
|
36
|
+
// Globs must use `/` even on Windows; sort for deterministic build order.
|
|
37
|
+
// Skip sass partials (_*.scss) — they are imports, not entry points.
|
|
38
|
+
const matches = globSync(entry, { posix: true }).filter(match => !path.basename(match).startsWith('_')).sort()
|
|
39
|
+
if (!matches.length) {
|
|
40
|
+
log({ tag: 'style', error: true, text: 'Entry does not exist:', link: entry })
|
|
41
|
+
missing = true
|
|
42
|
+
}
|
|
43
|
+
entryPoints.push(...matches)
|
|
44
|
+
} else if (!pathExists(entry)) {
|
|
45
|
+
log({ tag: 'style', error: true, text: 'Entry does not exist:', link: entry })
|
|
46
|
+
missing = true
|
|
47
|
+
} else {
|
|
48
|
+
entryPoints.push(entry)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (missing) continue
|
|
52
|
+
if (entryPoints.length > 1 && pathForFile(styleEntry.out)) {
|
|
53
|
+
log({ tag: 'error', text: 'Cannot output multiple style files to a single file. Please specify an output directory path instead.' })
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
for (const entryPoint of entryPoints) {
|
|
57
|
+
await this.compileEntry(entryPoint, styleEntry.out, styleEntry.options)
|
|
31
58
|
}
|
|
32
|
-
mkPath(styleEntry.out)
|
|
33
|
-
await this.compileEntry(styleEntry.in, styleEntry.out, styleEntry.options)
|
|
34
59
|
}
|
|
35
60
|
}
|
|
36
61
|
|
|
@@ -58,6 +83,7 @@ export default class Styles {
|
|
|
58
83
|
}
|
|
59
84
|
|
|
60
85
|
outfilePath = buildStyleOutputFilePath(infilePath, outfilePath)
|
|
86
|
+
mkPath(outfilePath) // resolved file path — mkPath on a dir out is a no-op
|
|
61
87
|
|
|
62
88
|
const stylesStart = performance.now()
|
|
63
89
|
let compiledSass
|
package/lib/utils/helpers.js
CHANGED
|
@@ -181,3 +181,33 @@ export function doesFileBelongToPath(filePath, configPaths) {
|
|
|
181
181
|
}
|
|
182
182
|
return false
|
|
183
183
|
}
|
|
184
|
+
|
|
185
|
+
// Derive the watch list from every task's `in` path when watch is `true`.
|
|
186
|
+
// File entries (scripts/styles/reactor) resolve to their parent dir so sibling
|
|
187
|
+
// imports still retrigger a rebuild; dir entries (markup/copy/images) are
|
|
188
|
+
// watched as-is. Imports that reach outside a task's own dir aren't covered —
|
|
189
|
+
// use an explicit `watch` array for that. includePaths is skipped on purpose:
|
|
190
|
+
// it defaults to node_modules, which must never be watched.
|
|
191
|
+
export function deriveWatchDirs(config) {
|
|
192
|
+
const paths = []
|
|
193
|
+
const collect = (task) => {
|
|
194
|
+
if (!task) return
|
|
195
|
+
for (const entry of [].concat(task)) {
|
|
196
|
+
if (!entry || typeof entry !== 'object') continue
|
|
197
|
+
if (entry.in) paths.push(...(Array.isArray(entry.in) ? entry.in : [entry.in]))
|
|
198
|
+
if (entry.component) paths.push(entry.component) // reactor source component
|
|
199
|
+
const tokenPaths = entry.options && entry.options.tokenPaths
|
|
200
|
+
if (tokenPaths) paths.push(...[].concat(tokenPaths))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
collect(config.scripts)
|
|
204
|
+
collect(config.styles)
|
|
205
|
+
collect(config.reactor)
|
|
206
|
+
collect(config.markup)
|
|
207
|
+
collect(config.copy)
|
|
208
|
+
collect(config.images)
|
|
209
|
+
// ponytail: extname()-based dir/file split misreads dot-named dirs (rare);
|
|
210
|
+
// the escape hatch is an explicit watch array.
|
|
211
|
+
return [...new Set(paths.map((p) => (path.extname(p) ? path.dirname(p) : p)))]
|
|
212
|
+
.filter((p) => p && p !== '.')
|
|
213
|
+
}
|
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.4.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "poops.js",
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
"liquidjs": "^10.24.0",
|
|
63
63
|
"livereload": "^0.9.3",
|
|
64
64
|
"marked": "^9.0.3",
|
|
65
|
+
"marked-github-alerts": "^1.0.1",
|
|
66
|
+
"marked-github-emoji": "^1.0.2",
|
|
65
67
|
"nunjucks": "^3.2.4",
|
|
66
68
|
"portscanner": "^2.2.0",
|
|
67
69
|
"printstyle": "^1.0.0",
|
package/poops.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import chokidar from 'chokidar'
|
|
4
4
|
import connect from 'connect'
|
|
5
5
|
import Copy from './lib/copy.js'
|
|
6
|
-
import { pathExists, doesFileBelongToPath, pathContainsPathSegment } from './lib/utils/helpers.js'
|
|
6
|
+
import { pathExists, doesFileBelongToPath, pathContainsPathSegment, deriveWatchDirs } 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'
|
|
@@ -249,7 +249,9 @@ if (!pathExists(configPath)) {
|
|
|
249
249
|
// Load poops.json
|
|
250
250
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
251
251
|
|
|
252
|
-
if (config.watch) {
|
|
252
|
+
if (config.watch === true) {
|
|
253
|
+
config.watch = deriveWatchDirs(config)
|
|
254
|
+
} else if (config.watch) {
|
|
253
255
|
config.watch = Array.isArray(config.watch) ? config.watch : [config.watch]
|
|
254
256
|
}
|
|
255
257
|
|