webspresso 0.0.97 ā 0.0.99
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 +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +4 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:model Command
|
|
5
|
+
* Scaffolds an ORM model in models/<Name>.js with zdb schema and optional migration
|
|
6
|
+
* @module bin/commands/make-model
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function pluralize(word) {
|
|
13
|
+
if (!word) return '';
|
|
14
|
+
const lower = word.toLowerCase();
|
|
15
|
+
if (lower.endsWith('y') && !/[aeiou]y$/.test(lower)) {
|
|
16
|
+
return lower.slice(0, -1) + 'ies';
|
|
17
|
+
}
|
|
18
|
+
if (lower.endsWith('s') || lower.endsWith('sh') || lower.endsWith('ch') || lower.endsWith('x') || lower.endsWith('z')) {
|
|
19
|
+
return lower + 'es';
|
|
20
|
+
}
|
|
21
|
+
return lower + 's';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseFields(fieldsArg) {
|
|
25
|
+
if (!fieldsArg || typeof fieldsArg !== 'string') return [];
|
|
26
|
+
const parts = fieldsArg.split(',').map((p) => p.trim()).filter(Boolean);
|
|
27
|
+
const fields = [];
|
|
28
|
+
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
const [name, type = 'string', modifier = ''] = part.split(':').map((s) => s.trim());
|
|
31
|
+
if (!name) continue;
|
|
32
|
+
fields.push({
|
|
33
|
+
name,
|
|
34
|
+
type: type.toLowerCase(),
|
|
35
|
+
isNullable: modifier.toLowerCase() === 'nullable' || modifier.toLowerCase() === 'optional' || name.endsWith('?'),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fields;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function generateSchemaLines(fields, softDeletes = false) {
|
|
43
|
+
const lines = [' id: zdb.id(),'];
|
|
44
|
+
|
|
45
|
+
for (const f of fields) {
|
|
46
|
+
if (f.name === 'id') continue;
|
|
47
|
+
let expr = 'zdb.string().min(1)';
|
|
48
|
+
|
|
49
|
+
switch (f.type) {
|
|
50
|
+
case 'text':
|
|
51
|
+
expr = 'zdb.text().optional()';
|
|
52
|
+
break;
|
|
53
|
+
case 'integer':
|
|
54
|
+
case 'int':
|
|
55
|
+
expr = f.isNullable ? 'zdb.integer().nullable().optional()' : 'zdb.integer().defaultTo(0)';
|
|
56
|
+
break;
|
|
57
|
+
case 'number':
|
|
58
|
+
case 'float':
|
|
59
|
+
case 'decimal':
|
|
60
|
+
expr = f.isNullable ? 'zdb.number().nullable().optional()' : 'zdb.number().defaultTo(0)';
|
|
61
|
+
break;
|
|
62
|
+
case 'boolean':
|
|
63
|
+
case 'bool':
|
|
64
|
+
expr = 'zdb.boolean().defaultTo(false)';
|
|
65
|
+
break;
|
|
66
|
+
case 'json':
|
|
67
|
+
expr = 'zdb.json().optional()';
|
|
68
|
+
break;
|
|
69
|
+
case 'date':
|
|
70
|
+
case 'datetime':
|
|
71
|
+
expr = 'zdb.date().optional()';
|
|
72
|
+
break;
|
|
73
|
+
case 'email':
|
|
74
|
+
expr = 'zdb.email().optional()';
|
|
75
|
+
break;
|
|
76
|
+
case 'file':
|
|
77
|
+
expr = 'zdb.file().optional()';
|
|
78
|
+
break;
|
|
79
|
+
case 'id':
|
|
80
|
+
expr = 'zdb.id()';
|
|
81
|
+
break;
|
|
82
|
+
case 'string':
|
|
83
|
+
default:
|
|
84
|
+
expr = f.isNullable ? 'zdb.string().nullable().optional()' : 'zdb.string().min(1)';
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
lines.push(` ${f.name}: ${expr},`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (softDeletes) {
|
|
92
|
+
lines.push(' deletedAt: zdb.softDelete().optional(),');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lines.push(' createdAt: zdb.timestamps().createdAt,');
|
|
96
|
+
lines.push(' updatedAt: zdb.timestamps().updatedAt,');
|
|
97
|
+
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function generateModelContent(modelName, tableName, fields, softDeletes = false) {
|
|
102
|
+
const schemaCode = generateSchemaLines(fields, softDeletes);
|
|
103
|
+
const softScope = softDeletes ? `,\n scopes: {\n default: (q) => q.whereNull('deletedAt'),\n }` : '';
|
|
104
|
+
|
|
105
|
+
return `'use strict';
|
|
106
|
+
|
|
107
|
+
const { defineModel, zdb } = require('webspresso');
|
|
108
|
+
|
|
109
|
+
module.exports = defineModel({
|
|
110
|
+
name: '${modelName}',
|
|
111
|
+
table: '${tableName}',
|
|
112
|
+
schema: {
|
|
113
|
+
${schemaCode}
|
|
114
|
+
}${softScope},
|
|
115
|
+
});
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function generateMigrationContent(tableName, fields, softDeletes = false) {
|
|
120
|
+
const colLines = [' table.increments(\'id\').primary();'];
|
|
121
|
+
|
|
122
|
+
for (const f of fields) {
|
|
123
|
+
if (f.name === 'id') continue;
|
|
124
|
+
switch (f.type) {
|
|
125
|
+
case 'text':
|
|
126
|
+
colLines.push(` table.text('${f.name}')${f.isNullable ? '.nullable()' : ''};`);
|
|
127
|
+
break;
|
|
128
|
+
case 'integer':
|
|
129
|
+
case 'int':
|
|
130
|
+
colLines.push(` table.integer('${f.name}')${f.isNullable ? '.nullable()' : '.defaultTo(0)'};`);
|
|
131
|
+
break;
|
|
132
|
+
case 'number':
|
|
133
|
+
case 'float':
|
|
134
|
+
case 'decimal':
|
|
135
|
+
colLines.push(` table.float('${f.name}')${f.isNullable ? '.nullable()' : '.defaultTo(0)'};`);
|
|
136
|
+
break;
|
|
137
|
+
case 'boolean':
|
|
138
|
+
case 'bool':
|
|
139
|
+
colLines.push(` table.boolean('${f.name}').defaultTo(false);`);
|
|
140
|
+
break;
|
|
141
|
+
case 'json':
|
|
142
|
+
colLines.push(` table.json('${f.name}').nullable();`);
|
|
143
|
+
break;
|
|
144
|
+
case 'date':
|
|
145
|
+
case 'datetime':
|
|
146
|
+
colLines.push(` table.datetime('${f.name}').nullable();`);
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
colLines.push(` table.string('${f.name}')${f.isNullable ? '.nullable()' : '.notNullable()'};`);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (softDeletes) {
|
|
155
|
+
colLines.push(' table.datetime(\'deletedAt\').nullable();');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
colLines.push(' table.timestamps(true, true);');
|
|
159
|
+
|
|
160
|
+
return `'use strict';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Migration: Create ${tableName} table
|
|
164
|
+
*/
|
|
165
|
+
exports.up = function(knex) {
|
|
166
|
+
return knex.schema.createTable('${tableName}', (table) => {
|
|
167
|
+
${colLines.join('\n')}
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
exports.down = function(knex) {
|
|
172
|
+
return knex.schema.dropTableIfExists('${tableName}');
|
|
173
|
+
};
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function registerCommand(program) {
|
|
178
|
+
program
|
|
179
|
+
.command('make:model <name>')
|
|
180
|
+
.alias('model:make')
|
|
181
|
+
.description('Scaffold an ORM model in models/<Name>.js with zdb schema')
|
|
182
|
+
.option('-f, --fields <fields>', 'Comma-separated fields (e.g. title:string,content:text,published:boolean)')
|
|
183
|
+
.option('-t, --table <table>', 'Custom table name (defaults to pluralized lowercase)')
|
|
184
|
+
.option('-m, --migration', 'Generate corresponding Knex migration file in migrations/')
|
|
185
|
+
.option('--soft-deletes', 'Include deletedAt column and default scope for soft delete')
|
|
186
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
187
|
+
.action((nameArg, options) => {
|
|
188
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
189
|
+
const modelsDir = path.join(rootDir, 'models');
|
|
190
|
+
const migrationsDir = path.join(rootDir, 'migrations');
|
|
191
|
+
|
|
192
|
+
if (!fs.existsSync(modelsDir)) {
|
|
193
|
+
fs.mkdirSync(modelsDir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Format Model Name (PascalCase, e.g. post -> Post, blog_post -> BlogPost)
|
|
197
|
+
const cleanName = String(nameArg || '').trim();
|
|
198
|
+
const modelName = cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
|
199
|
+
const tableName = options.table ? String(options.table).trim() : pluralize(cleanName.toLowerCase());
|
|
200
|
+
|
|
201
|
+
const modelFilePath = path.join(modelsDir, `${modelName}.js`);
|
|
202
|
+
if (fs.existsSync(modelFilePath)) {
|
|
203
|
+
console.error(`ā Model file already exists: models/${modelName}.js`);
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const fields = parseFields(options.fields);
|
|
209
|
+
const modelContent = generateModelContent(modelName, tableName, fields, Boolean(options.softDeletes));
|
|
210
|
+
|
|
211
|
+
fs.writeFileSync(modelFilePath, modelContent, 'utf-8');
|
|
212
|
+
console.log(`\nā
Created model: models/${modelName}.js (table: "${tableName}")`);
|
|
213
|
+
|
|
214
|
+
// Optional migration generation
|
|
215
|
+
if (options.migration) {
|
|
216
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
217
|
+
fs.mkdirSync(migrationsDir, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const now = new Date();
|
|
221
|
+
const timestamp = [
|
|
222
|
+
now.getFullYear(),
|
|
223
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
224
|
+
String(now.getDate()).padStart(2, '0'),
|
|
225
|
+
'_',
|
|
226
|
+
String(now.getHours()).padStart(2, '0'),
|
|
227
|
+
String(now.getMinutes()).padStart(2, '0'),
|
|
228
|
+
String(now.getSeconds()).padStart(2, '0'),
|
|
229
|
+
].join('');
|
|
230
|
+
|
|
231
|
+
const migrationFileName = `${timestamp}_create_${tableName}_table.js`;
|
|
232
|
+
const migrationFilePath = path.join(migrationsDir, migrationFileName);
|
|
233
|
+
const migrationContent = generateMigrationContent(tableName, fields, Boolean(options.softDeletes));
|
|
234
|
+
|
|
235
|
+
fs.writeFileSync(migrationFilePath, migrationContent, 'utf-8');
|
|
236
|
+
console.log(`ā
Created migration: migrations/${migrationFileName}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('');
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
registerCommand,
|
|
245
|
+
pluralize,
|
|
246
|
+
parseFields,
|
|
247
|
+
generateSchemaLines,
|
|
248
|
+
generateModelContent,
|
|
249
|
+
generateMigrationContent,
|
|
250
|
+
};
|
package/bin/commands/new.js
CHANGED
|
@@ -46,6 +46,8 @@ function registerCommand(program) {
|
|
|
46
46
|
.description('Create a new Webspresso project')
|
|
47
47
|
.option('-t, --template <template>', 'Template to use (minimal, full)', 'minimal')
|
|
48
48
|
.option('--no-tailwind', 'Skip Tailwind CSS setup')
|
|
49
|
+
.option('--tailwind-v4', 'Use Tailwind CSS v4 (Oxide engine, zero-config)')
|
|
50
|
+
.option('--tailwind-version <version>', 'Tailwind version (3 or 4)', '3')
|
|
49
51
|
.option('--no-agents', 'Skip AI Agent guidelines setup (.agents/, AGENTS.md, CLAUDE.md, .cursorrules)')
|
|
50
52
|
.option('-i, --install', 'Auto install dependencies and build CSS')
|
|
51
53
|
.option('-y, --yes', 'Non-interactive: no database, skip install unless -i/--install, skip dev server')
|
|
@@ -657,20 +659,43 @@ Visit http://localhost:3000
|
|
|
657
659
|
|
|
658
660
|
// Add Tailwind if requested (default: true)
|
|
659
661
|
if (useTailwind) {
|
|
660
|
-
|
|
662
|
+
const isV4 = Boolean(options.tailwindV4 || options.tailwindVersion === '4');
|
|
663
|
+
console.log(`\nšØ Setting up Tailwind CSS ${isV4 ? 'v4 (Oxide engine, zero-config)' : 'v3'}...\n`);
|
|
661
664
|
|
|
662
665
|
// Create src directory
|
|
663
666
|
fs.mkdirSync(path.join(projectPath, 'src'), { recursive: true });
|
|
664
667
|
|
|
665
|
-
// Create
|
|
666
|
-
|
|
668
|
+
// Create public/css directory
|
|
669
|
+
fs.mkdirSync(path.join(projectPath, 'public', 'css'), { recursive: true });
|
|
670
|
+
fs.writeFileSync(path.join(projectPath, 'public', 'css', 'style.css'), '/* Run npm run build:css */\n');
|
|
671
|
+
|
|
672
|
+
// Update package.json
|
|
673
|
+
const updatedPackageJson = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf-8'));
|
|
674
|
+
updatedPackageJson.devDependencies = updatedPackageJson.devDependencies || {};
|
|
675
|
+
updatedPackageJson.scripts = updatedPackageJson.scripts || {};
|
|
676
|
+
|
|
677
|
+
if (isV4) {
|
|
678
|
+
const inputCss = `@import "tailwindcss";
|
|
679
|
+
|
|
680
|
+
@source "../pages/**/*.{njk,js}";
|
|
681
|
+
@source "../views/**/*.njk";
|
|
682
|
+
@source "../src/**/*.js";
|
|
683
|
+
`;
|
|
684
|
+
fs.writeFileSync(path.join(projectPath, 'src', 'input.css'), inputCss);
|
|
685
|
+
|
|
686
|
+
updatedPackageJson.devDependencies['@tailwindcss/cli'] = '^4.0.0';
|
|
687
|
+
updatedPackageJson.devDependencies['tailwindcss'] = '^4.0.0';
|
|
688
|
+
|
|
689
|
+
updatedPackageJson.scripts['build:css'] = '@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --minify';
|
|
690
|
+
updatedPackageJson.scripts['watch:css'] = '@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --watch';
|
|
691
|
+
} else {
|
|
692
|
+
const inputCss = `@tailwind base;
|
|
667
693
|
@tailwind components;
|
|
668
694
|
@tailwind utilities;
|
|
669
695
|
`;
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
696
|
+
fs.writeFileSync(path.join(projectPath, 'src', 'input.css'), inputCss);
|
|
697
|
+
|
|
698
|
+
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
674
699
|
module.exports = {
|
|
675
700
|
content: [
|
|
676
701
|
'./pages/**/*.{njk,js}',
|
|
@@ -683,33 +708,25 @@ module.exports = {
|
|
|
683
708
|
plugins: [],
|
|
684
709
|
}
|
|
685
710
|
`;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const postcssConfig = `module.exports = {
|
|
711
|
+
fs.writeFileSync(path.join(projectPath, 'tailwind.config.js'), tailwindConfig);
|
|
712
|
+
|
|
713
|
+
const postcssConfig = `module.exports = {
|
|
690
714
|
plugins: {
|
|
691
715
|
tailwindcss: {},
|
|
692
716
|
autoprefixer: {},
|
|
693
717
|
},
|
|
694
718
|
}
|
|
695
719
|
`;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const updatedPackageJson = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf-8'));
|
|
706
|
-
updatedPackageJson.devDependencies = updatedPackageJson.devDependencies || {};
|
|
707
|
-
updatedPackageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
708
|
-
updatedPackageJson.devDependencies['postcss'] = '^8.4.35';
|
|
709
|
-
updatedPackageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
720
|
+
fs.writeFileSync(path.join(projectPath, 'postcss.config.js'), postcssConfig);
|
|
721
|
+
|
|
722
|
+
updatedPackageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
723
|
+
updatedPackageJson.devDependencies['postcss'] = '^8.4.35';
|
|
724
|
+
updatedPackageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
725
|
+
|
|
726
|
+
updatedPackageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
|
|
727
|
+
updatedPackageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
|
|
728
|
+
}
|
|
710
729
|
|
|
711
|
-
updatedPackageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
|
|
712
|
-
updatedPackageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
|
|
713
730
|
updatedPackageJson.scripts.dev = 'webspresso dev';
|
|
714
731
|
updatedPackageJson.scripts.start = 'npm run build:css && NODE_ENV=production node server.js';
|
|
715
732
|
|
|
@@ -718,7 +735,7 @@ module.exports = {
|
|
|
718
735
|
JSON.stringify(updatedPackageJson, null, 2) + '\n'
|
|
719
736
|
);
|
|
720
737
|
|
|
721
|
-
console.log(
|
|
738
|
+
console.log(`ā
Tailwind CSS ${isV4 ? 'v4' : 'v3'} setup complete!`);
|
|
722
739
|
}
|
|
723
740
|
|
|
724
741
|
// Scaffold AI Agent guidelines unless --no-agents is specified
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - openapi / api:docs Command
|
|
5
|
+
* Generate and export OpenAPI 3 specification from application routes
|
|
6
|
+
* @module bin/commands/openapi
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { scanRoutes } = require('../../src/discovery/scan-routes');
|
|
12
|
+
const { compileRouteTable } = require('../../src/routing/route-table');
|
|
13
|
+
const { buildOpenApiDocument } = require('../../core/openapi/build-from-api-routes');
|
|
14
|
+
|
|
15
|
+
function registerCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('openapi')
|
|
18
|
+
.alias('api:docs')
|
|
19
|
+
.description('Generate and export OpenAPI 3 specification from application routes')
|
|
20
|
+
.option('-o, --out <file>', 'Output JSON file path (e.g. openapi.json or public/openapi.json)')
|
|
21
|
+
.option('-r, --root <dir>', 'Root directory of the project', process.cwd())
|
|
22
|
+
.option('--pages <dir>', 'Pages directory relative to root', 'pages')
|
|
23
|
+
.option('--title <title>', 'OpenAPI specification title override')
|
|
24
|
+
.option('--api-version <version>', 'OpenAPI specification version override')
|
|
25
|
+
.option('--description <desc>', 'OpenAPI specification description override')
|
|
26
|
+
.option('--server <url>', 'Default server URL in spec', 'http://localhost:3000')
|
|
27
|
+
.option('--include-orm', 'Include ORM model schemas in components', false)
|
|
28
|
+
.option('--indent <spaces>', 'JSON indentation spaces', '2')
|
|
29
|
+
.option('-p, --print', 'Print specification JSON to stdout', false)
|
|
30
|
+
.action((options) => {
|
|
31
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
32
|
+
const pagesRelDir = options.pages || 'pages';
|
|
33
|
+
const pagesDir = path.resolve(rootDir, pagesRelDir);
|
|
34
|
+
const apiDir = path.resolve(pagesDir, 'api');
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(pagesDir) && !fs.existsSync(apiDir)) {
|
|
37
|
+
console.warn(`[webspresso] Warning: Pages directory not found at: ${pagesDir}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Discover routes
|
|
41
|
+
const descriptors = scanRoutes({
|
|
42
|
+
rootDir,
|
|
43
|
+
pages: fs.existsSync(pagesDir) ? { dir: pagesRelDir } : true,
|
|
44
|
+
api: fs.existsSync(apiDir) ? { dir: path.join(pagesRelDir, 'api') } : true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const routeTable = compileRouteTable(descriptors, { isDev: true });
|
|
48
|
+
const routes = routeTable.list();
|
|
49
|
+
|
|
50
|
+
// Read package.json metadata if available
|
|
51
|
+
let pkg = {};
|
|
52
|
+
const pkgPath = path.join(rootDir, 'package.json');
|
|
53
|
+
if (fs.existsSync(pkgPath)) {
|
|
54
|
+
try {
|
|
55
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore invalid json
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const docTitle = options.title || pkg.name || 'Webspresso API';
|
|
62
|
+
const docVersion = options.apiVersion || pkg.version || '1.0.0';
|
|
63
|
+
const docDescription =
|
|
64
|
+
options.description ||
|
|
65
|
+
pkg.description ||
|
|
66
|
+
'OpenAPI 3.0 specification generated from Webspresso file-based API routes.';
|
|
67
|
+
|
|
68
|
+
const doc = buildOpenApiDocument({
|
|
69
|
+
routes,
|
|
70
|
+
pagesDir,
|
|
71
|
+
includeOrmSchemas: Boolean(options.includeOrm),
|
|
72
|
+
info: {
|
|
73
|
+
title: docTitle,
|
|
74
|
+
version: docVersion,
|
|
75
|
+
description: docDescription,
|
|
76
|
+
},
|
|
77
|
+
servers: [{ url: String(options.server).replace(/\/$/, '') }],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const indent = parseInt(options.indent, 10) || 2;
|
|
81
|
+
const formattedJson = JSON.stringify(doc, null, indent);
|
|
82
|
+
|
|
83
|
+
if (options.out) {
|
|
84
|
+
const targetPath = path.resolve(rootDir, options.out);
|
|
85
|
+
const targetDir = path.dirname(targetPath);
|
|
86
|
+
if (!fs.existsSync(targetDir)) {
|
|
87
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
fs.writeFileSync(targetPath, formattedJson, 'utf8');
|
|
90
|
+
|
|
91
|
+
const relPath = path.relative(rootDir, targetPath);
|
|
92
|
+
const pathCount = Object.keys(doc.paths || {}).length;
|
|
93
|
+
console.log(`\x1b[32mā\x1b[0m Generated OpenAPI 3 specification with ${pathCount} path(s) -> \x1b[1m${relPath}\x1b[0m`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!options.out || options.print) {
|
|
97
|
+
console.log(formattedJson);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { registerCommand };
|
package/bin/commands/page.js
CHANGED
|
@@ -19,14 +19,17 @@ const path = require('path');
|
|
|
19
19
|
function registerCommand(program) {
|
|
20
20
|
program
|
|
21
21
|
.command('page [route]')
|
|
22
|
+
.alias('make:page')
|
|
23
|
+
.alias('page:make')
|
|
22
24
|
.description('Add a new SSR page (Nunjucks template + companion load() loader)')
|
|
23
25
|
.option('--no-loader', 'Skip creating companion .js loader file')
|
|
24
26
|
.option('--locales', 'Create page-level locale files')
|
|
27
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
25
28
|
.action(async (routeArg, options) => {
|
|
26
|
-
const
|
|
29
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
30
|
+
const pagesDir = path.join(rootDir, 'pages');
|
|
27
31
|
if (!fs.existsSync(pagesDir)) {
|
|
28
|
-
|
|
29
|
-
process.exit(1);
|
|
32
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
let route = routeArg ? String(routeArg).trim() : '';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prune old background queue jobs (CLI)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
8
|
+
|
|
9
|
+
function registerCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('queue:prune')
|
|
12
|
+
.description('Delete completed or failed queue jobs older than the given retention window')
|
|
13
|
+
.option('--days <n>', 'Delete jobs older than this many days', '7')
|
|
14
|
+
.option('--status <statuses>', 'Comma-separated statuses to delete (e.g. completed, failed)', 'completed')
|
|
15
|
+
.option('--table <name>', 'Queue table name', 'webspresso_jobs')
|
|
16
|
+
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
17
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
const days = parseInt(options.days, 10);
|
|
20
|
+
if (Number.isNaN(days) || days < 0) {
|
|
21
|
+
console.error('ā --days must be a non-negative integer');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const statuses = (options.status || 'completed')
|
|
26
|
+
.split(',')
|
|
27
|
+
.map((s) => s.trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
|
|
30
|
+
const { config, path: configPath } = loadDbConfig(options.config);
|
|
31
|
+
console.log(`\nš¦ Using config: ${configPath}`);
|
|
32
|
+
console.log(` Environment: ${options.env}\n`);
|
|
33
|
+
|
|
34
|
+
const knex = await createDbInstance(config, options.env);
|
|
35
|
+
const olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const { DatabaseQueueAdapter } = require('../../core/queue/adapters/database');
|
|
39
|
+
const adapter = new DatabaseQueueAdapter({
|
|
40
|
+
knex,
|
|
41
|
+
tableName: options.table,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const deleted = await adapter.clean({
|
|
45
|
+
status: statuses,
|
|
46
|
+
olderThan,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
console.log(`ā
Deleted ${deleted} job(s) [status: ${statuses.join(', ')}] with timestamp before ${olderThan.toISOString()}.\n`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error('ā queue:prune failed:', err.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
} finally {
|
|
54
|
+
await knex.destroy();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { registerCommand };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - routes Command
|
|
5
|
+
* Inspect and display compiled application routes
|
|
6
|
+
* @module bin/commands/routes
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { scanRoutes } = require('../../src/discovery/scan-routes');
|
|
12
|
+
const { compileRouteTable } = require('../../src/routing/route-table');
|
|
13
|
+
const { formatRouteTable } = require('../../src/dev/banner');
|
|
14
|
+
|
|
15
|
+
function registerCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('routes')
|
|
18
|
+
.description('List and inspect all discovered application routes')
|
|
19
|
+
.option('-a, --api', 'Filter to only API endpoints')
|
|
20
|
+
.option('-p, --pages', 'Filter to only SSR page routes')
|
|
21
|
+
.option('-j, --json', 'Output routes as JSON')
|
|
22
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
23
|
+
.action((options) => {
|
|
24
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
25
|
+
|
|
26
|
+
const descriptors = scanRoutes({
|
|
27
|
+
rootDir,
|
|
28
|
+
pages: fs.existsSync(path.join(rootDir, 'pages')) ? { dir: 'pages' } : true,
|
|
29
|
+
api: fs.existsSync(path.join(rootDir, 'pages', 'api')) ? { dir: 'pages/api' } : true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const routeTable = compileRouteTable(descriptors, { isDev: true });
|
|
33
|
+
let routes = routeTable.list();
|
|
34
|
+
|
|
35
|
+
if (options.api) {
|
|
36
|
+
routes = routes.filter((r) => r.type === 'api' || (r.path && r.path.startsWith('/api')));
|
|
37
|
+
} else if (options.pages) {
|
|
38
|
+
routes = routes.filter((r) => r.type === 'page' && (!r.path || !r.path.startsWith('/api')));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.json) {
|
|
42
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(formatRouteTable(routes, { limit: 200 }));
|
|
48
|
+
console.log('');
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { registerCommand };
|
package/bin/commands/service.js
CHANGED
|
@@ -199,6 +199,7 @@ function registerCommand(program) {
|
|
|
199
199
|
.description('Scaffold a new service in services/<domain>/<action>.js')
|
|
200
200
|
.option('-t, --transaction', 'Enable ACID transaction wrapper (transaction: true)')
|
|
201
201
|
.option('-a, --auth <role>', 'Require authentication or specific role')
|
|
202
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
202
203
|
.action(async (nameArg, options) => {
|
|
203
204
|
const fs = require('fs');
|
|
204
205
|
let serviceName = nameArg ? String(nameArg).trim() : '';
|
|
@@ -223,7 +224,8 @@ function registerCommand(program) {
|
|
|
223
224
|
const actionName = parts.pop();
|
|
224
225
|
const domainPath = parts.join(path.sep);
|
|
225
226
|
|
|
226
|
-
const
|
|
227
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
228
|
+
const servicesDir = path.resolve(rootDir, 'services');
|
|
227
229
|
const targetDir = path.join(servicesDir, domainPath);
|
|
228
230
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
229
231
|
|
|
@@ -241,13 +243,14 @@ function registerCommand(program) {
|
|
|
241
243
|
|
|
242
244
|
const content = `'use strict';
|
|
243
245
|
|
|
246
|
+
const { defineService } = require('webspresso');
|
|
244
247
|
const { z } = require('zod');
|
|
245
248
|
|
|
246
249
|
/**
|
|
247
250
|
* Service: ${serviceName}
|
|
248
251
|
* @type {import('webspresso').ServiceDefinition}
|
|
249
252
|
*/
|
|
250
|
-
module.exports = {
|
|
253
|
+
module.exports = defineService({
|
|
251
254
|
// Input validation schema (Zod)
|
|
252
255
|
schema: z.object({
|
|
253
256
|
// Define input properties here
|
|
@@ -262,7 +265,7 @@ module.exports = {
|
|
|
262
265
|
data: input,
|
|
263
266
|
};
|
|
264
267
|
},
|
|
265
|
-
};
|
|
268
|
+
});
|
|
266
269
|
`;
|
|
267
270
|
|
|
268
271
|
fs.writeFileSync(serviceFile, content, 'utf8');
|