webspresso 0.0.101 → 0.0.102

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.
@@ -0,0 +1,247 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Nunjucks Template Pre-compiler & AOT Bundle
5
+ * Pre-compiles Nunjucks templates into zero-parse, pure JavaScript functions to
6
+ * eliminate disk reading and AST tokenization/parsing overhead during SSR.
7
+ * @module src/template/precompile
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const nunjucks = require('nunjucks');
13
+ const { parseNjkFrontmatter, frontmatterToPatches, seedProdRouteCache } = require('../njk-frontmatter');
14
+
15
+ const DEFAULT_PRECOMPILED_REL_PATH = path.join('.webspresso', 'templates.compiled.js');
16
+
17
+ /**
18
+ * Recursively scans a directory for template files (.njk, .html).
19
+ * @param {string} dir
20
+ * @param {string} [baseDir=dir]
21
+ * @returns {Array<{ absolutePath: string, relativePath: string }>}
22
+ */
23
+ function scanTemplateDir(dir, baseDir = dir) {
24
+ if (!fs.existsSync(dir)) return [];
25
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
26
+ const results = [];
27
+
28
+ for (const entry of entries) {
29
+ const full = path.join(dir, entry.name);
30
+ if (entry.isDirectory()) {
31
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
32
+ results.push(...scanTemplateDir(full, baseDir));
33
+ } else if (entry.isFile()) {
34
+ const ext = path.extname(entry.name);
35
+ if (ext === '.njk' || ext === '.html') {
36
+ const rel = path.relative(baseDir, full).split(path.sep).join('/');
37
+ results.push({ absolutePath: full, relativePath: rel });
38
+ }
39
+ }
40
+ }
41
+
42
+ return results;
43
+ }
44
+
45
+ /**
46
+ * Normalizes a template name for uniform lookup.
47
+ * @param {string} name
48
+ * @returns {string}
49
+ */
50
+ function normalizeTemplateKey(name) {
51
+ if (typeof name !== 'string') return '';
52
+ return name.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^views\//, '');
53
+ }
54
+
55
+ /**
56
+ * Precompiles all Nunjucks templates in views/ and pages/ directories into an AOT bundle.
57
+ *
58
+ * @param {Object} [options={}]
59
+ * @param {string} [options.rootDir=process.cwd()] - Project root directory
60
+ * @param {string} [options.viewsDir] - Custom views directory path
61
+ * @param {string} [options.pagesDir] - Custom pages directory path
62
+ * @param {string} [options.outDir] - Directory where precompiled bundle is written
63
+ * @param {string} [options.outFile] - Custom output file path
64
+ * @param {import('nunjucks').Environment} [options.env] - Optional Nunjucks Environment
65
+ * @returns {{ templateCount: number, manifestPath: string, bundle: Object }}
66
+ */
67
+ function precompileTemplates(options = {}) {
68
+ const rootDir = options.rootDir ? path.resolve(options.rootDir) : process.cwd();
69
+ const viewsDir = options.viewsDir ? path.resolve(options.viewsDir) : path.join(rootDir, 'views');
70
+ const pagesDir = options.pagesDir ? path.resolve(options.pagesDir) : path.join(rootDir, 'pages');
71
+
72
+ const env = options.env || new nunjucks.Environment([], { autoescape: true });
73
+
74
+ const templates = {};
75
+ const frontmatter = {};
76
+ const processedKeys = new Set();
77
+
78
+ const dirsToScan = [
79
+ { dir: viewsDir, prefix: '', isViews: true },
80
+ { dir: pagesDir, prefix: '', isPages: true },
81
+ ];
82
+
83
+ for (const { dir, isViews } of dirsToScan) {
84
+ if (!fs.existsSync(dir)) continue;
85
+ const files = scanTemplateDir(dir);
86
+
87
+ for (const file of files) {
88
+ try {
89
+ const raw = fs.readFileSync(file.absolutePath, 'utf8');
90
+ const { body, fm, hasDelimiter } = parseNjkFrontmatter(raw);
91
+ const patches = frontmatterToPatches(fm);
92
+
93
+ const relToRoot = path.relative(rootDir, file.absolutePath).split(path.sep).join('/');
94
+ frontmatter[relToRoot] = {
95
+ useStringRender: false,
96
+ templateBody: null,
97
+ metaPatch: patches.metaPatch,
98
+ dataPatch: patches.dataPatch,
99
+ };
100
+
101
+ const compiledArray = nunjucks.precompileString(body, {
102
+ name: file.relativePath,
103
+ asFunction: false,
104
+ env,
105
+ wrapper: (items) => items,
106
+ });
107
+
108
+ if (compiledArray && compiledArray[0] && compiledArray[0].template) {
109
+ const code = compiledArray[0].template;
110
+ templates[file.relativePath] = code;
111
+
112
+ // Also register canonical stripped name for views
113
+ const normalized = normalizeTemplateKey(file.relativePath);
114
+ if (normalized !== file.relativePath) {
115
+ templates[normalized] = code;
116
+ }
117
+ if (isViews && !file.relativePath.startsWith('views/')) {
118
+ templates[`views/${file.relativePath}`] = code;
119
+ }
120
+
121
+ processedKeys.add(file.relativePath);
122
+ }
123
+ } catch (err) {
124
+ console.warn(`[webspresso] Failed to precompile template "${file.relativePath}":`, err.message);
125
+ }
126
+ }
127
+ }
128
+
129
+ const bundle = {
130
+ version: 1,
131
+ generatedAt: new Date().toISOString(),
132
+ templates,
133
+ frontmatter,
134
+ };
135
+
136
+ const outFile = options.outFile
137
+ ? path.resolve(options.outFile)
138
+ : (options.outDir
139
+ ? path.join(path.resolve(options.outDir), 'templates.compiled.js')
140
+ : path.join(rootDir, DEFAULT_PRECOMPILED_REL_PATH));
141
+
142
+ const outDir = path.dirname(outFile);
143
+ if (!fs.existsSync(outDir)) {
144
+ fs.mkdirSync(outDir, { recursive: true });
145
+ }
146
+
147
+ const fileContent = `/**
148
+ * Webspresso Precompiled Nunjucks Templates Bundle
149
+ * Generated: ${bundle.generatedAt}
150
+ * Total Templates: ${processedKeys.size}
151
+ */
152
+ module.exports = ${JSON.stringify(bundle, null, 2)};
153
+ `;
154
+
155
+ fs.writeFileSync(outFile, fileContent, 'utf8');
156
+
157
+ return {
158
+ templateCount: processedKeys.size,
159
+ manifestPath: outFile,
160
+ bundle,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Loads precompiled templates bundle from disk and constructs a PrecompiledLoader.
166
+ *
167
+ * @param {Object} [options={}]
168
+ * @param {string} [options.rootDir=process.cwd()] - Project root directory
169
+ * @param {string} [options.bundlePath] - Custom bundle file path
170
+ * @returns {{ loader: import('nunjucks').PrecompiledLoader, templateCount: number, bundle: Object }|null}
171
+ */
172
+ function loadPrecompiledTemplates(options = {}) {
173
+ const rootDir = options.rootDir ? path.resolve(options.rootDir) : process.cwd();
174
+ const bundlePath = options.bundlePath
175
+ ? path.resolve(options.bundlePath)
176
+ : path.join(rootDir, DEFAULT_PRECOMPILED_REL_PATH);
177
+
178
+ if (!fs.existsSync(bundlePath)) {
179
+ return null;
180
+ }
181
+
182
+ try {
183
+ // Delete require cache in case bundle was updated
184
+ if (require.cache[require.resolve(bundlePath)]) {
185
+ delete require.cache[require.resolve(bundlePath)];
186
+ }
187
+
188
+ const bundle = require(bundlePath);
189
+ if (!bundle || typeof bundle.templates !== 'object') {
190
+ return null;
191
+ }
192
+
193
+ const compiledMap = {};
194
+ for (const [name, code] of Object.entries(bundle.templates)) {
195
+ try {
196
+ compiledMap[name] = (new Function(code))();
197
+ } catch (compileErr) {
198
+ console.warn(`[webspresso] Failed to evaluate precompiled template "${name}":`, compileErr.message);
199
+ }
200
+ }
201
+
202
+ // Hydrate frontmatter cache for instant loadNjkRouteTemplate execution
203
+ if (bundle.frontmatter && typeof bundle.frontmatter === 'object') {
204
+ const absMap = {};
205
+ for (const [relPath, fmData] of Object.entries(bundle.frontmatter)) {
206
+ absMap[path.resolve(rootDir, relPath)] = fmData;
207
+ }
208
+ seedProdRouteCache(absMap);
209
+ }
210
+
211
+ const loader = new nunjucks.PrecompiledLoader(compiledMap);
212
+ loader.cache = {};
213
+ const origGetSource = loader.getSource.bind(loader);
214
+
215
+ loader.getSource = function(name) {
216
+ if (this.precompiled[name]) {
217
+ return {
218
+ src: { type: 'code', obj: this.precompiled[name] },
219
+ path: name,
220
+ };
221
+ }
222
+ const clean = normalizeTemplateKey(name);
223
+ if (this.precompiled[clean]) {
224
+ return {
225
+ src: { type: 'code', obj: this.precompiled[clean] },
226
+ path: clean,
227
+ };
228
+ }
229
+ return null;
230
+ };
231
+
232
+ return {
233
+ loader,
234
+ templateCount: Object.keys(compiledMap).length,
235
+ bundle,
236
+ };
237
+ } catch (err) {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ module.exports = {
243
+ DEFAULT_PRECOMPILED_REL_PATH,
244
+ precompileTemplates,
245
+ loadPrecompiledTemplates,
246
+ normalizeTemplateKey,
247
+ };