webspresso 0.0.68 → 0.0.70

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 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
@@ -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
- // Check for existing Webspresso files
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 { continueAnyway } = await inquirer.prompt([
56
- {
57
- type: 'confirm',
58
- name: 'continueAnyway',
59
- message: '⚠️ Current directory is not empty. Continue anyway?',
60
- default: false
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
- projectName = projectNameArg;
107
- projectPath = path.resolve(projectNameArg);
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 ${projectName} already exists!`);
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
- // Ask about database
118
- const { useDatabase, databaseType } = await inquirer.prompt([
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
- if (useDatabase && databaseType) {
143
- const { generateSeed } = await inquirer.prompt([
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: 'generateSeed',
147
- message: 'Generate seed data based on existing models?',
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
- useSeed = generateSeed;
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
- // Ask if user wants to start dev server
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
@@ -90,11 +90,17 @@ export function setAppContext(partial: { db?: DatabaseInstance | null }): void;
90
90
  export function mountPages(
91
91
  app: Application,
92
92
  options: Record<string, unknown>
93
- ): unknown;
93
+ ): {
94
+ routeMetadata: unknown[];
95
+ registerDynamicFileRoutes: () => void;
96
+ };
94
97
 
95
98
  export function filePathToRoute(filePath: string, pagesDir: string): string;
96
99
 
97
- export function extractMethodFromFilename(filename: string): string | null;
100
+ export function extractMethodFromFilename(filename: string): {
101
+ method: string;
102
+ baseName: string;
103
+ };
98
104
 
99
105
  export function scanDirectory(
100
106
  dir: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.68",
3
+ "version": "0.0.70",
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": [
@@ -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
- return {
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
- return { method: 'get', baseName: parts.join('.') };
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
- // Clear cache in development mode
209
- if (isDev && require.cache[require.resolve(configPath)]) {
210
- delete require.cache[require.resolve(configPath)];
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 (!isDev && configCache.has(configPath)) {
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,25 +504,19 @@ function mountPages(app, options) {
431
504
  }
432
505
  }
433
506
 
434
- // Sort routes: specific routes first, then dynamic, then catch-all
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
- };
450
-
451
- // Register API routes
452
- for (const route of sortRoutes(apiRoutes)) {
507
+ // Sort routes: static before dynamic before catch-all; then more literal segments, then deeper paths
508
+ const sortRoutes = (routes) => routes.sort(compareRouteRegistrationOrder);
509
+ sortRoutes(apiRoutes);
510
+ sortRoutes(ssrRoutes);
511
+
512
+ const apiStatic = apiRoutes.filter((r) => routeRegistrationMeta(r.routePath).tier === 0);
513
+ const apiDynamic = apiRoutes.filter((r) => routeRegistrationMeta(r.routePath).tier !== 0);
514
+ const ssrStatic = ssrRoutes.filter((r) => routeRegistrationMeta(r.routePath).tier === 0);
515
+ const ssrDynamic = ssrRoutes.filter((r) => routeRegistrationMeta(r.routePath).tier !== 0);
516
+
517
+ /** Register API routes (shared by static phase and dynamic phase). */
518
+ const registerApiRoutes = (routes) => {
519
+ for (const route of routes) {
453
520
  const handler = require(route.fullPath);
454
521
  const handlerFn = typeof handler === 'function' ? handler : handler.default || handler.handler;
455
522
  const routeMiddleware = handler.middleware;
@@ -523,10 +590,12 @@ function mountPages(app, options) {
523
590
  });
524
591
 
525
592
  log(` ${route.method.toUpperCase()} ${route.routePath} -> ${route.file}`);
526
- }
527
-
528
- // Register SSR routes
529
- for (const route of sortRoutes(ssrRoutes)) {
593
+ }
594
+ };
595
+
596
+ /** Register SSR GET routes (shared by static phase and dynamic phase). */
597
+ const registerSsrRoutes = (routes) => {
598
+ for (const route of routes) {
530
599
  app.get(route.routePath, async (req, res, next) => {
531
600
  try {
532
601
  // Detect locale
@@ -655,8 +724,19 @@ function mountPages(app, options) {
655
724
  });
656
725
 
657
726
  log(` GET ${route.routePath} -> ${route.file}`);
658
- }
659
-
727
+ }
728
+ };
729
+
730
+ // Static / literal file routes first so plugins can register reserved paths (e.g. /_admin)
731
+ // before catch-all dynamics like /:slug shadow them.
732
+ registerApiRoutes(apiStatic);
733
+ registerSsrRoutes(ssrStatic);
734
+
735
+ const registerDynamicFileRoutes = () => {
736
+ registerApiRoutes(apiDynamic);
737
+ registerSsrRoutes(ssrDynamic);
738
+ };
739
+
660
740
  // Return route metadata for plugins
661
741
  const routeMetadata = [
662
742
  ...ssrRoutes.map(r => ({
@@ -674,8 +754,8 @@ function mountPages(app, options) {
674
754
  isDynamic: r.routePath.includes(':') || r.routePath.includes('*')
675
755
  }))
676
756
  ];
677
-
678
- return routeMetadata;
757
+
758
+ return { routeMetadata, registerDynamicFileRoutes };
679
759
  }
680
760
 
681
761
  module.exports = {
@@ -686,6 +766,8 @@ module.exports = {
686
766
  loadI18n,
687
767
  createTranslator,
688
768
  detectLocale,
689
- resolveMiddlewares
769
+ resolveMiddlewares,
770
+ routeRegistrationMeta,
771
+ compareRouteRegistrationOrder,
690
772
  };
691
773
 
package/src/server.js CHANGED
@@ -440,7 +440,7 @@ function createApp(options = {}) {
440
440
  if (!isTest) {
441
441
  console.log('\nMounting routes:');
442
442
  }
443
- const routeMetadata = mountPages(app, {
443
+ const { routeMetadata, registerDynamicFileRoutes } = mountPages(app, {
444
444
  pagesDir,
445
445
  nunjucks: nunjucksEnv,
446
446
  middlewares,
@@ -449,7 +449,7 @@ function createApp(options = {}) {
449
449
  db: options.db ?? null,
450
450
  clientRuntime,
451
451
  });
452
-
452
+
453
453
  // Set route metadata in plugin manager
454
454
  pluginManager.setRoutes(routeMetadata);
455
455
 
@@ -491,7 +491,11 @@ function createApp(options = {}) {
491
491
  clientRuntime,
492
492
  });
493
493
  }
494
-
494
+
495
+ // Dynamic / catch-all file routes after plugins and setupRoutes so paths like /_admin
496
+ // or custom /login are not shadowed by pages/[slug].njk (/:slug).
497
+ registerDynamicFileRoutes();
498
+
495
499
  // Helper to create error page context with fsy
496
500
  function createErrorContext(req, extraData = {}) {
497
501
  const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
@@ -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)