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
package/core/utils/crypto.js
CHANGED
|
@@ -67,7 +67,125 @@ function safeCompare(a, b) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Compute SHA-256 hash of a string, Buffer, or value.
|
|
72
|
+
*
|
|
73
|
+
* Zero-dependency wrapper around native `crypto.createHash('sha256')`.
|
|
74
|
+
* Safely coerces non-Buffer/non-string values to strings (or empty string for null/undefined).
|
|
75
|
+
*
|
|
76
|
+
* @param {string|Buffer|any} data - Data to hash
|
|
77
|
+
* @param {import('crypto').BinaryToTextEncoding|'buffer'} [encoding='hex'] - Output encoding ('hex', 'base64', 'base64url', or 'buffer')
|
|
78
|
+
* @returns {string|Buffer} Digest in specified encoding, or Buffer if encoding is 'buffer'
|
|
79
|
+
*/
|
|
80
|
+
function sha256(data, encoding = 'hex') {
|
|
81
|
+
if (data === null || data === undefined) {
|
|
82
|
+
data = '';
|
|
83
|
+
}
|
|
84
|
+
const input = Buffer.isBuffer(data) || typeof data === 'string' ? data : String(data);
|
|
85
|
+
const hash = crypto.createHash('sha256').update(input);
|
|
86
|
+
return encoding === 'buffer' ? hash.digest() : hash.digest(encoding || 'hex');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compute HMAC-SHA256 signature of data using a secret key.
|
|
91
|
+
*
|
|
92
|
+
* Zero-dependency wrapper around native `crypto.createHmac('sha256', secret)`.
|
|
93
|
+
*
|
|
94
|
+
* @param {string|Buffer|any} data - Data to sign
|
|
95
|
+
* @param {string|Buffer} secret - Secret key
|
|
96
|
+
* @param {import('crypto').BinaryToTextEncoding|'buffer'} [encoding='hex'] - Output encoding ('hex', 'base64', 'base64url', or 'buffer')
|
|
97
|
+
* @returns {string|Buffer} Signature in specified encoding, or Buffer if encoding is 'buffer'
|
|
98
|
+
*/
|
|
99
|
+
function hmacSha256(data, secret, encoding = 'hex') {
|
|
100
|
+
if (secret === null || secret === undefined) {
|
|
101
|
+
throw new TypeError('A secret key is required for HMAC computation');
|
|
102
|
+
}
|
|
103
|
+
if (data === null || data === undefined) {
|
|
104
|
+
data = '';
|
|
105
|
+
}
|
|
106
|
+
const input = Buffer.isBuffer(data) || typeof data === 'string' ? data : String(data);
|
|
107
|
+
const hmac = crypto.createHmac('sha256', secret).update(input);
|
|
108
|
+
return encoding === 'buffer' ? hmac.digest() : hmac.digest(encoding || 'hex');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate a cryptographically secure random token string or buffer.
|
|
113
|
+
*
|
|
114
|
+
* @param {number} [byteLength=32] - Number of random bytes
|
|
115
|
+
* @param {import('crypto').BinaryToTextEncoding|'buffer'} [encoding='hex'] - Output encoding ('hex', 'base64', 'base64url', or 'buffer')
|
|
116
|
+
* @returns {string|Buffer}
|
|
117
|
+
*/
|
|
118
|
+
function randomToken(byteLength = 32, encoding = 'hex') {
|
|
119
|
+
const bytes = crypto.randomBytes(byteLength);
|
|
120
|
+
return encoding === 'buffer' ? bytes : bytes.toString(encoding || 'hex');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate a cryptographically secure random string with an optional prefix.
|
|
125
|
+
* E.g. randomId('whsec', 24) -> 'whsec_a1b2c3...'
|
|
126
|
+
*
|
|
127
|
+
* @param {string} [prefix=''] - Identifier prefix
|
|
128
|
+
* @param {number} [byteLength=16] - Random byte length
|
|
129
|
+
* @param {import('crypto').BinaryToTextEncoding} [encoding='hex'] - Output encoding
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
function randomId(prefix = '', byteLength = 16, encoding = 'hex') {
|
|
133
|
+
const token = randomToken(byteLength, encoding);
|
|
134
|
+
if (!prefix) return token;
|
|
135
|
+
const p = prefix.endsWith('_') ? prefix : `${prefix}_`;
|
|
136
|
+
return `${p}${token}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate a timestamped unique ID with optional prefix.
|
|
141
|
+
* E.g. randomTimestampedId('dlv', 8) -> 'dlv_1729221234567_abcdef1234567890'
|
|
142
|
+
*
|
|
143
|
+
* @param {string} [prefix=''] - Identifier prefix
|
|
144
|
+
* @param {number} [byteLength=8] - Random byte length
|
|
145
|
+
* @param {import('crypto').BinaryToTextEncoding} [encoding='hex'] - Output encoding
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
function randomTimestampedId(prefix = '', byteLength = 8, encoding = 'hex') {
|
|
149
|
+
const token = randomToken(byteLength, encoding);
|
|
150
|
+
const p = prefix ? (prefix.endsWith('_') ? prefix : `${prefix}_`) : '';
|
|
151
|
+
return `${p}${Date.now()}_${token}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Encode string or Buffer to Base64URL string (RFC 4648 §5).
|
|
156
|
+
*
|
|
157
|
+
* @param {string|Buffer|any} input
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
function base64UrlEncode(input) {
|
|
161
|
+
if (input === null || input === undefined) return '';
|
|
162
|
+
return Buffer.isBuffer(input)
|
|
163
|
+
? input.toString('base64url')
|
|
164
|
+
: Buffer.from(String(input), 'utf8').toString('base64url');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Decode Base64URL string to UTF-8 string.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} input
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
function base64UrlDecode(input) {
|
|
174
|
+
if (!input) return '';
|
|
175
|
+
return Buffer.from(String(input), 'base64url').toString('utf8');
|
|
176
|
+
}
|
|
177
|
+
|
|
70
178
|
module.exports = {
|
|
71
179
|
safeCompare,
|
|
72
180
|
timingSafeEqual: safeCompare,
|
|
181
|
+
sha256,
|
|
182
|
+
hashSha256: sha256,
|
|
183
|
+
hmacSha256,
|
|
184
|
+
createHmacSha256: hmacSha256,
|
|
185
|
+
randomToken,
|
|
186
|
+
randomId,
|
|
187
|
+
randomTimestampedId,
|
|
188
|
+
base64UrlEncode,
|
|
189
|
+
base64UrlDecode,
|
|
73
190
|
};
|
|
191
|
+
|
package/core/utils/fetch.js
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
* @module core/utils/fetch
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const { parseDurationMs } = require('./time');
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Executes a fetch request with timeout and abort controller support.
|
|
18
18
|
*
|
|
19
19
|
* @param {string | URL | Request} url - Target URL or Request instance
|
|
20
|
-
* @param {RequestInit & { timeout?: number }} [options] - Standard RequestInit plus optional timeout (ms)
|
|
20
|
+
* @param {RequestInit & { timeout?: number | string }} [options] - Standard RequestInit plus optional timeout (ms or '5s')
|
|
21
21
|
* @returns {Promise<Response>}
|
|
22
22
|
*/
|
|
23
23
|
async function fetchWithTimeout(url, options = {}) {
|
|
@@ -38,13 +38,14 @@ async function fetchWithTimeout(url, options = {}) {
|
|
|
38
38
|
let timer = null;
|
|
39
39
|
let callerSignalListener = null;
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
const timeoutMs = timeout !== undefined ? parseDurationMs(timeout, 10000) : 10000;
|
|
42
|
+
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
|
|
42
43
|
timer = setTimeout(() => {
|
|
43
|
-
const err = new Error(`Request timed out after ${
|
|
44
|
+
const err = new Error(`Request timed out after ${timeoutMs}ms`);
|
|
44
45
|
err.name = 'TimeoutError';
|
|
45
46
|
err.code = 'ETIMEDOUT';
|
|
46
47
|
controller.abort(err);
|
|
47
|
-
},
|
|
48
|
+
}, timeoutMs);
|
|
48
49
|
if (timer && typeof timer.unref === 'function') {
|
|
49
50
|
timer.unref();
|
|
50
51
|
}
|
package/core/utils/fs.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Native Filesystem & Directory Walking Utilities
|
|
5
|
+
* Zero-dependency recursive and flat directory scanner with configurable filters.
|
|
6
|
+
* @module core/utils/fs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Standard test file extensions and type declarations to ignore by default
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_TEST_SUFFIXES = [
|
|
16
|
+
'.test.js',
|
|
17
|
+
'.test.cjs',
|
|
18
|
+
'.test.mjs',
|
|
19
|
+
'.test.ts',
|
|
20
|
+
'.spec.js',
|
|
21
|
+
'.spec.cjs',
|
|
22
|
+
'.spec.mjs',
|
|
23
|
+
'.spec.ts',
|
|
24
|
+
'.d.ts',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Recursively (or flatly) scans a directory for files matching specified criteria.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} dir - Directory path to scan
|
|
31
|
+
* @param {Object} [options]
|
|
32
|
+
* @param {string[]|Set<string>|null} [options.extensions] - Allowed extensions (e.g. ['.js', '.cjs']). Null allows any extension.
|
|
33
|
+
* @param {boolean} [options.recursive=true] - Whether to scan subdirectories recursively
|
|
34
|
+
* @param {boolean} [options.ignoreTests=true] - Whether to ignore test files (.test.js, .spec.js, .d.ts)
|
|
35
|
+
* @param {string[]} [options.ignorePrefixes=['.', '_']] - Prefixes of files/directories to ignore
|
|
36
|
+
* @param {string[]} [options.ignoreNames=['node_modules']] - Exact file or directory names to ignore
|
|
37
|
+
* @param {Function} [options.filter] - Custom filter function (fullPath, dirent) => boolean
|
|
38
|
+
* @param {string} [options.baseDir] - Base directory for computing relative paths (defaults to dir)
|
|
39
|
+
* @param {boolean} [options.detailed=false] - If true, returns array of objects with { absolutePath, relativePath, name, ext }
|
|
40
|
+
* @returns {string[]|Array<{ absolutePath: string, relativePath: string, name: string, ext: string }>}
|
|
41
|
+
*/
|
|
42
|
+
function walkDirectory(dir, options = {}) {
|
|
43
|
+
const {
|
|
44
|
+
extensions = null,
|
|
45
|
+
recursive = true,
|
|
46
|
+
ignoreTests = true,
|
|
47
|
+
ignorePrefixes = ['.', '_'],
|
|
48
|
+
ignoreNames = ['node_modules'],
|
|
49
|
+
filter = null,
|
|
50
|
+
baseDir = dir,
|
|
51
|
+
detailed = false,
|
|
52
|
+
} = options;
|
|
53
|
+
|
|
54
|
+
if (!dir || typeof dir !== 'string') return [];
|
|
55
|
+
|
|
56
|
+
const resolvedDir = path.resolve(dir);
|
|
57
|
+
if (!fs.existsSync(resolvedDir)) return [];
|
|
58
|
+
|
|
59
|
+
const extSet = extensions
|
|
60
|
+
? new Set(Array.from(extensions).map(e => (e.startsWith('.') ? e.toLowerCase() : `.${e.toLowerCase()}`)))
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
const results = [];
|
|
64
|
+
const resolvedBase = path.resolve(baseDir);
|
|
65
|
+
|
|
66
|
+
function walk(currentDir) {
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
70
|
+
} catch (_) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const name = entry.name;
|
|
76
|
+
|
|
77
|
+
// Ignore specified exact names (e.g. 'node_modules')
|
|
78
|
+
if (ignoreNames && ignoreNames.includes(name)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Ignore specified prefixes (e.g. '.' or '_')
|
|
83
|
+
if (ignorePrefixes && ignorePrefixes.some(prefix => name.startsWith(prefix))) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fullPath = path.join(currentDir, name);
|
|
88
|
+
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
if (recursive) {
|
|
91
|
+
walk(fullPath);
|
|
92
|
+
}
|
|
93
|
+
} else if (entry.isFile()) {
|
|
94
|
+
const ext = path.extname(name).toLowerCase();
|
|
95
|
+
|
|
96
|
+
// Check extensions if specified
|
|
97
|
+
if (extSet && !extSet.has(ext)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check test file suffixes
|
|
102
|
+
if (ignoreTests && DEFAULT_TEST_SUFFIXES.some(suffix => name.toLowerCase().endsWith(suffix))) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Custom filter function
|
|
107
|
+
if (typeof filter === 'function' && !filter(fullPath, entry)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (detailed) {
|
|
112
|
+
const relPath = path.relative(resolvedBase, fullPath).split(path.sep).join('/');
|
|
113
|
+
results.push({
|
|
114
|
+
absolutePath: fullPath,
|
|
115
|
+
relativePath: relPath,
|
|
116
|
+
name,
|
|
117
|
+
ext,
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
results.push(fullPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
walk(resolvedDir);
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Convenient wrapper for walkDirectory with detailed: true
|
|
132
|
+
* @param {string} dir
|
|
133
|
+
* @param {Object} [options]
|
|
134
|
+
* @returns {Array<{ absolutePath: string, relativePath: string, name: string, ext: string }>}
|
|
135
|
+
*/
|
|
136
|
+
function walkDirectoryDetailed(dir, options = {}) {
|
|
137
|
+
return walkDirectory(dir, { ...options, detailed: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
walkDirectory,
|
|
142
|
+
walkDirectoryDetailed,
|
|
143
|
+
DEFAULT_TEST_SUFFIXES,
|
|
144
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency JSON Utilities
|
|
5
|
+
* Safe, fault-tolerant JSON parse and stringify helpers that never throw unhandled exceptions.
|
|
6
|
+
* @module core/utils/json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Safely parses a JSON string without throwing exceptions.
|
|
11
|
+
* Returns the provided fallback value if parsing fails or input is missing.
|
|
12
|
+
* If input is already an object or array (and not a string), returns input directly.
|
|
13
|
+
*
|
|
14
|
+
* @template T
|
|
15
|
+
* @param {string|unknown} input - Value or JSON string to parse
|
|
16
|
+
* @param {T} [fallback=null] - Value to return if parsing fails
|
|
17
|
+
* @param {Function} [reviver] - Optional JSON.parse reviver function
|
|
18
|
+
* @returns {any|T}
|
|
19
|
+
*/
|
|
20
|
+
function safeJsonParse(input, fallback = null, reviver) {
|
|
21
|
+
if (input === null || input === undefined || input === '') {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
if (typeof input !== 'string') {
|
|
25
|
+
return input;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(input, reviver);
|
|
30
|
+
} catch (_) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Safely serializes a value to a JSON string without throwing exceptions.
|
|
37
|
+
* Handles circular references and BigInt values gracefully.
|
|
38
|
+
*
|
|
39
|
+
* @param {unknown} value - Value to serialize
|
|
40
|
+
* @param {string|number|null} [space=null] - Indentation spaces
|
|
41
|
+
* @param {string} [fallback=''] - Fallback value if serialization fails
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function safeJsonStringify(value, space = null, fallback = '') {
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return JSON.stringify(value, null, space ?? undefined);
|
|
50
|
+
} catch (_) {
|
|
51
|
+
// Attempt circular-safe serialization
|
|
52
|
+
try {
|
|
53
|
+
const seen = new WeakSet();
|
|
54
|
+
return JSON.stringify(
|
|
55
|
+
value,
|
|
56
|
+
(key, val) => {
|
|
57
|
+
if (typeof val === 'bigint') {
|
|
58
|
+
return val.toString();
|
|
59
|
+
}
|
|
60
|
+
if (typeof val === 'object' && val !== null) {
|
|
61
|
+
if (seen.has(val)) {
|
|
62
|
+
return '[Circular]';
|
|
63
|
+
}
|
|
64
|
+
seen.add(val);
|
|
65
|
+
}
|
|
66
|
+
return val;
|
|
67
|
+
},
|
|
68
|
+
space ?? undefined
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks whether a given string is valid JSON without throwing.
|
|
78
|
+
*
|
|
79
|
+
* @param {string|unknown} str - String to test
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function isValidJson(str) {
|
|
83
|
+
if (typeof str !== 'string' || !str.trim()) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
JSON.parse(str);
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
safeJsonParse,
|
|
96
|
+
safeJsonStringify,
|
|
97
|
+
isValidJson,
|
|
98
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency Object & Array Utilities
|
|
5
|
+
* Safe property picking, omission, array wrapping, and deep cloning.
|
|
6
|
+
* @module core/utils/object
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure a value is wrapped in an array.
|
|
11
|
+
* Returns empty array for null or undefined.
|
|
12
|
+
* Returns array as-is if already an array.
|
|
13
|
+
* Wraps any other value in a single-element array.
|
|
14
|
+
*
|
|
15
|
+
* @template T
|
|
16
|
+
* @param {T|T[]|null|undefined} value - Value to ensure as array
|
|
17
|
+
* @returns {T[]}
|
|
18
|
+
* @example
|
|
19
|
+
* ensureArray(null) // -> []
|
|
20
|
+
* ensureArray(undefined) // -> []
|
|
21
|
+
* ensureArray('foo') // -> ['foo']
|
|
22
|
+
* ensureArray(['bar']) // -> ['bar']
|
|
23
|
+
*/
|
|
24
|
+
function ensureArray(value) {
|
|
25
|
+
if (value === undefined || value === null) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return Array.isArray(value) ? value : [value];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pick specific keys from an object.
|
|
33
|
+
*
|
|
34
|
+
* @param {Record<string, any>|null|undefined} obj - Source object
|
|
35
|
+
* @param {string[]|string} keys - Keys to pick
|
|
36
|
+
* @returns {Record<string, any>} Object with picked keys
|
|
37
|
+
*/
|
|
38
|
+
function pick(obj, keys) {
|
|
39
|
+
if (!obj || typeof obj !== 'object') {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
const result = {};
|
|
43
|
+
const keyList = Array.isArray(keys) ? keys : [keys];
|
|
44
|
+
for (const key of keyList) {
|
|
45
|
+
if (key in obj) {
|
|
46
|
+
result[key] = obj[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Omit specific keys from an object.
|
|
54
|
+
*
|
|
55
|
+
* @param {Record<string, any>|null|undefined} obj - Source object
|
|
56
|
+
* @param {string[]|string} keys - Keys to omit
|
|
57
|
+
* @returns {Record<string, any>} Object without omitted keys
|
|
58
|
+
*/
|
|
59
|
+
function omit(obj, keys) {
|
|
60
|
+
if (!obj || typeof obj !== 'object') {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
const result = {};
|
|
64
|
+
const omitSet = new Set(Array.isArray(keys) ? keys : [keys]);
|
|
65
|
+
for (const key in obj) {
|
|
66
|
+
if (!omitSet.has(key)) {
|
|
67
|
+
result[key] = obj[key];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Deep clone an object, array, or Date instance safely.
|
|
75
|
+
* Strips prototype pollution attempts (__proto__, constructor, prototype).
|
|
76
|
+
*
|
|
77
|
+
* @template T
|
|
78
|
+
* @param {T} obj - Target object to clone
|
|
79
|
+
* @returns {T} Cloned copy
|
|
80
|
+
*/
|
|
81
|
+
function deepClone(obj) {
|
|
82
|
+
if (obj === null || typeof obj !== 'object') {
|
|
83
|
+
return obj;
|
|
84
|
+
}
|
|
85
|
+
if (obj instanceof Date) {
|
|
86
|
+
return new Date(obj.getTime());
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(obj)) {
|
|
89
|
+
return obj.map(deepClone);
|
|
90
|
+
}
|
|
91
|
+
const cloned = Object.create(null);
|
|
92
|
+
for (const key of Object.keys(obj)) {
|
|
93
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
cloned[key] = deepClone(obj[key]);
|
|
97
|
+
}
|
|
98
|
+
return { ...cloned };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
ensureArray,
|
|
103
|
+
pick,
|
|
104
|
+
omit,
|
|
105
|
+
deepClone,
|
|
106
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency RegExp Utilities
|
|
5
|
+
* Fast, secure regular expression escaping and glob-to-regex conversion helpers.
|
|
6
|
+
* @module core/utils/regex
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Escapes all regular expression special characters in a string.
|
|
11
|
+
* Safe against ReDoS and regex injection when inserting dynamic substrings into RegExp.
|
|
12
|
+
*
|
|
13
|
+
* @param {string|unknown} str - String to escape
|
|
14
|
+
* @returns {string} Escaped string safe for new RegExp()
|
|
15
|
+
*/
|
|
16
|
+
function escapeRegExp(str) {
|
|
17
|
+
if (str == null) return '';
|
|
18
|
+
return String(str).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Converts a glob pattern string (e.g. 'users/*', 'api/**', 'order.*') into a RegExp.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} pattern - Glob pattern string
|
|
25
|
+
* @param {Object} [options]
|
|
26
|
+
* @param {'/'|'.'|null} [options.separator='/'] - Segment separator ('/' for URLs/paths, '.' for events/dots, null for flat)
|
|
27
|
+
* @param {boolean} [options.exact=true] - Whether to match entire string (^...$)
|
|
28
|
+
* @param {string} [options.flags=''] - RegExp flags (e.g. 'i')
|
|
29
|
+
* @returns {RegExp}
|
|
30
|
+
*/
|
|
31
|
+
function globToRegExp(pattern, options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
separator = '/',
|
|
34
|
+
exact = true,
|
|
35
|
+
flags = '',
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
if (typeof pattern !== 'string') {
|
|
39
|
+
return exact ? /(?!)/ : /(?:)/;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (pattern === '*' || pattern === '**') {
|
|
43
|
+
return new RegExp(exact ? '^.*$' : '.*', flags);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. Fully escape all regex characters first
|
|
47
|
+
let escaped = escapeRegExp(pattern);
|
|
48
|
+
|
|
49
|
+
// 2. Replace escaped wildcard sequences
|
|
50
|
+
if (separator === '/') {
|
|
51
|
+
// URL or path globbing
|
|
52
|
+
escaped = escaped
|
|
53
|
+
.replace(/\\\*\\\*/g, '.*')
|
|
54
|
+
.replace(/\\\*/g, '[^/]*')
|
|
55
|
+
.replace(/\\\?/g, '.');
|
|
56
|
+
} else if (separator === '.') {
|
|
57
|
+
// Dot-notation event globbing (e.g. order.* or order.**)
|
|
58
|
+
escaped = escaped
|
|
59
|
+
.replace(/\\\*\\\*/g, '.*')
|
|
60
|
+
.replace(/\\\*/g, '[^.]+')
|
|
61
|
+
.replace(/\\\?/g, '.');
|
|
62
|
+
} else {
|
|
63
|
+
// Flat wildcard matching
|
|
64
|
+
escaped = escaped
|
|
65
|
+
.replace(/\\\*+/g, '.*')
|
|
66
|
+
.replace(/\\\?/g, '.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const regexStr = exact ? `^${escaped}$` : escaped;
|
|
70
|
+
return new RegExp(regexStr, flags);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
escapeRegExp,
|
|
75
|
+
escapeRegex: escapeRegExp,
|
|
76
|
+
globToRegExp,
|
|
77
|
+
};
|