webspresso 0.0.104 → 0.0.106
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/AGENTS.md +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- package/src/template/precompile.js +8 -19
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - console Command
|
|
5
|
+
* Launches interactive REPL console with preloaded framework context
|
|
6
|
+
* @module bin/commands/console
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { startConsole, evaluateInConsole } = require('../../core/console');
|
|
10
|
+
|
|
11
|
+
function registerCommand(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('console')
|
|
14
|
+
.alias('c')
|
|
15
|
+
.description('Start interactive Webspresso REPL console with preloaded db, models, and services')
|
|
16
|
+
.option('-e, --eval <code-string>', 'Evaluate JavaScript code and print result, then exit')
|
|
17
|
+
.option('-E, --env <environment>', 'Environment name (default: development)')
|
|
18
|
+
.option('-c, --config <path>', 'Path to database config file (optional)')
|
|
19
|
+
.option('--no-history', 'Disable command history persistence')
|
|
20
|
+
.option('--no-banner', 'Do not display startup banner')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
try {
|
|
23
|
+
if (options.eval) {
|
|
24
|
+
const { output, error } = await evaluateInConsole(options.eval, {
|
|
25
|
+
configPath: options.config,
|
|
26
|
+
env: options.env,
|
|
27
|
+
});
|
|
28
|
+
if (error) {
|
|
29
|
+
console.error(error.stack || error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (output !== undefined && output !== 'undefined') {
|
|
33
|
+
console.log(output);
|
|
34
|
+
}
|
|
35
|
+
process.exit(0);
|
|
36
|
+
} else {
|
|
37
|
+
await startConsole({
|
|
38
|
+
configPath: options.config,
|
|
39
|
+
env: options.env,
|
|
40
|
+
history: options.history,
|
|
41
|
+
banner: options.banner,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('❌ Failed to start Webspresso console:', err.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
registerCommand,
|
|
53
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
7
|
+
const { runCloudMigration, freeStaleMigrationLock, getMigrationStatus } = require('../../core/orm/migrate-cloud');
|
|
7
8
|
|
|
8
9
|
function registerCommand(program) {
|
|
9
10
|
program
|
|
@@ -11,15 +12,59 @@ function registerCommand(program) {
|
|
|
11
12
|
.description('Run pending database migrations')
|
|
12
13
|
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
13
14
|
.option('-c, --config <path>', 'Path to database config file')
|
|
15
|
+
.option('--safe', 'Run in cloud-safe mode with connectivity retries and stale lock auto-healing')
|
|
16
|
+
.option('--dry-run', 'Preview pending migrations without applying them')
|
|
17
|
+
.option('--unlock-if-stale', 'Automatically release stale migration locks from aborted jobs')
|
|
18
|
+
.option('--force-unlock', 'Force release migration table lock immediately')
|
|
19
|
+
.option('--retries <n>', 'Database connection retry attempts', (v) => parseInt(v, 10), 5)
|
|
14
20
|
.action(async (options) => {
|
|
15
21
|
const { config, path: configPath } = loadDbConfig(options.config);
|
|
16
22
|
console.log(`\n📦 Using config: ${configPath}`);
|
|
17
23
|
console.log(` Environment: ${options.env}\n`);
|
|
18
24
|
|
|
19
25
|
const knex = await createDbInstance(config, options.env);
|
|
20
|
-
|
|
26
|
+
const migrationConfig = config.migrations || {};
|
|
27
|
+
|
|
21
28
|
try {
|
|
22
|
-
|
|
29
|
+
if (options.forceUnlock) {
|
|
30
|
+
console.log('🔓 Releasing migration lock table...');
|
|
31
|
+
await freeStaleMigrationLock(knex, migrationConfig);
|
|
32
|
+
console.log('✅ Migration lock freed.\n');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.safe || options.dryRun || options.unlockIfStale) {
|
|
37
|
+
const result = await runCloudMigration(knex, {
|
|
38
|
+
migrationConfig,
|
|
39
|
+
dryRun: Boolean(options.dryRun),
|
|
40
|
+
unlockIfStale: Boolean(options.unlockIfStale || options.safe),
|
|
41
|
+
waitForDb: Boolean(options.safe),
|
|
42
|
+
retries: options.retries || 5,
|
|
43
|
+
logger: (msg) => console.log(` → ${msg}`),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (result.dryRun) {
|
|
47
|
+
if (result.pendingBefore.length === 0) {
|
|
48
|
+
console.log('✅ Dry-run: Database is up to date. No pending migrations.\n');
|
|
49
|
+
} else {
|
|
50
|
+
console.log(`\n🔍 Dry-run: ${result.pendingBefore.length} migration(s) pending:`);
|
|
51
|
+
for (const m of result.pendingBefore) {
|
|
52
|
+
console.log(` ○ ${m}`);
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (result.applied.length === 0) {
|
|
60
|
+
console.log('✅ Already up to date.\n');
|
|
61
|
+
} else {
|
|
62
|
+
console.log(`\n✅ Done. ${result.applied.length} migration(s) completed in ${result.durationMs}ms.\n`);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Standard knex migration run
|
|
23
68
|
const [batch, migrations] = await knex.migrate.latest(migrationConfig);
|
|
24
69
|
|
|
25
70
|
if (migrations.length === 0) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI DB Studio Command
|
|
5
|
+
* Launches visual database browser and SQL console
|
|
6
|
+
* @module bin/commands/db-studio
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
12
|
+
const { startStudio } = require('../../core/studio');
|
|
13
|
+
|
|
14
|
+
function registerCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('db:studio')
|
|
17
|
+
.alias('studio')
|
|
18
|
+
.description('Open interactive visual database browser and SQL runner in your browser')
|
|
19
|
+
.option('-p, --port <port>', 'Port to listen on', '4567')
|
|
20
|
+
.option('-H, --host <host>', 'Host to bind to', '127.0.0.1')
|
|
21
|
+
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
22
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
23
|
+
.option('--open', 'Automatically open browser', true)
|
|
24
|
+
.option('--no-open', 'Do not open browser')
|
|
25
|
+
.action(async (options = {}) => {
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
28
|
+
if (!fs.existsSync(pkgPath)) {
|
|
29
|
+
console.error('❌ Not a Webspresso project! Run this command in your project directory.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let dbConfig;
|
|
34
|
+
let configPath;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const loaded = loadDbConfig(options.config);
|
|
38
|
+
dbConfig = loaded.config;
|
|
39
|
+
configPath = loaded.path;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('❌ Database configuration could not be loaded:', err.message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(`\n📦 Using config: ${path.relative(cwd, configPath)} (${options.env})`);
|
|
46
|
+
const knex = await createDbInstance(dbConfig, options.env);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const port = parseInt(options.port, 10) || 4567;
|
|
50
|
+
const host = options.host || '127.0.0.1';
|
|
51
|
+
const open = options.open !== false;
|
|
52
|
+
|
|
53
|
+
await startStudio({
|
|
54
|
+
knex,
|
|
55
|
+
port,
|
|
56
|
+
host,
|
|
57
|
+
open,
|
|
58
|
+
silent: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Keep process open until interrupted
|
|
62
|
+
const cleanup = async () => {
|
|
63
|
+
console.log('\n🛑 Stopping DB Studio...');
|
|
64
|
+
try {
|
|
65
|
+
await knex.destroy();
|
|
66
|
+
} catch (_) {}
|
|
67
|
+
process.exit(0);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
process.on('SIGINT', cleanup);
|
|
71
|
+
process.on('SIGTERM', cleanup);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error('❌ Failed to start DB Studio:', err.message);
|
|
74
|
+
try {
|
|
75
|
+
await knex.destroy();
|
|
76
|
+
} catch (_) {}
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
registerCommand,
|
|
84
|
+
};
|
|
@@ -10,6 +10,26 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
|
|
12
12
|
|
|
13
|
+
function resolveProjectDbConfig(dir, targetEnv) {
|
|
14
|
+
for (const filename of ['webspresso.db.js', 'knexfile.js']) {
|
|
15
|
+
const full = path.resolve(dir || process.cwd(), filename);
|
|
16
|
+
if (fs.existsSync(full)) {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.statSync(full).isFile()) {
|
|
19
|
+
const loaded = require(full);
|
|
20
|
+
const resolved = (loaded && typeof loaded === 'object') ? (loaded[targetEnv] || loaded) : null;
|
|
21
|
+
if (resolved && typeof resolved === 'object' && resolved.client) {
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// ignore
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
/**
|
|
14
34
|
* Known weak secret patterns that should never be used in production
|
|
15
35
|
*/
|
|
@@ -65,10 +85,14 @@ async function runDeployCheck(options = {}) {
|
|
|
65
85
|
const successes = [];
|
|
66
86
|
const warnings = [];
|
|
67
87
|
const errors = [];
|
|
88
|
+
const remediations = [];
|
|
68
89
|
|
|
69
|
-
const addPass = (check, message) => successes.push({ check, message });
|
|
70
|
-
const addWarn = (check, message) => warnings.push({ check, message });
|
|
71
|
-
const addError = (check, message) => errors.push({ check, message });
|
|
90
|
+
const addPass = (check, message) => successes.push({ check, title: check, message });
|
|
91
|
+
const addWarn = (check, message) => warnings.push({ check, title: check, message });
|
|
92
|
+
const addError = (check, message) => errors.push({ check, title: check, message });
|
|
93
|
+
const addRemediation = (action) => {
|
|
94
|
+
if (!remediations.includes(action)) remediations.push(action);
|
|
95
|
+
};
|
|
72
96
|
|
|
73
97
|
// Load environment variables from process.env and local .env files
|
|
74
98
|
const localEnv = {
|
|
@@ -77,6 +101,25 @@ async function runDeployCheck(options = {}) {
|
|
|
77
101
|
...process.env,
|
|
78
102
|
};
|
|
79
103
|
|
|
104
|
+
// ----------------------------------------------------
|
|
105
|
+
// Target Platform Detection
|
|
106
|
+
// ----------------------------------------------------
|
|
107
|
+
let targetPlatform = (options.platform || 'auto').toLowerCase();
|
|
108
|
+
const isPlatformAuto = targetPlatform === 'auto';
|
|
109
|
+
if (isPlatformAuto) {
|
|
110
|
+
if (localEnv.K_SERVICE || fs.existsSync(path.join(cwd, 'cloudbuild.yaml')) || fs.existsSync(path.join(cwd, '.gcloudignore'))) {
|
|
111
|
+
targetPlatform = 'cloudrun';
|
|
112
|
+
} else if (localEnv.AWS_EXECUTION_ENV || localEnv.AWS_LAMBDA_FUNCTION_NAME || fs.existsSync(path.join(cwd, 'apprunner.yaml')) || fs.existsSync(path.join(cwd, 'ecs-task-definition.json'))) {
|
|
113
|
+
targetPlatform = 'aws';
|
|
114
|
+
} else if (fs.existsSync(path.join(cwd, 'wrangler.toml'))) {
|
|
115
|
+
targetPlatform = 'cloudflare';
|
|
116
|
+
} else if (fs.existsSync(path.join(cwd, 'Dockerfile'))) {
|
|
117
|
+
targetPlatform = 'docker';
|
|
118
|
+
} else {
|
|
119
|
+
targetPlatform = 'generic';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
80
123
|
// ----------------------------------------------------
|
|
81
124
|
// 1. Project & Server Basics
|
|
82
125
|
// ----------------------------------------------------
|
|
@@ -194,22 +237,63 @@ async function runDeployCheck(options = {}) {
|
|
|
194
237
|
const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith('.js') || f.endsWith('.ts'));
|
|
195
238
|
addPass('Migrations', `Found ${files.length} migration file(s) in migrations/`);
|
|
196
239
|
|
|
197
|
-
// Try verifying migration status if db config exists
|
|
198
|
-
const dbConfig =
|
|
240
|
+
// Try verifying migration status and connection pool if db config exists
|
|
241
|
+
const dbConfig = resolveProjectDbConfig(cwd, targetEnv);
|
|
199
242
|
if (dbConfig) {
|
|
243
|
+
// Check connection pool for serverless/container scale-out
|
|
244
|
+
const isSqlite = dbConfig.client === 'better-sqlite3' || dbConfig.client === 'sqlite3';
|
|
245
|
+
if (!isSqlite && (dbConfig.client === 'pg' || dbConfig.client === 'mysql2' || dbConfig.client === 'mysql')) {
|
|
246
|
+
const pool = dbConfig.pool || {};
|
|
247
|
+
if (pool.max > 10) {
|
|
248
|
+
addWarn('DB Connection Pool', `DB pool max is ${pool.max}. Large pools per container can exhaust database connections when autoscaling. Consider max <= 5 or an external pooler.`);
|
|
249
|
+
} else if (pool.serverless || (pool.min === 0 && (pool.max || 5) <= 5)) {
|
|
250
|
+
addPass('DB Connection Pool', `Database connection pool configured for serverless scaling (min: ${pool.min ?? 0}, max: ${pool.max ?? 5})`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if ((localEnv.K_SERVICE || localEnv.AWS_EXECUTION_ENV || pool.serverless) && pool.min > 0) {
|
|
254
|
+
addWarn('DB Connection Pool', `DB pool min is ${pool.min}. For serverless scale-to-zero, min: 0 is recommended so idle containers release connections.`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
200
258
|
try {
|
|
201
|
-
const db = createDbInstance(dbConfig);
|
|
202
|
-
const
|
|
203
|
-
if (status
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
259
|
+
const db = createDbInstance(dbConfig, targetEnv);
|
|
260
|
+
const knexInstance = db.knex || db;
|
|
261
|
+
if (knexInstance?.migrate?.status) {
|
|
262
|
+
const status = await knexInstance.migrate.status();
|
|
263
|
+
if (status > 0) {
|
|
264
|
+
addWarn('Pending Migrations', `There are ${status} pending database migration(s) to run (run: npx webspresso db:migrate)`);
|
|
265
|
+
} else if (status === 0) {
|
|
266
|
+
addPass('Pending Migrations', 'All database migrations are applied up to date');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (knexInstance && typeof knexInstance.destroy === 'function') {
|
|
270
|
+
await knexInstance.destroy();
|
|
207
271
|
}
|
|
208
|
-
await db.knex.destroy();
|
|
209
272
|
} catch (err) {
|
|
210
273
|
addWarn('Database Connectivity', `Could not check migration status: ${err.message}`);
|
|
211
274
|
}
|
|
212
275
|
}
|
|
276
|
+
|
|
277
|
+
// Check cloud migration job setup for Cloud Run or AWS
|
|
278
|
+
if (targetPlatform === 'cloudrun') {
|
|
279
|
+
const hasJob = fs.existsSync(path.join(cwd, 'cloudrun-job.yaml')) ||
|
|
280
|
+
fs.existsSync(path.join(cwd, 'deploy-cloudrun-migration.sh')) ||
|
|
281
|
+
(fs.existsSync(path.join(cwd, 'cloudbuild.yaml')) && fs.readFileSync(path.join(cwd, 'cloudbuild.yaml'), 'utf8').includes('run jobs'));
|
|
282
|
+
if (hasJob) {
|
|
283
|
+
addPass('Cloud Migration Strategy', 'Cloud Run migration job configuration found');
|
|
284
|
+
} else {
|
|
285
|
+
addWarn('Cloud Migration Strategy', 'Running migrations inside scaled web containers can cause race conditions. Scaffold an isolated Cloud Run Job: npx webspresso add:migration-job --platform cloudrun');
|
|
286
|
+
}
|
|
287
|
+
} else if (targetPlatform === 'aws') {
|
|
288
|
+
const hasEcsTask = fs.existsSync(path.join(cwd, 'ecs-migration-task.json')) ||
|
|
289
|
+
fs.existsSync(path.join(cwd, 'deploy-aws-migration.sh')) ||
|
|
290
|
+
(fs.existsSync(path.join(cwd, 'apprunner.yaml')) && fs.readFileSync(path.join(cwd, 'apprunner.yaml'), 'utf8').includes('db:migrate'));
|
|
291
|
+
if (hasEcsTask) {
|
|
292
|
+
addPass('Cloud Migration Strategy', 'AWS ECS Task or App Runner pre-deploy migration configuration found');
|
|
293
|
+
} else {
|
|
294
|
+
addWarn('Cloud Migration Strategy', 'Recommend running migrations as an isolated ECS Task or App Runner pre-run: npx webspresso add:migration-job --platform aws');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
213
297
|
} else {
|
|
214
298
|
addPass('Migrations', 'No migrations directory (skipping migration status check)');
|
|
215
299
|
}
|
|
@@ -249,13 +333,146 @@ async function runDeployCheck(options = {}) {
|
|
|
249
333
|
}
|
|
250
334
|
}
|
|
251
335
|
|
|
336
|
+
// ----------------------------------------------------
|
|
337
|
+
// 8. Stateless Scale-Out & Session Store
|
|
338
|
+
// ----------------------------------------------------
|
|
339
|
+
const hasRedis = Boolean(localEnv.REDIS_URL || localEnv.SESSION_STORE === 'redis');
|
|
340
|
+
if (hasRedis) {
|
|
341
|
+
addPass('Distributed Session Store', `Distributed session store configured for scale-out (${localEnv.SESSION_STORE || 'Redis'})`);
|
|
342
|
+
} else if (targetEnv === 'production' && serverCode && (serverCode.includes('auth') || serverCode.includes('session'))) {
|
|
343
|
+
addWarn('Distributed Session Store', 'In-memory session store detected in production. For stateless scale-out (Cloud Run, AWS ECS/Fargate, Kubernetes), configure a distributed store via REDIS_URL=redis://... or SESSION_STORE=redis.');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ----------------------------------------------------
|
|
347
|
+
// 9. Platform-Specific Cloud Preflight
|
|
348
|
+
// ----------------------------------------------------
|
|
349
|
+
const dbConfig = resolveProjectDbConfig(cwd, targetEnv);
|
|
350
|
+
const isSqlite = !dbConfig || dbConfig.client === 'better-sqlite3' || dbConfig.client === 'sqlite3';
|
|
351
|
+
|
|
352
|
+
// 9.1 Ephemeral Storage (Database & Uploads)
|
|
353
|
+
if (isSqlite && (targetPlatform === 'cloudrun' || targetPlatform === 'aws')) {
|
|
354
|
+
const pName = targetPlatform === 'cloudrun' ? 'Google Cloud Run' : 'AWS (App Runner / ECS)';
|
|
355
|
+
const dbRec = targetPlatform === 'cloudrun' ? 'Google Cloud SQL (PostgreSQL/MySQL)' : 'AWS RDS / Aurora';
|
|
356
|
+
addWarn(
|
|
357
|
+
'Ephemeral Storage (Database)',
|
|
358
|
+
`${pName} uses an ephemeral container filesystem. Local SQLite databases will be lost on scale-down or redeploy. Migrate to ${dbRec}.`
|
|
359
|
+
);
|
|
360
|
+
addRemediation(`Configure managed database: Use ${dbRec} via webspresso.db.js or DATABASE_URL`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (serverCode && (serverCode.includes("storage: 'local'") || serverCode.includes('uploadPlugin({') || serverCode.includes('uploadPlugin()'))) {
|
|
364
|
+
const hasCloudStorage = serverCode.includes("storage: 'gcs'") || serverCode.includes("storage: 's3'") || serverCode.includes("storage: 'cloudflare'");
|
|
365
|
+
if (!hasCloudStorage && (targetPlatform === 'cloudrun' || targetPlatform === 'aws')) {
|
|
366
|
+
const pStorage = targetPlatform === 'cloudrun' ? 'Google Cloud Storage (GCS)' : 'AWS S3';
|
|
367
|
+
addWarn(
|
|
368
|
+
'Ephemeral Storage (Uploads)',
|
|
369
|
+
`Local disk file upload storage detected. Container restarts on ${targetPlatform === 'cloudrun' ? 'Cloud Run' : 'AWS'} will cause uploaded files to be lost. Configure ${pStorage}.`
|
|
370
|
+
);
|
|
371
|
+
addRemediation(`Configure ${pStorage}: uploadPlugin({ storage: '${targetPlatform === 'cloudrun' ? 'gcs' : 's3'}' })`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 9.2 Shutdown Timeout Alignment
|
|
376
|
+
if (targetPlatform === 'cloudrun') {
|
|
377
|
+
if (serverCode) {
|
|
378
|
+
const match = serverCode.match(/timeout:\s*(\d+)/);
|
|
379
|
+
if (match) {
|
|
380
|
+
const timeoutMs = parseInt(match[1], 10);
|
|
381
|
+
if (timeoutMs > 10000) {
|
|
382
|
+
addWarn('Shutdown Timeout Alignment', `Webspresso shutdown timeout (${timeoutMs}ms) exceeds Cloud Run default SIGTERM deadline (10000ms). Container may be forcibly SIGKILLed before draining.`);
|
|
383
|
+
addRemediation('Set shutdown timeout <= 10000ms in createApp({ server: { shutdown: { timeout: 9000 } } })');
|
|
384
|
+
} else {
|
|
385
|
+
addPass('Shutdown Timeout Alignment', `Shutdown timeout (${timeoutMs}ms) matches Cloud Run 10s SIGTERM deadline`);
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
addPass('Shutdown Timeout Alignment', 'Default shutdown timeout (10s) compatible with Cloud Run SIGTERM deadline');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} else if (targetPlatform === 'aws') {
|
|
392
|
+
addPass('Shutdown Timeout Alignment', 'Default shutdown timeout (10s) well within AWS ECS 30s stopTimeout');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 9.3 Platform Artifacts & Secrets Inspection
|
|
396
|
+
if (targetPlatform === 'cloudrun') {
|
|
397
|
+
const gcloudignorePath = path.join(cwd, '.gcloudignore');
|
|
398
|
+
if (fs.existsSync(gcloudignorePath)) {
|
|
399
|
+
const gignore = fs.readFileSync(gcloudignorePath, 'utf8');
|
|
400
|
+
if (!gignore.includes('.env')) {
|
|
401
|
+
addError('Google Cloud Ignore', '.env is NOT excluded in .gcloudignore! Secrets may leak into Google Cloud Build.');
|
|
402
|
+
addRemediation('Add .env to .gcloudignore');
|
|
403
|
+
} else {
|
|
404
|
+
addPass('Google Cloud Ignore', '.gcloudignore present and properly excludes .env');
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
addWarn('Google Cloud Ignore', '.gcloudignore not found (recommend running: npx webspresso add:cloudrun)');
|
|
408
|
+
addRemediation('Generate Cloud Run configuration: npx webspresso add:cloudrun');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const cloudbuildPath = path.join(cwd, 'cloudbuild.yaml');
|
|
412
|
+
if (fs.existsSync(cloudbuildPath)) {
|
|
413
|
+
addPass('Cloud Build Pipeline', 'cloudbuild.yaml CI/CD pipeline found');
|
|
414
|
+
} else {
|
|
415
|
+
addWarn('Cloud Build Pipeline', 'cloudbuild.yaml not found (run: npx webspresso add:cloudrun)');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (serverCode) {
|
|
419
|
+
if (serverCode.includes('process.env.PORT') || serverCode.includes('PORT')) {
|
|
420
|
+
addPass('Cloud Run PORT Binding', 'Binds to dynamic process.env.PORT (required for Cloud Run 8080)');
|
|
421
|
+
} else {
|
|
422
|
+
addWarn('Cloud Run PORT Binding', 'Ensure server listens on process.env.PORT (Cloud Run assigns port 8080)');
|
|
423
|
+
addRemediation('Listen on process.env.PORT: app.listen(process.env.PORT || 3000)');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} else if (targetPlatform === 'aws') {
|
|
427
|
+
const apprunnerPath = path.join(cwd, 'apprunner.yaml');
|
|
428
|
+
const ecsTaskPath = path.join(cwd, 'ecs-task-definition.json');
|
|
429
|
+
|
|
430
|
+
if (fs.existsSync(apprunnerPath)) {
|
|
431
|
+
addPass('AWS App Runner Config', 'apprunner.yaml found');
|
|
432
|
+
}
|
|
433
|
+
if (fs.existsSync(ecsTaskPath)) {
|
|
434
|
+
try {
|
|
435
|
+
const ecsTask = JSON.parse(fs.readFileSync(ecsTaskPath, 'utf8'));
|
|
436
|
+
if (ecsTask.containerDefinitions?.[0]?.logConfiguration?.logDriver === 'awslogs') {
|
|
437
|
+
addPass('AWS CloudWatch Logs', 'CloudWatch logDriver (awslogs) configured in ECS task definition');
|
|
438
|
+
}
|
|
439
|
+
addPass('AWS ECS Task Definition', 'ecs-task-definition.json found and valid JSON');
|
|
440
|
+
} catch (err) {
|
|
441
|
+
addError('AWS ECS Task Definition', `ecs-task-definition.json is invalid JSON: ${err.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!fs.existsSync(apprunnerPath) && !fs.existsSync(ecsTaskPath)) {
|
|
446
|
+
addWarn('AWS Configuration', 'Neither apprunner.yaml nor ecs-task-definition.json found');
|
|
447
|
+
addRemediation('Generate AWS configuration: npx webspresso add:aws');
|
|
448
|
+
}
|
|
449
|
+
} else if (targetPlatform === 'cloudflare') {
|
|
450
|
+
const wranglerPath = path.join(cwd, 'wrangler.toml');
|
|
451
|
+
if (fs.existsSync(wranglerPath)) {
|
|
452
|
+
const wrangler = fs.readFileSync(wranglerPath, 'utf8');
|
|
453
|
+
if (wrangler.includes('nodejs_compat')) {
|
|
454
|
+
addPass('Cloudflare Worker Config', 'wrangler.toml found with nodejs_compat flag');
|
|
455
|
+
} else {
|
|
456
|
+
addWarn('Cloudflare Worker Config', 'wrangler.toml missing compatibility_flags = ["nodejs_compat"]');
|
|
457
|
+
addRemediation('Add compatibility_flags = ["nodejs_compat"] to wrangler.toml');
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
addWarn('Cloudflare Worker Config', 'wrangler.toml not found (run: npx webspresso add:cloudflare)');
|
|
461
|
+
addRemediation('Generate Cloudflare Worker files: npx webspresso add:cloudflare');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
252
465
|
const passed = errors.length === 0;
|
|
253
466
|
|
|
254
467
|
return {
|
|
255
468
|
passed,
|
|
469
|
+
platform: targetPlatform,
|
|
470
|
+
isPlatformAuto,
|
|
256
471
|
successes,
|
|
472
|
+
passes: successes,
|
|
257
473
|
warnings,
|
|
258
474
|
errors,
|
|
475
|
+
remediations,
|
|
259
476
|
};
|
|
260
477
|
}
|
|
261
478
|
|
|
@@ -269,6 +486,7 @@ function registerCommand(program) {
|
|
|
269
486
|
.alias('check:deploy')
|
|
270
487
|
.description('Run production preflight check (secrets, container hardening, database, and optimizations)')
|
|
271
488
|
.option('-e, --env <environment>', 'Target environment to evaluate (default: production)', 'production')
|
|
489
|
+
.option('-p, --platform <platform>', 'Target cloud platform (cloudrun, aws, cloudflare, docker, or auto)', 'auto')
|
|
272
490
|
.option('--strict', 'Fail with exit code 1 if any warnings are detected', false)
|
|
273
491
|
.option('--json', 'Output preflight results as JSON', false)
|
|
274
492
|
.action(async (...actionArgs) => {
|
|
@@ -280,6 +498,7 @@ function registerCommand(program) {
|
|
|
280
498
|
const result = await runDeployCheck({
|
|
281
499
|
cwd: process.cwd(),
|
|
282
500
|
env: options.env || 'production',
|
|
501
|
+
platform: options.platform || 'auto',
|
|
283
502
|
});
|
|
284
503
|
|
|
285
504
|
if (options.json) {
|
|
@@ -291,7 +510,9 @@ function registerCommand(program) {
|
|
|
291
510
|
}
|
|
292
511
|
|
|
293
512
|
console.log('\n🚀 Webspresso Production Deployment Preflight');
|
|
294
|
-
console.log('
|
|
513
|
+
console.log('=============================================');
|
|
514
|
+
console.log(`Target Platform : ${result.platform.toUpperCase()} ${result.isPlatformAuto ? '(auto-detected)' : ''}`);
|
|
515
|
+
console.log(`Environment : ${options.env || 'production'}\n`);
|
|
295
516
|
|
|
296
517
|
if (result.successes.length > 0) {
|
|
297
518
|
console.log('Passed Checks:');
|
|
@@ -317,6 +538,14 @@ function registerCommand(program) {
|
|
|
317
538
|
console.log('');
|
|
318
539
|
}
|
|
319
540
|
|
|
541
|
+
if (result.remediations && result.remediations.length > 0) {
|
|
542
|
+
console.log('Recommended Actions:');
|
|
543
|
+
for (const rem of result.remediations) {
|
|
544
|
+
console.log(` 👉 ${rem}`);
|
|
545
|
+
}
|
|
546
|
+
console.log('');
|
|
547
|
+
}
|
|
548
|
+
|
|
320
549
|
console.log('---------------------------------------------');
|
|
321
550
|
console.log(`Summary: ${result.successes.length} passed, ${result.warnings.length} warning(s), ${result.errors.length} error(s).\n`);
|
|
322
551
|
|
package/bin/commands/dev.js
CHANGED
|
@@ -31,7 +31,7 @@ function registerCommand(program) {
|
|
|
31
31
|
program
|
|
32
32
|
.command('dev')
|
|
33
33
|
.description('Start development server')
|
|
34
|
-
.option('-p, --port <port>', 'Port number', '
|
|
34
|
+
.option('-p, --port <port>', 'Port number', '7331')
|
|
35
35
|
.option('--no-css', 'Skip CSS watch (if Tailwind is set up)')
|
|
36
36
|
.option('--no-js', 'Skip client JS watch (if esbuild / watch:js is set up)')
|
|
37
37
|
.action((options) => {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI Export Command
|
|
5
|
+
* Prerender pages into static HTML files for static hosting
|
|
6
|
+
* @module bin/commands/export
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { exportStaticSite } = require('../../core/export');
|
|
12
|
+
|
|
13
|
+
function registerCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('export')
|
|
16
|
+
.description('Export and prerender application pages into static HTML files for Cloudflare Pages, S3, or Vercel')
|
|
17
|
+
.option('-o, --out <dir>', 'Output directory for exported files', 'dist')
|
|
18
|
+
.option('-b, --base-url <url>', 'Base URL for canonical links and sitemap.xml')
|
|
19
|
+
.option('-c, --clean', 'Clean output directory before export', true)
|
|
20
|
+
.option('--no-clean', 'Do not clean output directory before export')
|
|
21
|
+
.option('--no-assets', 'Skip copying public static assets')
|
|
22
|
+
.option('--no-clean-urls', 'Generate "path.html" instead of "path/index.html"')
|
|
23
|
+
.option('--sitemap', 'Generate sitemap.xml for prerendered routes', false)
|
|
24
|
+
.option('--concurrency <n>', 'Number of concurrent render requests', '5')
|
|
25
|
+
.option('--silent', 'Suppress log output', false)
|
|
26
|
+
.action(async (options = {}) => {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
29
|
+
if (!fs.existsSync(pkgPath)) {
|
|
30
|
+
console.error('❌ Not a Webspresso project! Run this command in your project directory.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const outDir = options.out || 'dist';
|
|
35
|
+
const clean = options.clean !== false;
|
|
36
|
+
const cleanUrls = options.cleanUrls !== false;
|
|
37
|
+
const copyAssets = options.assets !== false;
|
|
38
|
+
const sitemap = options.sitemap || Boolean(options.baseUrl);
|
|
39
|
+
const baseUrl = options.baseUrl;
|
|
40
|
+
const concurrency = parseInt(options.concurrency, 10) || 5;
|
|
41
|
+
const silent = options.silent === true;
|
|
42
|
+
|
|
43
|
+
// Check if project has a custom server.js
|
|
44
|
+
let serverConfig = {};
|
|
45
|
+
const serverJsPath = path.join(cwd, 'server.js');
|
|
46
|
+
if (fs.existsSync(serverJsPath)) {
|
|
47
|
+
try {
|
|
48
|
+
// If server.js exports a function or config object
|
|
49
|
+
const srv = require(serverJsPath);
|
|
50
|
+
if (srv && typeof srv === 'object') {
|
|
51
|
+
serverConfig = srv;
|
|
52
|
+
}
|
|
53
|
+
} catch (_) {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await exportStaticSite({
|
|
58
|
+
cwd,
|
|
59
|
+
outDir,
|
|
60
|
+
clean,
|
|
61
|
+
cleanUrls,
|
|
62
|
+
copyAssets,
|
|
63
|
+
sitemap,
|
|
64
|
+
baseUrl,
|
|
65
|
+
concurrency,
|
|
66
|
+
silent,
|
|
67
|
+
serverConfig,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (result.errors && result.errors.length > 0) {
|
|
71
|
+
console.error(`⚠️ Static export completed with ${result.errors.length} error(s).`);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
} else {
|
|
74
|
+
if (!silent) {
|
|
75
|
+
console.log(`✅ Static export succeeded: ${result.rendered} page(s) ready in "${outDir}".`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error('❌ Static export failed:', err.message);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
registerCommand,
|
|
87
|
+
};
|
package/bin/commands/new.js
CHANGED
|
@@ -312,10 +312,10 @@ module.exports = { loadEnv };
|
|
|
312
312
|
|
|
313
313
|
const envSchema = z.object({
|
|
314
314
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
315
|
-
PORT: z.coerce.number().int().positive().default(
|
|
315
|
+
PORT: z.coerce.number().int().positive().default(7331),
|
|
316
316
|
DEFAULT_LOCALE: z.string().min(1).default('en'),
|
|
317
317
|
SUPPORTED_LOCALES: z.string().min(1).default('en,de'),
|
|
318
|
-
BASE_URL: z.string().url().default('http://localhost:
|
|
318
|
+
BASE_URL: z.string().url().default('http://localhost:7331'),
|
|
319
319
|
DATABASE_URL: z.string().optional(),
|
|
320
320
|
});
|
|
321
321
|
|
|
@@ -382,11 +382,11 @@ app.listen(env.PORT, () => {
|
|
|
382
382
|
|
|
383
383
|
// Create .env.example (see config/load-env.js for merge order)
|
|
384
384
|
let envExample = `# Copy to .env and adjust. Optional overrides: .env.local, .env.<NODE_ENV>, .env.<NODE_ENV>.local
|
|
385
|
-
PORT=
|
|
385
|
+
PORT=7331
|
|
386
386
|
NODE_ENV=development
|
|
387
387
|
DEFAULT_LOCALE=en
|
|
388
388
|
SUPPORTED_LOCALES=en,de
|
|
389
|
-
BASE_URL=http://localhost:
|
|
389
|
+
BASE_URL=http://localhost:7331
|
|
390
390
|
`;
|
|
391
391
|
|
|
392
392
|
if (useDatabase && databaseType) {
|
|
@@ -645,7 +645,7 @@ cp .env.example .env
|
|
|
645
645
|
npm run dev
|
|
646
646
|
\`\`\`
|
|
647
647
|
|
|
648
|
-
Visit http://localhost:
|
|
648
|
+
Visit http://localhost:7331
|
|
649
649
|
|
|
650
650
|
## Configuration
|
|
651
651
|
|