@ynode/squirrellyify 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +247 -0
  3. package/package.json +65 -0
  4. package/src/plugin.js +275 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Michael Welter <nme@mikinho.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # @ynode/squirrellyify
2
+
3
+ Copyright (c) 2025 Michael Welter <me@mikinho.com>
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@ynode/squirrellyify.svg)](https://www.npmjs.com/package/@ynode/squirrellyify)
6
+ [![build](https://img.shields.io/github/actions/workflow/status/yammm/ynode-squirrellyify/your-workflow.yml)](https://github.com/yammm/ynode-squirrellyify/actions)
7
+ [![license](https://img.shields.io/npm/l/@ynode/squirrellyify.svg)](https://github.com/yammm/ynode-squirrellyify/blob/main/LICENSE)
8
+
9
+ A simple and fast plugin for using the [Squirrelly](https://squirrelly.js.org/) template engine with
10
+ [Fastify](https://www.fastify.io/).
11
+
12
+ ## Features
13
+
14
+ - 🐿️ **Modern Templating:** Full support for Squirrelly v9 features.
15
+ - ⚡ **High Performance:** Template caching is enabled by default in production.
16
+ - 📁 **Layouts & Partials:** Built-in support for layouts and shared partials.
17
+ - 🧬 **Encapsulation-Aware:** Respects Fastify's encapsulation model for s ed configurations.
18
+ - 🛡️ **Secure:** Protects against path traversal attacks in template names.
19
+ - 🔧 **Extensible:** Easily add custom Squirrelly helpers and filters.
20
+
21
+ ## Installation
22
+
23
+ You need to install `squirrelly` and `fastify` alongside this plugin.
24
+
25
+ ```bash
26
+ npm install @ynode/squirrellyify squirrelly fastify
27
+ ```
28
+
29
+ ## Basic Usage
30
+
31
+ 1. **Register the plugin.**
32
+ 2. **Use the `reply.view()` decorator in your routes.**
33
+
34
+ By default, the plugin looks for templates in a `views` directory in your project's root.
35
+
36
+ **File structure:**
37
+
38
+ ```text
39
+ .
40
+ ├── views/
41
+ │ └── index.sqrl
42
+ └── server.js
43
+ ```
44
+
45
+ **`views/index.sqrl`**
46
+
47
+ ```html
48
+ <h1>Hello, {{ it.name }}!</h1>
49
+ ```
50
+
51
+ **`server.js`**
52
+
53
+ ```javascript
54
+ import Fastify from "fastify";
55
+ import squirrellyify from "@ynode/squirrellyify";
56
+ import path from "node:path";
57
+ import { fileURLToPath } from "node:url";
58
+
59
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
60
+
61
+ const fastify = Fastify({
62
+ logger: true,
63
+ });
64
+
65
+ fastify.register(squirrellyify, {
66
+ templates: path.join(__dirname, "views"),
67
+ });
68
+
69
+ fastify.get("/", (request, reply) => {
70
+ return reply.view("index", { name: "World" });
71
+ });
72
+
73
+ fastify.listen({ port: 3000 }, (err) => {
74
+ if (err) throw err;
75
+ });
76
+ ```
77
+
78
+ ## Configuration Options
79
+
80
+ You can pass an options object when registering the plugin.
81
+
82
+ | Option | Type | Default | Description |
83
+ | ------------------ | -------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
84
+ | `templates` | `string \| string[]` | `path.join(process.cwd(), "views")` | The directory or directories to search for page and layout templates. Searched in the provided order. |
85
+ | `partials` | `string \| string[]` | `[]` | The directory or directories for partial templates. All partials are loaded on startup and are available globally by their filename. |
86
+ | `layout` | `string` | `undefined` | The name of the default layout file to use (without extension). Can be overridden per-route. |
87
+ | `defaultExtension` | `string` | `"sqrl"` | The file extension for all template files. |
88
+ | `cache` | `boolean` | `NODE_ENV === "production"` | If `true`, compiled templates and resolved file paths will be cached in memory. |
89
+ | `sqrl` | `object` | `undefined` | An object to configure Squirrelly. Currently supports `{ helpers: {}, filters: {} }` for adding custom functions. |
90
+
91
+ ## Advanced Usage
92
+
93
+ ### Layouts
94
+
95
+ Layouts are wrappers for your page templates. The rendered page content is injected into the `body`
96
+ variable within the layout.
97
+
98
+ **`views/layouts/main.sqrl`**
99
+
100
+ ```html
101
+ <!DOCTYPE html>
102
+ <html lang="en">
103
+ <head>
104
+ <title>{{ it.title }}</title>
105
+ </head>
106
+ <body>
107
+ <header>My Awesome Site</header>
108
+ <main>
109
+ {{@block("content")}} {{@try}} {{it.body | safe}} {{#catch => err}} Uh-oh, error!
110
+ Message was '{{err.message}}' {{/try}} {{/block}}
111
+ </main>
112
+ </body>
113
+ </html>
114
+ ```
115
+
116
+ **`views/about.sqrl`**
117
+
118
+ ```html
119
+ {{@extends("layout", it)}} {{#content}}
120
+ <h2>About Us</h2>
121
+ <p>This is the about page content.</p>
122
+
123
+ {{/extends}}
124
+ ```
125
+
126
+ You can specify a layout in three ways (in order of precedence):
127
+
128
+ 1. **In the `reply.view()` data object:**
129
+
130
+ ```javascript
131
+ fastify.get("/about", (request, reply) => {
132
+ const pageData = { title: "About Page" };
133
+ // Use `main.sqrl` as the layout for this request
134
+ return reply.view("about", { ...pageData, layout: "layouts/main" });
135
+ });
136
+
137
+ // To disable the default layout for a specific route:
138
+ fastify.get("/no-layout", (request, reply) => {
139
+ return reply.view("some-page", { layout: false });
140
+ });
141
+ ```
142
+
143
+ 2. **As a default plugin option:**
144
+
145
+ ```javascript
146
+ fastify.register(squirrellyify, {
147
+ templates: "views",
148
+ layout: "layouts/main", // All views will use this layout by default
149
+ });
150
+ ```
151
+
152
+ ### Partials
153
+
154
+ Partials are reusable chunks of template code. Create a `partials` directory and place your files
155
+ there. They will be automatically registered by their filename.
156
+
157
+ **`partials/user-card.sqrl`**
158
+
159
+ ```html
160
+ <div class="card">
161
+ <h3>{{ it.name }}</h4>
162
+ <p>{{ it.email }}</p>
163
+ </div>
164
+ ```
165
+
166
+ **`views/index.sqrl`**
167
+
168
+ ```html
169
+ <h1>Users</h1>
170
+ {{ include('user-card', { name: 'John Doe', email: 'john@example.com' }) /}}
171
+ ```
172
+
173
+ **Register the `partials` directory:**
174
+
175
+ ```javascript
176
+ fastify.register(squirrellyify, {
177
+ templates: "views",
178
+ partials: "partials",
179
+ });
180
+ ```
181
+
182
+ ### Scoped Configuration (Encapsulation)
183
+
184
+ This plugin supports Fastify's encapsulation model. You can register it multiple times with
185
+ different settings for different route prefixes.
186
+
187
+ ```javascript
188
+ import Fastify from "fastify";
189
+ import squirrellyify from "@ynode/squirrellyify";
190
+ import path from "node:path";
191
+
192
+ const fastify = Fastify();
193
+
194
+ // Register with default settings
195
+ fastify.register(squirrellyify, {
196
+ templates: path.join(__dirname, "views"),
197
+ layout: "layouts/main",
198
+ });
199
+
200
+ fastify.get("/", (req, reply) => {
201
+ // Renders from ./views/index.sqrl with layouts/main.sqrl
202
+ return reply.view("index", { title: "Homepage" });
203
+ });
204
+
205
+ // Create a separate scope for an "admin" section
206
+ fastify.register(
207
+ (instance, opts, done) => {
208
+ // Override the templates directory and layout for this scope
209
+ instance.views = path.join(__dirname, "admin/views");
210
+ instance.layout = "layouts/admin";
211
+
212
+ instance.get("/", (req, reply) => {
213
+ // Renders from ./admin/views/dashboard.sqrl with layouts/admin.sqrl
214
+ return reply.view("dashboard", { title: "Admin Panel" });
215
+ });
216
+
217
+ done();
218
+ },
219
+ { prefix: "/admin" },
220
+ );
221
+ ```
222
+
223
+ ### Custom Helpers and Filters
224
+
225
+ You can extend Squirrelly with custom helper and filter functions via the `sqrl` option.
226
+
227
+ ```javascript
228
+ fastify.register(squirrellyify, {
229
+ templates: "views",
230
+ sqrl: {
231
+ helpers: {
232
+ capitalize: (str) => {
233
+ return str.charAt(0).toUpperCase() + str.slice(1);
234
+ },
235
+ },
236
+ filters: {
237
+ truncate: (str, len) => {
238
+ return str.length > len ? str.substring(0, len) + "..." : str;
239
+ },
240
+ },
241
+ },
242
+ });
243
+ ```
244
+
245
+ ## License
246
+
247
+ [MIT](./LICENSE)
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@ynode/squirrellyify",
3
+ "version": "1.0.1",
4
+ "description": "Fastify plugin for rendering Squirrelly templates.",
5
+ "main": "src/plugin.js",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "Michael Welter <me@mikinho.com>",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/yammm/ynode-squirrellyify.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/yammm/ynode-squirrellyify/issues"
15
+ },
16
+ "homepage": "https://github.com/yammm/ynode-squirrellyify#readme",
17
+ "keywords": [
18
+ "fastify",
19
+ "template",
20
+ "view",
21
+ "squirrelly"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "devDependencies": {
27
+ "@eslint/js": "^9.37.0",
28
+ "@eslint/json": "^0.13.2",
29
+ "@eslint/markdown": "^7.4.0",
30
+ "@mikinho/autover": "^2.0.1",
31
+ "eslint": "^9.37.0",
32
+ "eslint-config-prettier": "^10.1.8",
33
+ "eslint-plugin-prettier": "^5.5.4",
34
+ "globals": "^16.4.0",
35
+ "prettier": "^3.6.2",
36
+ "rimraf": "^6.0.1",
37
+ "yuidocjs": "^0.10.2"
38
+ },
39
+ "scripts": {
40
+ "docs": "node scripts/gen-docs.mjs",
41
+ "docs:clean": "rimraf docs || rmdir /s /q docs 2> NUL || true",
42
+ "docs:open": "node -e \"import('node:child_process').then(m=>m.exec(process.platform==='win32'?'start docs/index.html':(process.platform==='darwin'?'open docs/index.html':'xdg-open docs/index.html')))\"",
43
+ "format": "prettier --write .",
44
+ "format:check": "prettier --check .",
45
+ "lint": "eslint .",
46
+ "lint:fix": "eslint . --fix",
47
+ "ver:preview": "npx autover --no-amend --dry-run --short",
48
+ "ver:apply": "npx autover --guard-unchanged --short",
49
+ "test": "eslint --no-warn-ignored $(git diff --cached --name-only --diff-filter=ACMRTUXB | tr '\\n' ' ')",
50
+ "prepublishOnly": "npm test || true",
51
+ "postversion": "git push && git push --tags"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "files": [
57
+ "src",
58
+ "README.md",
59
+ "LICENSE"
60
+ ],
61
+ "dependencies": {
62
+ "fastify-plugin": "^5.1.0",
63
+ "squirrelly": "^9.1.0"
64
+ }
65
+ }
package/src/plugin.js ADDED
@@ -0,0 +1,275 @@
1
+ /**
2
+ * A Squirrelly Fastify plugin
3
+ *
4
+ * @module @ynode/squirrellyify
5
+ */
6
+
7
+ /*
8
+ The MIT License (MIT)
9
+
10
+ Copyright (c) 2025 Michael Welter <me@mikinho.com>
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
13
+ this software and associated documentation files (the "Software"), to deal in
14
+ the Software without restriction, including without limitation the rights to
15
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
16
+ the Software, and to permit persons to whom the Software is furnished to do so,
17
+ subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
24
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
25
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
26
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
27
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+ */
29
+
30
+ import fs from "node:fs/promises";
31
+ import path from "node:path";
32
+
33
+ import fp from "fastify-plugin";
34
+ import Sqrl from "squirrelly";
35
+
36
+ /**
37
+ * @typedef {import("fastify").FastifyInstance} FastifyInstance
38
+ * @typedef {import("fastify").FastifyReply} FastifyReply
39
+ * @typedef {import("squirrelly").SqrlConfig} SqrlConfig
40
+ */
41
+
42
+ /**
43
+ * This plugin adds a "view" decorator to the Fastify reply object,
44
+ * allowing for the rendering of Squirrelly templates with support for layouts and partials.
45
+ *
46
+ * @param {FastifyInstance} fastify The Fastify instance.
47
+ * @param {object} options Plugin options.
48
+ * @param {string|string[]} [options.templates] The directory or directories where page and layout templates are stored. Defaults to "views". Directories are searched in order.
49
+ * @param {string|string[]} [options.partials] The directory or directories where partial templates are stored.
50
+ * @param {string} [options.layout] The name of the default layout file to use (without extension).
51
+ * @param {string} [options.defaultExtension="sqrl"] The default extension for template files.
52
+ * @param {boolean} [options.cache] Enables template caching. Defaults to true if NODE_ENV is "production".
53
+ */
54
+ async function squirrellyify(fastify, options = {}) {
55
+ // Get initial options and set defaults from the plugin registration
56
+ const initialTemplatesDirs = Array.isArray(options.templates)
57
+ ? options.templates
58
+ : typeof options.templates === "string"
59
+ ? [options.templates]
60
+ : [path.join(process.cwd(), "views")];
61
+
62
+ const initialPartialsDirs = Array.isArray(options.partials)
63
+ ? options.partials
64
+ : typeof options.partials === "string"
65
+ ? [options.partials]
66
+ : [];
67
+
68
+ const initialLayout = options.layout;
69
+ const defaultExtension = options.defaultExtension || "sqrl";
70
+ const extensionWithDot = `.${defaultExtension}`;
71
+ const useCache = options.cache ?? process.env.NODE_ENV === "production";
72
+ const templateCache = new Map();
73
+ const pathCache = new Map();
74
+ const templateMeta = new Map();
75
+
76
+ // Allow passing optional Squirrelly compile/render configuration
77
+ const sqrlConfig = options.sqrl?.config;
78
+
79
+ // Allow Passing Custom Squirrelly Configuration
80
+ if (options.sqrl) {
81
+ if (options.sqrl.helpers) {
82
+ Object.entries(options.sqrl.helpers).forEach(([name, fn]) => {
83
+ Sqrl.helpers.define(name, fn);
84
+ });
85
+ }
86
+ if (options.sqrl.filters) {
87
+ Object.entries(options.sqrl.filters).forEach(([name, fn]) => {
88
+ Sqrl.filters.define(name, fn);
89
+ });
90
+ }
91
+ }
92
+
93
+ // Pre-load and define all partials globally on startup from all partial directories
94
+ if (initialPartialsDirs.length > 0) {
95
+ for (const partialsDir of initialPartialsDirs) {
96
+ try {
97
+ const files = await fs.readdir(partialsDir);
98
+ await Promise.all(
99
+ files.map(async (file) => {
100
+ if (file.endsWith(extensionWithDot)) {
101
+ const partialPath = path.join(partialsDir, file);
102
+ const partialName = path.basename(file, extensionWithDot);
103
+ const content = await fs.readFile(partialPath, "utf-8");
104
+ fastify.log.trace(`Loaded partial: ${partialName}`);
105
+ Sqrl.templates.define(partialName, Sqrl.compile(content, sqrlConfig));
106
+ }
107
+ }),
108
+ );
109
+ } catch (error) {
110
+ fastify.log.error(`Error loading partials from ${partialsDir}: ${error.message}`);
111
+ throw error;
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Compiles a template from a file path and caches it if enabled.
118
+ */
119
+ async function getTemplate(templatePath) {
120
+ if (useCache && templateCache.has(templatePath)) {
121
+ return templateCache.get(templatePath);
122
+ }
123
+ const content = await fs.readFile(templatePath, "utf-8");
124
+ const hasLayoutTag = /{{\s*(?:@extends|!layout)\s*\(/.test(content);
125
+ const compiled = Sqrl.compile(content, sqrlConfig);
126
+ templateMeta.set(templatePath, { hasLayoutTag });
127
+ if (useCache) {
128
+ templateCache.set(templatePath, compiled);
129
+ }
130
+ return compiled;
131
+ }
132
+
133
+ /**
134
+ * Because template comes from route code, a mistaken ../ could escape the views dir.
135
+ * Disallow path separators and .. in template/layout names.
136
+ */
137
+ function assertSafeName(name) {
138
+ if (
139
+ name.includes("..") ||
140
+ name.includes(path.sep) ||
141
+ name.includes("/") ||
142
+ name.includes("\\")
143
+ ) {
144
+ throw new Error(`Illegal template name: ${name}`);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Renders a Squirrelly template and sends it as an HTML response.
150
+ * @this {FastifyReply}
151
+ * @param {string} template The name of the template file (without extension).
152
+ * @param {object} [data={}] The data to pass to the template. Can include a `layout` property to specify a layout file or set to `false` to disable layout for this request.
153
+ */
154
+ async function view(template, data = {}) {
155
+ try {
156
+ assertSafeName(template);
157
+ if (data.layout && data.layout !== false) {
158
+ assertSafeName(data.layout);
159
+ }
160
+
161
+ const instance = this.request.server;
162
+
163
+ const aggregatedTemplatesDirs = [];
164
+ let scopedLayout = null;
165
+ let currentInstance = instance;
166
+ while (currentInstance) {
167
+ if (currentInstance.views) {
168
+ const dirs = Array.isArray(currentInstance.views)
169
+ ? currentInstance.views
170
+ : [currentInstance.views];
171
+ aggregatedTemplatesDirs.push(...dirs);
172
+ }
173
+ if (
174
+ scopedLayout === null &&
175
+ currentInstance.layout !== null &&
176
+ currentInstance.layout !== undefined
177
+ ) {
178
+ scopedLayout = currentInstance.layout;
179
+ }
180
+ // Defensive: parent may be undefined or private
181
+ currentInstance = currentInstance.parent ?? null;
182
+ }
183
+
184
+ const combinedDirs = [
185
+ ...new Set([...aggregatedTemplatesDirs, ...initialTemplatesDirs]),
186
+ ];
187
+ const allSearchDirs = [...new Set([...combinedDirs, ...initialPartialsDirs])];
188
+
189
+ async function findTemplatePath(templateName) {
190
+ const templateFile = `${templateName}${extensionWithDot}`;
191
+ const cacheKey = `${allSearchDirs.join(";")}:${templateFile}`; // Create a unique key
192
+
193
+ if (useCache && pathCache.has(cacheKey)) {
194
+ return pathCache.get(cacheKey);
195
+ }
196
+
197
+ for (const dir of allSearchDirs) {
198
+ const fullPath = path.join(dir, templateFile);
199
+ try {
200
+ await fs.access(fullPath);
201
+ if (useCache) {
202
+ pathCache.set(cacheKey, fullPath); // Cache the found path
203
+ }
204
+ return fullPath;
205
+ } catch (error) {
206
+ fastify.log.trace(error);
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+
212
+ // 1. Find and render the page template
213
+ const pagePath = await findTemplatePath(template);
214
+ if (!pagePath) {
215
+ throw new Error(
216
+ `Template "${template}" not found in [${allSearchDirs.join(", ")}]`,
217
+ );
218
+ }
219
+
220
+ const pageTemplate = await getTemplate(pagePath);
221
+ const pageHtml = pageTemplate(data, sqrlConfig ?? Sqrl.defaultConfig);
222
+
223
+ // 2. Determine which layout to use
224
+ const currentLayout = scopedLayout !== null ? scopedLayout : initialLayout;
225
+ const layoutFile = data.layout === false ? null : data.layout || currentLayout;
226
+
227
+ if (!layoutFile) {
228
+ return this.type("text/html").send(pageHtml);
229
+ }
230
+
231
+ const hasLayoutTag = templateMeta.get(pagePath)?.hasLayoutTag === true;
232
+ if (hasLayoutTag) {
233
+ return this.type("text/html").send(pageHtml);
234
+ }
235
+
236
+ // 3. Find and render the layout, injecting the page content
237
+ const layoutPath = await findTemplatePath(layoutFile);
238
+ if (!layoutPath) {
239
+ throw new Error(
240
+ `Layout "${layoutFile}" not found in [${allSearchDirs.join(", ")}]`,
241
+ );
242
+ }
243
+
244
+ const layoutTemplate = await getTemplate(layoutPath);
245
+ const layoutData = { ...data, ...data.layoutData, body: pageHtml };
246
+ const finalHtml = layoutTemplate(layoutData, sqrlConfig ?? Sqrl.defaultConfig);
247
+
248
+ return this.type("text/html").send(finalHtml);
249
+ } catch (error) {
250
+ fastify.log.error(error);
251
+ if (process.env.NODE_ENV === "production") {
252
+ // In production, send a generic error and don't leak details
253
+ this.status(500).send("An internal server error occurred.");
254
+ } else {
255
+ // In development, it's okay to send the detailed error
256
+ this.code(500).send(error);
257
+ }
258
+ }
259
+ }
260
+
261
+ // Decorate the reply object with the main view function
262
+ fastify.decorateReply("view", view);
263
+
264
+ // Decorate the fastify instance so users can override settings in different scopes
265
+ fastify.decorate("views", null);
266
+ fastify.decorate("layout", null);
267
+
268
+ // Also expose the Squirrelly engine itself for advanced configuration (e.g., adding helpers/filters)
269
+ fastify.decorate("Sqrl", Sqrl);
270
+ }
271
+
272
+ export default fp(squirrellyify, {
273
+ fastify: "5.x",
274
+ name: "squirrellyify",
275
+ });