@ynode/squirrellyify 1.5.1 → 1.6.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.
Files changed (3) hide show
  1. package/README.md +20 -29
  2. package/package.json +1 -1
  3. package/src/plugin.js +57 -19
package/README.md CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  Copyright (c) 2025 Michael Welter <me@mikinho.com>
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@ynode/squirrellyify.svg)](https://www.npmjs.com/package/@ynode/squirrellyify)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![npm version](https://img.shields.io/npm/v/@ynode/squirrellyify.svg)](https://www.npmjs.com/package/@ynode/squirrellyify) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
6
 
8
- A simple and fast plugin for using the [Squirrelly](https://squirrelly.js.org/) template engine with
9
- [Fastify](https://www.fastify.io/).
7
+ A simple and fast plugin for using the [Squirrelly](https://squirrelly.js.org/) template engine with [Fastify](https://www.fastify.io/).
10
8
 
11
9
  ## Features
12
10
 
@@ -76,8 +74,7 @@ fastify.listen({ port: 3000 }, (err) => {
76
74
 
77
75
  ### Request-Scoped View Data
78
76
 
79
- `reply.view(template, data)` automatically merges request-scoped values from `reply.locals` and `reply.context` into the
80
- template data:
77
+ `reply.view(template, data)` automatically merges request-scoped values from `reply.locals` and `reply.context` into the template data:
81
78
 
82
79
  ```javascript
83
80
  fastify.addHook("preHandler", async (request, reply) => {
@@ -100,23 +97,22 @@ Merge precedence is:
100
97
 
101
98
  You can pass an options object when registering the plugin.
102
99
 
103
- | Option | Type | Default | Description |
104
- | ------------------- | -------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
105
- | `templates` | `string \| string[]` | `path.join(process.cwd(), "views")` | The directory or directories to search for page and layout templates. Searched in the provided order. |
106
- | `partials` | `string \| string[]` | `[]` | The directory or directories for partial templates. All partials are loaded on startup and available by name. |
107
- | `partialsRecursive` | `boolean` | `true` | If `true`, partials are loaded recursively from subdirectories. Names use forward slashes (for example, `emails/header`). |
108
- | `partialsNamespace` | `boolean \| string` | `false` | Optional namespace prefix for partial names. Use `true` to prefix with each partials directory basename, or provide a custom string. |
109
- | `layout` | `string` | `undefined` | The name of the default layout file to use (without extension). Can be overridden per-route. |
110
- | `defaultExtension` | `string` | `"sqrl"` | The file extension for all template files. Leading `.` is optional (for example, `"html"` or `".html"`). |
111
- | `cache` | `boolean` | `NODE_ENV === "production"` | If `true`, compiled templates and resolved file paths will be cached in memory. |
112
- | `sqrl` | `object` | `undefined` | Squirrelly options. Supports `{ scope: "global" \| "scoped", config, helpers, filters }`. |
100
+ | Option | Type | Default | Description |
101
+ | --- | --- | --- | --- |
102
+ | `templates` | `string \| string[]` | `path.join(process.cwd(), "views")` | The directory or directories to search for page and layout templates. Searched in the provided order. |
103
+ | `partials` | `string \| string[]` | `[]` | The directory or directories for partial templates. All partials are loaded on startup and available by name. |
104
+ | `partialsRecursive` | `boolean` | `true` | If `true`, partials are loaded recursively from subdirectories. Names use forward slashes (for example, `emails/header`). |
105
+ | `partialsNamespace` | `boolean \| string` | `false` | Optional namespace prefix for partial names. Use `true` to prefix with each partials directory basename, or provide a custom string. |
106
+ | `layout` | `string` | `undefined` | The name of the default layout file to use (without extension). Can be overridden per-route. |
107
+ | `defaultExtension` | `string` | `"sqrl"` | The file extension for all template files. Leading `.` is optional (for example, `"html"` or `".html"`). |
108
+ | `cache` | `boolean` | `NODE_ENV === "production"` | If `true`, compiled templates and resolved file paths will be cached in memory. |
109
+ | `sqrl` | `object` | `undefined` | Squirrelly options. Supports `{ scope: "global" \| "scoped", config, helpers, filters }`. |
113
110
 
114
111
  Runtime API after registration:
115
112
 
116
113
  - `fastify.viewHelpers.define(name, fn)`, `fastify.viewHelpers.get(name)`, `fastify.viewHelpers.remove(name)`
117
114
  - `fastify.viewFilters.define(name, fn)`, `fastify.viewFilters.get(name)`, `fastify.viewFilters.remove(name)`
118
- - `fastify.viewPartials.define(name, templateOrFn)`, `fastify.viewPartials.get(name)`,
119
- `fastify.viewPartials.remove(name)`
115
+ - `fastify.viewPartials.define(name, templateOrFn)`, `fastify.viewPartials.get(name)`, `fastify.viewPartials.remove(name)`
120
116
  - `fastify.viewCache.clear()`, `fastify.viewCache.stats()`
121
117
 
122
118
  These APIs are scope-aware:
@@ -124,8 +120,7 @@ These APIs are scope-aware:
124
120
  - In `global` mode they modify shared helpers/filters/partials.
125
121
  - In `scoped` mode they only affect the current plugin registration scope.
126
122
 
127
- The cache API is process-local and lets you invalidate compiled template/path caches at runtime when `cache: true` is
128
- used.
123
+ The cache API is process-local and lets you invalidate compiled template/path caches at runtime when `cache: true` is used.
129
124
 
130
125
  Invalid option types are rejected at plugin registration time with descriptive errors.
131
126
 
@@ -133,8 +128,7 @@ Invalid option types are rejected at plugin registration time with descriptive e
133
128
 
134
129
  ### Layouts
135
130
 
136
- Layouts are wrappers for your page templates. The rendered page content is injected into the `body` variable within the
137
- layout.
131
+ Layouts are wrappers for your page templates. The rendered page content is injected into the `body` variable within the layout.
138
132
 
139
133
  **`views/layouts/main.sqrl`**
140
134
 
@@ -147,8 +141,8 @@ layout.
147
141
  <body>
148
142
  <header>My Awesome Site</header>
149
143
  <main>
150
- {{@block("content")}} {{@try}} {{it.body | safe}} {{#catch => err}} Uh-oh, error! Message was
151
- '{{err.message}}' {{/try}} {{/block}}
144
+ {{@block("content")}} {{@try}} {{it.body | safe}} {{#catch => err}} Uh-oh, error!
145
+ Message was '{{err.message}}' {{/try}} {{/block}}
152
146
  </main>
153
147
  </body>
154
148
  </html>
@@ -192,8 +186,7 @@ You can specify a layout in three ways (in order of precedence):
192
186
 
193
187
  ### Partials
194
188
 
195
- Partials are reusable chunks of template code. Create a `partials` directory and place your files there. By default,
196
- partials are loaded recursively and registered by forward-slash path from the partials directory root.
189
+ Partials are reusable chunks of template code. Create a `partials` directory and place your files there. By default, partials are loaded recursively and registered by forward-slash path from the partials directory root.
197
190
 
198
191
  **`partials/user-card.sqrl`**
199
192
 
@@ -258,8 +251,7 @@ fastify.register(squirrellyify, {
258
251
 
259
252
  ### Scoped Configuration (Encapsulation)
260
253
 
261
- This plugin supports Fastify's encapsulation model. You can register it multiple times with different settings for
262
- different route prefixes.
254
+ This plugin supports Fastify's encapsulation model. You can register it multiple times with different settings for different route prefixes.
263
255
 
264
256
  ```javascript
265
257
  import Fastify from "fastify";
@@ -331,4 +323,3 @@ fastify.register(squirrellyify, {
331
323
  ## License
332
324
 
333
325
  This project is licensed under the [MIT License](./LICENSE).
334
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynode/squirrellyify",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Fastify plugin for rendering Squirrelly templates.",
5
5
  "main": "src/plugin.js",
6
6
  "type": "module",
package/src/plugin.js CHANGED
@@ -38,7 +38,12 @@ import {
38
38
  resolveUseCache,
39
39
  validatePluginOptions,
40
40
  } from "./config.js";
41
- import { buildTemplateSearchDirs, collectViewScope, createTemplateResolver, preloadPartials } from "./resolver.js";
41
+ import {
42
+ buildTemplateSearchDirs,
43
+ collectViewScope,
44
+ createTemplateResolver,
45
+ preloadPartials,
46
+ } from "./resolver.js";
42
47
  import { createRuntimeApi } from "./runtime-api.js";
43
48
  import { assertSafeName } from "./safety.js";
44
49
 
@@ -75,7 +80,9 @@ async function squirrellyify(fastify, options = {}) {
75
80
  }
76
81
 
77
82
  const log =
78
- typeof fastify.log?.child === "function" ? fastify.log.child({ name: "@ynode/squirrellyify" }) : fastify.log;
83
+ typeof fastify.log?.child === "function"
84
+ ? fastify.log.child({ name: "@ynode/squirrellyify" })
85
+ : fastify.log;
79
86
 
80
87
  const initialTemplatesDirs = resolveInitialTemplateDirs(options);
81
88
  const initialPartialsDirs = resolveInitialPartialsDirs(options);
@@ -83,11 +90,17 @@ async function squirrellyify(fastify, options = {}) {
83
90
  const { extensionWithDot } = resolveExtension(options);
84
91
  const useCache = resolveUseCache(options);
85
92
  const { sqrlScope, sqrlConfig } = resolveSqrlConfig(options);
86
- const { defineSqrlHelper, defineSqrlFilter, defineSqrlTemplate, viewHelpers, viewFilters, viewPartials } =
87
- createRuntimeApi({
88
- sqrlScope,
89
- sqrlConfig,
90
- });
93
+ const {
94
+ defineSqrlHelper,
95
+ defineSqrlFilter,
96
+ defineSqrlTemplate,
97
+ viewHelpers,
98
+ viewFilters,
99
+ viewPartials,
100
+ } = createRuntimeApi({
101
+ sqrlScope,
102
+ sqrlConfig,
103
+ });
91
104
 
92
105
  if (options.sqrl?.helpers) {
93
106
  Object.entries(options.sqrl.helpers).forEach(([name, fn]) => {
@@ -110,12 +123,13 @@ async function squirrellyify(fastify, options = {}) {
110
123
  sqrlConfig,
111
124
  });
112
125
 
113
- const { findTemplatePath, getTemplate, hasLayoutTag, clearCaches, cacheStats } = createTemplateResolver({
114
- fastify,
115
- extensionWithDot,
116
- useCache,
117
- sqrlConfig,
118
- });
126
+ const { findTemplatePath, getTemplate, hasLayoutTag, clearCaches, cacheStats } =
127
+ createTemplateResolver({
128
+ fastify,
129
+ extensionWithDot,
130
+ useCache,
131
+ sqrlConfig,
132
+ });
119
133
 
120
134
  /**
121
135
  * Renders a Squirrelly template and sends it as an HTML response.
@@ -126,7 +140,8 @@ async function squirrellyify(fastify, options = {}) {
126
140
  async function view(template, data = {}) {
127
141
  try {
128
142
  const requestData = data && typeof data === "object" ? data : {};
129
- const replyContext = this.context && typeof this.context === "object" ? this.context : {};
143
+ const replyContext =
144
+ this.context && typeof this.context === "object" ? this.context : {};
130
145
  const replyLocals = this.locals && typeof this.locals === "object" ? this.locals : {};
131
146
  const mergedData = {
132
147
  ...replyContext,
@@ -141,12 +156,17 @@ async function squirrellyify(fastify, options = {}) {
141
156
 
142
157
  const instance = this.request.server;
143
158
  const { aggregatedTemplatesDirs, scopedLayout } = collectViewScope(instance);
144
- const templateSearchDirs = buildTemplateSearchDirs(aggregatedTemplatesDirs, initialTemplatesDirs);
159
+ const templateSearchDirs = buildTemplateSearchDirs(
160
+ aggregatedTemplatesDirs,
161
+ initialTemplatesDirs,
162
+ );
145
163
 
146
164
  // 1. Find and render the page template
147
165
  const pagePath = await findTemplatePath(template, templateSearchDirs);
148
166
  if (!pagePath) {
149
- throw new Error(`Template "${template}" not found in [${templateSearchDirs.join(", ")}]`);
167
+ throw new Error(
168
+ `Template "${template}" not found in [${templateSearchDirs.join(", ")}]`,
169
+ );
150
170
  }
151
171
 
152
172
  const pageTemplate = await getTemplate(pagePath);
@@ -154,7 +174,8 @@ async function squirrellyify(fastify, options = {}) {
154
174
 
155
175
  // 2. Determine which layout to use
156
176
  const currentLayout = scopedLayout !== null ? scopedLayout : initialLayout;
157
- const layoutFile = mergedData.layout === false ? null : mergedData.layout || currentLayout;
177
+ const layoutFile =
178
+ mergedData.layout === false ? null : mergedData.layout || currentLayout;
158
179
 
159
180
  if (!layoutFile) {
160
181
  return this.type("text/html").send(pageHtml);
@@ -167,12 +188,16 @@ async function squirrellyify(fastify, options = {}) {
167
188
  // 3. Find and render the layout, injecting the page content
168
189
  const layoutPath = await findTemplatePath(layoutFile, templateSearchDirs);
169
190
  if (!layoutPath) {
170
- throw new Error(`Layout "${layoutFile}" not found in [${templateSearchDirs.join(", ")}]`);
191
+ throw new Error(
192
+ `Layout "${layoutFile}" not found in [${templateSearchDirs.join(", ")}]`,
193
+ );
171
194
  }
172
195
 
173
196
  const layoutTemplate = await getTemplate(layoutPath);
174
197
  const layoutPayload =
175
- mergedData.layoutData && typeof mergedData.layoutData === "object" ? mergedData.layoutData : {};
198
+ mergedData.layoutData && typeof mergedData.layoutData === "object"
199
+ ? mergedData.layoutData
200
+ : {};
176
201
  const layoutData = { ...mergedData, ...layoutPayload, body: pageHtml };
177
202
  const finalHtml = await layoutTemplate(layoutData, sqrlConfig);
178
203
 
@@ -189,6 +214,19 @@ async function squirrellyify(fastify, options = {}) {
189
214
  }
190
215
  }
191
216
 
217
+ // Pre-decorate context for V8 optimization performance in consumers
218
+ try {
219
+ fastify.decorateReply("context", null);
220
+ } catch (err) {
221
+ if (err.code !== "FST_ERR_DEC_ALREADY_PRESENT") {
222
+ throw err;
223
+ }
224
+ }
225
+
226
+ fastify.addHook("onRequest", async (request, reply) => {
227
+ reply.context = {};
228
+ });
229
+
192
230
  // Decorate the reply object with the main view function
193
231
  fastify.decorateReply("view", view);
194
232