webspresso 0.0.68 → 0.0.69
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/README.md +8 -0
- package/bin/commands/new.js +147 -64
- package/index.d.ts +4 -1
- package/package.json +4 -1
- package/src/file-router.js +95 -34
- package/templates/skills/webspresso-usage/SKILL.md +9 -1
package/README.md
CHANGED
|
@@ -2151,10 +2151,15 @@ In production, keep the plugin disabled or protect it with `authorize` / your ow
|
|
|
2151
2151
|
|
|
2152
2152
|
## Development
|
|
2153
2153
|
|
|
2154
|
+
Native addons (**better-sqlite3**, **bcrypt**, **sharp**) are compiled for your current Node ABI. After switching Node major versions (e.g. nvm, fnm, Volta), run **`npm run rebuild:native`** or a clean install: `rm -rf node_modules && npm ci`. **chokidar** is not ABI-tied like those drivers; if file watching misbehaves, reinstall dependencies. The repo includes [`.nvmrc`](.nvmrc) (Node 20 LTS) as a known-good default for this project.
|
|
2155
|
+
|
|
2154
2156
|
```bash
|
|
2155
2157
|
# Install dependencies
|
|
2156
2158
|
npm install
|
|
2157
2159
|
|
|
2160
|
+
# If you changed Node version and see MODULE_VERSION or .node load errors:
|
|
2161
|
+
npm run rebuild:native
|
|
2162
|
+
|
|
2158
2163
|
# Run tests
|
|
2159
2164
|
npm test
|
|
2160
2165
|
|
|
@@ -2163,6 +2168,9 @@ npm run test:watch
|
|
|
2163
2168
|
|
|
2164
2169
|
# Run tests with coverage
|
|
2165
2170
|
npm run test:coverage
|
|
2171
|
+
|
|
2172
|
+
# Micro-benchmarks (Vitest bench; also runs in CI on the test matrix)
|
|
2173
|
+
npm run bench
|
|
2166
2174
|
```
|
|
2167
2175
|
|
|
2168
2176
|
## License
|
package/bin/commands/new.js
CHANGED
|
@@ -9,6 +9,30 @@ const path = require('path');
|
|
|
9
9
|
const { runInstallation, startDevServer } = require('../utils/project');
|
|
10
10
|
const { getSeedFileTemplate } = require('../utils/seed');
|
|
11
11
|
|
|
12
|
+
/** `.` / `./` mean scaffold into the current working directory */
|
|
13
|
+
function isCurrentDirAlias(name) {
|
|
14
|
+
if (name == null || typeof name !== 'string') return false;
|
|
15
|
+
const t = name.trim();
|
|
16
|
+
return t === '.' || t === './';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** package.json `name` from a directory path (must match interactive validator rules) */
|
|
20
|
+
function derivePackageNameFromDir(dirPath) {
|
|
21
|
+
const base = path.basename(path.resolve(dirPath));
|
|
22
|
+
if (/^[a-z0-9-_]+$/i.test(base)) return base;
|
|
23
|
+
return 'webspresso-app';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function assertNotExistingWebspressoProject(projectPath) {
|
|
27
|
+
if (
|
|
28
|
+
fs.existsSync(path.join(projectPath, 'server.js')) ||
|
|
29
|
+
fs.existsSync(path.join(projectPath, 'pages'))
|
|
30
|
+
) {
|
|
31
|
+
console.error('❌ Target directory already contains a Webspresso project (server.js or pages/).');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
function registerCommand(program) {
|
|
13
37
|
program
|
|
14
38
|
.command('new [project-name]')
|
|
@@ -16,9 +40,12 @@ function registerCommand(program) {
|
|
|
16
40
|
.option('-t, --template <template>', 'Template to use (minimal, full)', 'minimal')
|
|
17
41
|
.option('--no-tailwind', 'Skip Tailwind CSS setup')
|
|
18
42
|
.option('-i, --install', 'Auto install dependencies and build CSS')
|
|
43
|
+
.option('-y, --yes', 'Non-interactive: no database, skip install unless -i/--install, skip dev server')
|
|
19
44
|
.action(async (projectNameArg, options) => {
|
|
20
45
|
const useTailwind = options.tailwind !== false;
|
|
21
46
|
const autoInstall = options.install === true;
|
|
47
|
+
const skipPrompts = options.yes === true;
|
|
48
|
+
const stdinNotTty = !process.stdin.isTTY;
|
|
22
49
|
|
|
23
50
|
let projectName;
|
|
24
51
|
let projectPath;
|
|
@@ -43,27 +70,27 @@ function registerCommand(program) {
|
|
|
43
70
|
useCurrentDir = true;
|
|
44
71
|
projectPath = process.cwd();
|
|
45
72
|
|
|
46
|
-
|
|
47
|
-
if (fs.existsSync(path.join(projectPath, 'server.js')) ||
|
|
48
|
-
fs.existsSync(path.join(projectPath, 'pages'))) {
|
|
49
|
-
console.error('❌ Current directory already contains a Webspresso project!');
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
73
|
+
assertNotExistingWebspressoProject(projectPath);
|
|
52
74
|
|
|
53
75
|
// Warn if there are existing files
|
|
54
76
|
if (hasExistingFiles) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
const autoProceed = skipPrompts || stdinNotTty;
|
|
78
|
+
if (autoProceed) {
|
|
79
|
+
console.log('ℹ️ Current directory is not empty; scaffolding alongside existing files.');
|
|
80
|
+
} else {
|
|
81
|
+
const { continueAnyway } = await inquirer.prompt([
|
|
82
|
+
{
|
|
83
|
+
type: 'confirm',
|
|
84
|
+
name: 'continueAnyway',
|
|
85
|
+
message: '⚠️ Current directory is not empty. Continue anyway?',
|
|
86
|
+
default: true
|
|
87
|
+
}
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
if (!continueAnyway) {
|
|
91
|
+
console.log('Cancelled.');
|
|
92
|
+
process.exit(0);
|
|
61
93
|
}
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
if (!continueAnyway) {
|
|
65
|
-
console.log('Cancelled.');
|
|
66
|
-
process.exit(0);
|
|
67
94
|
}
|
|
68
95
|
}
|
|
69
96
|
|
|
@@ -102,53 +129,92 @@ function registerCommand(program) {
|
|
|
102
129
|
projectName = dirName;
|
|
103
130
|
projectPath = path.resolve(dirName);
|
|
104
131
|
}
|
|
132
|
+
} else if (isCurrentDirAlias(projectNameArg)) {
|
|
133
|
+
useCurrentDir = true;
|
|
134
|
+
projectPath = process.cwd();
|
|
135
|
+
projectName = derivePackageNameFromDir(projectPath);
|
|
136
|
+
|
|
137
|
+
assertNotExistingWebspressoProject(projectPath);
|
|
138
|
+
|
|
139
|
+
const currentDirFiles = fs.readdirSync(projectPath);
|
|
140
|
+
const hasExistingFiles = currentDirFiles.some((f) => !f.startsWith('.'));
|
|
141
|
+
if (hasExistingFiles) {
|
|
142
|
+
const autoProceed = skipPrompts || stdinNotTty;
|
|
143
|
+
if (autoProceed) {
|
|
144
|
+
console.log('ℹ️ Current directory is not empty; scaffolding alongside existing files.');
|
|
145
|
+
} else {
|
|
146
|
+
const { continueAnyway } = await inquirer.prompt([
|
|
147
|
+
{
|
|
148
|
+
type: 'confirm',
|
|
149
|
+
name: 'continueAnyway',
|
|
150
|
+
message: '⚠️ Current directory is not empty. Continue anyway?',
|
|
151
|
+
default: true
|
|
152
|
+
}
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
if (!continueAnyway) {
|
|
156
|
+
console.log('Cancelled.');
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
105
161
|
} else {
|
|
106
|
-
|
|
107
|
-
projectPath = path.resolve(
|
|
162
|
+
const trimmed = projectNameArg.trim();
|
|
163
|
+
projectPath = path.resolve(trimmed);
|
|
164
|
+
projectName = derivePackageNameFromDir(projectPath);
|
|
108
165
|
|
|
109
166
|
if (fs.existsSync(projectPath)) {
|
|
110
|
-
console.error(`❌ Directory ${
|
|
167
|
+
console.error(`❌ Directory ${trimmed} already exists!`);
|
|
111
168
|
process.exit(1);
|
|
112
169
|
}
|
|
113
170
|
}
|
|
114
171
|
|
|
115
172
|
console.log(`\n🚀 Creating new Webspresso project: ${projectName}\n`);
|
|
116
173
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
type: 'confirm',
|
|
121
|
-
name: 'useDatabase',
|
|
122
|
-
message: 'Will you use a database?',
|
|
123
|
-
default: false
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
type: 'list',
|
|
127
|
-
name: 'databaseType',
|
|
128
|
-
message: 'Which database?',
|
|
129
|
-
choices: [
|
|
130
|
-
{ name: 'SQLite (better-sqlite3)', value: 'better-sqlite3' },
|
|
131
|
-
{ name: 'PostgreSQL (pg)', value: 'pg' },
|
|
132
|
-
{ name: 'MySQL (mysql2)', value: 'mysql2' },
|
|
133
|
-
{ name: 'None', value: null }
|
|
134
|
-
],
|
|
135
|
-
default: 'better-sqlite3',
|
|
136
|
-
when: (answers) => answers.useDatabase
|
|
137
|
-
}
|
|
138
|
-
]);
|
|
139
|
-
|
|
140
|
-
// Ask about seed data if database is selected
|
|
174
|
+
let useDatabase = false;
|
|
175
|
+
let databaseType = null;
|
|
141
176
|
let useSeed = false;
|
|
142
|
-
|
|
143
|
-
|
|
177
|
+
|
|
178
|
+
if (skipPrompts) {
|
|
179
|
+
useDatabase = false;
|
|
180
|
+
databaseType = null;
|
|
181
|
+
useSeed = false;
|
|
182
|
+
} else {
|
|
183
|
+
const dbAnswers = await inquirer.prompt([
|
|
144
184
|
{
|
|
145
185
|
type: 'confirm',
|
|
146
|
-
name: '
|
|
147
|
-
message: '
|
|
186
|
+
name: 'useDatabase',
|
|
187
|
+
message: 'Will you use a database?',
|
|
148
188
|
default: false
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: 'list',
|
|
192
|
+
name: 'databaseType',
|
|
193
|
+
message: 'Which database?',
|
|
194
|
+
choices: [
|
|
195
|
+
{ name: 'SQLite (better-sqlite3)', value: 'better-sqlite3' },
|
|
196
|
+
{ name: 'PostgreSQL (pg)', value: 'pg' },
|
|
197
|
+
{ name: 'MySQL (mysql2)', value: 'mysql2' },
|
|
198
|
+
{ name: 'None', value: null }
|
|
199
|
+
],
|
|
200
|
+
default: 'better-sqlite3',
|
|
201
|
+
when: (answers) => answers.useDatabase
|
|
149
202
|
}
|
|
150
203
|
]);
|
|
151
|
-
|
|
204
|
+
useDatabase = dbAnswers.useDatabase;
|
|
205
|
+
databaseType = dbAnswers.databaseType;
|
|
206
|
+
|
|
207
|
+
if (useDatabase && databaseType) {
|
|
208
|
+
const { generateSeed } = await inquirer.prompt([
|
|
209
|
+
{
|
|
210
|
+
type: 'confirm',
|
|
211
|
+
name: 'generateSeed',
|
|
212
|
+
message: 'Generate seed data based on existing models?',
|
|
213
|
+
default: false
|
|
214
|
+
}
|
|
215
|
+
]);
|
|
216
|
+
useSeed = generateSeed;
|
|
217
|
+
}
|
|
152
218
|
}
|
|
153
219
|
|
|
154
220
|
// Create directory structure (skip root if using current dir)
|
|
@@ -652,28 +718,46 @@ module.exports = {
|
|
|
652
718
|
if (autoInstall) {
|
|
653
719
|
await runInstallation(projectPath, useTailwind);
|
|
654
720
|
|
|
655
|
-
|
|
656
|
-
const { shouldStartDev } = await inquirer.prompt([
|
|
657
|
-
{
|
|
658
|
-
type: 'confirm',
|
|
659
|
-
name: 'shouldStartDev',
|
|
660
|
-
message: 'Start development server?',
|
|
661
|
-
default: true
|
|
662
|
-
}
|
|
663
|
-
]);
|
|
664
|
-
|
|
665
|
-
if (shouldStartDev) {
|
|
666
|
-
startDevServer(projectPath, useTailwind);
|
|
667
|
-
} else {
|
|
721
|
+
if (skipPrompts) {
|
|
668
722
|
console.log('✅ Project ready!\n');
|
|
669
723
|
console.log('Start developing:');
|
|
670
724
|
if (!useCurrentDir) {
|
|
671
725
|
console.log(` cd ${projectName}`);
|
|
672
726
|
}
|
|
673
727
|
console.log(' npm run dev\n');
|
|
728
|
+
} else {
|
|
729
|
+
const { shouldStartDev } = await inquirer.prompt([
|
|
730
|
+
{
|
|
731
|
+
type: 'confirm',
|
|
732
|
+
name: 'shouldStartDev',
|
|
733
|
+
message: 'Start development server?',
|
|
734
|
+
default: true
|
|
735
|
+
}
|
|
736
|
+
]);
|
|
737
|
+
|
|
738
|
+
if (shouldStartDev) {
|
|
739
|
+
startDevServer(projectPath, useTailwind);
|
|
740
|
+
} else {
|
|
741
|
+
console.log('✅ Project ready!\n');
|
|
742
|
+
console.log('Start developing:');
|
|
743
|
+
if (!useCurrentDir) {
|
|
744
|
+
console.log(` cd ${projectName}`);
|
|
745
|
+
}
|
|
746
|
+
console.log(' npm run dev\n');
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
} else if (skipPrompts) {
|
|
750
|
+
console.log('\n✅ Project created successfully!\n');
|
|
751
|
+
console.log('Next steps:');
|
|
752
|
+
if (!useCurrentDir) {
|
|
753
|
+
console.log(` cd ${projectName}`);
|
|
754
|
+
}
|
|
755
|
+
console.log(' npm install');
|
|
756
|
+
if (useTailwind) {
|
|
757
|
+
console.log(' npm run build:css');
|
|
674
758
|
}
|
|
759
|
+
console.log(' npm run dev\n');
|
|
675
760
|
} else {
|
|
676
|
-
// Ask if user wants to install dependencies
|
|
677
761
|
const { shouldInstall } = await inquirer.prompt([
|
|
678
762
|
{
|
|
679
763
|
type: 'confirm',
|
|
@@ -686,7 +770,6 @@ module.exports = {
|
|
|
686
770
|
if (shouldInstall) {
|
|
687
771
|
await runInstallation(projectPath, useTailwind);
|
|
688
772
|
|
|
689
|
-
// Ask if user wants to start dev server
|
|
690
773
|
const { shouldStartDev } = await inquirer.prompt([
|
|
691
774
|
{
|
|
692
775
|
type: 'confirm',
|
package/index.d.ts
CHANGED
|
@@ -94,7 +94,10 @@ export function mountPages(
|
|
|
94
94
|
|
|
95
95
|
export function filePathToRoute(filePath: string, pagesDir: string): string;
|
|
96
96
|
|
|
97
|
-
export function extractMethodFromFilename(filename: string):
|
|
97
|
+
export function extractMethodFromFilename(filename: string): {
|
|
98
|
+
method: string;
|
|
99
|
+
baseName: string;
|
|
100
|
+
};
|
|
98
101
|
|
|
99
102
|
export function scanDirectory(
|
|
100
103
|
dir: string,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.69",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"test:e2e:ui": "playwright test --ui",
|
|
16
16
|
"test:e2e:debug": "playwright test --debug",
|
|
17
17
|
"test:e2e:headed": "playwright test --headed",
|
|
18
|
+
"bench": "vitest bench --run",
|
|
19
|
+
"bench:compare": "vitest bench --run --compare benchmark-results.json",
|
|
18
20
|
"check:types": "tsc --project tests/ts-smoke/tsconfig.json",
|
|
21
|
+
"rebuild:native": "npm rebuild better-sqlite3 bcrypt sharp",
|
|
19
22
|
"release": "release-it"
|
|
20
23
|
},
|
|
21
24
|
"keywords": [
|
package/src/file-router.js
CHANGED
|
@@ -16,6 +16,12 @@ const i18nCache = new Map();
|
|
|
16
16
|
// Cache for route configs in production
|
|
17
17
|
const configCache = new Map();
|
|
18
18
|
|
|
19
|
+
// Dev-only: avoid require() on every SSR request when the .js file is unchanged (mtime)
|
|
20
|
+
const routeConfigDevCache = new Map();
|
|
21
|
+
|
|
22
|
+
// Cache for API filename -> { method, baseName } (basename keys; stable per process)
|
|
23
|
+
const methodFromFilenameCache = new Map();
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
* Convert a file path to an Express route pattern
|
|
21
27
|
* @param {string} filePath - Relative path from pages/
|
|
@@ -51,26 +57,87 @@ function filePathToRoute(filePath, ext) {
|
|
|
51
57
|
return route;
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Metadata for ordering route registration: more specific Express paths must be
|
|
62
|
+
* registered before less specific ones (static before dynamic; more literal
|
|
63
|
+
* segments before fewer; deeper paths before shallower among same class).
|
|
64
|
+
* @param {string} routePath
|
|
65
|
+
* @returns {{ tier: number, literalSegCount: number, paramSegCount: number, depth: number, routePath: string }}
|
|
66
|
+
*/
|
|
67
|
+
function routeRegistrationMeta(routePath) {
|
|
68
|
+
const hasCatchAll = routePath.includes('*');
|
|
69
|
+
const hasDynamic = routePath.includes(':');
|
|
70
|
+
let tier;
|
|
71
|
+
if (hasCatchAll) tier = 2;
|
|
72
|
+
else if (hasDynamic) tier = 1;
|
|
73
|
+
else tier = 0;
|
|
74
|
+
|
|
75
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
76
|
+
let literalSegCount = 0;
|
|
77
|
+
let paramSegCount = 0;
|
|
78
|
+
for (const seg of segments) {
|
|
79
|
+
if (seg === '*' || (seg.length > 0 && seg.includes('*'))) continue;
|
|
80
|
+
if (seg.includes(':')) paramSegCount += 1;
|
|
81
|
+
else literalSegCount += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
tier,
|
|
86
|
+
literalSegCount,
|
|
87
|
+
paramSegCount,
|
|
88
|
+
depth: segments.length,
|
|
89
|
+
routePath,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compare two routes for registration order (negative if a before b).
|
|
95
|
+
* @param {{ routePath: string }} a
|
|
96
|
+
* @param {{ routePath: string }} b
|
|
97
|
+
*/
|
|
98
|
+
function compareRouteRegistrationOrder(a, b) {
|
|
99
|
+
const ma = routeRegistrationMeta(a.routePath);
|
|
100
|
+
const mb = routeRegistrationMeta(b.routePath);
|
|
101
|
+
if (ma.tier !== mb.tier) return ma.tier - mb.tier;
|
|
102
|
+
if (ma.literalSegCount !== mb.literalSegCount) {
|
|
103
|
+
return mb.literalSegCount - ma.literalSegCount;
|
|
104
|
+
}
|
|
105
|
+
if (ma.depth !== mb.depth) return mb.depth - ma.depth;
|
|
106
|
+
if (ma.paramSegCount !== mb.paramSegCount) return ma.paramSegCount - mb.paramSegCount;
|
|
107
|
+
return ma.routePath.localeCompare(mb.routePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
54
110
|
/**
|
|
55
111
|
* Extract HTTP method from API filename
|
|
56
112
|
* @param {string} filename - Filename like health.get.js
|
|
57
113
|
* @returns {{ method: string, baseName: string }}
|
|
58
114
|
*/
|
|
59
115
|
function extractMethodFromFilename(filename) {
|
|
116
|
+
const hit = methodFromFilenameCache.get(filename);
|
|
117
|
+
if (hit !== undefined) {
|
|
118
|
+
return hit;
|
|
119
|
+
}
|
|
120
|
+
|
|
60
121
|
const methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
61
122
|
const parts = filename.replace('.js', '').split('.');
|
|
62
|
-
|
|
123
|
+
let result;
|
|
124
|
+
|
|
63
125
|
if (parts.length > 1) {
|
|
64
126
|
const lastPart = parts[parts.length - 1].toLowerCase();
|
|
65
127
|
if (methods.includes(lastPart)) {
|
|
66
|
-
|
|
128
|
+
result = {
|
|
67
129
|
method: lastPart,
|
|
68
|
-
baseName: parts.slice(0, -1).join('.')
|
|
130
|
+
baseName: parts.slice(0, -1).join('.'),
|
|
69
131
|
};
|
|
70
132
|
}
|
|
71
133
|
}
|
|
72
|
-
|
|
73
|
-
|
|
134
|
+
|
|
135
|
+
if (!result) {
|
|
136
|
+
result = { method: 'get', baseName: parts.join('.') };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
methodFromFilenameCache.set(filename, result);
|
|
140
|
+
return result;
|
|
74
141
|
}
|
|
75
142
|
|
|
76
143
|
/**
|
|
@@ -201,25 +268,31 @@ function createTranslator(translations) {
|
|
|
201
268
|
*/
|
|
202
269
|
function loadRouteConfig(configPath, isDev) {
|
|
203
270
|
if (!fs.existsSync(configPath)) {
|
|
271
|
+
routeConfigDevCache.delete(configPath);
|
|
204
272
|
return null;
|
|
205
273
|
}
|
|
206
|
-
|
|
274
|
+
|
|
207
275
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
276
|
+
if (isDev) {
|
|
277
|
+
const stats = fs.statSync(configPath);
|
|
278
|
+
const devCached = routeConfigDevCache.get(configPath);
|
|
279
|
+
if (devCached && devCached.mtime >= stats.mtimeMs) {
|
|
280
|
+
return devCached.config;
|
|
281
|
+
}
|
|
282
|
+
if (require.cache[require.resolve(configPath)]) {
|
|
283
|
+
delete require.cache[require.resolve(configPath)];
|
|
284
|
+
}
|
|
285
|
+
const config = require(configPath);
|
|
286
|
+
routeConfigDevCache.set(configPath, { mtime: stats.mtimeMs, config });
|
|
287
|
+
return config;
|
|
211
288
|
}
|
|
212
|
-
|
|
213
|
-
if (
|
|
289
|
+
|
|
290
|
+
if (configCache.has(configPath)) {
|
|
214
291
|
return configCache.get(configPath);
|
|
215
292
|
}
|
|
216
|
-
|
|
293
|
+
|
|
217
294
|
const config = require(configPath);
|
|
218
|
-
|
|
219
|
-
if (!isDev) {
|
|
220
|
-
configCache.set(configPath, config);
|
|
221
|
-
}
|
|
222
|
-
|
|
295
|
+
configCache.set(configPath, config);
|
|
223
296
|
return config;
|
|
224
297
|
} catch (err) {
|
|
225
298
|
console.error(`Error loading route config ${configPath}:`, err.message);
|
|
@@ -431,22 +504,8 @@ function mountPages(app, options) {
|
|
|
431
504
|
}
|
|
432
505
|
}
|
|
433
506
|
|
|
434
|
-
// Sort routes:
|
|
435
|
-
const sortRoutes = (routes) =>
|
|
436
|
-
return routes.sort((a, b) => {
|
|
437
|
-
const aHasCatchAll = a.routePath.includes('*');
|
|
438
|
-
const bHasCatchAll = b.routePath.includes('*');
|
|
439
|
-
const aHasDynamic = a.routePath.includes(':');
|
|
440
|
-
const bHasDynamic = b.routePath.includes(':');
|
|
441
|
-
|
|
442
|
-
if (aHasCatchAll && !bHasCatchAll) return 1;
|
|
443
|
-
if (!aHasCatchAll && bHasCatchAll) return -1;
|
|
444
|
-
if (aHasDynamic && !bHasDynamic) return 1;
|
|
445
|
-
if (!aHasDynamic && bHasDynamic) return -1;
|
|
446
|
-
|
|
447
|
-
return a.routePath.localeCompare(b.routePath);
|
|
448
|
-
});
|
|
449
|
-
};
|
|
507
|
+
// Sort routes: static before dynamic before catch-all; then more literal segments, then deeper paths
|
|
508
|
+
const sortRoutes = (routes) => routes.sort(compareRouteRegistrationOrder);
|
|
450
509
|
|
|
451
510
|
// Register API routes
|
|
452
511
|
for (const route of sortRoutes(apiRoutes)) {
|
|
@@ -686,6 +745,8 @@ module.exports = {
|
|
|
686
745
|
loadI18n,
|
|
687
746
|
createTranslator,
|
|
688
747
|
detectLocale,
|
|
689
|
-
resolveMiddlewares
|
|
748
|
+
resolveMiddlewares,
|
|
749
|
+
routeRegistrationMeta,
|
|
750
|
+
compareRouteRegistrationOrder,
|
|
690
751
|
};
|
|
691
752
|
|
|
@@ -6,7 +6,7 @@ description: >-
|
|
|
6
6
|
session auth (createAuth, quickAuth, webspresso/core/auth), Nunjucks/fsy helpers,
|
|
7
7
|
i18n, lifecycle hooks, Zod API validation, ORM (zdb, defineModel, repository,
|
|
8
8
|
query builder, migrations), plugins (admin, analytics, sitemap, SEO, audit,
|
|
9
|
-
recaptcha, rest resources, file upload), CLI, env vars, and testing. Use when working in this
|
|
9
|
+
recaptcha, rest resources, file upload), CLI (`new .`, `--yes`, `-i`), env vars, and testing. Use when working in this
|
|
10
10
|
repo or any Webspresso app—adding routes, APIs, models, plugins, auth, client-side
|
|
11
11
|
sprinkles or page transitions, or debugging routing, ctx.db, session, or templates.
|
|
12
12
|
---
|
|
@@ -220,6 +220,8 @@ Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and pl
|
|
|
220
220
|
| Command | Role |
|
|
221
221
|
|---------|------|
|
|
222
222
|
| `webspresso new` | Scaffold project — includes **`config/load-env.js`** (`.env` chain), **`config/env.schema.js`** (Zod), **`config/app.js`** (`createApp` paths + optional `db` if `webspresso.db.js` exists), thin **`server.js`** |
|
|
223
|
+
| `webspresso new .` / `new ./` | Scaffold **into the current directory** (same as interactive “install here”). `package.json` **`name`** = folder basename, or **`webspresso-app`** if basename is not npm-safe. **Aborts** if **`server.js`** or **`pages/`** already exists (already a Webspresso layout). |
|
|
224
|
+
| `webspresso new … --yes` | **Non-interactive:** skips DB/seed prompts (no DB), skips “install now?” unless you pass **`-i` / `--install`**, skips “start dev server?” after install. Use for CI and agent tools. |
|
|
223
225
|
| `webspresso dev` / `start` | Servers |
|
|
224
226
|
| `webspresso page` / `api` | Interactive scaffolding |
|
|
225
227
|
| `webspresso db:*` | migrate, rollback, status, make |
|
|
@@ -232,6 +234,12 @@ Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and pl
|
|
|
232
234
|
| `webspresso admin:setup` / `admin:password` | Admin users |
|
|
233
235
|
| `webspresso audit:prune` | Audit log retention |
|
|
234
236
|
|
|
237
|
+
**`webspresso new` — current dir & automation**
|
|
238
|
+
|
|
239
|
+
- **`new .`** does **not** treat the cwd as “directory already exists”; it scaffolds **in place** next to any existing non-dot files.
|
|
240
|
+
- **Non-empty cwd:** interactive prompt defaults to **continue**; if **stdin is not a TTY** (piped/agent) or you pass **`--yes`**, the “continue?” step is skipped and a short **info** line is printed instead.
|
|
241
|
+
- **Typical agent / vibe-coding one-liners:** `webspresso new . --yes --no-tailwind` · `webspresso new . --yes -i` (scaffold + install, no dev server prompt).
|
|
242
|
+
|
|
235
243
|
---
|
|
236
244
|
|
|
237
245
|
## 12. Environment variables (common)
|