webspresso 0.0.95 → 0.0.96
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/ADMIN.md +225 -0
- package/.agents/AGENTS.md +72 -0
- package/.agents/ARCHITECTURE.md +119 -0
- package/.agents/AUTH.md +103 -0
- package/.agents/CLI.md +115 -0
- package/.agents/COMPRESSION.md +59 -0
- package/.agents/CONVENTIONS.md +46 -0
- package/.agents/EMAIL.md +141 -0
- package/.agents/ERRORS.md +201 -0
- package/.agents/MCP.md +103 -0
- package/.agents/ORM.md +230 -0
- package/.agents/PLUGINS.md +419 -0
- package/.agents/QUEUE.md +113 -0
- package/.agents/REALTIME.md +213 -0
- package/.agents/ROUTING.md +292 -0
- package/.agents/SERVICES.md +341 -0
- package/.agents/SHUTDOWN.md +67 -0
- package/.agents/STREAMING.md +73 -0
- package/.agents/WORKFLOW.md +27 -0
- package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
- package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
- package/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/agents-init.js +169 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/new.js +13 -2
- package/bin/commands/page.js +75 -89
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/compileSchema.js +1 -1
- package/core/errors/middleware.js +22 -2
- package/core/errors/normalize.js +29 -2
- package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
- package/index.d.ts +306 -0
- package/index.js +35 -1
- package/package.json +5 -3
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- package/plugins/csrf/index.js +3 -2
- package/plugins/csv/engine.js +447 -0
- package/plugins/csv/index.js +94 -0
- package/plugins/csv/middleware.js +80 -0
- package/plugins/csv/services.js +138 -0
- package/plugins/file-manager/admin-component.js +613 -0
- package/plugins/file-manager/api-handlers.js +261 -0
- package/plugins/file-manager/core.js +601 -0
- package/plugins/file-manager/index.js +139 -0
- package/plugins/file-manager/security.js +207 -0
- package/plugins/index.js +8 -0
- package/plugins/xlsx/engine.js +397 -0
- package/plugins/xlsx/index.js +91 -0
- package/plugins/xlsx/middleware.js +87 -0
- package/plugins/xlsx/services.js +118 -0
- package/src/api/api-loader.js +233 -0
- package/src/api/define-api.js +35 -0
- package/src/discovery/file-route-parser.js +201 -0
- package/src/discovery/scan-routes.js +265 -0
- package/src/file-router.js +6 -0
- package/src/modules/define-module.js +99 -0
- package/src/modules/module-discovery.js +117 -0
- package/src/pages/define-page.js +36 -0
- package/src/pages/page-loader.js +214 -0
- package/src/routing/mount-discovered-routes.js +78 -0
- package/src/routing/route-table.js +191 -0
- package/src/server.js +112 -15
- package/src/services/builtins/file-manager.js +178 -0
- package/src/services/index.js +2 -0
- package/bin/commands/skill.js +0 -187
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
- package/templates/skills/webspresso-usage/SKILL.md +0 -48
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in File Manager Services
|
|
3
|
+
* @module src/services/builtins/file-manager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs/promises');
|
|
8
|
+
const { z } = require('zod');
|
|
9
|
+
const { ValidationError, NotFoundError } = require('../../../core/errors');
|
|
10
|
+
const {
|
|
11
|
+
listFiles,
|
|
12
|
+
createDirectory,
|
|
13
|
+
renameItem,
|
|
14
|
+
deleteItem,
|
|
15
|
+
moveItem,
|
|
16
|
+
saveUploadedFile,
|
|
17
|
+
} = require('../../../plugins/file-manager/core');
|
|
18
|
+
const { parseBase64 } = require('./media');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create built-in file manager services map
|
|
22
|
+
* @param {Object} [options]
|
|
23
|
+
* @param {string} [options.baseDir]
|
|
24
|
+
* @param {string} [options.publicBasePath]
|
|
25
|
+
* @returns {Record<string, Object>}
|
|
26
|
+
*/
|
|
27
|
+
function createFileManagerServices(options = {}) {
|
|
28
|
+
const defaultBaseDir = path.resolve(
|
|
29
|
+
options.baseDir || path.join(process.cwd(), 'public', 'uploads')
|
|
30
|
+
);
|
|
31
|
+
const defaultPublicBase = options.publicBasePath || '/uploads';
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
'fileManager.list': {
|
|
35
|
+
schema: z.object({
|
|
36
|
+
path: z.string().optional().default(''),
|
|
37
|
+
search: z.string().optional(),
|
|
38
|
+
type: z.string().optional(),
|
|
39
|
+
sort: z.enum(['name', 'size', 'mtime', 'ext']).optional().default('name'),
|
|
40
|
+
order: z.enum(['asc', 'desc']).optional().default('asc'),
|
|
41
|
+
page: z.number().int().positive().optional(),
|
|
42
|
+
perPage: z.number().int().positive().optional(),
|
|
43
|
+
baseDir: z.string().optional(),
|
|
44
|
+
publicBasePath: z.string().optional(),
|
|
45
|
+
}),
|
|
46
|
+
async handler(input) {
|
|
47
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
48
|
+
const publicBasePath = input.publicBasePath || defaultPublicBase;
|
|
49
|
+
|
|
50
|
+
return listFiles(baseDir, input.path, {
|
|
51
|
+
search: input.search,
|
|
52
|
+
type: input.type,
|
|
53
|
+
sort: input.sort,
|
|
54
|
+
order: input.order,
|
|
55
|
+
page: input.page,
|
|
56
|
+
perPage: input.perPage,
|
|
57
|
+
publicBasePath,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
'fileManager.mkdir': {
|
|
63
|
+
schema: z.object({
|
|
64
|
+
path: z.string().optional().default(''),
|
|
65
|
+
name: z.string().min(1, 'Directory name is required'),
|
|
66
|
+
baseDir: z.string().optional(),
|
|
67
|
+
}),
|
|
68
|
+
async handler(input) {
|
|
69
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
70
|
+
return createDirectory(baseDir, input.path, input.name);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
'fileManager.rename': {
|
|
75
|
+
schema: z.object({
|
|
76
|
+
path: z.string().min(1, 'Target path is required'),
|
|
77
|
+
newName: z.string().min(1, 'New name is required'),
|
|
78
|
+
baseDir: z.string().optional(),
|
|
79
|
+
}),
|
|
80
|
+
async handler(input) {
|
|
81
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
82
|
+
return renameItem(baseDir, input.path, input.newName);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
'fileManager.delete': {
|
|
87
|
+
schema: z.object({
|
|
88
|
+
path: z.string().min(1, 'Path is required'),
|
|
89
|
+
baseDir: z.string().optional(),
|
|
90
|
+
}),
|
|
91
|
+
async handler(input) {
|
|
92
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
93
|
+
return deleteItem(baseDir, input.path);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
'fileManager.move': {
|
|
98
|
+
schema: z.object({
|
|
99
|
+
source: z.string().min(1, 'Source path is required'),
|
|
100
|
+
destination: z.string().optional().default(''),
|
|
101
|
+
baseDir: z.string().optional(),
|
|
102
|
+
}),
|
|
103
|
+
async handler(input) {
|
|
104
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
105
|
+
return moveItem(baseDir, input.source, input.destination);
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
'fileManager.upload': {
|
|
110
|
+
schema: z.object({
|
|
111
|
+
path: z.string().optional().default(''),
|
|
112
|
+
buffer: z.any().optional(),
|
|
113
|
+
base64: z.string().optional(),
|
|
114
|
+
filePath: z.string().optional(),
|
|
115
|
+
originalName: z.string().min(1).default('upload.bin'),
|
|
116
|
+
mimeType: z.string().optional(),
|
|
117
|
+
baseDir: z.string().optional(),
|
|
118
|
+
publicBasePath: z.string().optional(),
|
|
119
|
+
}),
|
|
120
|
+
async handler(input) {
|
|
121
|
+
let {
|
|
122
|
+
path: targetRelPath,
|
|
123
|
+
buffer,
|
|
124
|
+
base64,
|
|
125
|
+
filePath,
|
|
126
|
+
originalName,
|
|
127
|
+
mimeType,
|
|
128
|
+
baseDir: customBaseDir,
|
|
129
|
+
publicBasePath: customPublicBase,
|
|
130
|
+
} = input;
|
|
131
|
+
const baseDir = customBaseDir ? path.resolve(customBaseDir) : defaultBaseDir;
|
|
132
|
+
const publicBasePath = customPublicBase || defaultPublicBase;
|
|
133
|
+
|
|
134
|
+
if (buffer && !(buffer instanceof Buffer) && !(buffer instanceof Uint8Array)) {
|
|
135
|
+
if (typeof buffer === 'string') {
|
|
136
|
+
buffer = Buffer.from(buffer);
|
|
137
|
+
} else {
|
|
138
|
+
throw new ValidationError('Invalid buffer provided');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!buffer && base64) {
|
|
143
|
+
const parsed = parseBase64(base64);
|
|
144
|
+
buffer = parsed.buffer;
|
|
145
|
+
if (parsed.mimeType && (!mimeType || mimeType === 'application/octet-stream')) {
|
|
146
|
+
mimeType = parsed.mimeType;
|
|
147
|
+
}
|
|
148
|
+
} else if (!buffer && filePath) {
|
|
149
|
+
try {
|
|
150
|
+
buffer = await fs.readFile(path.resolve(filePath));
|
|
151
|
+
if (!originalName || originalName === 'upload.bin') {
|
|
152
|
+
originalName = path.basename(filePath);
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
throw new NotFoundError(`Source file not found: ${filePath}`, { cause: err });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!buffer || buffer.length === 0) {
|
|
160
|
+
throw new ValidationError('No file content provided');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return saveUploadedFile(
|
|
164
|
+
baseDir,
|
|
165
|
+
targetRelPath,
|
|
166
|
+
buffer,
|
|
167
|
+
originalName,
|
|
168
|
+
mimeType,
|
|
169
|
+
publicBasePath
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
createFileManagerServices,
|
|
178
|
+
};
|
package/src/services/index.js
CHANGED
|
@@ -43,6 +43,7 @@ function createServiceRegistry(options = {}) {
|
|
|
43
43
|
const { createAuthServices } = require('./builtins/auth');
|
|
44
44
|
const { createMailServices } = require('./builtins/mail');
|
|
45
45
|
const { createMediaServices } = require('./builtins/media');
|
|
46
|
+
const { createFileManagerServices } = require('./builtins/file-manager');
|
|
46
47
|
const { createSystemServices } = require('./builtins/system');
|
|
47
48
|
const { createExchangeServices } = require('./builtins/exchange');
|
|
48
49
|
|
|
@@ -54,6 +55,7 @@ module.exports = {
|
|
|
54
55
|
createAuthServices,
|
|
55
56
|
createMailServices,
|
|
56
57
|
createMediaServices,
|
|
58
|
+
createFileManagerServices,
|
|
57
59
|
createSystemServices,
|
|
58
60
|
createExchangeServices,
|
|
59
61
|
memoize,
|
package/bin/commands/skill.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill Command — scaffold Agent Skill (SKILL.md) for Cursor / AI tools
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
let _inquirer;
|
|
9
|
-
const inquirer = {
|
|
10
|
-
prompt: (...args) => {
|
|
11
|
-
if (!_inquirer) _inquirer = require('inquirer');
|
|
12
|
-
return _inquirer.prompt(...args);
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$/;
|
|
17
|
-
|
|
18
|
-
function validateSkillName(name) {
|
|
19
|
-
const t = String(name).trim().toLowerCase();
|
|
20
|
-
if (!t) return 'Skill name is required';
|
|
21
|
-
if (t.length > 64) return 'Skill name must be at most 64 characters';
|
|
22
|
-
if (!NAME_PATTERN.test(t)) {
|
|
23
|
-
return 'Use lowercase letters, numbers, and hyphens only (e.g. my-skill-name)';
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function skillDir(base, skillName) {
|
|
29
|
-
return path.join(base, '.agents', 'skills', skillName);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function defaultDescription(skillName) {
|
|
34
|
-
const label = skillName.replace(/-/g, ' ');
|
|
35
|
-
return `Guides the agent through tasks for ${label}. Use when the user works on ${label} or asks about related workflows.`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Bundled presets: CLI flag → template directory (all *.md copied) */
|
|
39
|
-
const PRESETS = {
|
|
40
|
-
webspresso: {
|
|
41
|
-
skillName: 'webspresso-usage',
|
|
42
|
-
templateDir: path.join(__dirname, '../../templates/skills/webspresso-usage'),
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function buildSkillMarkdown(skillName, description) {
|
|
47
|
-
const title = skillName
|
|
48
|
-
.split('-')
|
|
49
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
50
|
-
.join(' ');
|
|
51
|
-
const descYaml = JSON.stringify(description.trim());
|
|
52
|
-
|
|
53
|
-
return `---
|
|
54
|
-
name: ${skillName}
|
|
55
|
-
description: ${descYaml}
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
# ${title}
|
|
59
|
-
|
|
60
|
-
## Instructions
|
|
61
|
-
|
|
62
|
-
- Describe step-by-step what the agent should do.
|
|
63
|
-
- Add constraints, file paths, and conventions for this project.
|
|
64
|
-
|
|
65
|
-
## When to use
|
|
66
|
-
|
|
67
|
-
- List concrete user phrases or tasks that should trigger this skill.
|
|
68
|
-
|
|
69
|
-
## Examples
|
|
70
|
-
|
|
71
|
-
\`\`\`
|
|
72
|
-
Example prompt or command
|
|
73
|
-
\`\`\`
|
|
74
|
-
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function registerCommand(program) {
|
|
80
|
-
program
|
|
81
|
-
.command('skill [name]')
|
|
82
|
-
.description('Create an Agent Skill folder with SKILL.md')
|
|
83
|
-
.option('-g, --global', 'Write to ~/.agents/skills/ instead of ./.agents/skills/')
|
|
84
|
-
.option('-d, --description <text>', 'Skill description (for YAML frontmatter)')
|
|
85
|
-
.option('-f, --force', 'Overwrite existing SKILL.md')
|
|
86
|
-
.option(
|
|
87
|
-
'-p, --preset <name>',
|
|
88
|
-
'Install bundled skill: webspresso → agent reference (SKILL.md + REFERENCE-*.md in .agents/skills/webspresso-usage/)',
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
.action(async (nameArg, options) => {
|
|
92
|
-
const presetKey = options.preset ? String(options.preset).trim().toLowerCase() : '';
|
|
93
|
-
|
|
94
|
-
if (presetKey) {
|
|
95
|
-
const preset = PRESETS[presetKey];
|
|
96
|
-
if (!preset) {
|
|
97
|
-
console.error(`❌ Unknown preset "${presetKey}". Available: ${Object.keys(PRESETS).join(', ')}`);
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
const templateDir = preset.templateDir;
|
|
101
|
-
if (!templateDir || !fs.existsSync(templateDir)) {
|
|
102
|
-
console.error(`❌ Preset template directory missing: ${templateDir}`);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
const mdFiles = fs
|
|
106
|
-
.readdirSync(templateDir)
|
|
107
|
-
.filter((f) => f.endsWith('.md'));
|
|
108
|
-
if (mdFiles.length === 0) {
|
|
109
|
-
console.error(`❌ No .md files in preset: ${templateDir}`);
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
const root = options.global ? os.homedir() : process.cwd();
|
|
113
|
-
const dir = skillDir(root, preset.skillName);
|
|
114
|
-
const skillFile = path.join(dir, 'SKILL.md');
|
|
115
|
-
|
|
116
|
-
if (!options.force && fs.existsSync(skillFile)) {
|
|
117
|
-
console.error(`❌ Already exists: ${skillFile}`);
|
|
118
|
-
console.error(' Use --force to overwrite.');
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
123
|
-
for (const file of mdFiles) {
|
|
124
|
-
fs.copyFileSync(path.join(templateDir, file), path.join(dir, file));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
console.log(`\n✅ Installed bundled skill "${presetKey}" →\n ${dir}/\n`);
|
|
128
|
-
console.log(` Files: ${mdFiles.join(', ')}\n`);
|
|
129
|
-
console.log(' Edit if needed, then restart Cursor or reload the window.\n');
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let skillName = nameArg ? String(nameArg).trim().toLowerCase() : '';
|
|
134
|
-
|
|
135
|
-
if (!skillName) {
|
|
136
|
-
const { name } = await inquirer.prompt([
|
|
137
|
-
{
|
|
138
|
-
type: 'input',
|
|
139
|
-
name: 'name',
|
|
140
|
-
message: 'Skill name (kebab-case, e.g. review-api):',
|
|
141
|
-
validate: validateSkillName,
|
|
142
|
-
},
|
|
143
|
-
]);
|
|
144
|
-
skillName = name.trim().toLowerCase();
|
|
145
|
-
} else {
|
|
146
|
-
const v = validateSkillName(skillName);
|
|
147
|
-
if (v !== true) {
|
|
148
|
-
console.error(`❌ ${v}`);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
|
-
skillName = skillName.trim().toLowerCase();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let description = options.description;
|
|
155
|
-
if (!description) {
|
|
156
|
-
const { desc } = await inquirer.prompt([
|
|
157
|
-
{
|
|
158
|
-
type: 'input',
|
|
159
|
-
name: 'desc',
|
|
160
|
-
message: 'Short description (what/when the agent should use this skill):',
|
|
161
|
-
default: defaultDescription(skillName),
|
|
162
|
-
},
|
|
163
|
-
]);
|
|
164
|
-
description = desc || defaultDescription(skillName);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const root = options.global ? os.homedir() : process.cwd();
|
|
168
|
-
const dir = skillDir(root, skillName);
|
|
169
|
-
const skillFile = path.join(dir, 'SKILL.md');
|
|
170
|
-
|
|
171
|
-
if (!options.force && fs.existsSync(skillFile)) {
|
|
172
|
-
console.error(`❌ Already exists: ${skillFile}`);
|
|
173
|
-
console.error(' Use --force to overwrite.');
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
178
|
-
|
|
179
|
-
const body = buildSkillMarkdown(skillName, description);
|
|
180
|
-
fs.writeFileSync(skillFile, body, 'utf8');
|
|
181
|
-
|
|
182
|
-
console.log(`\n✅ Agent skill created:\n ${skillFile}\n`);
|
|
183
|
-
console.log(' Edit SKILL.md, then restart Cursor or reload the window if needed.\n');
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
module.exports = { registerCommand };
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
# Webspresso — framework reference (SSR, API, ORM)
|
|
2
|
-
|
|
3
|
-
Sections follow the source repo layout (`index.js`, `src/`, `core/orm`, `doc/index.html`).
|
|
4
|
-
|
|
5
|
-
## 1. What this framework is
|
|
6
|
-
|
|
7
|
-
- **Architecture**: Modern full-stack Node.js framework with Next.js/Nuxt-style file-based routing.
|
|
8
|
-
- **SSR**: Native server-side rendering with **Nunjucks**; URLs map directly to files under `pages/`.
|
|
9
|
-
- **API**: 100% file-based handlers under `pages/api/` with HTTP method suffixes (`.get.js`, `.post.js`, `[id].delete.js`) and declarative **Zod** validation (`req.input`).
|
|
10
|
-
- ⚠️ **CRITICAL ANTI-PATTERN**: **NEVER** create a `routes/` or `src/routes/` directory. **NEVER** use `app.get()`, `app.post()`, or `express.Router()`. All application routes must live under `pages/` and `pages/api/`.
|
|
11
|
-
- **i18n**: JSON locales; `t('key')` in templates.
|
|
12
|
-
- **ORM**: Knex-backed layer in `core/orm` — `defineModel`, `zdb` schema helpers, repositories, query builder, migrations.
|
|
13
|
-
- **Plugins**: Register in `createApp({ plugins })`; optional `db` passed as `ctx.db`.
|
|
14
|
-
|
|
15
|
-
Public API surface: `require('webspresso')` / [`index.js`](../../../index.js) — `createApp`, **`resolveClientRuntime`**, **`CLIENT_RUNTIME_BASE`**, file-router utilities, `createHelpers`, plugin manager, ORM exports, built-in plugins. **Session auth** lives in [`core/auth`](../../../core/auth) — import **`require('webspresso/core/auth')`** (`createAuth`, `quickAuth`, `hash`, `verify`, `setupAuthMiddleware`, `createRememberTokensTable`, policy helpers); wire with **`createApp({ auth })`**.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 2. Typical project layout
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
project/
|
|
23
|
-
├── server.js # listen(); uses createApp()
|
|
24
|
-
├── pages/
|
|
25
|
-
│ ├── _hooks.js # global lifecycle hooks (optional)
|
|
26
|
-
│ ├── locales/en.json # global i18n
|
|
27
|
-
│ ├── index.njk # GET /
|
|
28
|
-
│ ├── about/index.njk # GET /about
|
|
29
|
-
│ ├── blog/[slug].njk # dynamic
|
|
30
|
-
│ └── api/
|
|
31
|
-
│ ├── health.get.js
|
|
32
|
-
│ └── posts.post.js
|
|
33
|
-
├── views/ # layouts (e.g. layout.njk)
|
|
34
|
-
├── public/ # static assets
|
|
35
|
-
├── models/ # ORM models (defineModel)
|
|
36
|
-
├── migrations/
|
|
37
|
-
├── webspresso.db.js # or knexfile.js
|
|
38
|
-
└── plugins/ # app-specific plugins (optional)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## 3. `createApp(options)` — essentials
|
|
44
|
-
|
|
45
|
-
**Required**
|
|
46
|
-
|
|
47
|
-
- `pagesDir` — path to `pages/`.
|
|
48
|
-
|
|
49
|
-
**Common optional**
|
|
50
|
-
|
|
51
|
-
| Option | Role |
|
|
52
|
-
|--------|------|
|
|
53
|
-
| `viewsDir` | Layouts / partials (Nunjucks paths) |
|
|
54
|
-
| `publicDir` | Static files (`express.static`) |
|
|
55
|
-
| `db` | DB instance from `createDatabase()` → **`ctx.db`** in `load`, `meta`, plugins |
|
|
56
|
-
| `middlewares` | Named map; reference by string in route/API config (`middleware: ['auth']`) |
|
|
57
|
-
| `plugins` | Array of plugin factories/objects |
|
|
58
|
-
| `errorPages` | `{ notFound?: string \| Function, serverError?: string \| Function, timeout?: string \| Function }` — Nunjucks template path (e.g. `notFound: '404.njk'`, relative to `viewsDir`) or Express handler fn `(req, res, ctx) => ...`. Template context receives `{ fsy, locale, isDev, url, method }` (and `{ error, status }` for 500). File-based SSR/API errors are passed to this Express error middleware via `next(err)`. **`serverError` / `timeout` as a template path** is not used for paths under **`/api`** (those get default JSON). |
|
|
59
|
-
| `timeout` | e.g. `'30s'` or `false` |
|
|
60
|
-
| `helmet` | `true` / `false` / object |
|
|
61
|
-
| `assets` | `{ version, manifestPath, prefix }` for `fsy.asset` / `fsy.css` / `fsy.js` |
|
|
62
|
-
| `pageAssets` | Opt-in **`true`** or **`{ enabled?, stylesheets?, scripts? }`**. When on, route **`load()`** may return reserved keys **`stylesheets`** (string or list; also `{ href, media? }` objects) and **`scripts`** (string, `{ src, defer?, async?, type? }`, or list). They are removed from the root Nunjucks context and passed as **`pageHead`** with **`pageAssets: true`**. The app layout must print them (see [`views/layout.njk`](../../../views/layout.njk) in the package). Default **off** — `stylesheets` / `scripts` in **`load()`** behave as normal data keys. |
|
|
63
|
-
| `clientRuntime` | Opt-in **`{ alpine?: boolean \| object, swup?: boolean \| object }`**. Serves **`/__webspresso/client-runtime/*`** (Alpine 3, swup 4 + Head + Scripts plugins + bootstrap). Template context **`clientRuntime`**; include [`views/partials/webspresso-client-runtime.njk`](../../../views/partials/webspresso-client-runtime.njk) and set **`<main id="swup">`** when swup is on. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`** (`1` or `true`). Admin / dev dashboard HTML is unchanged (Mithril). Use **`data-no-swup`** on links for full page loads. HTMX is not used. |
|
|
64
|
-
| `auth` | `AuthManager` from **`createAuth()`** / **`quickAuth()`** (`webspresso/core/auth`). Mounts cookie parser + **`express-session`** + per-request **`authenticate`**; sets **`req.user`**, **`req.auth`**. Injects named route middleware **`auth`** and **`guest`** (overwrites same keys in `middlewares` if you passed both — avoid reusing those names for custom handlers). |
|
|
65
|
-
| `setupRoutes(app, ctx)` | Low-level server extension hook (e.g. mounting third-party sub-apps or raw middleware). ⚠️ **NEVER** use `setupRoutes` to define application domain or API routes. All application endpoints **MUST** live under `pages/` and `pages/api/`. |
|
|
66
|
-
|
|
67
|
-
**Returns:** `{ app, nunjucksEnv, pluginManager, authMiddleware }` — `authMiddleware` is **`null`** when `auth` was not configured.
|
|
68
|
-
|
|
69
|
-
### Client runtime — implementation notes
|
|
70
|
-
|
|
71
|
-
- **Package helpers:** `resolveClientRuntime(options)` merges **`createApp({ clientRuntime })`** with env; **`CLIENT_RUNTIME_BASE`** is **`/__webspresso/client-runtime`** (script URLs under that path).
|
|
72
|
-
- **After swup navigation:** bootstrap runs **`Alpine.initTree`** on **`#swup`** on swup’s **`content:replace`** so new SSR markup gets Alpine bindings.
|
|
73
|
-
- **Default `ignoreVisit` (bootstrap):** links under **`/_admin`**, **`/_webspresso`**, elements with **`data-no-swup`**, plus swup’s usual rules (e.g. `target="_blank"`, other origin).
|
|
74
|
-
- **CSP / Helmet:** production **`script-src 'self'`** works for **`/__webspresso/client-runtime/`**; some Alpine builds may need **`unsafe-eval`** — validate for your version or use a stricter build.
|
|
75
|
-
- **Longer doc:** **[`doc/index.html#client-runtime`](../../../doc/index.html#client-runtime)** · README **Client runtime**.
|
|
76
|
-
|
|
77
|
-
### Dual authentication (Session + JWT & Refresh Tokens) — essentials
|
|
78
|
-
|
|
79
|
-
- **Import:** `const { createAuth, quickAuth, hash, verify, signJwt, verifyJwt, decodeJwt, createRememberTokensTable } = require('webspresso/core/auth')` (published `core/` tree on npm; **not** re-exported from package root).
|
|
80
|
-
- **Dual Architecture:**
|
|
81
|
-
- **Session + Cookie (Stateful):** Primary for browser SSR routes and Admin Panel (`/`, `/_admin`). Uses `httpOnly` secure cookies.
|
|
82
|
-
- **JWT + Refresh Token (Stateless):** Built-in zero-dependency HS256 auth for REST APIs, Mobile Apps, and SPAs (`/api/*`). Uses `Authorization: Bearer <token>` for access tokens and rotatable Refresh Tokens.
|
|
83
|
-
- **`createAuth({ findUserById, findUserByCredentials, session: { secret }, jwt: { secret, refreshSecret, expiresIn, refreshExpiresIn }, rememberTokens?, ... })`** — adapter pattern; optional **remember-me** via `rememberTokens` and opt-in **JWT / Refresh Tokens**.
|
|
84
|
-
- **`quickAuth({ db, userModel, identifierField, passwordField, rememberMe, jwt })`** — wires **`getRepository`** + bcrypt **`verify`**; optional Knex **`remember_tokens`** and **JWT / Refresh Token** manager (`jwt: true` or `jwt: { expiresIn: '15m', refreshExpiresIn: '30d' }`).
|
|
85
|
-
- **JWT Helper Methods:**
|
|
86
|
-
- **`auth.generateUserToken(user, options)`** — generates short-lived access JWT (`type: 'access'`).
|
|
87
|
-
- **`auth.generateRefreshToken(user, options)`** — generates long-lived refresh JWT (`type: 'refresh'`).
|
|
88
|
-
- **`auth.refreshAccessToken(refreshToken, options)`** — verifies refresh token and issues new access token + rotated refresh token.
|
|
89
|
-
- **`auth.requireJwt()`** — guard middleware enforcing valid `Bearer` access token.
|
|
90
|
-
- **Request API** (after global authenticate): **`req.auth.attempt(id, password, { remember })`**, **`login`**, **`logout`**, **`check`**, **`guest`**, **`user`**, **`id`**, **`can` / `cannot` / `authorize`** (policies: **`auth.definePolicy`**, **`defineGate`**, **`beforePolicy`**).
|
|
91
|
-
- **Route config:** `middleware: ['auth']` (SSR logged in), `middleware: ['jwt']` (API Bearer token), or `['guest']` (logged-out only). For JSON APIs mounted in **`setupRoutes`**, use **`ctx.authMiddleware.requireJwt()`** or **`requireAuth({ api: true })`**.
|
|
92
|
-
- **Login page pitfall:** a **`pages/login.njk`** can register **before** `setupRoutes` and bypass **`requireGuest`**. Prefer login GET/POST in **`setupRoutes`** with templates under **`views/`** only, or omit **`pages/login.njk`** — see [`tests/e2e/auth.spec.js`](../../../tests/e2e/auth.spec.js).
|
|
93
|
-
- **Admin panel** uses a **separate** session (`req.session.adminUser`, `/_admin/api/auth/*`); it does **not** replace **`createApp({ auth })`** for site users.
|
|
94
|
-
- **Site users in the admin UI (`userManagement`):** Opt-in on **`adminPanelPlugin`**. Set **`userManagement: { enabled: true, model: 'User', fields?: { ... } }`** so the SPA shows **Users**: sidebar **All Users** / **Add User** link to **`/_admin/models/{model}`** and **`/_admin/models/{model}/new`** (same RecordList/RecordForm as other models). **`/_admin/users`**, **`/_admin/users/new`**, **`/_admin/users/:id/edit`** remain SPA aliases that redirect there; **`/_admin/users/sessions`** is **Active Sessions**. The **`model`** must match site auth (e.g. **`quickAuth({ userModel: 'User', ... })`** / **`createAuth`**) and must have **`admin: { enabled: true, ... }`** on **`defineModel`** so admin CRUD metadata loads; otherwise Users screens look empty. Pass **`auth: authManager`** with the **same** **`AuthManager`** as **`createApp({ auth: authManager })`** for **Active Sessions** / revoke (**`rememberTokens`** / **`remember_me`**); without **`auth`**, user CRUD still works via **`db.getRepository(model)`**, but session endpoints return empty or “not enabled”.
|
|
95
|
-
- **Wiring:** `plugins: [ adminPanelPlugin({ db, auth: authManager, userManagement: { enabled: true, model: 'User' } }) ]` alongside `createApp({ ..., auth: authManager })`. Admin staff log in at **`/_admin`**; end users use your normal site login — two different cookies/sessions.
|
|
96
|
-
|
|
97
|
-
Longer narrative: **[`doc/index.html#authentication`](../../../doc/index.html#authentication)** · **[`#admin-user-management`](../../../doc/index.html#admin-user-management)** · README **Authentication (session & JWT)** and **Admin Panel Plugin**.
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
## 4. File-based routing (SSR)
|
|
102
|
-
|
|
103
|
-
| Path pattern | URL |
|
|
104
|
-
|--------------|-----|
|
|
105
|
-
| `pages/index.njk` | `/` |
|
|
106
|
-
| `pages/about/index.njk` | `/about` |
|
|
107
|
-
| `pages/tools/[slug].njk` | `/tools/:slug` |
|
|
108
|
-
| `pages/docs/[...rest].njk` | `/docs/*` (catch-all) |
|
|
109
|
-
|
|
110
|
-
**Route config** — sibling `.js` file (e.g. `pages/tools/index.js`):
|
|
111
|
-
|
|
112
|
-
- `middleware` — array of Express functions, **string names** from `createApp({ middlewares })`, or **`['name', options]` tuples** when the registry entry is a **factory** `(options) => (req, res, next) => …` (bare string calls the factory with `{}`). Built-in **`auth`** / **`guest`** (with `createApp({ auth })`) are factories — e.g. **`['auth', { api: true }]`** for JSON 401 on APIs.
|
|
113
|
-
- `load(req, ctx)` — async; return object merged into template context; use **`ctx.db`** when `createApp({ db })` is set.
|
|
114
|
-
- `meta(req, ctx)` — title, description, etc.
|
|
115
|
-
- `hooks` — `beforeLoad`, `afterRender`, etc. (see hook order below).
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## 5. File-based API (`pages/api/`)
|
|
120
|
-
|
|
121
|
-
| File | Route |
|
|
122
|
-
|------|-------|
|
|
123
|
-
| `api/health.get.js` | `GET /api/health` |
|
|
124
|
-
| `api/items.post.js` | `POST /api/items` |
|
|
125
|
-
| `api/users/[id].get.js` | `GET /api/users/:id` |
|
|
126
|
-
|
|
127
|
-
**Shapes**
|
|
128
|
-
|
|
129
|
-
1. **Function** — `module.exports = async (req, res) => { ... }`
|
|
130
|
-
2. **Object** — **`handler`**, optional **`middleware`** (names, functions, or **`['name', options]`** tuples with factory registry entries), optional **`schema`**
|
|
131
|
-
|
|
132
|
-
**Order:** `req.db` (if any) → **Zod** `schema` → **`middleware`** → **`handler`**.
|
|
133
|
-
|
|
134
|
-
**Zod** — `schema: ({ z }) => ({ body, query, params, response })` → **`req.input`**; invalid → **400** `{ error: 'Validation Error', issues }`.
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## 6. Global and route hooks
|
|
139
|
-
|
|
140
|
-
**Global:** `pages/_hooks.js` exports `onRequest`, `beforeLoad`, `afterLoad`, `beforeRender`, `afterRender`, `onError`, etc.
|
|
141
|
-
|
|
142
|
-
**`onError(ctx, err)`** — runs when an unhandled error occurs in a **file-based** SSR route or **`pages/api/*`** handler (after `console.error`, before the central `errorPages` handler). Optional second argument **`err`** matches **`ctx.error`**. Use for APM (Sentry, New Relic `noticeError`, …).
|
|
143
|
-
|
|
144
|
-
**Rough order:** global `onRequest` → route middleware chain → `load` → render → `afterRender`. (See README “Hook Execution Order” for full list.)
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## 7. i18n
|
|
149
|
-
|
|
150
|
-
- **Global:** `pages/locales/<locale>.json` (nested keys).
|
|
151
|
-
- **Route-specific:** `pages/<route>/locales/<locale>.json` overrides global.
|
|
152
|
-
- Templates: `{{ t('nav.home') }}`.
|
|
153
|
-
- Env: `DEFAULT_LOCALE`, `SUPPORTED_LOCALES` (comma-separated).
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
## 8. Template helpers (`fsy`)
|
|
158
|
-
|
|
159
|
-
Available in all Nunjucks templates:
|
|
160
|
-
|
|
161
|
-
- **URL:** `fsy.url`, `fsy.fullUrl`, `fsy.route`, `fsy.canonical`
|
|
162
|
-
- **Request:** `fsy.q`, `fsy.param`, `fsy.hdr`
|
|
163
|
-
- **Utils:** `fsy.slugify`, `fsy.truncate`, `fsy.prettyBytes`, `fsy.prettyMs`
|
|
164
|
-
- **Dates:** `fsy.date`, `fsy.dateFormat`, `fsy.dateFromNow`, `fsy.dateDiff`, …
|
|
165
|
-
- **Assets & Versioning:** `fsy.asset(path)` (returns versioned `?v=...` or Vite/Webpack manifest resolved URL), `fsy.css(href, attrs)` (generates `<link>` tag), `fsy.js(src, attrs)` (generates `<script>` tag with boolean attributes), `fsy.img(src, alt, attrs)` (generates `<img>` tag). Configured via `createApp({ assets: { version, manifestPath, prefix } })`.
|
|
166
|
-
- **Dev:** `fsy.isDev()`
|
|
167
|
-
- **SEO:** `fsy.jsonld`
|
|
168
|
-
|
|
169
|
-
Analytics plugin adds `fsy.analyticsHead`, `fsy.verificationTags`, etc., when configured.
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
## 9. ORM overview
|
|
174
|
-
|
|
175
|
-
**Define schema** with **`zdb`** (`zdb.id()`, `zdb.uuid()`, `zdb.nanoid()`, `zdb.string({...})`, **`zdb.file({ maxLength, nullable })`** — URL/path string for uploaded assets, `zdb.foreignKey`, `zdb.foreignUuid`, `zdb.foreignNanoid`, `zdb.timestamp`, `zdb.json`, …).
|
|
176
|
-
|
|
177
|
-
**Define model** with **`defineModel({ name, table, schema, relations, scopes, hidden, admin })`**.
|
|
178
|
-
|
|
179
|
-
- **Relations:** `belongsTo`, `hasMany`, `hasOne` with `model: () => OtherModel`.
|
|
180
|
-
- **Scopes:** `softDelete`, `timestamps`, optional `tenant` column.
|
|
181
|
-
- **`hidden`:** columns never exposed in admin/API (e.g. `password_hash`).
|
|
182
|
-
- **`zdb.file()`:** varchar column for uploaded asset URL/path; admin forms render a file upload widget when `uploadPlugin` is registered (or `adminPanelPlugin({ uploadUrl })`). Optional `ui: { label, accept, maxBytes }`. For `zdb.string()` columns use `admin.customFields: { col: { type: 'file-upload' } }`.
|
|
183
|
-
- **Nanoid PK:** `zdb.nanoid()` / `zdb.nanoid({ maxLength: 12 })` — string primary key; migrations use `string(length)`. On **`create()`**, omitting the PK auto-fills a URL-safe id (built-in generator, same alphabet as `nanoid`). Use **`zdb.foreignNanoid('table', { maxLength })`** when the parent uses nanoid PKs; **`generateNanoid`** is exported from `webspresso` for manual ids. In API **`schema`**, use **`z.nanoid()`** / **`z.nanoid(12)`** / **`z.nanoid({ maxLength })`** (the `z` from `schema: ({ z })` is extended by Webspresso). **`zodNanoid`** / **`extendZ`** are also exported for non-route use.
|
|
184
|
-
|
|
185
|
-
**Database:** `createDatabase({ client, connection, models: './models' })` — auto-loads `models/*.js` (ignore `_prefix`).
|
|
186
|
-
|
|
187
|
-
**Repository:** `db.getRepository('User')` → `findById`, `findOne`, `findAll`, `create`, `update`, `delete`, `query()`, …
|
|
188
|
-
|
|
189
|
-
**Query builder:** `UserRepo.query().where(...).with('relation').orderBy(...).list()` / `.first()` / `.paginate()` / `.count()`. **`with()`** eager-loads relations; **`count()`** ignores builder `.limit`/`.offset` for total; see ORM docs for edge cases.
|
|
190
|
-
|
|
191
|
-
**Migrations:** `webspresso db:migrate`, `db:rollback`, `db:status`, `db:make`, `db:scaffold` (all models → create migrations).
|
|
192
|
-
|
|
193
|
-
**Transactions:** `db.transaction(async (trx) => { trx.getRepository('User') })`.
|
|
194
|
-
|
|
195
|
-
**Query cache (optional):** `createDatabase({ ..., cache: true })` or `cache: { defaultStrategy: 'auto'|'smart', memory: { maxEntries, defaultTtlMs }, provider?: custom }`. Opt-in per model: `defineModel({ ..., cache: 'auto'|'smart'|true })`. API: `db.cache` → `getMetrics()`, `purge()`, `invalidateModel(name)`, `invalidateTags(tags[])`, `resetMetrics()`. Reads bypass cache when using a transaction knex. Admin UI: `ormCacheAdminPlugin({ db })` (needs `admin-panel` and `cache` enabled).
|
|
196
|
-
|
|
197
|
-
Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and plugins. **`pages/api/`** handlers receive **`req.db`** (and route **`middleware`** runs after it). Outside requests, use **`getDb()`** / **`hasDb()`**; for **`setupRoutes`**-only routes, use **`attachDbMiddleware`**.
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## 10. Plugins (built-in — concise)
|
|
202
|
-
|
|
203
|
-
| Plugin | Purpose |
|
|
204
|
-
|--------|---------|
|
|
205
|
-
| `dashboardPlugin` | Dev route `/_webspresso` — route list |
|
|
206
|
-
| `sitemapPlugin` | `/sitemap.xml`, robots; optional DB-driven URLs |
|
|
207
|
-
| `analyticsPlugin` | GA / GTM / Yandex / Bing / Facebook — `fsy` helpers |
|
|
208
|
-
| `adminPanelPlugin` | SPA admin CRUD — needs **`db`**; supports custom HTML & external URL pages (`url`, `html`, `htmlFile`, `layout: false`) and SSR script/style imports (`scripts`, `styles`, `registerScript`, `registerStyle`); optional **`uploadUrl`** (or infer from **`uploadPlugin`**); optional **`userManagement: { enabled, model, fields }`** + **`auth`** (same **`AuthManager`** as **`createApp({ auth })`**) for site-user CRUD + remember-me session UI — see **Session authentication** above |
|
|
209
|
-
| `dataExchangePlugin` | Admin-only **Excel export** + **CSV/XLSX import** under `${adminPath}/api/data-exchange/*`; register **after** `adminPanelPlugin` with same `db` / `adminPath`; optional `maxRows`, `maxFileBytes`; adds UI buttons + bulk `export-xlsx` |
|
|
210
|
-
| `redirectPlugin` | Configurable **301–308** redirects in `register()` — runs **before** file-based SSR routes; `rules` (`from` path or `RegExp`, `to`, `status`, `methods`), `preserveQuery`, `allowExternal`, `trailingSlash`, `defaultMethods`; docs **[`doc/index.html#plugins-redirect`](../../../doc/index.html#plugins-redirect)**, README **Redirect plugin** |
|
|
211
|
-
| `uploadPlugin` | `POST` multipart (`multer`), `createLocalFileProvider` or custom `provider`; set **`mimeAllowlist`** / **`maxBytes`** in production; pairs with admin **`zdb.file()`** / **`customFields.file-upload`** when registered before **`adminPanelPlugin`** |
|
|
212
|
-
| `siteAnalyticsPlugin` | Self-hosted page views + admin charts |
|
|
213
|
-
| `auditLogPlugin` | Admin mutation audit trail |
|
|
214
|
-
| `recaptchaPlugin` | v2/v3 + middleware |
|
|
215
|
-
| `emailPlugin` | MJML + Nodemailer sending with optional admin UI and auth bridge |
|
|
216
|
-
| `polarPlugin` | Polar.sh billing: checkout, portal, webhooks, tier sync (`webspresso polar:migrate`) |
|
|
217
|
-
| `seoCheckerPlugin` | Dev SEO panel |
|
|
218
|
-
| `restResourcePlugin` | Opt-in REST CRUD from models; `?include=` uses ORM eager load (single-level relations only) |
|
|
219
|
-
| `ormCacheAdminPlugin` | Admin page for ORM cache metrics / purge / invalidate (`db.cache` required) |
|
|
220
|
-
|
|
221
|
-
**Custom plugin:** `name`, `version`, `register(ctx)`, `onRoutesReady(ctx)` — use `ctx.app`, `ctx.db`, `ctx.addHelper`, `ctx.addRoute`, `ctx.usePlugin('other')`. Plugin failures **warn**; app keeps running.
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## 11. CLI (project directory)
|
|
226
|
-
|
|
227
|
-
| Command | Role |
|
|
228
|
-
|---------|------|
|
|
229
|
-
| `webspresso new` | Scaffold project |
|
|
230
|
-
| `webspresso new .` / `new ./` | Scaffold **into the current directory** |
|
|
231
|
-
| `webspresso new … --yes` | **Non-interactive** (agents / CI); use `-i` to install deps |
|
|
232
|
-
| `webspresso dev` / `start` | Servers |
|
|
233
|
-
| `webspresso page` / `api` | Interactive scaffolding |
|
|
234
|
-
| `webspresso db:*` | migrate, rollback, status, make |
|
|
235
|
-
| `webspresso upgrade` | Bump **`webspresso`** in **`package.json`** |
|
|
236
|
-
| `webspresso seed` | Seed data |
|
|
237
|
-
| `webspresso doctor` | Sanity checks |
|
|
238
|
-
| `webspresso skill` | Cursor **`SKILL.md`** scaffold |
|
|
239
|
-
| `webspresso skill --preset webspresso` | Copy bundled Webspresso agent skill (`SKILL.md` + `REFERENCE-*.md`) |
|
|
240
|
-
| `webspresso add tailwind` | Tailwind setup |
|
|
241
|
-
| `webspresso favicon:generate` | Favicons + manifest |
|
|
242
|
-
| `webspresso admin:setup` / `admin:password` | Admin users |
|
|
243
|
-
| `webspresso audit:prune` | Audit log retention |
|
|
244
|
-
|
|
245
|
-
**`webspresso new` — automation:** **`new .`** scaffolds in place. **Typical one-liners:** `webspresso new . --yes --no-tailwind` · `webspresso new . --yes -i`.
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
## 12. Environment variables (common)
|
|
250
|
-
|
|
251
|
-
| Var | Notes |
|
|
252
|
-
|-----|-------|
|
|
253
|
-
| `NODE_ENV` | `development` / `production` |
|
|
254
|
-
| `DEFAULT_LOCALE` | Default locale |
|
|
255
|
-
| `SUPPORTED_LOCALES` | Comma-separated |
|
|
256
|
-
| `BASE_URL` | Canonical / links |
|
|
257
|
-
| `DATABASE_URL` | DB connection |
|
|
258
|
-
| `SESSION_SECRET` | Session cookie signing |
|
|
259
|
-
| `WEBSPRESSO_ALPINE` | Forces **`clientRuntime.alpine`** when `1` / `true` |
|
|
260
|
-
| `WEBSPRESSO_SWUP` | Forces **`clientRuntime.swup`** when `1` / `true` |
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
|
-
## 13. Testing
|
|
265
|
-
|
|
266
|
-
- **Unit / integration:** `npm test` (Vitest).
|
|
267
|
-
- **E2E:** `npm run test:e2e` (Playwright).
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
## 14. Pitfalls (for agents)
|
|
272
|
-
|
|
273
|
-
1. **NEVER use Express routes (`app.get` / `routes/`)** — Webspresso is 100% file-based. Never create `routes/` or call `app.get()`. All routes must live in `pages/` and `pages/api/`.
|
|
274
|
-
2. **File-router order** — understand precedence vs manual routes.
|
|
275
|
-
3. **`ctx.db`** — only when `createApp({ db })` is set.
|
|
276
|
-
4. **ORM hidden fields** — never expose to clients unintentionally.
|
|
277
|
-
5. **Zod on API** — assume **`req.input`** when schema is set.
|
|
278
|
-
6. **Built-in `auth`** — framework owns **`middlewares.auth`** / **`guest`** names.
|
|
279
|
-
7. **`pages/login.njk`** — can shadow custom login; prefer **`setupRoutes`** + **`views/`**.
|
|
280
|
-
8. **Client runtime + swup** — need **`<main id="swup">`** and the client-runtime partial when enabled.
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
## 15. When to read this file (framework)
|
|
285
|
-
|
|
286
|
-
- Pages, API routes, ORM, migrations, **SSR `createApp`**, first-party **plugins**, i18n, **session auth**, **client runtime** (Alpine / swup).
|
|
287
|
-
- Not for: in-process **application kernel** (event bus + `kernel.createApp`) — see **`REFERENCE-kernel.md`**.
|
|
288
|
-
|
|
289
|
-
Authoritative long-form: **[README.md](../../../README.md)**, **[`doc/index.html`](../../../doc/index.html)**.
|