webspresso 0.0.108 → 0.0.110
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/LICENSE +21 -0
- package/README.md +3 -3
- package/bin/commands/add-esbuild.js +7 -3
- package/bin/commands/add-tailwind.js +9 -5
- package/bin/commands/build.js +20 -4
- package/bin/commands/dev.js +57 -7
- package/bin/commands/doctor.js +42 -1
- package/bin/commands/favicon-generate.js +10 -3
- package/bin/commands/new.js +8 -3
- package/bin/commands/start.js +3 -1
- package/bin/utils/add-command.js +16 -0
- package/core/app.js +45 -3
- package/core/auth/authenticate.js +3 -3
- package/core/auth/hash.js +167 -43
- package/core/auth/index.js +13 -1
- package/core/auth/jwt.js +21 -2
- package/core/auth/manager.js +63 -14
- package/core/auth/middleware.js +4 -2
- package/core/auth/oauth.js +5 -1
- package/core/auth/tokens.js +7 -2
- package/core/auth/totp.js +42 -6
- package/core/auth/user-agent.js +2 -0
- package/core/client/adapter/datastar.js +1401 -0
- package/core/client/api.js +1337 -0
- package/core/client/controller.js +1706 -0
- package/core/client/debug.js +367 -0
- package/core/client/devtool.js +1134 -0
- package/core/client/forms.js +1164 -0
- package/core/client/fragments.js +894 -0
- package/core/client/index.d.ts +59 -0
- package/core/client/index.js +391 -0
- package/core/client/lite.js +175 -0
- package/core/client/server-fragments.js +868 -0
- package/core/client/storage.js +339 -0
- package/core/client/utils.js +281 -0
- package/core/compression/middleware.js +277 -26
- package/core/context/app-context.js +25 -3
- package/core/context/request.js +32 -4
- package/core/errors/middleware.js +14 -0
- package/core/events/dispatcher.js +33 -0
- package/core/forms/flash.js +136 -102
- package/core/health/middleware.js +36 -6
- package/core/http/pipeline.js +45 -6
- package/core/index.js +25 -7
- package/core/kernel/view.js +41 -10
- package/core/metrics/middleware.js +30 -6
- package/core/metrics/registry.js +13 -5
- package/core/orm/cache/fingerprint.js +32 -11
- package/core/orm/cache/layer.js +19 -2
- package/core/orm/hidden.js +145 -0
- package/core/orm/model.js +48 -2
- package/core/orm/query-builder.js +343 -6
- package/core/orm/query-filters.js +14 -2
- package/core/orm/repository.js +64 -13
- package/core/orm/schema-helpers.js +9 -6
- package/core/orm/transaction.js +50 -6
- package/core/orm/types.js +4 -0
- package/core/queue/adapters/database.js +184 -2
- package/core/queue/adapters/memory.js +153 -5
- package/core/queue/adapters/redis.js +59 -4
- package/core/queue/job.js +14 -1
- package/core/queue/manager.js +115 -0
- package/core/resilience/circuit-breaker.js +27 -8
- package/core/services/builtins/exchange.js +174 -7
- package/core/services/builtins/file-manager.js +36 -0
- package/core/services/builtins/mail.js +27 -0
- package/core/services/builtins/queue.js +144 -0
- package/core/services/builtins/security.js +118 -0
- package/core/services/builtins/webhook.js +110 -0
- package/core/services/executor.js +38 -16
- package/core/services/index.js +4 -1
- package/core/sse/stream.js +28 -5
- package/core/utils/timeout.js +8 -1
- package/core/validation/compiler.js +10 -7
- package/index.d.ts +1320 -74
- package/index.js +128 -8
- package/package.json +75 -27
- package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
- package/plugins/admin-panel/index.js +19 -8
- package/plugins/csrf/index.js +3 -3
- package/plugins/data-exchange/export.js +214 -0
- package/plugins/data-exchange/import.js +48 -5
- package/plugins/data-exchange/index.js +70 -28
- package/plugins/data-exchange/preview.js +266 -0
- package/plugins/data-exchange/template.js +242 -0
- package/plugins/email/admin-component.js +783 -141
- package/plugins/email/api-handlers.js +51 -1
- package/plugins/email/index.js +5 -0
- package/plugins/email/service.js +48 -0
- package/plugins/file-manager/admin-component.js +1546 -70
- package/plugins/file-manager/api-handlers.js +166 -1
- package/plugins/file-manager/core.js +598 -8
- package/plugins/file-manager/index.js +8 -0
- package/plugins/index.js +7 -0
- package/plugins/mcp/auth.js +3 -3
- package/plugins/oauth/index.js +36 -3
- package/plugins/queue/admin-component.js +1092 -0
- package/plugins/queue/api-handlers.js +248 -0
- package/plugins/queue/index.js +69 -0
- package/plugins/rest-resources/index.js +7 -2
- package/plugins/security/admin-component.js +689 -0
- package/plugins/security/api-handlers.js +169 -0
- package/plugins/security/audit.js +153 -0
- package/plugins/security/bruteforce.js +244 -0
- package/plugins/security/firewall.js +459 -0
- package/plugins/security/headers.js +214 -0
- package/plugins/security/honeypot.js +234 -0
- package/plugins/security/incident-recorder.js +137 -0
- package/plugins/security/index.js +211 -0
- package/plugins/seo-checker/analyzer.js +20 -7
- package/plugins/upload/index.js +36 -0
- package/plugins/webhooks/admin-component.js +702 -175
- package/plugins/webhooks/api-handlers.js +75 -1
- package/plugins/webhooks/dispatcher.js +290 -11
- package/plugins/webhooks/index.js +37 -2
- package/plugins/webhooks/sender.js +56 -0
- package/plugins/xlsx/engine.js +19 -4
- package/src/api/api-loader.js +19 -1
- package/src/bundle/index.js +443 -17
- package/src/client-runtime/bootstrap-swup.js +9 -0
- package/src/client-runtime/mount.js +15 -20
- package/src/client-runtime/resolve.js +3 -2
- package/src/dev/api-typegen.js +1300 -0
- package/src/dev/banner.js +10 -1
- package/src/dev/colors.js +1 -1
- package/src/dev/index.js +7 -1
- package/src/dev/typegen.js +173 -17
- package/src/dev-reload/index.js +1 -1
- package/src/discovery/file-route-parser.js +17 -3
- package/src/discovery/scan-routes.js +4 -1
- package/src/file-router.js +253 -69
- package/src/helpers.js +46 -26
- package/src/njk-frontmatter.js +30 -4
- package/src/routing/cascading.js +26 -8
- package/src/routing/mount-discovered-routes.js +76 -0
- package/src/server.js +231 -70
- package/views/partials/webspresso-client-runtime.njk +1 -9
- package/.agents/ADMIN.md +0 -426
- package/.agents/AGENTS.md +0 -96
- package/.agents/ARCHITECTURE.md +0 -163
- package/.agents/AUTH.md +0 -581
- package/.agents/CACHE.md +0 -148
- package/.agents/CLI.md +0 -649
- package/.agents/COMPRESSION.md +0 -59
- package/.agents/CONTEXT.md +0 -152
- package/.agents/CONVENTIONS.md +0 -50
- package/.agents/EARLY_HINTS.md +0 -120
- package/.agents/EMAIL.md +0 -223
- package/.agents/ERRORS.md +0 -260
- package/.agents/EVENTS.md +0 -179
- package/.agents/FLAGS.md +0 -118
- package/.agents/HEALTH.md +0 -118
- package/.agents/LIMITS.md +0 -109
- package/.agents/LOGGING.md +0 -93
- package/.agents/MCP.md +0 -103
- package/.agents/METRICS.md +0 -127
- package/.agents/MIGRATION_JOBS.md +0 -95
- package/.agents/ORM.md +0 -700
- package/.agents/PLUGINS.md +0 -729
- package/.agents/QUEUE.md +0 -290
- package/.agents/RATE_LIMIT.md +0 -147
- package/.agents/REALTIME.md +0 -213
- package/.agents/RESILIENCE.md +0 -235
- package/.agents/ROUTING.md +0 -605
- package/.agents/SCALE_OUT.md +0 -109
- package/.agents/SECRETS.md +0 -117
- package/.agents/SERVICES.md +0 -341
- package/.agents/SHUTDOWN.md +0 -74
- package/.agents/STREAMING.md +0 -108
- package/.agents/WORKFLOW.md +0 -27
- package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
- package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
- package/.agents/skills/webspresso-framework/SKILL.md +0 -89
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
- package/.agents/skills/webspresso-services/SKILL.md +0 -134
- package/.cursorrules +0 -14
- package/AGENTS.md +0 -203
- package/CLAUDE.md +0 -54
- package/core/kernel/run-demo.js +0 -58
- package/plugins/admin-panel/styles.js +0 -9
- package/plugins/data-exchange/export-xlsx.js +0 -119
- package/src/client-runtime/bootstrap-alpine-swup.js +0 -34
package/plugins/xlsx/engine.js
CHANGED
|
@@ -6,7 +6,19 @@
|
|
|
6
6
|
* @module plugins/xlsx/engine
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
let ExcelJS = null;
|
|
10
|
+
function getExcelJS() {
|
|
11
|
+
if (!ExcelJS) {
|
|
12
|
+
try {
|
|
13
|
+
ExcelJS = require('exceljs');
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"The 'exceljs' package is required to use xlsxPlugin / XLSX services. Install it with: npm install exceljs"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return ExcelJS;
|
|
21
|
+
}
|
|
10
22
|
const { PassThrough } = require('stream');
|
|
11
23
|
|
|
12
24
|
/**
|
|
@@ -125,7 +137,8 @@ function applyHeaderStyle(worksheet, style = {}) {
|
|
|
125
137
|
* @returns {Promise<Buffer>}
|
|
126
138
|
*/
|
|
127
139
|
async function generate(options = {}) {
|
|
128
|
-
const
|
|
140
|
+
const Excel = getExcelJS();
|
|
141
|
+
const workbook = new Excel.Workbook();
|
|
129
142
|
workbook.creator = options.creator || 'Webspresso';
|
|
130
143
|
workbook.created = options.created || new Date();
|
|
131
144
|
if (options.title) workbook.title = options.title;
|
|
@@ -225,7 +238,8 @@ async function generate(options = {}) {
|
|
|
225
238
|
* @returns {Promise<Array<Object>|Object.<string, Array<Object>>>}
|
|
226
239
|
*/
|
|
227
240
|
async function parse(input, options = {}) {
|
|
228
|
-
const
|
|
241
|
+
const Excel = getExcelJS();
|
|
242
|
+
const workbook = new Excel.Workbook();
|
|
229
243
|
|
|
230
244
|
if (Buffer.isBuffer(input)) {
|
|
231
245
|
await workbook.xlsx.load(input);
|
|
@@ -378,7 +392,8 @@ async function fromModel(repository, filterOrQuery = {}, options = {}) {
|
|
|
378
392
|
* @returns {ExcelJS.stream.xlsx.WorkbookWriter}
|
|
379
393
|
*/
|
|
380
394
|
function createStreamWriter(outputStream, options = {}) {
|
|
381
|
-
|
|
395
|
+
const Excel = getExcelJS();
|
|
396
|
+
return new Excel.stream.xlsx.WorkbookWriter({
|
|
382
397
|
stream: outputStream,
|
|
383
398
|
useStyles: options.useStyles !== false,
|
|
384
399
|
useSharedStrings: options.useSharedStrings || false,
|
package/src/api/api-loader.js
CHANGED
|
@@ -18,6 +18,22 @@ const { executeApiPipeline, enrichRouteError } = require('../../core/http');
|
|
|
18
18
|
* @param {Object|Function} schemaDef
|
|
19
19
|
* @returns {Object|null}
|
|
20
20
|
*/
|
|
21
|
+
|
|
22
|
+
const warnedNonRouteModules = new Set();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Warn once per file that an API-directory module exports no handler.
|
|
26
|
+
* @param {string} source - Project-relative file path
|
|
27
|
+
*/
|
|
28
|
+
function warnNonRouteModule(source) {
|
|
29
|
+
if (warnedNonRouteModules.has(source) || process.env.NODE_ENV === 'production') return;
|
|
30
|
+
warnedNonRouteModules.add(source);
|
|
31
|
+
console.warn(
|
|
32
|
+
`[webspresso] ${source} is in an API directory but exports no handler; responding 404. ` +
|
|
33
|
+
'Prefix helper files with "_" (e.g. _client.js) to keep them out of routing.'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
function compileApiSchema(schemaDef) {
|
|
22
38
|
if (!schemaDef) return null;
|
|
23
39
|
if (typeof schemaDef === 'function') {
|
|
@@ -125,7 +141,9 @@ function createApiHandler(descriptor, context) {
|
|
|
125
141
|
handlerFn = apiDef.handler || (typeof apiModule === 'function' ? apiModule : null);
|
|
126
142
|
|
|
127
143
|
if (typeof handlerFn !== 'function') {
|
|
128
|
-
|
|
144
|
+
// A helper module living in an API directory is not a route: answer 404 instead of a 500
|
|
145
|
+
warnNonRouteModule(descriptor.source);
|
|
146
|
+
return next();
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
compiledSchema = compileApiSchema(apiDef.schema);
|
package/src/bundle/index.js
CHANGED
|
@@ -8,6 +8,52 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
+
const { deriveControllerNameFromPath } = require('../../core/client/controller');
|
|
12
|
+
|
|
13
|
+
const SERVER_ONLY_BUILTINS = new Set([
|
|
14
|
+
'fs',
|
|
15
|
+
'fs/promises',
|
|
16
|
+
'net',
|
|
17
|
+
'tls',
|
|
18
|
+
'child_process',
|
|
19
|
+
'cluster',
|
|
20
|
+
'dgram',
|
|
21
|
+
'dns',
|
|
22
|
+
'http2',
|
|
23
|
+
'worker_threads',
|
|
24
|
+
'v8',
|
|
25
|
+
'vm',
|
|
26
|
+
'async_hooks',
|
|
27
|
+
'inspector',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const SERVER_ONLY_PACKAGES = new Set([
|
|
31
|
+
'knex',
|
|
32
|
+
'better-sqlite3',
|
|
33
|
+
'pg',
|
|
34
|
+
'pg-native',
|
|
35
|
+
'mysql',
|
|
36
|
+
'mysql2',
|
|
37
|
+
'sqlite3',
|
|
38
|
+
'oracledb',
|
|
39
|
+
'tedious',
|
|
40
|
+
'mssql',
|
|
41
|
+
'ioredis',
|
|
42
|
+
'redis',
|
|
43
|
+
'express',
|
|
44
|
+
'helmet',
|
|
45
|
+
'compression',
|
|
46
|
+
'cookie-parser',
|
|
47
|
+
'express-session',
|
|
48
|
+
'nodemailer',
|
|
49
|
+
'mjml',
|
|
50
|
+
'bcrypt',
|
|
51
|
+
'bcryptjs',
|
|
52
|
+
'argon2',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const SERVER_ONLY_INTERNAL_PATTERN =
|
|
56
|
+
/(?:^|\/)(?:core\/(?:orm|services|auth|secrets|queue|mail|http|metrics|health|rate-limit)|src\/(?:server|file-router|api|pages)|models|services|jobs|migrations|pages\/api)(?:\/|$)/;
|
|
11
57
|
|
|
12
58
|
const DEFAULT_ENTRYPOINTS = [
|
|
13
59
|
'src/client/main.js',
|
|
@@ -78,11 +124,13 @@ function findPageEntryPoints(cwd = process.cwd()) {
|
|
|
78
124
|
}
|
|
79
125
|
}
|
|
80
126
|
|
|
81
|
-
// 2.
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
// 2. Co-located page client files: pages/**/*.client.{js,ts,jsx,tsx} and src/pages/**/*.client.{js,ts,jsx,tsx}
|
|
128
|
+
const pageDirs = [path.resolve(cwd, 'pages'), path.resolve(cwd, 'src/pages')];
|
|
129
|
+
const validExts = new Set(['.js', '.mjs', '.ts', '.jsx', '.tsx']);
|
|
130
|
+
|
|
131
|
+
for (const dirToScan of pageDirs) {
|
|
132
|
+
if (!fs.existsSync(dirToScan)) continue;
|
|
133
|
+
function walkPages(currentDir) {
|
|
86
134
|
let list;
|
|
87
135
|
try {
|
|
88
136
|
list = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -93,11 +141,11 @@ function findPageEntryPoints(cwd = process.cwd()) {
|
|
|
93
141
|
if (item.name.startsWith('.')) continue;
|
|
94
142
|
const fullPath = path.join(currentDir, item.name);
|
|
95
143
|
if (item.isDirectory()) {
|
|
96
|
-
|
|
144
|
+
walkPages(fullPath);
|
|
97
145
|
} else if (item.isFile()) {
|
|
98
146
|
const ext = path.extname(item.name).toLowerCase();
|
|
99
|
-
if (validExts.has(ext) && item.name.includes('.client.')) {
|
|
100
|
-
const rel = path.relative(
|
|
147
|
+
if (validExts.has(ext) && item.name.includes('.client.') && !item.name.endsWith('.d.ts')) {
|
|
148
|
+
const rel = path.relative(dirToScan, fullPath).replace(/\\/g, '/');
|
|
101
149
|
const cleanName = rel.replace(/\.client\.[a-z0-9]+$/i, '');
|
|
102
150
|
const entryName = 'pages/' + cleanName;
|
|
103
151
|
if (!pageEntries[entryName]) {
|
|
@@ -107,12 +155,293 @@ function findPageEntryPoints(cwd = process.cwd()) {
|
|
|
107
155
|
}
|
|
108
156
|
}
|
|
109
157
|
}
|
|
110
|
-
|
|
158
|
+
walkPages(dirToScan);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. Co-located component/view client files: views/**/*.client.{js,ts,jsx,tsx} and src/views/**/*.client.{js,ts,jsx,tsx}
|
|
162
|
+
const viewDirs = [path.resolve(cwd, 'views'), path.resolve(cwd, 'src/views')];
|
|
163
|
+
for (const dirToScan of viewDirs) {
|
|
164
|
+
if (!fs.existsSync(dirToScan)) continue;
|
|
165
|
+
function walkViews(currentDir) {
|
|
166
|
+
let list;
|
|
167
|
+
try {
|
|
168
|
+
list = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
169
|
+
} catch (_) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for (const item of list) {
|
|
173
|
+
if (item.name.startsWith('.')) continue;
|
|
174
|
+
const fullPath = path.join(currentDir, item.name);
|
|
175
|
+
if (item.isDirectory()) {
|
|
176
|
+
walkViews(fullPath);
|
|
177
|
+
} else if (item.isFile()) {
|
|
178
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
179
|
+
if (validExts.has(ext) && item.name.includes('.client.') && !item.name.endsWith('.d.ts')) {
|
|
180
|
+
const rel = path.relative(dirToScan, fullPath).replace(/\\/g, '/');
|
|
181
|
+
const cleanName = rel.replace(/\.client\.[a-z0-9]+$/i, '');
|
|
182
|
+
const entryName = cleanName.startsWith('components/') ? cleanName : `views/${cleanName}`;
|
|
183
|
+
if (!pageEntries[entryName]) {
|
|
184
|
+
pageEntries[entryName] = fullPath;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
walkViews(dirToScan);
|
|
111
191
|
}
|
|
112
192
|
|
|
113
193
|
return pageEntries;
|
|
114
194
|
}
|
|
115
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Discovers all `.client.{js,ts,mjs,jsx,tsx}` controller files across `pages/` and `views/`
|
|
198
|
+
* and validates deterministic controller names for collisions.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} [cwd=process.cwd()]
|
|
201
|
+
* @returns {Array<{ controllerName: string, entryName: string, filePath: string, relativePath: string }>}
|
|
202
|
+
*/
|
|
203
|
+
function discoverClientControllers(cwd = process.cwd()) {
|
|
204
|
+
const pageEntries = findPageEntryPoints(cwd);
|
|
205
|
+
const discovered = [];
|
|
206
|
+
const byControllerName = new Map();
|
|
207
|
+
|
|
208
|
+
for (const [entryName, filePath] of Object.entries(pageEntries)) {
|
|
209
|
+
const relativePath = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
210
|
+
const controllerName = deriveControllerNameFromPath(relativePath);
|
|
211
|
+
if (!controllerName) continue;
|
|
212
|
+
|
|
213
|
+
if (byControllerName.has(controllerName)) {
|
|
214
|
+
const existing = byControllerName.get(controllerName);
|
|
215
|
+
if (existing.filePath !== filePath) {
|
|
216
|
+
const err = new Error(
|
|
217
|
+
`Controller name collision: "${controllerName}" is defined by both "${existing.relativePath}" and "${relativePath}".`
|
|
218
|
+
);
|
|
219
|
+
err.code = 'CONTROLLER_NAME_COLLISION';
|
|
220
|
+
err.controllerName = controllerName;
|
|
221
|
+
err.files = [existing.relativePath, relativePath];
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const record = {
|
|
227
|
+
controllerName,
|
|
228
|
+
entryName,
|
|
229
|
+
filePath,
|
|
230
|
+
relativePath,
|
|
231
|
+
};
|
|
232
|
+
byControllerName.set(controllerName, record);
|
|
233
|
+
discovered.push(record);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return discovered;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Checks whether an import specifier or resolved path crosses into a server-only module boundary.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} specifier - Raw import/require string
|
|
243
|
+
* @param {string} importerPath - Absolute path of the importing client file
|
|
244
|
+
* @param {string} [cwd=process.cwd()] - Project root directory
|
|
245
|
+
* @returns {{ forbidden: boolean, reason?: string }}
|
|
246
|
+
*/
|
|
247
|
+
function checkServerImportViolation(specifier, importerPath, cwd = process.cwd()) {
|
|
248
|
+
if (!specifier || typeof specifier !== 'string') return { forbidden: false };
|
|
249
|
+
const raw = specifier.trim();
|
|
250
|
+
|
|
251
|
+
// 1. Node built-in modules (`node:fs`, `fs`, `child_process`, etc.)
|
|
252
|
+
if (raw.startsWith('node:')) {
|
|
253
|
+
const modName = raw.slice(5);
|
|
254
|
+
if (SERVER_ONLY_BUILTINS.has(modName) || modName.startsWith('fs') || modName.startsWith('child_process')) {
|
|
255
|
+
return {
|
|
256
|
+
forbidden: true,
|
|
257
|
+
reason: `Node.js server built-in module "${raw}" cannot be imported into browser client bundle ("${path.relative(cwd, importerPath)}").`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (SERVER_ONLY_BUILTINS.has(raw)) {
|
|
262
|
+
return {
|
|
263
|
+
forbidden: true,
|
|
264
|
+
reason: `Node.js server built-in module "${raw}" cannot be imported into browser client bundle ("${path.relative(cwd, importerPath)}").`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 2. Server-only packages (`knex`, `express`, `better-sqlite3`, `pg`, etc.)
|
|
269
|
+
const pkgRoot = raw.startsWith('@')
|
|
270
|
+
? raw.split('/').slice(0, 2).join('/')
|
|
271
|
+
: raw.split('/')[0];
|
|
272
|
+
if (!raw.startsWith('.') && !raw.startsWith('/') && SERVER_ONLY_PACKAGES.has(pkgRoot)) {
|
|
273
|
+
return {
|
|
274
|
+
forbidden: true,
|
|
275
|
+
reason: `Server-only package "${raw}" cannot be imported into browser client bundle ("${path.relative(cwd, importerPath)}").`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 3. Webspresso server subpath imports (`webspresso/core/orm`, `webspresso/core/services`, etc.)
|
|
280
|
+
if (!raw.startsWith('.') && SERVER_ONLY_INTERNAL_PATTERN.test(raw)) {
|
|
281
|
+
return {
|
|
282
|
+
forbidden: true,
|
|
283
|
+
reason: `Server-only Webspresso module "${raw}" cannot be imported into browser client bundle ("${path.relative(cwd, importerPath)}").`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 4. Relative or absolute path imports into server files (`pages/api/*`, `models/*`, `services/*`, server page loaders)
|
|
288
|
+
if (raw.startsWith('.') || raw.startsWith('/')) {
|
|
289
|
+
const resolved = path.resolve(path.dirname(importerPath), raw);
|
|
290
|
+
const relToRoot = path.relative(cwd, resolved).replace(/\\/g, '/');
|
|
291
|
+
|
|
292
|
+
if (SERVER_ONLY_INTERNAL_PATTERN.test(relToRoot)) {
|
|
293
|
+
return {
|
|
294
|
+
forbidden: true,
|
|
295
|
+
reason: `Server module "${relToRoot}" (imported as "${raw}" in "${path.relative(cwd, importerPath)}") cannot be bundled into browser code.`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Disallow importing a server page loader (`pages/notes.js`) from `pages/notes.client.ts`
|
|
300
|
+
if (
|
|
301
|
+
(relToRoot.startsWith('pages/') || relToRoot.startsWith('src/pages/')) &&
|
|
302
|
+
!relToRoot.includes('.client')
|
|
303
|
+
) {
|
|
304
|
+
// Check if it resolves to a non-client .js/.ts file in pages/
|
|
305
|
+
const candidates = [resolved, `${resolved}.js`, `${resolved}.ts`];
|
|
306
|
+
for (const cand of candidates) {
|
|
307
|
+
if (fs.existsSync(cand) && fs.statSync(cand).isFile() && !cand.includes('.client.')) {
|
|
308
|
+
return {
|
|
309
|
+
forbidden: true,
|
|
310
|
+
reason: `Server page loader "${path.relative(cwd, cand)}" cannot be imported into browser client bundle ("${path.relative(cwd, importerPath)}").`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { forbidden: false };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Statically scans client entry files and their local relative dependency graph
|
|
322
|
+
* to reject any runtime import/require of server-only modules (ignores `import type`).
|
|
323
|
+
*
|
|
324
|
+
* @param {string[]|Record<string, string>} entryPoints
|
|
325
|
+
* @param {string} [cwd=process.cwd()]
|
|
326
|
+
*/
|
|
327
|
+
function validateClientBundleBoundaries(entryPoints, cwd = process.cwd()) {
|
|
328
|
+
const fileList = Array.isArray(entryPoints)
|
|
329
|
+
? entryPoints
|
|
330
|
+
: entryPoints && typeof entryPoints === 'object'
|
|
331
|
+
? Object.values(entryPoints)
|
|
332
|
+
: [];
|
|
333
|
+
|
|
334
|
+
const visited = new Set();
|
|
335
|
+
|
|
336
|
+
function scanFile(filePath) {
|
|
337
|
+
const absPath = path.resolve(cwd, filePath);
|
|
338
|
+
if (visited.has(absPath) || !fs.existsSync(absPath)) return;
|
|
339
|
+
visited.add(absPath);
|
|
340
|
+
|
|
341
|
+
let source;
|
|
342
|
+
try {
|
|
343
|
+
source = fs.readFileSync(absPath, 'utf8');
|
|
344
|
+
} catch (_) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Strip block and line comments so commented-out imports don't trigger false positives
|
|
349
|
+
const cleanSource = source
|
|
350
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
351
|
+
.replace(/^\s*\/\/.*$/gm, '');
|
|
352
|
+
|
|
353
|
+
const specs = [];
|
|
354
|
+
|
|
355
|
+
// Match runtime ESM imports (excluding `import type ...` and `export type ...`)
|
|
356
|
+
const esmImportRegex = /\b(?:import|export)\s+(?!type\b)(?:[\s\S]*?\bfrom\s+)?['"]([^'"]+)['"]/g;
|
|
357
|
+
let m;
|
|
358
|
+
while ((m = esmImportRegex.exec(cleanSource)) !== null) {
|
|
359
|
+
const fullMatch = m[0];
|
|
360
|
+
// Skip `import { type Foo } from '...'` if ALL imported bindings are `type`
|
|
361
|
+
const braceMatch = fullMatch.match(/\{([^}]+)\}/);
|
|
362
|
+
if (braceMatch && !/^[^{]+\{/.test(fullMatch.replace(/^(?:import|export)\s+/, ''))) {
|
|
363
|
+
const items = braceMatch[1]
|
|
364
|
+
.split(',')
|
|
365
|
+
.map((s) => s.trim())
|
|
366
|
+
.filter(Boolean);
|
|
367
|
+
if (items.length > 0 && items.every((item) => /^type\s+/.test(item))) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
specs.push(m[1]);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Match CommonJS require('...') and dynamic import('...')
|
|
375
|
+
const callImportRegex = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
376
|
+
while ((m = callImportRegex.exec(cleanSource)) !== null) {
|
|
377
|
+
specs.push(m[1]);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const spec of specs) {
|
|
381
|
+
const check = checkServerImportViolation(spec, absPath, cwd);
|
|
382
|
+
if (check.forbidden) {
|
|
383
|
+
const err = new Error(check.reason);
|
|
384
|
+
err.code = 'SERVER_DEPENDENCY_IN_CLIENT';
|
|
385
|
+
err.importer = absPath;
|
|
386
|
+
err.specifier = spec;
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Follow relative client imports recursively
|
|
391
|
+
if (spec.startsWith('.')) {
|
|
392
|
+
const baseResolved = path.resolve(path.dirname(absPath), spec);
|
|
393
|
+
const candidates = [
|
|
394
|
+
baseResolved,
|
|
395
|
+
`${baseResolved}.ts`,
|
|
396
|
+
`${baseResolved}.js`,
|
|
397
|
+
`${baseResolved}.mjs`,
|
|
398
|
+
`${baseResolved}.tsx`,
|
|
399
|
+
`${baseResolved}.jsx`,
|
|
400
|
+
path.join(baseResolved, 'index.ts'),
|
|
401
|
+
path.join(baseResolved, 'index.js'),
|
|
402
|
+
];
|
|
403
|
+
for (const cand of candidates) {
|
|
404
|
+
if (fs.existsSync(cand) && fs.statSync(cand).isFile()) {
|
|
405
|
+
scanFile(cand);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const entry of fileList) {
|
|
414
|
+
scanFile(entry);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Creates an esbuild plugin that blocks server-only modules from entering browser bundles.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} [cwd=process.cwd()]
|
|
422
|
+
*/
|
|
423
|
+
function createServerBoundaryPlugin(cwd = process.cwd()) {
|
|
424
|
+
return {
|
|
425
|
+
name: 'webspresso-server-boundary',
|
|
426
|
+
setup(build) {
|
|
427
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
428
|
+
if (!args.importer) return null;
|
|
429
|
+
const check = checkServerImportViolation(args.path, args.importer, cwd);
|
|
430
|
+
if (check.forbidden) {
|
|
431
|
+
return {
|
|
432
|
+
errors: [
|
|
433
|
+
{
|
|
434
|
+
text: check.reason,
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
116
445
|
/**
|
|
117
446
|
* Simple glob expansion for client entrypoints (supports single and double wildcards)
|
|
118
447
|
* @param {string} pattern - Glob pattern (e.g. 'src/client/pages/*.js')
|
|
@@ -367,6 +696,9 @@ function createDevBundler(options = {}, devReloadHub = null) {
|
|
|
367
696
|
);
|
|
368
697
|
}
|
|
369
698
|
|
|
699
|
+
discoverClientControllers(cwd);
|
|
700
|
+
validateClientBundleBoundaries(entryPoints, cwd);
|
|
701
|
+
|
|
370
702
|
const outbase = options.outbase || (fs.existsSync(path.resolve(cwd, 'src/client')) ? path.resolve(cwd, 'src/client') : undefined);
|
|
371
703
|
const esbuildConfig = {
|
|
372
704
|
entryPoints,
|
|
@@ -378,7 +710,7 @@ function createDevBundler(options = {}, devReloadHub = null) {
|
|
|
378
710
|
format,
|
|
379
711
|
metafile,
|
|
380
712
|
outbase,
|
|
381
|
-
plugins: [devReloadPlugin, ...userPlugins],
|
|
713
|
+
plugins: [createServerBoundaryPlugin(cwd), devReloadPlugin, ...userPlugins],
|
|
382
714
|
...(options.esbuildOptions || {}),
|
|
383
715
|
};
|
|
384
716
|
|
|
@@ -443,12 +775,11 @@ const { generateManifest, createManifestPlugin } = require('./manifest');
|
|
|
443
775
|
*/
|
|
444
776
|
async function buildBundle(options = {}) {
|
|
445
777
|
const cwd = options.cwd || process.cwd();
|
|
446
|
-
const esbuild = resolveEsbuild(options, cwd);
|
|
447
|
-
if (!esbuild) {
|
|
448
|
-
throw new Error("esbuild is not installed. Run 'npm install --save-dev esbuild' to build assets.");
|
|
449
|
-
}
|
|
450
|
-
|
|
451
778
|
const entryPoints = resolveEntryPoints(options.entryPoints, cwd);
|
|
779
|
+
|
|
780
|
+
discoverClientControllers(cwd);
|
|
781
|
+
validateClientBundleBoundaries(entryPoints, cwd);
|
|
782
|
+
|
|
452
783
|
const hasEntryPoints = Array.isArray(entryPoints)
|
|
453
784
|
? entryPoints.length > 0
|
|
454
785
|
: Boolean(entryPoints && Object.keys(entryPoints).length > 0);
|
|
@@ -459,6 +790,23 @@ async function buildBundle(options = {}) {
|
|
|
459
790
|
|
|
460
791
|
const outdir = options.outdir ? path.resolve(cwd, options.outdir) : path.resolve(cwd, 'public/dist');
|
|
461
792
|
const publicDir = options.publicDir ? path.resolve(cwd, options.publicDir) : path.resolve(cwd, 'public');
|
|
793
|
+
const manifestPath = options.manifestPath || path.join(outdir, 'manifest.json');
|
|
794
|
+
|
|
795
|
+
const esbuild = resolveEsbuild(options, cwd);
|
|
796
|
+
if (!esbuild) {
|
|
797
|
+
if (options.esbuild === null) {
|
|
798
|
+
throw new Error("esbuild is not installed. Run 'npm install --save-dev esbuild' to build assets.");
|
|
799
|
+
}
|
|
800
|
+
return buildBundleFallback({
|
|
801
|
+
cwd,
|
|
802
|
+
entryPoints,
|
|
803
|
+
outdir,
|
|
804
|
+
publicDir,
|
|
805
|
+
manifestPath,
|
|
806
|
+
hash: options.hash !== false,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
462
810
|
const bundle = options.bundle !== false;
|
|
463
811
|
const minify = options.minify !== undefined ? options.minify : true;
|
|
464
812
|
const sourcemap = options.sourcemap !== undefined ? options.sourcemap : true;
|
|
@@ -466,7 +814,6 @@ async function buildBundle(options = {}) {
|
|
|
466
814
|
const format = options.format || 'esm';
|
|
467
815
|
const outbase = options.outbase || (fs.existsSync(path.resolve(cwd, 'src/client')) ? path.resolve(cwd, 'src/client') : undefined);
|
|
468
816
|
const entryNames = options.entryNames || (options.hash !== false ? '[dir]/[name].[hash]' : '[dir]/[name]');
|
|
469
|
-
const manifestPath = options.manifestPath || path.join(outdir, 'manifest.json');
|
|
470
817
|
const userPlugins = Array.isArray(options.plugins) ? options.plugins : [];
|
|
471
818
|
|
|
472
819
|
const manifestPlugin = createManifestPlugin({
|
|
@@ -481,13 +828,20 @@ async function buildBundle(options = {}) {
|
|
|
481
828
|
outdir,
|
|
482
829
|
bundle,
|
|
483
830
|
minify,
|
|
831
|
+
treeShaking: true,
|
|
832
|
+
legalComments: options.legalComments || 'none',
|
|
833
|
+
define: {
|
|
834
|
+
'process.env.NODE_ENV': '"production"',
|
|
835
|
+
...(options.define || {}),
|
|
836
|
+
},
|
|
837
|
+
drop: options.drop !== undefined ? options.drop : ['debugger'],
|
|
484
838
|
sourcemap,
|
|
485
839
|
target,
|
|
486
840
|
format,
|
|
487
841
|
entryNames,
|
|
488
842
|
outbase,
|
|
489
843
|
metafile: true,
|
|
490
|
-
plugins: [manifestPlugin, ...userPlugins],
|
|
844
|
+
plugins: [createServerBoundaryPlugin(cwd), manifestPlugin, ...userPlugins],
|
|
491
845
|
...(options.esbuildOptions || {}),
|
|
492
846
|
};
|
|
493
847
|
|
|
@@ -504,6 +858,74 @@ async function buildBundle(options = {}) {
|
|
|
504
858
|
};
|
|
505
859
|
}
|
|
506
860
|
|
|
861
|
+
/**
|
|
862
|
+
* Built-in fallback asset builder when esbuild is not installed in node_modules.
|
|
863
|
+
* Compiles `.ts` / `.js` client entrypoints, hashes filenames for cache-busting,
|
|
864
|
+
* and writes `manifest.json`.
|
|
865
|
+
*/
|
|
866
|
+
function buildBundleFallback({ cwd, entryPoints, outdir, publicDir, manifestPath, hash = true }) {
|
|
867
|
+
const crypto = require('crypto');
|
|
868
|
+
let ts = null;
|
|
869
|
+
try {
|
|
870
|
+
ts = require('typescript');
|
|
871
|
+
} catch (_) {}
|
|
872
|
+
|
|
873
|
+
const entriesMap = Array.isArray(entryPoints)
|
|
874
|
+
? Object.fromEntries(
|
|
875
|
+
entryPoints.map((fp) => {
|
|
876
|
+
const base = path.basename(fp, path.extname(fp)).replace(/\.client$/i, '');
|
|
877
|
+
return [base, fp];
|
|
878
|
+
})
|
|
879
|
+
)
|
|
880
|
+
: entryPoints;
|
|
881
|
+
|
|
882
|
+
const metafile = { inputs: {}, outputs: {} };
|
|
883
|
+
|
|
884
|
+
for (const [entryKey, rawFilePath] of Object.entries(entriesMap)) {
|
|
885
|
+
const absIn = path.resolve(cwd, rawFilePath);
|
|
886
|
+
if (!fs.existsSync(absIn)) continue;
|
|
887
|
+
const rawCode = fs.readFileSync(absIn, 'utf8');
|
|
888
|
+
let outCode = rawCode;
|
|
889
|
+
|
|
890
|
+
if ((absIn.endsWith('.ts') || absIn.endsWith('.tsx')) && ts) {
|
|
891
|
+
const transpiled = ts.transpileModule(rawCode, {
|
|
892
|
+
compilerOptions: {
|
|
893
|
+
module: ts.ModuleKind.ESNext,
|
|
894
|
+
target: ts.ScriptTarget.ES2020,
|
|
895
|
+
},
|
|
896
|
+
fileName: absIn,
|
|
897
|
+
});
|
|
898
|
+
outCode = transpiled.outputText;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const digest = hash
|
|
902
|
+
? '.' + crypto.createHash('sha256').update(outCode).digest('hex').slice(0, 8).toUpperCase()
|
|
903
|
+
: '';
|
|
904
|
+
const relOutFile = `${entryKey}${digest}.js`;
|
|
905
|
+
const absOut = path.join(outdir, relOutFile);
|
|
906
|
+
fs.mkdirSync(path.dirname(absOut), { recursive: true });
|
|
907
|
+
fs.writeFileSync(absOut, outCode, 'utf8');
|
|
908
|
+
|
|
909
|
+
const relInput = path.relative(cwd, absIn).replace(/\\/g, '/');
|
|
910
|
+
const relOutput = path.relative(cwd, absOut).replace(/\\/g, '/');
|
|
911
|
+
metafile.inputs[relInput] = { bytes: Buffer.byteLength(rawCode, 'utf8') };
|
|
912
|
+
metafile.outputs[relOutput] = {
|
|
913
|
+
entryPoint: relInput,
|
|
914
|
+
bytes: Buffer.byteLength(outCode, 'utf8'),
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const manifest = generateManifest(metafile, { outdir, publicDir });
|
|
919
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
920
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
result: { errors: [], warnings: [], metafile },
|
|
924
|
+
manifest,
|
|
925
|
+
manifestPath,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
507
929
|
module.exports = {
|
|
508
930
|
createDevBundler,
|
|
509
931
|
buildBundle,
|
|
@@ -511,6 +933,10 @@ module.exports = {
|
|
|
511
933
|
createManifestPlugin,
|
|
512
934
|
findDefaultEntryPoints,
|
|
513
935
|
findPageEntryPoints,
|
|
936
|
+
discoverClientControllers,
|
|
937
|
+
checkServerImportViolation,
|
|
938
|
+
validateClientBundleBoundaries,
|
|
939
|
+
createServerBoundaryPlugin,
|
|
514
940
|
scanClientPages,
|
|
515
941
|
expandGlob,
|
|
516
942
|
resolveEntryPoints,
|
|
@@ -22,5 +22,14 @@
|
|
|
22
22
|
plugins: [new SwupHeadPlugin(), new SwupScriptsPlugin()],
|
|
23
23
|
ignoreVisit: ignoreVisit,
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
// Re-initialize Alpine components after swup replaces page content if Alpine is loaded on the page
|
|
27
|
+
swup.hooks.on('content:replace', function () {
|
|
28
|
+
var root = document.querySelector('#swup');
|
|
29
|
+
if (root && typeof window !== 'undefined' && window.Alpine && typeof window.Alpine.initTree === 'function') {
|
|
30
|
+
window.Alpine.initTree(root);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
25
34
|
window.__webspressoSwup = swup;
|
|
26
35
|
})();
|