poops 1.8.0 → 1.9.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
@@ -615,6 +615,34 @@ Both engines support the same feature set (collections, pagination, search index
615
615
 
616
616
  Both engines process `.html` and `.md` files in addition to their native extension.
617
617
 
618
+ #### Templates from an npm package
619
+
620
+ Layouts and partials can live in an installed package, so a shared theme ships as a dependency instead of copied files. Reference it by package name — any include/extend name containing a `/` is resolved from `node_modules`:
621
+
622
+ ```nunjucks
623
+ {% extends "my-theme/layout.html" %}
624
+ {% block content %}
625
+ <h1>{{ page.title }}</h1>
626
+ {% endblock %}
627
+ ```
628
+
629
+ Or from front matter, so the page carries no template syntax:
630
+
631
+ ```yaml
632
+ ---
633
+ layout: my-theme/layout
634
+ ---
635
+ ```
636
+
637
+ A theme package must:
638
+
639
+ - **Not restrict subpaths with `exports`** — or map its templates explicitly, e.g. `"exports": { "./*": "./*" }`. Otherwise Node blocks resolving the `.html` files by path.
640
+ - **Reference its own partials relatively** — `{% import "./nav.html" as nav %}`, not the bare name. A bare name (no `/`) is always searched in the consumer's project only, never the package.
641
+
642
+ Bundled filters (`toc`, `breadcrumb`, `og`, `canonical`, …) are engine-global, so package templates use them with no extra wiring.
643
+
644
+ Liquid resolves package templates the same way — `node_modules` is on its include roots, so `{% layout "my-theme/layout.liquid" %}` and `{% render "my-theme/partial.liquid" %}` resolve by package name too (a Liquid theme ships `.liquid` files). The `exports`/relative-partial rules above apply the same, except containment is by include root rather than the `/` name gate.
645
+
618
646
  #### Custom Engines
619
647
 
620
648
  The `engine` option also accepts a module specifier — an npm package name or a path relative to your project root. The module's default export must be an engine class:
@@ -34,6 +34,16 @@ export default class LiquidEngine {
34
34
  }
35
35
  } catch { /* ignore */ }
36
36
 
37
+ // Resolve package templates ({% render 'pkg/layout.liquid' %}) from the
38
+ // consumer's node_modules — parity with the Nunjucks engine. Every ancestor
39
+ // node_modules on the path is a root, so hoisted/scoped/pnpm installs all
40
+ // resolve, and liquidjs's containment guard passes without weakening it.
41
+ // Appended last so project templates always shadow a same-named package one.
42
+ for (let dir = templatesDir, prev = null; dir !== prev; prev = dir, dir = path.dirname(dir)) {
43
+ const nm = path.join(dir, 'node_modules')
44
+ if (fs.existsSync(nm)) roots.push(nm)
45
+ }
46
+
37
47
  // Dependency index for incremental rebuilds, fed by the recording parse
38
48
  // cache below. Persists across compiles; each page render overwrites its
39
49
  // own entry.
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs'
2
+ import { createRequire } from 'node:module'
2
3
  import { globSync } from 'glob'
3
4
  import nunjucks from 'nunjucks'
4
5
  import path from 'node:path'
@@ -15,6 +16,10 @@ class RelativeLoader extends nunjucks.Loader {
15
16
  constructor(templatesDir, includePaths, onTouch) {
16
17
  super()
17
18
  this.templatesDir = templatesDir
19
+ // Resolve package templates ({% extends 'pkg/layout.html' %}) from the
20
+ // consumer project's node_modules. Anchored at templatesDir so resolution
21
+ // walks up to the project root — also lets a fixture project be used in tests.
22
+ this.require = createRequire(path.join(templatesDir, 'package.json'))
18
23
  // Copy, don't alias+mutate: the caller reuses this array to build the
19
24
  // markup glob exclusions, so pushing '_*' here would leak into it.
20
25
  this.includePaths = [...(includePaths || []), '_*']
@@ -48,6 +53,14 @@ class RelativeLoader extends nunjucks.Loader {
48
53
  pattern = `${this.includePaths[0]}/${pattern}`
49
54
  }
50
55
  fullPath = globSync(toPosix(path.join(this.templatesDir, pattern)))[0]
56
+
57
+ // Not in the project tree — try a package template installed in the
58
+ // consumer's node_modules, addressed as 'pkg/template.html'. The '/'
59
+ // gate keeps bare local names (navtree.html) project-only, so this
60
+ // never fires on the common path.
61
+ if (!fullPath && name.includes('/')) {
62
+ try { fullPath = this.require.resolve(name) } catch { /* stays not-found below */ }
63
+ }
51
64
  }
52
65
  if (!fullPath || !fs.existsSync(fullPath)) {
53
66
  log({ tag: 'markup', error: true, text: 'Template not found:', link: name })
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.8.0",
4
+ "version": "1.9.0",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "poops.js",