poops 1.0.20 → 1.2.0

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
@@ -1,4 +1,5 @@
1
1
  # 💩 Poops [![npm version](https://img.shields.io/npm/v/poops)](https://www.npmjs.com/package/poops)
2
+
2
3
  Straightforward, no-bullshit bundler for the web.
3
4
 
4
5
  > When your day is long
@@ -17,7 +18,7 @@ Straightforward, no-bullshit bundler for the web.
17
18
 
18
19
  [R.E.M. - Everybody Poops :poop:](https://www.youtube.com/watch?v=5rOiW_xY-kc)
19
20
 
20
- ----
21
+ ---
21
22
 
22
23
  Intuitive with a minimal learning curve and minimal docs, utilizing the most efficient transpilers and compilers available (like [dart-sass](https://sass-lang.com/dart-sass) and [esbuild](https://esbuild.github.io/)) Poops aims to be the simplest bundler option there is. If it's not, please do contribute so we can make it so! 🙏 All ideas and contributions are welcome.
23
24
 
@@ -25,20 +26,23 @@ It uses a simple config file where you define your input and output paths and it
25
26
 
26
27
  ## Features
27
28
 
28
- * Bundles SCSS/SASS to CSS
29
- * Uses [dart-sass](https://sass-lang.com/dart-sass) for SCSS/SASS bundling
30
- * Bundles JS/TS to IIFE/ESM/CJS
31
- * Uses [esbuild](https://esbuild.github.io/) for bundling and trinspiling JS/TS to IIFE/ESM/CJS
32
- * Optional JS and CSS minification using [esbuild](https://esbuild.github.io/)
33
- * Can produce minified code simultaneously with non-minified code! (cause I always forget to minify my code for production)
34
- * Supports source maps only for non minified - non production code (optional)
35
- * Supports multiple input and output paths
36
- * Resolves node modules
37
- * Can add a templatable banner to output files (optional)
38
- * Static site generation with [nunjucks](https://mozilla.github.io/nunjucks/) templating, with blogging option (optional)
39
- * Has a configurable local server (optional)
40
- * Rebuilds on file changes (optional)
41
- * Live reloads on file changes (optional)
29
+ - Bundles SCSS/SASS to CSS
30
+ - Uses [dart-sass](https://sass-lang.com/dart-sass) for SCSS/SASS bundling
31
+ - Design token support — import JSON tokens (W3C DTCG & Style Dictionary) as SCSS variables or maps
32
+ - PostCSS pipeline — use any PostCSS plugin including [Tailwind CSS](https://tailwindcss.com/)
33
+ - Bundles JS/TS/JSX/TSX to IIFE/ESM/CJS
34
+ - Uses [esbuild](https://esbuild.github.io/) for bundling and transpiling JS/TS/JSX/TSX to IIFE/ESM/CJS
35
+ - React pre-rendering (Reactor) renders React components to HTML at build time for static sites with optional hydration
36
+ - Optional JS and CSS minification using [esbuild](https://esbuild.github.io/)
37
+ - Can produce minified code simultaneously with non-minified code! (cause I always forget to minify my code for production)
38
+ - Supports source maps only for non minified - non production code (optional)
39
+ - Supports multiple input and output paths
40
+ - Resolves node modules
41
+ - Can add a templatable banner to output files (optional)
42
+ - Static site generation with swappable template engines: [Nunjucks](https://mozilla.github.io/nunjucks/) (default) or [Liquid](https://liquidjs.com/) — with blogging option (optional)
43
+ - Has a configurable local server (optional)
44
+ - Rebuilds on file changes (optional)
45
+ - Live reloads on file changes (optional)
42
46
 
43
47
  ## Quick Start
44
48
 
@@ -76,90 +80,102 @@ If you have installed Poops locally you can run it with `npx poops` or `npx 💩
76
80
 
77
81
  ## Configuration
78
82
 
79
- Configuring Poops is simple 😌. Let's presume that we have a `example/src/scss` and `example/src/js` directories and we want to bundle the files into `example/dist/css` and `example/dist/js`. If you also have markup files, you can use [nunjucks](https://mozilla.github.io/nunjucks/) templating engine to generate HTML files from your templates. Let's presume that we have a `example/src/markup` directory and we want to generate HTML files in the root of the your directory.
83
+ Configuring Poops is simple 😌. Let's presume that we have a `example/src/scss` and `example/src/js` directories and we want to bundle the files into `example/dist/css` and `example/dist/js`. If you also have markup files, you can use [Nunjucks](https://mozilla.github.io/nunjucks/) (default) or [Liquid](https://liquidjs.com/) templating engine to generate HTML files from your templates. Let's presume that we have a `example/src/markup` directory and we want to generate HTML files in the root of the your directory.
80
84
 
81
85
  Just create a `poops.json` file in the root of your project and add the following (you can see this sample config in this repo's root):
82
86
 
83
87
  ```json
84
88
  {
85
- "scripts": [{
86
- "in": "example/src/js/main.ts",
87
- "out": "example/dist/js/scripts.js",
88
- "options": {
89
- "sourcemap": true,
90
- "minify": true,
91
- "justMinified": false,
92
- "format": "iife",
93
- "target": "es2019"
89
+ "scripts": [
90
+ {
91
+ "in": "example/src/js/main.ts",
92
+ "out": "example/dist/js/scripts.js",
93
+ "options": {
94
+ "sourcemap": true,
95
+ "minify": true,
96
+ "justMinified": false,
97
+ "format": "iife",
98
+ "target": "es2019"
99
+ }
94
100
  }
95
- }],
96
- "styles": [{
97
- "in": "example/src/scss/index.scss",
98
- "out": "example/dist/css/styles.css",
99
- "options": {
100
- "sourcemap": true,
101
- "minify": true,
102
- "justMinified": false
101
+ ],
102
+ "reactor": [
103
+ {
104
+ "component": "example/src/js/App.jsx",
105
+ "inject": "app_html",
106
+ "in": "example/src/js/app-hydrate.jsx",
107
+ "out": "example/dist/js/app-hydrate.js",
108
+ "options": {
109
+ "minify": true,
110
+ "target": "es2019"
111
+ }
103
112
  }
104
- }],
113
+ ],
114
+ "styles": [
115
+ {
116
+ "in": "example/src/scss/index.scss",
117
+ "out": "example/dist/css/styles.css",
118
+ "options": {
119
+ "sourcemap": true,
120
+ "minify": true,
121
+ "justMinified": false
122
+ }
123
+ }
124
+ ],
105
125
  "markup": {
126
+ "engine": "nunjucks",
106
127
  "in": "example/src/markup",
107
128
  "out": "/",
108
- "options": {
109
- "site": {
110
- "title": "Poops",
111
- "description": "A super simple bundler for simple web projects."
112
- },
113
- "data": [
114
- "example/src/markup/data/links.json",
115
- "example/src/markup/data/poops.yaml"
116
- ],
117
- "includePaths": [
118
- "example/src/markup/_layouts",
119
- "example/src/markup/_partials"
120
- ]
121
- }
129
+ "site": {
130
+ "title": "Poops",
131
+ "description": "A super simple bundler for simple web projects."
132
+ },
133
+ "data": [
134
+ "data/links.json",
135
+ "data/poops.yaml"
136
+ ],
137
+ "includePaths": [
138
+ "_layouts",
139
+ "_partials"
140
+ ]
122
141
  },
123
- "copy": [
142
+ "copy": [
124
143
  {
125
144
  "in": "example/src/static",
126
145
  "out": "example/dist"
127
146
  }
128
147
  ],
129
148
  "banner": "/* {{ name }} v{{ version }} | {{ homepage }} | {{ license }} License */",
130
- "serve" : {
149
+ "serve": {
131
150
  "port": 4040,
132
151
  "base": "/"
133
152
  },
134
153
  "livereload": true,
135
- "watch": [
136
- "src"
137
- ],
138
- "includePaths": [
139
- "node_modules"
140
- ]
154
+ "watch": ["src"],
155
+ "includePaths": ["node_modules"]
141
156
  }
142
157
  ```
143
158
 
144
- All config properties are optional except `scripts`, `styles` or `markups`. You have to specify at least one of them. If you don't have anything to consume, you won't poop. 💩
159
+ All config properties are optional except `scripts`, `styles`, `postcss` or `markups`. You have to specify at least one of them. If you don't have anything to consume, you won't poop. 💩
145
160
 
146
161
  You can freely remove the properties that you don't need. For example, if you don't want to run a local server, just remove the `serve` property from the config.
147
162
 
148
163
  ### Scripts
149
164
 
150
- Scripts are bundled with [esbuild](https://esbuild.github.io/). You can specify multiple scripts to bundle. Each script has the following properties:
165
+ 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:
151
166
 
152
- * `in` - the input path, can be an array of file paths, but please just use one file path per script
153
- * `out` - the output path, can be a directory or a file path, but please just use it as a filename
154
- * `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.
167
+ - `in` - the input path, can be an array of file paths, but please just use one file path per script
168
+ - `out` - the output path, can be a directory or a file path, but please just use it as a filename
169
+ - `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.
155
170
 
156
171
  **Options:**
157
- * `sourcemap` - whether to generate sourcemaps or not, sourcemaps are generated only for non-minified files since they are useful for debugging. Default is `false`. This is a direct esbuild option
158
- * `minify` - whether to minify the output or not, minification is performed by `esbuild` and is only applied to non-minified files. Default is `false`
159
- * `justMinified` - whether you want to have a minified file as output only. Removes the non-minified file from the output. Useful for production builds. Default is `false`
160
- * `format` - the output format, can be `iife` or `esm` or `cjs` - this is a direct esbuild option
161
- * `target` - the target for the output, can be `es2018` or `es2019` or `es2020` or `esnext` for instance - this is a direct esbuild option
162
172
 
173
+ - `sourcemap` - whether to generate sourcemaps or not, sourcemaps are generated only for non-minified files since they are useful for debugging. Default is `false`. This is a direct esbuild option
174
+ - `minify` - whether to minify the output or not, minification is performed by `esbuild` and is only applied to non-minified files. Default is `false`
175
+ - `justMinified` - whether you want to have a minified file as output only. Removes the non-minified file from the output. Useful for production builds. Default is `false`
176
+ - `format` - the output format, can be `iife` or `esm` or `cjs` - this is a direct esbuild option
177
+ - `target` - the target for the output, can be `es2018` or `es2019` or `es2020` or `esnext` for instance - this is a direct esbuild option
178
+ - `jsx` - the JSX transform mode, can be `transform` (default) or `automatic`. Use `automatic` for React 17+ JSX runtime which doesn't require importing React in every file - this is a direct esbuild option
163
179
 
164
180
  `scripts` property can accept an array of script configurations or just a single script configuration. If you want to bundle multiple scripts, just add them to the `scripts` array:
165
181
 
@@ -192,20 +208,108 @@ Scripts are bundled with [esbuild](https://esbuild.github.io/). You can specify
192
208
  }
193
209
  ```
194
210
 
211
+ #### JSX/TSX (React) Example
212
+
213
+ To bundle a React app, just point `in` to your `.jsx` or `.tsx` entry file:
214
+
215
+ ```json
216
+ {
217
+ "scripts": [
218
+ {
219
+ "in": "src/js/app.jsx",
220
+ "out": "dist/js/app.js",
221
+ "options": {
222
+ "minify": true,
223
+ "format": "iife",
224
+ "jsx": "automatic"
225
+ }
226
+ }
227
+ ]
228
+ }
229
+ ```
230
+
231
+ Setting `jsx` to `automatic` uses React's JSX runtime (React 17+), so you don't need `import React from 'react'` in every file. If you omit `jsx` or set it to `transform`, the classic `React.createElement` transform is used.
232
+
195
233
  As noted earlier, if you don't want to bundle scripts, just remove the `scripts` property from the config.
196
234
 
235
+ ### Reactor (React Pre-rendering)
236
+
237
+ The `reactor` config key defines React components that are pre-rendered to HTML at build time (SSG) and optionally hydrated on the client. This is a separate pipeline from `scripts` — reactor entries have their own build step, watcher path, and logging tag.
238
+
239
+ Each reactor entry has the following properties:
240
+
241
+ - `component` — the file that default-exports a React component (rendered at build time with `renderToString`)
242
+ - `inject` — template global variable name for the rendered HTML (available in both Nunjucks and Liquid)
243
+ - `in` (optional) — client entry file for hydration (bundled for the browser)
244
+ - `out` (optional) — output path for the client bundle
245
+ - `options` (optional) — esbuild options for the client bundle (same as script entries: `minify`, `format`, `target`, `sourcemap`, etc.)
246
+
247
+ ```json
248
+ {
249
+ "reactor": [
250
+ {
251
+ "component": "src/js/App.jsx",
252
+ "inject": "app_html",
253
+ "in": "src/js/app-hydrate.jsx",
254
+ "out": "dist/js/app-hydrate.js",
255
+ "options": {
256
+ "minify": true,
257
+ "target": "es2019"
258
+ }
259
+ }
260
+ ]
261
+ }
262
+ ```
263
+
264
+ For backwards compatibility, `"ssg"` is also accepted as a config key — it is treated as an alias for `"reactor"`.
265
+
266
+ In your templates, use the `inject` name to insert the rendered HTML:
267
+
268
+ ```html
269
+ <div id="root">{{ app_html | safe }}</div>
270
+ <script src="js/app-hydrate.min.js"></script>
271
+ ```
272
+
273
+ If you only need server-side rendering without client hydration, omit `in` and `out`:
274
+
275
+ ```json
276
+ {
277
+ "reactor": [
278
+ {
279
+ "component": "src/js/App.jsx",
280
+ "inject": "app_html"
281
+ }
282
+ ]
283
+ }
284
+ ```
285
+
286
+ **How it works:**
287
+
288
+ 1. Poops bundles the component with `react-dom/server` for Node.js and calls `renderToString`
289
+ 2. The rendered HTML is stored and made available as a template global variable
290
+ 3. If `in`/`out` are specified, the client entry is bundled for the browser
291
+ 4. At runtime, React hydrates the pre-rendered HTML, making it interactive
292
+
293
+ Poops does not need `react` or `react-dom` as its own dependency — they are resolved from your project's `node_modules`. In watch mode, changes to files in the reactor component's directory trigger re-rendering and client re-bundling. Markup is recompiled only when the rendered output actually changes. Changes to other JS/TS files only trigger the scripts pipeline — the two are independent.
294
+
295
+ **Note:** If you don't need server-side pre-rendering, you can bundle a React app entirely through the regular `scripts` pipeline — just point `in` to your `.jsx`/`.tsx` entry file and use `createRoot` on the client. The `reactor` config is only needed when you want build-time HTML rendering with optional hydration.
296
+
197
297
  ### Styles
198
298
 
199
299
  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:
200
300
 
201
- * `in` - the input path, accepts only a path to a file
202
- * `out` - the output path, can be a directory or a file path, but please just use it as a filename
203
- * `options` - the options for the bundler.
301
+ - `in` - the input path, accepts only a path to a file
302
+ - `out` - the output path, can be a directory or a file path, but please just use it as a filename
303
+ - `options` - the options for the bundler.
204
304
 
205
305
  **Options:**
206
- * `sourcemap` - whether to generate sourcemaps or not, sourcemaps are generated only for non-minified files since they are useful for debugging. Default is `false`
207
- * `minify` - whether to minify the output or not, minification is performed by `esbuild`. Default is `false`
208
- * `justMinified` - whether you want to have a minified file as output only. Removes the non-minified file from the output. Useful for production builds. Defaults to `false`.
306
+
307
+ - `sourcemap` - whether to generate sourcemaps or not, sourcemaps are generated only for non-minified files since they are useful for debugging. Default is `false`
308
+ - `minify` - whether to minify the output or not, minification is performed by `esbuild`. Default is `false`
309
+ - `justMinified` - whether you want to have a minified file as output only. Removes the non-minified file from the output. Useful for production builds. Defaults to `false`.
310
+ - `tokenPaths` - a string or array of directory paths containing JSON design token files. Enables the [`sass-token-importer`](https://github.com/stamat/sass-token-importer) which lets you `@use` JSON tokens directly in SCSS. Supports [W3C DTCG](https://design-tokens.github.io/community-group/format/) and [Style Dictionary](https://amzn.github.io/style-dictionary/) formats with auto-detection.
311
+ - `tokenOutput` - output mode for design tokens: `"variables"` (default) generates flat SCSS variables, `"map"` generates nested Sass maps.
312
+ - `resolveAliases` - whether to resolve `{path.to.token}` alias references in design tokens. Default is `true`.
209
313
 
210
314
  `styles` property can accept an array of style configurations or just a single style configuration. If you want to bundle multiple styles, just add them to the `styles` array:
211
315
 
@@ -234,56 +338,482 @@ Styles are bundled with [Dart Sass](https://sass-lang.com/dart-sass). You can sp
234
338
  }
235
339
  ```
236
340
 
341
+ #### Design Tokens
342
+
343
+ You can import JSON design token files directly into your SCSS using the `token:` prefix. Define your tokens in JSON once and use them as SCSS variables — no manual variable files to keep in sync.
344
+
345
+ Given a token file `src/tokens/colors.json`:
346
+
347
+ ```json
348
+ {
349
+ "color": {
350
+ "$type": "color",
351
+ "primary": { "$value": "#0066cc" },
352
+ "secondary": { "$value": "#ff6600" },
353
+ "link": { "$value": "{color.primary}" }
354
+ }
355
+ }
356
+ ```
357
+
358
+ Add `tokenPaths` to your styles config:
359
+
360
+ ```json
361
+ {
362
+ "styles": [{
363
+ "in": "src/scss/index.scss",
364
+ "out": "dist/css/styles.css",
365
+ "options": {
366
+ "tokenPaths": ["src/tokens"]
367
+ }
368
+ }]
369
+ }
370
+ ```
371
+
372
+ Then use the `token:` prefix in your SCSS:
373
+
374
+ ```scss
375
+ @use "token:colors" as c;
376
+
377
+ .btn {
378
+ color: c.$color-primary;
379
+ }
380
+ .btn:hover {
381
+ color: c.$color-secondary;
382
+ }
383
+ a {
384
+ color: c.$color-link; // resolved from {color.primary} → #0066cc
385
+ }
386
+ ```
387
+
388
+ For Sass maps instead of flat variables, set `"tokenOutput": "map"`:
389
+
390
+ ```scss
391
+ @use "sass:map";
392
+ @use "token:colors" as c;
393
+
394
+ .btn {
395
+ color: map.get(c.$color, primary);
396
+ }
397
+ ```
398
+
237
399
  As noted earlier, if you don't want to bundle styles, just remove the `styles` property from the config.
238
400
 
239
- ### Markups 🚧
401
+ ### PostCSS (optional)
402
+
403
+ Process CSS files with [PostCSS](https://postcss.org/) and any PostCSS plugins. This is a separate pipeline from Styles (Sass) — use it for tools like [Tailwind CSS](https://tailwindcss.com/), [Autoprefixer](https://github.com/postcss/autoprefixer), or any other PostCSS plugin.
404
+
405
+ PostCSS and its plugins are **not** bundled with Poops. You need to install them in your project:
406
+
407
+ ```bash
408
+ npm i -D postcss
409
+ ```
240
410
 
241
- Poops can generate static pages for you. This feature is still under development, but available for testing from the v1.0.2. Your markup is templated with [nunjucks](https://mozilla.github.io/nunjucks/). You can specify multiple markup directories to template. **It's currently recommended to specify only one markup directory since this feature is still WIP 🚧.** Each markup directory has the following properties:
411
+ Each PostCSS entry has the following properties:
242
412
 
243
- * `in` - the input path, can be a directory or a file path, but please just use it as a directory path for now. All files in this directory will be processed and the structure of the directory will be preserved in the output directory with exception to directories that begin with an underscore `_` will be ignored.
244
- * `out` - the output path, can be only a directory path (for now)
245
- * `site` (optional) - global data that will be available to all templates in the markup directory. Like site title, description, social media links, etc. You can then use this data in your templates `{{ site.title }}` for instance.
246
- * `data` (optional) - is an array of JSON or YAML data files, that once loaded will be available to all templates in the markup directory. If you provide a path to a file for instance `links.json` with a `facebook` property, you can then use this data in your templates `{{ links.facebook }}`. The base name of the file will be used as the variable name, with spaces, dashes and dots replaced with underscores. So `the awesome-links.json` will be available as `{{ the_awesome_links.facebook }}` in your templates. The root directory of the data files is `in` directory. So if you have a `data` directory in your `in` directory, you can specify the data files like this `data: ["data/links.json"]`. The same goes for the YAML files.
247
- * `includePaths` - an array of paths to directories that will be added to the nunjucks include paths. Useful if you want to separate template partials and layouts. For instance, if you have a `_includes` directory with a `header.njk` partial that you want to include in your markup, you can add it to the include paths and then include the templates like this `{% include "header.njk" %}`, without specifying the full path to the partial. This will change in the future, to provide better ignore and include patterns for the markup directories.
413
+ - `in` - the input CSS file path
414
+ - `out` - the output path, can be a directory or a file path
415
+ - `options` - options for the pipeline
416
+
417
+ **Options:**
418
+
419
+ - `plugins` - an array of PostCSS plugin names to load. Each entry can be a string (plugin name) or a tuple `["plugin-name", { options }]` for passing options to the plugin.
420
+ - `minify` - whether to minify the output using `esbuild`. Default is `false`
421
+ - `justMinified` - output only the minified file. Default is `false`
422
+
423
+ `postcss` property can accept an array of configurations or a single configuration:
424
+
425
+ ```json
426
+ {
427
+ "postcss": {
428
+ "in": "src/css/main.css",
429
+ "out": "dist/css/main.css",
430
+ "options": {
431
+ "plugins": ["@tailwindcss/postcss"],
432
+ "minify": true
433
+ }
434
+ }
435
+ }
436
+ ```
437
+
438
+ You can also pass options to plugins using the tuple form:
439
+
440
+ ```json
441
+ {
442
+ "postcss": {
443
+ "in": "src/css/main.css",
444
+ "out": "dist/css/main.css",
445
+ "options": {
446
+ "plugins": [
447
+ ["autoprefixer", { "grid": true }]
448
+ ]
449
+ }
450
+ }
451
+ }
452
+ ```
453
+
454
+ **Build order:** PostCSS runs after Styles and Markups in the build pipeline. This means PostCSS plugins can reference the compiled markup output (e.g. Tailwind scanning HTML for utility classes). In watch mode, PostCSS is re-triggered after Styles or Markups recompile.
455
+
456
+ #### Tailwind CSS Example
457
+
458
+ The `example-tailwind/` directory demonstrates using Tailwind CSS v4 with Poops. To run it:
459
+
460
+ ```bash
461
+ npm i -D postcss @tailwindcss/postcss tailwindcss
462
+ node poops.js -c example-tailwind/poops.json
463
+ ```
464
+
465
+ The example config (`example-tailwind/poops.json`):
466
+
467
+ ```json
468
+ {
469
+ "postcss": {
470
+ "in": "example-tailwind/src/css/main.css",
471
+ "out": "example-tailwind/dist/css/main.css",
472
+ "options": {
473
+ "plugins": ["@tailwindcss/postcss"],
474
+ "minify": true
475
+ }
476
+ },
477
+ "markup": {
478
+ "in": "example-tailwind/src/markup",
479
+ "out": "example-tailwind/dist",
480
+ "site": {
481
+ "title": "Poops + Tailwind",
482
+ "description": "A Tailwind CSS example for Poops"
483
+ },
484
+ "includePaths": ["_layouts", "_partials"]
485
+ },
486
+ "serve": { "port": 4041, "base": "/example-tailwind/dist" },
487
+ "livereload": true,
488
+ "watch": ["example-tailwind/src"]
489
+ }
490
+ ```
491
+
492
+ The CSS entry file (`src/css/main.css`) simply imports Tailwind:
493
+
494
+ ```css
495
+ @import "tailwindcss";
496
+ ```
497
+
498
+ Then use Tailwind utility classes directly in your markup templates. Tailwind v4 auto-detects content sources, so no `tailwind.config.js` is needed.
499
+
500
+ **Using Sass + Tailwind together:** If you want both Sass and Tailwind, keep them as separate pipelines writing to separate output files. The Sass pipeline compiles `.scss` to CSS, while the PostCSS pipeline handles Tailwind independently. They don't need to chain into each other unless you want PostCSS to post-process the Sass output (e.g. with Autoprefixer) — in that case, point `postcss.in` to the Sass output file and `postcss.out` to a different file so the original Sass output is preserved for re-processing.
501
+
502
+ ### Markups
503
+
504
+ - `engine` (optional) - the template engine to use. Can be `"nunjucks"` (default) or `"liquid"`. [Nunjucks](https://mozilla.github.io/nunjucks/) is a Mozilla template engine inspired by Jinja2. [Liquid](https://liquidjs.com/) is a Shopify-compatible template engine. Both engines support the same tags, filters, collections, search index, and sitemap features documented below.
505
+ - `in` - the input path, can be a directory or a file path, but please just use it as a directory path for now. All files in this directory will be processed and the structure of the directory will be preserved in the output directory with exception to directories that begin with an underscore `_` will be ignored.
506
+ - `out` - the output path, can be only a directory path (for now)
507
+ - `site` (optional) - global data that will be available to all templates in the markup directory. Like site title, description, social media links, etc. You can then use this data in your templates `{{ site.title }}` for instance.
508
+ - `data` (optional) - is an array of JSON or YAML data files, that once loaded will be available to all templates in the markup directory. If you provide a path to a file for instance `links.json` with a `facebook` property, you can then use this data in your templates `{{ links.facebook }}`. The base name of the file will be used as the variable name, with spaces, dashes and dots replaced with underscores. So `the awesome-links.json` will be available as `{{ the_awesome_links.facebook }}` in your templates. The root directory of the data files is `in` directory. So if you have a `data` directory in your `in` directory, you can specify the data files like this `data: ["data/links.json"]`. The same goes for the YAML files.
509
+ - `includePaths` - an array of paths to directories that will be added to the template engine's include paths. Useful if you want to separate template partials and layouts. For instance, if you have a `_includes` directory with a `header.njk` (or `header.liquid`) partial that you want to include in your markup, you can add it to the include paths and then include the templates like this `{% include "header.njk" %}`, without specifying the full path to the partial.
248
510
 
249
511
  **💡 NOTE:** If, for instance, you are building a simple static onepager for your library, and want to pass a version variable from your `package.json`, Poops automatically reads your `package.json` if it exists in your working directory and sets the global variable `package` to the parsed JSON. So you can use it in your markup files, for example like this: `{{ package.version }}`.
250
512
 
513
+ Here is a sample markup configuration using the default Nunjucks engine:
251
514
 
252
- Here is a sample markup configuration:
515
+ ```json
516
+ {
517
+ "markup": {
518
+ "in": "src/markup",
519
+ "out": "dist",
520
+ "site": {
521
+ "title": "My Awesome Site",
522
+ "description": "This is my awesome site"
523
+ },
524
+ "data": [
525
+ "data/links.json",
526
+ "data/other.yaml"
527
+ ],
528
+ "includePaths": [
529
+ "_includes"
530
+ ]
531
+ }
532
+ }
533
+ ```
253
534
 
254
- ```JSON
535
+ To use Liquid instead, set the `engine` property:
536
+
537
+ ```json
538
+ {
539
+ "markup": {
540
+ "engine": "liquid",
541
+ "in": "src/liquid",
542
+ "out": "dist",
543
+ "site": {
544
+ "title": "My Awesome Site",
545
+ "description": "This is my awesome site"
546
+ },
547
+ "data": [
548
+ "_data/links.json",
549
+ "_data/other.yaml"
550
+ ],
551
+ "includePaths": [
552
+ "_layouts",
553
+ "_partials"
554
+ ]
555
+ }
556
+ }
557
+ ```
558
+
559
+ If your project doesn't have markups, you can remove the `markup` property from the config entirely. No code will be executed for this property.
560
+
561
+ #### Nunjucks vs Liquid
562
+
563
+ Both engines support the same feature set (collections, pagination, search index, sitemap, custom tags, and filters). The main differences are in template syntax:
564
+
565
+ | Feature | Nunjucks | Liquid |
566
+ |---------|----------|--------|
567
+ | File extension | `.njk` | `.liquid` |
568
+ | Inheritance | `{% extends "base.html" %}` | `{% layout "base.liquid" %}` |
569
+ | Default values | `{{ x or "fallback" }}` | `{{ x \| default: "fallback" }}` |
570
+ | Contains check | `{% if "x" in items %}` | `{% if items contains "x" %}` |
571
+ | Safe output | `{{ html \| safe }}` | `{{ html }}` (no escaping by default) |
572
+ | Includes | `{% include "partial.njk" %}` | `{% render "partial.liquid" %}` |
573
+
574
+ Both engines process `.html` and `.md` files in addition to their native extension.
575
+
576
+ #### Custom Tags
577
+
578
+ ##### image
579
+
580
+ Poops can generate responsive `<img>` elements with `srcset` attributes. Image processing (resize, format conversion) is handled externally — Poops discovers the generated variants on disk and produces the correct HTML markup.
581
+
582
+ **Naming convention:** Your image tool should output variants as `{name}-{width}w.{ext}`. For example, given `photo.jpg`, the expected variants are: `photo-320w.jpg`, `photo-640w.jpg`, `photo-320w.webp`, `photo-640w.webp`, etc.
583
+
584
+ **`{% image %}` tag** — generates a full `<img>` element:
585
+
586
+ Nunjucks:
587
+ ```nunjucks
588
+ {% image 'static/photo.jpg', alt='Hero', class='hero-img', sizes='(max-width: 640px) 100vw, 50vw' %}
589
+ ```
590
+
591
+ Liquid:
592
+ ```liquid
593
+ {% image 'static/photo.jpg', alt: 'Hero', class: 'hero-img', sizes: '(max-width: 640px) 100vw, 50vw' %}
594
+ ```
595
+
596
+ Output:
597
+
598
+ ```html
599
+ <img
600
+ src="static/photo-640w.jpg"
601
+ srcset="
602
+ static/photo-320w.webp 320w,
603
+ static/photo-640w.webp 640w,
604
+ static/photo-960w.webp 960w
605
+ "
606
+ sizes="(max-width: 640px) 100vw, 50vw"
607
+ alt="Hero"
608
+ class="hero-img"
609
+ loading="lazy"
610
+ />
611
+ ```
612
+
613
+ - Scans the output directory for files matching `{name}-{width}w.{ext}`
614
+ - Groups by format, prefers `avif` > `webp` > original format for srcset
615
+ - Uses the middle-sized variant as `src` fallback
616
+ - Prepends `relativePathPrefix` automatically
617
+ - Defaults: `sizes="100vw"`, `loading="lazy"`
618
+ - Falls back to a plain `<img src="...">` if no variants are found
619
+
620
+ ##### googleFonts
621
+
622
+ Generates Google Fonts `<link>` tags with preconnect hints. Accepts an array of font names (strings) or font objects with weight/italic options.
623
+
624
+ Nunjucks (supports inline arrays):
625
+ ```nunjucks
626
+ {% googleFonts ["Open Sans", "Roboto"] %}
627
+ ```
628
+
629
+ Liquid (pass a variable — inline arrays are not supported in Liquid syntax):
630
+ ```liquid
631
+ {% googleFonts fonts %}
632
+ ```
633
+
634
+ Where `fonts` is defined in a data file (e.g. `fonts.json`):
635
+ ```json
636
+ ["Open Sans", "Roboto"]
637
+ ```
638
+
639
+ Output:
640
+
641
+ ```html
642
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
643
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
644
+ <link
645
+ href="https://fonts.googleapis.com/css2?family=Open+Sans&family=Roboto&display=swap"
646
+ rel="stylesheet"
647
+ />
648
+ ```
649
+
650
+ With specific weights and italics (Nunjucks):
651
+
652
+ ```nunjucks
653
+ {% googleFonts ["DM Sans", {name: "Poppins", weights: [400, 700], ital: true}] %}
654
+ ```
655
+
656
+ With specific weights and italics (Liquid — via data file):
657
+
658
+ ```json
659
+ ["DM Sans", {"name": "Poppins", "weights": [400, 700], "ital": true}]
660
+ ```
661
+
662
+ Font object options:
663
+
664
+ - `name` — font family name
665
+ - `weights` — array of weight values (e.g. `[400, 700]`)
666
+ - `ital` — set to `true` to include italic variants
667
+ - `display` — font-display strategy, defaults to `swap` (Nunjucks only, as a keyword argument)
668
+
669
+ ##### highlight
670
+
671
+ Syntax-highlights code blocks at build time using [highlight.js](https://highlightjs.org/), eliminating layout shift caused by client-side highlighting. Code is pre-highlighted in the HTML output — you only need the highlight.js CSS theme on the client, not the JS.
672
+
673
+ **`{% highlight %}` tag** — wraps a code block with syntax highlighting (same syntax in both engines):
674
+
675
+ ```
676
+ {% highlight 'javascript' %}
677
+ const greet = (name) => {
678
+ return `Hello, ${name}!`;
679
+ };
680
+ {% endhighlight %}
681
+ ```
682
+
683
+ Output:
684
+
685
+ ```html
686
+ <pre><code class="hljs language-javascript"><span class="hljs-keyword">const</span> greet = <span class="hljs-function">...</span></code></pre>
687
+ ```
688
+
689
+ The language argument is optional. If omitted, highlight.js will attempt to auto-detect the language.
690
+
691
+ **Markdown code fences** are also highlighted automatically at build time:
692
+
693
+ ````md
694
+ ```json
695
+ { "name": "poops" }
696
+ ```
697
+ ````
698
+
699
+ Registered languages: `javascript`/`js`, `typescript`/`ts`, `css`, `scss`, `html`, `xml`, `json`, `bash`/`sh`, `shell`, `python`/`py`, `ruby`/`rb`, `php`, `java`, `c`, `cpp`, `csharp`/`cs`, `go`, `rust`/`rs`, `yaml`/`yml`, `markdown`/`md`, `sql`, `diff`.
700
+
701
+ #### Custom Filters
702
+
703
+ All filters are available in both engines. The only syntax difference is how arguments are passed: Nunjucks uses parentheses `| filter("arg")`, Liquid uses a colon `| filter: "arg"`.
704
+
705
+ - `slugify` — slugifies a string. Usage: `{{ "My Awesome Title" | slugify }}` will output `my-awesome-title`
706
+
707
+ - `jsonify` — serializes a value to JSON. Usage: `{{ myObject | jsonify }}`
708
+
709
+ - `markdown` — renders a markdown string to HTML. Usage: `{{ "**bold**" | markdown }}`
710
+
711
+ - `date` — formats a date string. Uses [dayjs](https://day.js.org/) format tokens. A default format can be set via the `timeDateFormat` config option.
712
+ - Nunjucks: `{{ "2024-01-15" | date("MMMM D, YYYY") }}`
713
+ - Liquid: `{{ "2024-01-15" | date: "MMMM D, YYYY" }}`
714
+
715
+ - `concat` — returns a new array with the value appended (does not mutate the original):
716
+ - Nunjucks: `{{ items | concat("c") }}`
717
+ - Liquid: `{{ items | concat: "c" }}`
718
+
719
+ - `push` — appends a value to an array in place (mutates the original):
720
+ - Nunjucks: `{{ items | push("c") }}`
721
+ - Liquid: `{{ items | push: "c" }}`
722
+
723
+ - `svg` — reads an SVG file and injects it inline. The path is resolved relative to the project root. Returns empty string if the file doesn't exist or isn't an SVG. Usage: `{{ 'src/icons/logo.svg' | svg }}`
724
+
725
+ - `highlight` — syntax-highlights a code string at build time using highlight.js. Takes an optional language argument. If the language is omitted, highlight.js will auto-detect it. Returns a `<pre><code class="hljs">` block with highlighted markup.
726
+ - Nunjucks: `{{ someCodeVariable | highlight('javascript') }}`
727
+ - Liquid: `{{ someCodeVariable | highlight: 'javascript' }}`
728
+
729
+ - `srcset` — returns just the srcset attribute value:
730
+
731
+ ```html
732
+ <img
733
+ src="static/photo-640w.jpg"
734
+ srcset="{{ 'static/photo.jpg' | srcset }}"
735
+ sizes="100vw"
736
+ alt="Hero"
737
+ />
738
+ ```
739
+
740
+ Returns: `static/photo-320w.webp 320w, static/photo-640w.webp 640w, static/photo-960w.webp 960w`
741
+
742
+ #### Search Index & Sitemap
743
+
744
+ Poops can automatically generate a JSON search index and/or an XML sitemap from your compiled pages. Both are generated in a single pass during the markup compilation phase.
745
+
746
+ To enable, add `searchIndex` and/or `sitemap` to your markup config:
747
+
748
+ ```json
255
749
  {
256
- "markups": {
750
+ "markup": {
257
751
  "in": "src/markup",
258
752
  "out": "dist",
259
753
  "options": {
260
- "site": {
261
- "title": "My Awesome Site",
262
- "description": "This is my awesome site"
263
- },
264
- "data": [
265
- "data/links.json",
266
- "data/other.yaml"
267
- ],
268
- "includePaths": [
269
- "_includes"
270
- ]
754
+ "searchIndex": "search-index.json",
755
+ "sitemap": "sitemap.xml"
271
756
  }
272
757
  }
273
758
  }
274
759
  ```
275
760
 
276
- If your project doesn't have markups, you can remove the `markups` property from the config entirely. No code will be executed for this property.
761
+ The string shorthand sets the output filename with default options. For more control, use the object form:
277
762
 
278
- #### Custom Filters
763
+ ```json
764
+ {
765
+ "searchIndex": {
766
+ "output": "search-index.json",
767
+ "minWordLength": 3,
768
+ "maxKeywords": 20,
769
+ "globalFrequencyCeiling": 0.8,
770
+ "stopWords": "path/to/custom-stop-words.json"
771
+ },
772
+ "sitemap": {
773
+ "output": "sitemap.xml"
774
+ }
775
+ }
776
+ ```
777
+
778
+ **Search Index options:**
279
779
 
280
- * `slugify` - slugifies a string. Usage: `{{ "My Awesome Title" | slugify }}` will output `my-awesome-title`
780
+ - `output` output filename, written to the markup output directory
781
+ - `minWordLength` — minimum word length to consider as a keyword (default: `3`)
782
+ - `maxKeywords` — maximum keywords per page (default: `20`)
783
+ - `globalFrequencyCeiling` — drop words appearing in more than this fraction of all pages (default: `0.8`, meaning words found in 80%+ of pages are dropped as non-discriminating)
784
+ - `stopWords` — customise stop word filtering:
785
+ - omit or `undefined` — uses the bundled English stop words
786
+ - `false` — disables stop word filtering entirely
787
+ - `["word1", "word2"]` — inline array of stop words
788
+ - `"path/to/file.json"` — path to a JSON array file (relative to project root)
789
+
790
+ **Search Index output format:**
791
+
792
+ All front matter fields are passed through to the index automatically. Internal fields (`content`, `isIndex`, `layout`, `published`) are stripped. If a page defines `keywords` in its front matter, those are used as-is instead of auto-extracted ones.
793
+
794
+ ```json
795
+ [
796
+ {
797
+ "title": "My Post",
798
+ "date": "2024-01-15",
799
+ "description": "A great post about things.",
800
+ "collection": "blog",
801
+ "tags": ["javascript", "bundler"],
802
+ "url": "blog/my-post.html",
803
+ "keywords": ["javascript", "bundler", "webpack", "esbuild"]
804
+ }
805
+ ]
806
+ ```
807
+
808
+ **Sitemap** generates a standard `sitemap.xml` with `<loc>` and `<lastmod>` (from front matter `date`). If `site.url` is set in your markup config, it is prepended to all URLs. Collection index/pagination pages are included in the sitemap but excluded from the search index.
809
+
810
+ Pages with `published: false` in their front matter are excluded from both outputs.
281
811
 
282
812
  ### Copy
283
813
 
284
814
  Configuration entry to copy files or directories - copy your static files like images and fonts, for instance, from `src` to `dist` directory. This feature was added to enable moving static files if you deploy GitHub pages via a GitHub action. If you don't want to use this feature, simply exclude the `copy` property from your config file.
285
815
 
286
- Here is a sample copy configuration which will copy the `static` directory and it's contents to the `dist` directory:
816
+ Here is a sample copy configuration which will copy the `static` directory and it's contents to the `dist` directory:
287
817
 
288
818
  ```JSON
289
819
  {
@@ -322,7 +852,7 @@ You can specify a list of input paths and pass them to an output directory, for
322
852
  }
323
853
  ```
324
854
 
325
- **💡 NOTE:** Copy can also accept some basic **GLOB** as input paths. Does NOT support **EXTGLOB** yet. Don't expect too much of it, but for instance these paths will work:
855
+ **💡 NOTE:** Copy can also accept **GLOB** and **EXTGLOB** patterns as input paths, except POSIX character classes (e.g. `[[:alpha:]]`):
326
856
 
327
857
  ```JSON
328
858
  {
@@ -333,6 +863,9 @@ You can specify a list of input paths and pass them to an output directory, for
333
863
  "notes/doc?.txt",
334
864
  "notes/memo*.txt",
335
865
  "notes/log[!123a].txt",
866
+ "assets/!(vendor)/*.js",
867
+ "fonts/@(woff|woff2)/*.+(woff|woff2)",
868
+ "docs/?(intro|overview).md"
336
869
  ],
337
870
  "out": "dist"
338
871
  }
@@ -343,14 +876,15 @@ You can specify a list of input paths and pass them to an output directory, for
343
876
 
344
877
  Here you can specify a banner that will be added to the top of the output files. It is templatable via mustache. The following variables are available from your project's `package.json`:
345
878
 
346
- * `name`
347
- * `version`
348
- * `homepage`
349
- * `license`
350
- * `author`
351
- * `description`
879
+ - `name`
880
+ - `version`
881
+ - `homepage`
882
+ - `license`
883
+ - `author`
884
+ - `description`
352
885
 
353
886
  Here is a sample banner template.
887
+
354
888
  ```
355
889
  /* {{ name }} v{{ version }} | {{ homepage }} | {{ license }} License */
356
890
  ```
@@ -360,20 +894,24 @@ You can always pass just a string, you don't have to template it.
360
894
  If you don't want to add a banner, just remove the `banner` property from the config.
361
895
 
362
896
  ### Local Server (optional)
897
+
363
898
  Sets up a local server for your project.
364
899
 
365
900
  Server options:
366
- * `port` - the port on which the server will run
367
- * `base` - the base path of the server, where your HTML files are located
901
+
902
+ - `port` - the port on which the server will run
903
+ - `base` - the base path of the server, where your HTML files are located
368
904
 
369
905
  If you don't want to run a local server, just remove the `serve` property from the config.
370
906
 
371
907
  ### Live Reload (optional)
908
+
372
909
  Sets up a livereload server for your project.
373
910
 
374
911
  Live reload options:
375
- * `port` - the port on which the livereload server will run
376
- * `exclude` - an array of files and directories to exclude from livereload
912
+
913
+ - `port` - the port on which the livereload server will run
914
+ - `exclude` - an array of files and directories to exclude from livereload
377
915
 
378
916
  `livereload` can only be `true`, which means that it will run on the default port (`35729`) or you can specify a port:
379
917
 
@@ -390,10 +928,7 @@ You can also exclude files and directories from livereload:
390
928
  ```json
391
929
  {
392
930
  "livereload": {
393
- "exclude": [
394
- "some_directory/**/*",
395
- "some_other_directory/**/*"
396
- ]
931
+ "exclude": ["some_directory/**/*", "some_other_directory/**/*"]
397
932
  }
398
933
  }
399
934
  ```
@@ -401,10 +936,14 @@ You can also exclude files and directories from livereload:
401
936
  In order for Livereload to work, you need to add the following script snippet to your HTML files in your development environment:
402
937
 
403
938
  ```html
404
- <script>document.write('<script src="http://'
405
- + (location.host || 'localhost').split(':')[0]
406
- + ':35729/livereload.js?snipver=1"></'
407
- + 'script>')</script>
939
+ <script>
940
+ document.write(
941
+ '<script src="http://' +
942
+ (location.host || "localhost").split(":")[0] +
943
+ ':35729/livereload.js?snipver=1"></' +
944
+ "script>",
945
+ );
946
+ </script>
408
947
  ```
409
948
 
410
949
  Be mindful of the port, if you have specified a custom port, you need to change the port in the snippet as well.
@@ -414,56 +953,53 @@ You can also use a browser extension for livereload, for instance here is one fo
414
953
  If you don't want to run livereload, just remove the `livereload` property from the config, or set it to false.
415
954
 
416
955
  ### Watch (optional)
956
+
417
957
  Sets up a watcher for your project which will rebuild your files on change.
418
958
 
419
959
  `watch` property accepts an array of paths to watch for changes. If you want to watch for changes in the `src` directory, just add it to the `watch` array:
420
960
 
421
961
  ```json
422
962
  {
423
- "watch": [
424
- "src"
425
- ]
963
+ "watch": ["src"]
426
964
  }
427
965
  ```
428
966
 
429
967
  If you don't want to watch for file changes, just remove the `watch` property from the config.
430
968
 
431
969
  ### Include Paths (optional)
970
+
432
971
  This property is used to specify paths that you want to resolve your imports from. Like `node_modules`. You don't need to specify the `includePaths`, `node_modules` are included by default. But if you do specify `includePaths`, you need to include `node_modules` as well, since this change will override the default behavior.
433
972
 
434
973
  Same as `watch` property, `includePaths` accepts an array of paths to include. If you want to include `lib` directory for instance, just add it to the `includePaths` array:
435
974
 
436
975
  ```json
437
976
  {
438
- "includePaths": [
439
- "node_modules", "lib"
440
- ]
977
+ "includePaths": ["node_modules", "lib"]
441
978
  }
442
979
  ```
443
980
 
444
981
  ## Todo
445
982
 
446
- * [ ] Run esbuild for each input path individually if there are multiple input paths
447
- * [ ] Styles `in` should be able to support array of inputs like we have it on scripts
448
- * [ ] Build a cli config creation helper tool. If the user doesn't have a config file, we can ask them a few questions and create a config file for them. Create Yeoman generator for poops projects.
449
- * [ ] Support for LESS styles... I guess... And Stylus... I guess...
450
- * [x] Add nunjucks static templating
451
- * [ ] Refactor nunjucks implementation
452
- * [ ] Complete documentation for nunjucks
453
- * [x] Add markdown support
454
- * [x] Front Matter support
455
- * [ ] Future implementation: alternative templating engine liquidjs?
456
- * [x] Future implementation: posts and custom collections, so we can have a real static site generator
457
- * [x] Collection pagination system
458
- * [x] Post published toggle
459
- * [x] RSS and ATOM generation for collections
460
- * [ ] Refactor!!!!
983
+ - [ ] Run esbuild for each input path individually if there are multiple input paths
984
+ - [ ] Styles `in` should be able to support array of inputs like we have it on scripts
985
+ - [ ] Build a cli config creation helper tool. If the user doesn't have a config file, we can ask them a few questions and create a config file for them. Create Yeoman generator for poops projects.
986
+ - [x] Add nunjucks static templating
987
+ - [x] Refactor nunjucks implementation
988
+ - [x] Complete documentation for nunjucks
989
+ - [x] Add markdown support
990
+ - [x] Front Matter support
991
+ - [x] Future implementation: posts and custom collections, so we can have a real static site generator
992
+ - [x] Collection pagination system
993
+ - [x] Post published toggle
994
+ - [x] RSS and ATOM generation for collections
995
+ - [x] Support for images and creating srcsets
996
+ - [x] Add Liquid template engine as a swappable alternative to Nunjucks
461
997
 
462
998
  ## Why?
463
999
 
464
1000
  Why doesn't anyone maintain GULP anymore? Why does Parcel hate config files? Why are Rollup and Webpack so complex to setup for simple tasks? Vite???? What's going on?
465
1001
 
466
- I'm tired... Tired of bullshit... I just want to bundle my scss/sass and/or my js/ts to css and iife/esm js, by providing input and output paths for both/one. And to be able to have minimal easily maintainable dependencies. I don't need plugins, I'll add the features manually for the practice I use. That's it. The f**king end.
1002
+ I'm tired... Tired of bullshit... I just want to bundle my scss/sass and/or my js/ts to css and iife/esm js, by providing input and output paths for both/one. And to be able to have minimal easily maintainable dependencies. I don't need plugins, I'll add the features manually for the practice I use. That's it. The f\*\*king end.
467
1003
 
468
1004
  To better illustrate it, here is a sample diff of Poops replacing Rollup:
469
1005