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.
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +99 -2
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +6 -0
- package/README.md +18 -5
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/routes.js +8 -0
- package/core/applySchema.js +18 -1
- package/index.d.ts +156 -1
- package/index.js +71 -16
- package/package.json +5 -5
- package/plugins/admin-panel/index.js +12 -0
- package/plugins/csrf/index.js +1 -1
- package/plugins/file-manager/index.js +13 -0
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/src/api/api-loader.js +17 -1
- package/src/api/validation-i18n.js +231 -0
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +70 -4
- package/src/file-router.js +153 -91
- package/src/njk-frontmatter.js +17 -1
- package/src/plugin-manager.js +12 -0
- package/src/routing/route-table.js +1 -1
- package/src/server.js +63 -8
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/template/precompile.js +247 -0
|
@@ -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
|
+
};
|