webspresso 0.0.101 → 0.0.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -68,6 +68,14 @@ export interface CreateAppOptions {
68
68
  compression?: boolean | CompressionOptions;
69
69
  /** Shutdown and lifecycle configuration */
70
70
  shutdown?: ShutdownOptions;
71
+ /**
72
+ * Ahead-Of-Time (AOT) route manifest: boolean to enable/disable loading, or explicit path to manifest JSON file
73
+ */
74
+ routesManifest?: boolean | string;
75
+ /**
76
+ * Precompiled Nunjucks templates: boolean to enable/disable loading, or explicit path to compiled bundle file
77
+ */
78
+ precompiledTemplates?: boolean | string;
71
79
  /** Development tooling and DX options */
72
80
  dev?: {
73
81
  banner?: boolean;
@@ -657,6 +665,25 @@ export function detectLocale(
657
665
  defaultLocale: string
658
666
  ): string;
659
667
 
668
+ export interface TranslateValidationIssuesOptions {
669
+ req?: Request;
670
+ t?: Translator;
671
+ locale?: string;
672
+ pagesDir?: string;
673
+ routeDir?: string;
674
+ isDev?: boolean;
675
+ }
676
+
677
+ export function translateValidationIssues<T = unknown>(
678
+ issues: T[],
679
+ options?: TranslateValidationIssuesOptions
680
+ ): T[];
681
+
682
+ export function getTranslatorForRequest(
683
+ req?: Request,
684
+ options?: { pagesDir?: string; routeDir?: string; isDev?: boolean }
685
+ ): { t: Translator; locale: string };
686
+
660
687
  export function parseNjkFrontmatter(content: string): {
661
688
  body: string;
662
689
  fm: Record<string, unknown> | null;
@@ -1561,9 +1588,38 @@ export interface AdminPanelPluginOptions {
1561
1588
  windowMs?: number;
1562
1589
  blockDurationMs?: number;
1563
1590
  };
1591
+ /** Lazy / On-Demand mount: defers plugin initialization until first request to adminPath or service execution */
1592
+ lazy?: boolean | LazyPluginOptions;
1564
1593
  [key: string]: unknown;
1565
1594
  }
1566
1595
 
1596
+ export interface LazyPluginOptions {
1597
+ name?: string;
1598
+ prefix?: string | string[];
1599
+ services?: string[];
1600
+ helpers?: string[];
1601
+ events?: string[];
1602
+ dependencies?: Record<string, string> | string[];
1603
+ csp?: Record<string, string[]>;
1604
+ onHydrate?: (realPlugin: WebspressoPlugin) => void;
1605
+ }
1606
+
1607
+ export interface LazyPlugin extends WebspressoPlugin {
1608
+ isLazy: true;
1609
+ readonly isHydrated: boolean;
1610
+ hydrate(): WebspressoPlugin;
1611
+ }
1612
+
1613
+ export function lazyPlugin(
1614
+ pluginFactory: (() => WebspressoPlugin) | WebspressoPlugin,
1615
+ lazyOptions?: LazyPluginOptions
1616
+ ): LazyPlugin;
1617
+
1618
+ export function defineLazyPlugin(
1619
+ pluginFactory: (() => WebspressoPlugin) | WebspressoPlugin,
1620
+ lazyOptions?: LazyPluginOptions
1621
+ ): LazyPlugin;
1622
+
1567
1623
  export function adminPanelPlugin(options?: AdminPanelPluginOptions): WebspressoPlugin;
1568
1624
 
1569
1625
  export function dataExchangePlugin(options?: Record<string, unknown>): WebspressoPlugin;
@@ -3142,11 +3198,14 @@ export interface ApiContext<TBody = any, TParams = Record<string, string>, TQuer
3142
3198
  query: TQuery;
3143
3199
  body: TBody;
3144
3200
  input: { body?: TBody; params?: TParams; query?: TQuery; headers?: Record<string, string> };
3201
+ locale: string;
3202
+ t: Translator;
3145
3203
  service: WebspressoServiceCaller;
3146
3204
  module: string | null;
3147
3205
  config: Record<string, unknown>;
3148
3206
  logger: Console;
3149
3207
  db: unknown;
3208
+ sse?: (sourceOrOptions?: any, maybeOptions?: any) => any;
3150
3209
  }
3151
3210
 
3152
3211
  export interface DefineApiOptions<TResult = any, TBody = any, TParams = Record<string, string>, TQuery = Record<string, string>> {
@@ -3213,7 +3272,21 @@ export function scanRoutes(options?: {
3213
3272
  pages?: boolean | { dir?: string; prefix?: string };
3214
3273
  api?: boolean | { dir?: string; prefix?: string };
3215
3274
  modules?: boolean | { dir?: string };
3216
- }): RouteDescriptor[];
3275
+ }): RouteDescriptor[] & { scannedPagesFiles?: string[]; scannedPagesDir?: string | null };
3276
+
3277
+ export function discoverAppRoutes(options?: {
3278
+ rootDir?: string;
3279
+ pages?: boolean | { dir?: string; prefix?: string };
3280
+ api?: boolean | { dir?: string; prefix?: string };
3281
+ modules?: boolean | { dir?: string };
3282
+ routes?: Array<{ method?: string; path: string }>;
3283
+ isDev?: boolean;
3284
+ }): {
3285
+ descriptors: RouteDescriptor[];
3286
+ routeTable: RouteTable;
3287
+ scannedPagesFiles: string[];
3288
+ scannedPagesDir: string | null;
3289
+ };
3217
3290
 
3218
3291
  export function parseFileRoute(
3219
3292
  relativePath: string,
@@ -3225,6 +3298,88 @@ export function compileRouteTable(
3225
3298
  options?: { explicitRoutes?: Array<{ method?: string; path: string }>; isDev?: boolean }
3226
3299
  ): RouteTable;
3227
3300
 
3301
+ export function generateRouteManifest(options?: {
3302
+ rootDir?: string;
3303
+ pages?: boolean | string | { dir?: string; prefix?: string };
3304
+ api?: boolean | string | { dir?: string; prefix?: string };
3305
+ modules?: boolean | { dir?: string };
3306
+ pagesDir?: string;
3307
+ outDir?: string;
3308
+ outFile?: string;
3309
+ silent?: boolean;
3310
+ }): {
3311
+ manifest: {
3312
+ version: number;
3313
+ generatedAt: string;
3314
+ pagesDirRel: string;
3315
+ scannedPagesFiles: string[];
3316
+ routes: Array<{
3317
+ type: 'page' | 'api';
3318
+ method: string;
3319
+ path: string;
3320
+ source: string;
3321
+ module?: string | null;
3322
+ moduleConfig?: { name?: string } | null;
3323
+ }>;
3324
+ };
3325
+ manifestPath: string;
3326
+ routeCount: number;
3327
+ scannedFilesCount: number;
3328
+ };
3329
+
3330
+ export function loadRouteManifest(options?: {
3331
+ rootDir?: string;
3332
+ manifestPath?: string;
3333
+ }): {
3334
+ manifest: {
3335
+ version: number;
3336
+ generatedAt: string;
3337
+ pagesDirRel: string;
3338
+ scannedPagesFiles: string[];
3339
+ routes: Array<{
3340
+ type: 'page' | 'api';
3341
+ method: string;
3342
+ path: string;
3343
+ source: string;
3344
+ module?: string | null;
3345
+ moduleConfig?: { name?: string } | null;
3346
+ }>;
3347
+ };
3348
+ descriptors: RouteDescriptor[] & { scannedPagesFiles?: string[]; scannedPagesDir?: string | null };
3349
+ } | null;
3350
+
3351
+ export function precompileTemplates(options?: {
3352
+ rootDir?: string;
3353
+ viewsDir?: string;
3354
+ pagesDir?: string;
3355
+ outDir?: string;
3356
+ outFile?: string;
3357
+ env?: any;
3358
+ }): {
3359
+ templateCount: number;
3360
+ manifestPath: string;
3361
+ bundle: {
3362
+ version: number;
3363
+ generatedAt: string;
3364
+ templates: Record<string, string>;
3365
+ frontmatter: Record<string, any>;
3366
+ };
3367
+ };
3368
+
3369
+ export function loadPrecompiledTemplates(options?: {
3370
+ rootDir?: string;
3371
+ bundlePath?: string;
3372
+ }): {
3373
+ loader: any;
3374
+ templateCount: number;
3375
+ bundle: {
3376
+ version: number;
3377
+ generatedAt: string;
3378
+ templates: Record<string, string>;
3379
+ frontmatter: Record<string, any>;
3380
+ };
3381
+ } | null;
3382
+
3228
3383
  export interface GenerateTypesOptions {
3229
3384
  /** Root directory of the project (default: process.cwd()) */
3230
3385
  rootDir?: string;
package/index.js CHANGED
@@ -58,13 +58,21 @@ const {
58
58
  const { definePage } = require('./src/pages/define-page');
59
59
  const { defineApi } = require('./src/api/define-api');
60
60
  const { defineModule } = require('./src/modules/define-module');
61
- const { scanRoutes } = require('./src/discovery/scan-routes');
61
+ const { scanRoutes, discoverAppRoutes } = require('./src/discovery/scan-routes');
62
+ const { generateRouteManifest, loadRouteManifest } = require('./src/discovery/route-manifest');
63
+ const { precompileTemplates, loadPrecompiledTemplates } = require('./src/template/precompile');
62
64
  const { parseFileRoute } = require('./src/discovery/file-route-parser');
63
65
  const { compileRouteTable, RouteTable } = require('./src/routing/route-table');
64
66
  const { mountDiscoveredRoutes } = require('./src/routing/mount-discovered-routes');
65
67
 
66
- // ORM exports (lazy loaded)
67
- const orm = require('./core/orm');
68
+ // Lazy-loaded ORM accessor
69
+ let _orm = null;
70
+ function getOrm() {
71
+ if (!_orm) {
72
+ _orm = require('./core/orm');
73
+ }
74
+ return _orm;
75
+ }
68
76
 
69
77
  // Lazy-loaded built-in plugins
70
78
  const plugins = require('./plugins');
@@ -99,12 +107,6 @@ module.exports = {
99
107
  discoverServices: require('./src/services').discoverServices,
100
108
  filePathToServiceName: require('./src/services').filePathToServiceName,
101
109
  toCamelCase: require('./src/services').toCamelCase,
102
- createAuthServices: require('./src/services').createAuthServices,
103
- createMailServices: require('./src/services').createMailServices,
104
- createMediaServices: require('./src/services').createMediaServices,
105
- createFileManagerServices: require('./src/services').createFileManagerServices,
106
- createSystemServices: require('./src/services').createSystemServices,
107
- createExchangeServices: require('./src/services').createExchangeServices,
108
110
  validateServiceInput: require('./src/services').validateServiceInput,
109
111
  executeService: require('./src/services').executeService,
110
112
 
@@ -171,12 +173,6 @@ module.exports = {
171
173
  createPluginManager,
172
174
  getPluginManager,
173
175
  resetPluginManager,
174
-
175
- // ORM
176
- ...orm,
177
-
178
- // Direct zdb export (for convenience)
179
- zdb: orm.zdb,
180
176
 
181
177
  // Discovery, Pages, API, Modules & Routing primitives
182
178
  definePage,
@@ -186,11 +182,18 @@ module.exports = {
186
182
  defineModule,
187
183
  module: defineModule,
188
184
  scanRoutes,
185
+ discoverAppRoutes,
186
+ generateRouteManifest,
187
+ loadRouteManifest,
188
+ precompileTemplates,
189
+ loadPrecompiledTemplates,
189
190
  parseFileRoute,
190
191
  compileRouteTable,
191
192
  RouteTable,
192
193
  mountDiscoveredRoutes,
193
194
  resolveMiddlewares: require('./src/routing/middleware-resolver').resolveMiddlewares,
195
+ translateValidationIssues: require('./src/api/validation-i18n').translateValidationIssues,
196
+ getTranslatorForRequest: require('./src/api/validation-i18n').getTranslatorForRequest,
194
197
 
195
198
  // Forms & Flash subsystem (AdonisJS & Django-inspired)
196
199
  createFlashMiddleware: require('./core/forms').createFlashMiddleware,
@@ -218,10 +221,62 @@ module.exports = {
218
221
  loadListeners: require('./core/events').loadListeners,
219
222
  };
220
223
 
221
- // Define lazy getters for plugins and heavy core subsystems
224
+ // Define lazy getters for plugins, ORM, and heavy core subsystems
222
225
  Object.defineProperties(module.exports, {
223
226
  ...Object.getOwnPropertyDescriptors(plugins),
224
227
 
228
+ // Built-in Services Creators (lazy loaded)
229
+ createAuthServices: { get: () => require('./src/services').createAuthServices, enumerable: true, configurable: true },
230
+ createMailServices: { get: () => require('./src/services').createMailServices, enumerable: true, configurable: true },
231
+ createMediaServices: { get: () => require('./src/services').createMediaServices, enumerable: true, configurable: true },
232
+ createFileManagerServices: { get: () => require('./src/services').createFileManagerServices, enumerable: true, configurable: true },
233
+ createSystemServices: { get: () => require('./src/services').createSystemServices, enumerable: true, configurable: true },
234
+ createExchangeServices: { get: () => require('./src/services').createExchangeServices, enumerable: true, configurable: true },
235
+
236
+ // ORM & Database Layer (lazy loaded)
237
+ orm: { get: () => getOrm(), enumerable: true, configurable: true },
238
+ createDatabase: { get: () => getOrm().createDatabase, enumerable: true, configurable: true },
239
+ createInMemoryDatabase: { get: () => getOrm().createInMemoryDatabase, enumerable: true, configurable: true },
240
+ defineModel: { get: () => getOrm().defineModel, enumerable: true, configurable: true },
241
+ zdb: { get: () => getOrm().zdb, enumerable: true, configurable: true },
242
+ BaseRepository: { get: () => getOrm().BaseRepository, enumerable: true, configurable: true },
243
+ Repository: { get: () => getOrm().Repository, enumerable: true, configurable: true },
244
+ createRepository: { get: () => getOrm().createRepository, enumerable: true, configurable: true },
245
+ getModel: { get: () => getOrm().getModel, enumerable: true, configurable: true },
246
+ getAllModels: { get: () => getOrm().getAllModels, enumerable: true, configurable: true },
247
+ hasModel: { get: () => getOrm().hasModel, enumerable: true, configurable: true },
248
+ clearRegistry: { get: () => getOrm().clearRegistry, enumerable: true, configurable: true },
249
+ isCascadeEnabled: { get: () => getOrm().isCascadeEnabled, enumerable: true, configurable: true },
250
+ extractColumnsFromSchema: { get: () => getOrm().extractColumnsFromSchema, enumerable: true, configurable: true },
251
+ getColumnMeta: { get: () => getOrm().getColumnMeta, enumerable: true, configurable: true },
252
+ generateNanoid: { get: () => getOrm().generateNanoid, enumerable: true, configurable: true },
253
+ zodNanoid: { get: () => getOrm().zodNanoid, enumerable: true, configurable: true },
254
+ extendZ: { get: () => getOrm().extendZ, enumerable: true, configurable: true },
255
+ omitHiddenColumns: { get: () => getOrm().omitHiddenColumns, enumerable: true, configurable: true },
256
+ sanitizeForOutput: { get: () => getOrm().sanitizeForOutput, enumerable: true, configurable: true },
257
+ ModelEvents: { get: () => getOrm().ModelEvents, enumerable: true, configurable: true },
258
+ Hooks: { get: () => getOrm().Hooks, enumerable: true, configurable: true },
259
+ HookCancellationError: { get: () => getOrm().HookCancellationError, enumerable: true, configurable: true },
260
+ createEventContext: { get: () => getOrm().createEventContext, enumerable: true, configurable: true },
261
+ getTransactionContext: { get: () => getOrm().getTransactionContext, enumerable: true, configurable: true },
262
+ createTransactionContext: { get: () => getOrm().createTransactionContext, enumerable: true, configurable: true },
263
+ runTransaction: { get: () => getOrm().runTransaction, enumerable: true, configurable: true },
264
+ createSchemaHelpers: { get: () => getOrm().createSchemaHelpers, enumerable: true, configurable: true },
265
+ loadRelations: { get: () => getOrm().loadRelations, enumerable: true, configurable: true },
266
+ loadRelation: { get: () => getOrm().loadRelation, enumerable: true, configurable: true },
267
+ parseRelationTree: { get: () => getOrm().parseRelationTree, enumerable: true, configurable: true },
268
+ QueryComplexityError: { get: () => getOrm().QueryComplexityError, enumerable: true, configurable: true },
269
+ validateQueryComplexity: { get: () => getOrm().validateQueryComplexity, enumerable: true, configurable: true },
270
+ resolveQueryLimits: { get: () => getOrm().resolveQueryLimits, enumerable: true, configurable: true },
271
+ DEFAULT_QUERY_LIMITS: { get: () => getOrm().DEFAULT_QUERY_LIMITS, enumerable: true, configurable: true },
272
+ buildSearchClause: { get: () => getOrm().buildSearchClause, enumerable: true, configurable: true },
273
+ tokenizeSearchTerm: { get: () => getOrm().tokenizeSearchTerm, enumerable: true, configurable: true },
274
+ detectDialect: { get: () => getOrm().detectDialect, enumerable: true, configurable: true },
275
+ defineFactory: { get: () => getOrm().defineFactory, enumerable: true, configurable: true },
276
+ FactoryBuilder: { get: () => getOrm().FactoryBuilder, enumerable: true, configurable: true },
277
+ diffSchema: { get: () => getOrm().diffSchema, enumerable: true, configurable: true },
278
+ makeMigrations: { get: () => getOrm().makeMigrations, enumerable: true, configurable: true },
279
+
225
280
  // Cascading Middleware & Layouts
226
281
  cascading: { get: () => require('./src/routing/cascading'), enumerable: true, configurable: true },
227
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.101",
3
+ "version": "0.0.102",
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",
@@ -47,21 +47,21 @@
47
47
  "test:services": "vitest run tests/**/service*",
48
48
  "test:plugins": "vitest run tests/**/plugin* tests/unit/*plugin*.test.js",
49
49
  "test:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template*",
50
- "test:branches": "vitest run tests/unit/*branches.test.js",
51
- "test:fast": "vitest run tests/unit/*branches.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js",
50
+ "test:branches": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js",
51
+ "test:fast": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js",
52
52
  "test:watch": "vitest",
53
53
  "test:coverage": "vitest run --coverage",
54
54
  "test:coverage:quick": "vitest run --coverage",
55
55
  "test:coverage:changed": "vitest run --coverage --changed",
56
56
  "test:coverage:full": "FULL_REPORT=true vitest run --coverage",
57
- "test:coverage:branches": "vitest run tests/unit/*branches.test.js --coverage",
57
+ "test:coverage:branches": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js --coverage",
58
58
  "test:coverage:unit": "vitest run tests/unit/ --coverage",
59
59
  "test:coverage:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template* --coverage",
60
60
  "test:coverage:auth": "vitest run tests/**/auth* tests/unit/server-auth-branches.test.js --coverage",
61
61
  "test:coverage:orm": "vitest run tests/**/orm* tests/**/database* --coverage",
62
62
  "test:coverage:services": "vitest run tests/**/service* --coverage",
63
63
  "test:coverage:plugins": "vitest run tests/**/plugin* tests/unit/*plugin*.test.js --coverage",
64
- "test:coverage:fast": "vitest run tests/unit/*branches.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js --coverage",
64
+ "test:coverage:fast": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js --coverage",
65
65
  "test:e2e": "playwright test",
66
66
  "test:e2e:ui": "playwright test --ui",
67
67
  "test:e2e:debug": "playwright test --debug",
@@ -55,8 +55,20 @@ function adminPanelPlugin(options = {}) {
55
55
  richTextSanitize = true,
56
56
  dataExchange: dataExchangeOption = true,
57
57
  loginRateLimit,
58
+ lazy: lazyOption,
58
59
  } = options;
59
60
 
61
+ if (lazyOption) {
62
+ const { lazyPlugin } = require('../lazy');
63
+ const lazyOpts = typeof lazyOption === 'object' ? lazyOption : {};
64
+ return lazyPlugin(() => adminPanelPlugin({ ...options, lazy: false }), {
65
+ name: 'admin-panel',
66
+ prefix: adminPath,
67
+ services: ['exchange.export', 'exchange.import'],
68
+ ...lazyOpts,
69
+ });
70
+ }
71
+
60
72
  const adminUsersConfig = typeof adminUsersOption === 'object' ? adminUsersOption : { enabled: adminUsersOption !== false };
61
73
 
62
74
  // Validate required options
@@ -199,7 +199,7 @@ function csrfPlugin(options = {}) {
199
199
 
200
200
  if (!submitted || !safeCompare(submitted, csrfTokenValue)) {
201
201
  res.status(errorStatus);
202
- if (req.accepts('html') && !req.path.startsWith('/api')) {
202
+ if (typeof req.accepts === 'function' && req.accepts('html') && !req.path.startsWith('/api')) {
203
203
  return res.send(`<h1>${errorStatus} Forbidden</h1><p>${errorMessage}</p>`);
204
204
  } else {
205
205
  return res.json({ error: errorMessage });
@@ -44,8 +44,21 @@ function fileManagerPlugin(options = {}) {
44
44
  permissions = null,
45
45
  security: securityOpts = { validateMagicBytes: true, sanitizeSvg: true },
46
46
  includePage = true,
47
+ lazy: lazyOption,
47
48
  } = options;
48
49
 
50
+ if (lazyOption) {
51
+ const { lazyPlugin } = require('../lazy');
52
+ const lazyOpts = typeof lazyOption === 'object' ? lazyOption : {};
53
+ return lazyPlugin(() => fileManagerPlugin({ ...options, lazy: false }), {
54
+ name: 'file-manager',
55
+ prefix: apiPrefix,
56
+ services: ['file-manager.list', 'file-manager.upload', 'file-manager.delete', 'file-manager.create-folder'],
57
+ dependencies: { 'admin-panel': '*' },
58
+ ...lazyOpts,
59
+ });
60
+ }
61
+
49
62
  const resolvedBaseDir = path.resolve(baseDir);
50
63
 
51
64
  const handlers = createFileManagerApiHandlers({
package/plugins/index.js CHANGED
@@ -61,6 +61,8 @@ module.exports = {
61
61
  get createApiKeyMiddleware() { return require('./api-key').createApiKeyMiddleware; },
62
62
  get webhooksPlugin() { return require('./webhooks').webhooksPlugin; },
63
63
  get WebhookDispatcher() { return require('./webhooks').WebhookDispatcher; },
64
+ get lazyPlugin() { return require('./lazy').lazyPlugin; },
65
+ get defineLazyPlugin() { return require('./lazy').defineLazyPlugin; },
64
66
  };
65
67
 
66
68