webspresso 0.0.96 → 0.0.98
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/ADMIN.md +153 -5
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/CLI.md +195 -1
- package/.agents/PLUGINS.md +153 -9
- package/.agents/QUEUE.md +43 -0
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
- package/.agents/skills/webspresso-framework/SKILL.md +56 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
- package/.agents/skills/webspresso-services/SKILL.md +101 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/admin-password.js +2 -1
- package/bin/commands/admin-setup.js +1 -1
- package/bin/commands/agents-init.js +51 -11
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/audit-prune.js +1 -1
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/favicon-generate.js +1 -1
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/orm-map.js +5 -4
- package/bin/commands/page.js +6 -3
- package/bin/commands/polar-migrate.js +1 -1
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/seed.js +1 -1
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +31 -17
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/index.js +12 -7
- package/core/orm/model.js +49 -7
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/manager.js +123 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +660 -10
- package/index.js +44 -96
- package/package.json +32 -12
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +124 -31
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +34 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/audit-log/admin-component.js +11 -0
- package/plugins/audit-log/api-handlers.js +34 -0
- package/plugins/audit-log/index.js +129 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/basic-auth/index.js +3 -1
- package/plugins/content/client/inline-edit.js +12 -12
- package/plugins/cors.js +6 -7
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/core.js +1 -1
- package/plugins/file-manager/security.js +33 -23
- package/plugins/index.js +49 -87
- package/plugins/mcp/transports/sse.js +4 -2
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +11 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +30 -8
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +3 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/file-router.js +213 -63
- package/src/helpers.js +958 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/server.js +629 -63
- package/src/services/discovery.js +6 -1
- package/src/services/index.js +6 -13
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Bundler — esbuild context() API incremental bundling & dev watching
|
|
3
|
+
*
|
|
4
|
+
* Integrates esbuild's official context() API for sub-millisecond incremental
|
|
5
|
+
* rebuilds, auto-detection of client entrypoints, zero zombie processes, and
|
|
6
|
+
* seamless integration with Webspresso Dev Reload (SSE).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_ENTRYPOINTS = [
|
|
13
|
+
'src/client/main.js',
|
|
14
|
+
'src/client/main.ts',
|
|
15
|
+
'src/client/index.js',
|
|
16
|
+
'src/client/index.ts',
|
|
17
|
+
'src/client/app.js',
|
|
18
|
+
'src/client/app.ts',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Recursively scans directory for page script entrypoints (.js, .mjs, .ts, .jsx, .tsx)
|
|
23
|
+
* Excludes .d.ts, .test.*, .spec.*, and hidden files
|
|
24
|
+
* @param {string} dir - Directory to scan (e.g. src/client/pages)
|
|
25
|
+
* @param {string} baseDir - Base client directory (e.g. src/client)
|
|
26
|
+
* @returns {Array<{ entryName: string, filePath: string }>}
|
|
27
|
+
*/
|
|
28
|
+
function scanClientPages(dir, baseDir) {
|
|
29
|
+
if (!fs.existsSync(dir)) return [];
|
|
30
|
+
const entries = [];
|
|
31
|
+
const validExts = new Set(['.js', '.mjs', '.ts', '.jsx', '.tsx']);
|
|
32
|
+
|
|
33
|
+
function walk(currentDir) {
|
|
34
|
+
let list;
|
|
35
|
+
try {
|
|
36
|
+
list = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
37
|
+
} catch (_) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const item of list) {
|
|
41
|
+
if (item.name.startsWith('.')) continue;
|
|
42
|
+
const fullPath = path.join(currentDir, item.name);
|
|
43
|
+
if (item.isDirectory()) {
|
|
44
|
+
walk(fullPath);
|
|
45
|
+
} else if (item.isFile()) {
|
|
46
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
47
|
+
if (validExts.has(ext)) {
|
|
48
|
+
if (item.name.endsWith('.d.ts') || item.name.includes('.test.') || item.name.includes('.spec.')) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
52
|
+
const entryName = relPath.slice(0, -ext.length);
|
|
53
|
+
entries.push({ entryName, filePath: fullPath });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
walk(dir);
|
|
60
|
+
return entries;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Discovers page-specific client entrypoints in src/client/pages/ or src/pages/
|
|
65
|
+
* @param {string} [cwd=process.cwd()] - Project root directory
|
|
66
|
+
* @returns {Record<string, string>} Map of entry names to absolute file paths (e.g. { 'pages/home': '.../src/client/pages/home.js' })
|
|
67
|
+
*/
|
|
68
|
+
function findPageEntryPoints(cwd = process.cwd()) {
|
|
69
|
+
const pageEntries = {};
|
|
70
|
+
|
|
71
|
+
// 1. Primary convention: src/client/pages/
|
|
72
|
+
const clientPagesDir = path.resolve(cwd, 'src/client/pages');
|
|
73
|
+
const clientDir = path.resolve(cwd, 'src/client');
|
|
74
|
+
if (fs.existsSync(clientPagesDir)) {
|
|
75
|
+
const scanned = scanClientPages(clientPagesDir, clientDir);
|
|
76
|
+
for (const item of scanned) {
|
|
77
|
+
pageEntries[item.entryName] = item.filePath;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Secondary convention: src/pages/ with *.client.{js,ts,jsx,tsx}
|
|
82
|
+
const srcPagesDir = path.resolve(cwd, 'src/pages');
|
|
83
|
+
if (fs.existsSync(srcPagesDir)) {
|
|
84
|
+
const validExts = new Set(['.js', '.mjs', '.ts', '.jsx', '.tsx']);
|
|
85
|
+
function walkSrcPages(currentDir) {
|
|
86
|
+
let list;
|
|
87
|
+
try {
|
|
88
|
+
list = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
89
|
+
} catch (_) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const item of list) {
|
|
93
|
+
if (item.name.startsWith('.')) continue;
|
|
94
|
+
const fullPath = path.join(currentDir, item.name);
|
|
95
|
+
if (item.isDirectory()) {
|
|
96
|
+
walkSrcPages(fullPath);
|
|
97
|
+
} else if (item.isFile()) {
|
|
98
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
99
|
+
if (validExts.has(ext) && item.name.includes('.client.')) {
|
|
100
|
+
const rel = path.relative(srcPagesDir, fullPath).replace(/\\/g, '/');
|
|
101
|
+
const cleanName = rel.replace(/\.client\.[a-z0-9]+$/i, '');
|
|
102
|
+
const entryName = 'pages/' + cleanName;
|
|
103
|
+
if (!pageEntries[entryName]) {
|
|
104
|
+
pageEntries[entryName] = fullPath;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
walkSrcPages(srcPagesDir);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return pageEntries;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Simple glob expansion for client entrypoints (supports single and double wildcards)
|
|
118
|
+
* @param {string} pattern - Glob pattern (e.g. 'src/client/pages/*.js')
|
|
119
|
+
* @param {string} [cwd=process.cwd()] - Base directory
|
|
120
|
+
* @returns {string[]} Matching absolute file paths
|
|
121
|
+
*/
|
|
122
|
+
function expandGlob(pattern, cwd = process.cwd()) {
|
|
123
|
+
const normalized = pattern.replace(/\\/g, '/');
|
|
124
|
+
const starIndex = normalized.indexOf('*');
|
|
125
|
+
if (starIndex === -1) {
|
|
126
|
+
const p = path.resolve(cwd, pattern);
|
|
127
|
+
return fs.existsSync(p) ? [p] : [];
|
|
128
|
+
}
|
|
129
|
+
const prefix = normalized.slice(0, starIndex);
|
|
130
|
+
const baseDir = path.resolve(cwd, prefix.endsWith('/') ? prefix : path.dirname(prefix));
|
|
131
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
132
|
+
|
|
133
|
+
const isRecursive = normalized.includes('**');
|
|
134
|
+
const regexPattern = '^' + normalized
|
|
135
|
+
.replace(/\./g, '\\.')
|
|
136
|
+
.replace(/\*\*\//g, '(.+/)?')
|
|
137
|
+
.replace(/\*\*/g, '.*')
|
|
138
|
+
.replace(/\*/g, '[^/]*') + '$';
|
|
139
|
+
const regex = new RegExp(regexPattern);
|
|
140
|
+
|
|
141
|
+
const matched = [];
|
|
142
|
+
function walk(dir) {
|
|
143
|
+
let items;
|
|
144
|
+
try {
|
|
145
|
+
items = fs.readdirSync(dir, { withFileTypes: true });
|
|
146
|
+
} catch (_) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (const item of items) {
|
|
150
|
+
if (item.name.startsWith('.')) continue;
|
|
151
|
+
const fullPath = path.join(dir, item.name);
|
|
152
|
+
const relPath = path.relative(cwd, fullPath).replace(/\\/g, '/');
|
|
153
|
+
if (item.isDirectory()) {
|
|
154
|
+
if (isRecursive) walk(fullPath);
|
|
155
|
+
} else if (item.isFile()) {
|
|
156
|
+
if (regex.test(relPath)) {
|
|
157
|
+
matched.push(fullPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
walk(baseDir);
|
|
163
|
+
return matched;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolves entry points from options, expanding globs or discovering defaults
|
|
168
|
+
* @param {string[]|Record<string, string>|string} [entryPoints]
|
|
169
|
+
* @param {string} [cwd=process.cwd()]
|
|
170
|
+
* @returns {string[]|Record<string, string>}
|
|
171
|
+
*/
|
|
172
|
+
function resolveEntryPoints(entryPoints, cwd = process.cwd()) {
|
|
173
|
+
if (!entryPoints || (Array.isArray(entryPoints) && entryPoints.length === 0)) {
|
|
174
|
+
return findDefaultEntryPoints(cwd);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Object mapping (e.g. { 'main': 'src/client/main.js', 'pages/home': 'src/client/pages/home.js' })
|
|
178
|
+
if (typeof entryPoints === 'object' && !Array.isArray(entryPoints)) {
|
|
179
|
+
return entryPoints;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Array of paths / globs
|
|
183
|
+
const list = Array.isArray(entryPoints) ? entryPoints : [entryPoints];
|
|
184
|
+
const expanded = [];
|
|
185
|
+
for (const item of list) {
|
|
186
|
+
if (typeof item === 'string' && item.includes('*')) {
|
|
187
|
+
expanded.push(...expandGlob(item, cwd));
|
|
188
|
+
} else if (typeof item === 'string') {
|
|
189
|
+
expanded.push(item);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return expanded;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Finds default client entry points if none were specified
|
|
198
|
+
* Discovers main bundle and page-specific entrypoints in src/client/pages/
|
|
199
|
+
* @param {string} [cwd=process.cwd()]
|
|
200
|
+
* @returns {string[]|Record<string, string>}
|
|
201
|
+
*/
|
|
202
|
+
function findDefaultEntryPoints(cwd = process.cwd()) {
|
|
203
|
+
let mainEntry = null;
|
|
204
|
+
for (const entry of DEFAULT_ENTRYPOINTS) {
|
|
205
|
+
const fullPath = path.resolve(cwd, entry);
|
|
206
|
+
if (fs.existsSync(fullPath)) {
|
|
207
|
+
mainEntry = fullPath;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const pageEntries = findPageEntryPoints(cwd);
|
|
213
|
+
const pageEntryKeys = Object.keys(pageEntries);
|
|
214
|
+
|
|
215
|
+
// If no page entries exist, return single-item array or empty array (100% backward compatible)
|
|
216
|
+
if (pageEntryKeys.length === 0) {
|
|
217
|
+
return mainEntry ? [mainEntry] : [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If page entries exist, return an entrypoints dictionary
|
|
221
|
+
const entryMap = {};
|
|
222
|
+
if (mainEntry) {
|
|
223
|
+
const ext = path.extname(mainEntry);
|
|
224
|
+
const mainBase = path.basename(mainEntry, ext); // 'main', 'index', or 'app'
|
|
225
|
+
entryMap[mainBase] = mainEntry;
|
|
226
|
+
}
|
|
227
|
+
for (const [key, filePath] of Object.entries(pageEntries)) {
|
|
228
|
+
entryMap[key] = filePath;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return entryMap;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolves esbuild module dynamically from project or options
|
|
236
|
+
* @param {object} options
|
|
237
|
+
* @param {string} cwd
|
|
238
|
+
* @returns {object|null}
|
|
239
|
+
*/
|
|
240
|
+
function resolveEsbuild(options = {}, cwd = process.cwd()) {
|
|
241
|
+
if (options.esbuild) return options.esbuild;
|
|
242
|
+
try {
|
|
243
|
+
return require('esbuild');
|
|
244
|
+
} catch (_) {
|
|
245
|
+
try {
|
|
246
|
+
return require(path.join(cwd, 'node_modules', 'esbuild'));
|
|
247
|
+
} catch (_) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Creates an esbuild-powered dev bundler using esbuild context() API
|
|
255
|
+
*
|
|
256
|
+
* @param {object} [options={}]
|
|
257
|
+
* @param {object} [devReloadHub=null]
|
|
258
|
+
* @returns {DevBundler}
|
|
259
|
+
*/
|
|
260
|
+
function createDevBundler(options = {}, devReloadHub = null) {
|
|
261
|
+
const cwd = options.cwd || process.cwd();
|
|
262
|
+
const esbuild = resolveEsbuild(options, cwd);
|
|
263
|
+
const isAvailable = Boolean(esbuild);
|
|
264
|
+
|
|
265
|
+
const entryPoints = resolveEntryPoints(options.entryPoints, cwd);
|
|
266
|
+
|
|
267
|
+
const outdir = options.outdir ? path.resolve(cwd, options.outdir) : path.resolve(cwd, 'public/js');
|
|
268
|
+
const bundle = options.bundle !== false;
|
|
269
|
+
const sourcemap = options.sourcemap !== undefined ? options.sourcemap : true;
|
|
270
|
+
const minify = options.minify !== undefined ? options.minify : false;
|
|
271
|
+
const target = options.target || ['es2020'];
|
|
272
|
+
const format = options.format || 'esm';
|
|
273
|
+
const metafile = options.metafile !== false;
|
|
274
|
+
const userPlugins = Array.isArray(options.plugins) ? options.plugins : [];
|
|
275
|
+
const logger = options.logger !== undefined ? options.logger : console;
|
|
276
|
+
|
|
277
|
+
let ctx = null;
|
|
278
|
+
let isWatching = false;
|
|
279
|
+
let isDisposed = false;
|
|
280
|
+
|
|
281
|
+
// Custom dev-reload plugin for esbuild
|
|
282
|
+
const devReloadPlugin = {
|
|
283
|
+
name: 'webspresso-dev-reload',
|
|
284
|
+
setup(build) {
|
|
285
|
+
let startTime = 0;
|
|
286
|
+
let hadError = false;
|
|
287
|
+
|
|
288
|
+
build.onStart(() => {
|
|
289
|
+
startTime = Date.now();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
build.onEnd((result) => {
|
|
293
|
+
const duration = Date.now() - startTime;
|
|
294
|
+
if (result.errors && result.errors.length > 0) {
|
|
295
|
+
hadError = true;
|
|
296
|
+
if (logger && logger.error) {
|
|
297
|
+
logger.error(`❌ [Webspresso esbuild] Build failed (${duration}ms):`, result.errors);
|
|
298
|
+
}
|
|
299
|
+
if (devReloadHub && typeof devReloadHub.broadcast === 'function') {
|
|
300
|
+
const firstErr = result.errors[0] || {};
|
|
301
|
+
const loc = firstErr.location || {};
|
|
302
|
+
devReloadHub.broadcast('error', {
|
|
303
|
+
title: 'esbuild Build Error',
|
|
304
|
+
message: firstErr.text || 'Build failed with syntax error',
|
|
305
|
+
file: loc.file,
|
|
306
|
+
line: loc.line,
|
|
307
|
+
column: loc.column,
|
|
308
|
+
lineText: loc.lineText,
|
|
309
|
+
errors: result.errors.map((e) => ({
|
|
310
|
+
text: e.text,
|
|
311
|
+
file: e.location && e.location.file,
|
|
312
|
+
line: e.location && e.location.line,
|
|
313
|
+
column: e.location && e.location.column,
|
|
314
|
+
lineText: e.location && e.location.lineText,
|
|
315
|
+
})),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (logger && logger.log) {
|
|
322
|
+
logger.log(`⚡ [Webspresso esbuild] Incremental rebuild finished in ${duration}ms`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (devReloadHub && typeof devReloadHub.broadcast === 'function') {
|
|
326
|
+
if (hadError) {
|
|
327
|
+
devReloadHub.broadcast('clear-error');
|
|
328
|
+
hadError = false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let hasCssOnly = false;
|
|
332
|
+
if (result.metafile && result.metafile.outputs) {
|
|
333
|
+
const outputFiles = Object.keys(result.metafile.outputs);
|
|
334
|
+
const hasJs = outputFiles.some((f) => f.endsWith('.js') || f.endsWith('.mjs'));
|
|
335
|
+
const hasCss = outputFiles.some((f) => f.endsWith('.css'));
|
|
336
|
+
if (hasCss && !hasJs) {
|
|
337
|
+
hasCssOnly = true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (hasCssOnly) {
|
|
342
|
+
devReloadHub.broadcast('css');
|
|
343
|
+
} else {
|
|
344
|
+
devReloadHub.broadcast('full');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
async function getOrInitContext() {
|
|
352
|
+
if (ctx) return ctx;
|
|
353
|
+
if (!isAvailable) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
"esbuild is not installed. Run 'npm install --save-dev esbuild' or 'npx webspresso add esbuild' to enable client bundling."
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const hasEntryPoints = Array.isArray(entryPoints)
|
|
360
|
+
? entryPoints.length > 0
|
|
361
|
+
: Boolean(entryPoints && Object.keys(entryPoints).length > 0);
|
|
362
|
+
|
|
363
|
+
if (!hasEntryPoints) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
"No entrypoints found for esbuild bundler. Place your entry script in 'src/client/main.js' or specify 'entryPoints' option."
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const outbase = options.outbase || (fs.existsSync(path.resolve(cwd, 'src/client')) ? path.resolve(cwd, 'src/client') : undefined);
|
|
370
|
+
const esbuildConfig = {
|
|
371
|
+
entryPoints,
|
|
372
|
+
outdir,
|
|
373
|
+
bundle,
|
|
374
|
+
sourcemap,
|
|
375
|
+
minify,
|
|
376
|
+
target,
|
|
377
|
+
format,
|
|
378
|
+
metafile,
|
|
379
|
+
outbase,
|
|
380
|
+
plugins: [devReloadPlugin, ...userPlugins],
|
|
381
|
+
...(options.esbuildOptions || {}),
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (typeof esbuild.context !== 'function') {
|
|
385
|
+
throw new Error("esbuild.context() is not available. Please ensure esbuild >= 0.17.0 is installed.");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
ctx = await esbuild.context(esbuildConfig);
|
|
389
|
+
return ctx;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function startWatch() {
|
|
393
|
+
if (isDisposed) throw new Error('DevBundler has already been disposed.');
|
|
394
|
+
if (isWatching) return ctx;
|
|
395
|
+
const context = await getOrInitContext();
|
|
396
|
+
await context.watch();
|
|
397
|
+
isWatching = true;
|
|
398
|
+
if (logger && logger.log) {
|
|
399
|
+
logger.log('⚡ [Webspresso esbuild] Incremental watch mode active using context() API');
|
|
400
|
+
}
|
|
401
|
+
return context;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function rebuild() {
|
|
405
|
+
if (isDisposed) throw new Error('DevBundler has already been disposed.');
|
|
406
|
+
const context = await getOrInitContext();
|
|
407
|
+
return await context.rebuild();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function dispose() {
|
|
411
|
+
if (isDisposed) return;
|
|
412
|
+
isDisposed = true;
|
|
413
|
+
isWatching = false;
|
|
414
|
+
if (ctx) {
|
|
415
|
+
try {
|
|
416
|
+
await ctx.dispose();
|
|
417
|
+
} catch (_) {}
|
|
418
|
+
ctx = null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
isAvailable: () => isAvailable,
|
|
424
|
+
isWatching: () => isWatching,
|
|
425
|
+
isDisposed: () => isDisposed,
|
|
426
|
+
getEntryPoints: () => entryPoints,
|
|
427
|
+
getOutdir: () => outdir,
|
|
428
|
+
startWatch,
|
|
429
|
+
rebuild,
|
|
430
|
+
dispose,
|
|
431
|
+
getContext: () => ctx,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const { generateManifest, createManifestPlugin } = require('./manifest');
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* One-shot production bundle builder with automatic metafile: true and manifest.json generation
|
|
439
|
+
*
|
|
440
|
+
* @param {object} [options={}]
|
|
441
|
+
* @returns {Promise<{ result: any, manifest: any, manifestPath: string }>}
|
|
442
|
+
*/
|
|
443
|
+
async function buildBundle(options = {}) {
|
|
444
|
+
const cwd = options.cwd || process.cwd();
|
|
445
|
+
const esbuild = resolveEsbuild(options, cwd);
|
|
446
|
+
if (!esbuild) {
|
|
447
|
+
throw new Error("esbuild is not installed. Run 'npm install --save-dev esbuild' to build assets.");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const entryPoints = resolveEntryPoints(options.entryPoints, cwd);
|
|
451
|
+
const hasEntryPoints = Array.isArray(entryPoints)
|
|
452
|
+
? entryPoints.length > 0
|
|
453
|
+
: Boolean(entryPoints && Object.keys(entryPoints).length > 0);
|
|
454
|
+
|
|
455
|
+
if (!hasEntryPoints) {
|
|
456
|
+
throw new Error("No client entry points found in 'src/client/main.js' or 'src/client/pages/'. Specify entryPoints option.");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const outdir = options.outdir ? path.resolve(cwd, options.outdir) : path.resolve(cwd, 'public/dist');
|
|
460
|
+
const publicDir = options.publicDir ? path.resolve(cwd, options.publicDir) : path.resolve(cwd, 'public');
|
|
461
|
+
const bundle = options.bundle !== false;
|
|
462
|
+
const minify = options.minify !== undefined ? options.minify : true;
|
|
463
|
+
const sourcemap = options.sourcemap !== undefined ? options.sourcemap : true;
|
|
464
|
+
const target = options.target || ['es2020'];
|
|
465
|
+
const format = options.format || 'esm';
|
|
466
|
+
const outbase = options.outbase || (fs.existsSync(path.resolve(cwd, 'src/client')) ? path.resolve(cwd, 'src/client') : undefined);
|
|
467
|
+
const entryNames = options.entryNames || (options.hash !== false ? '[dir]/[name].[hash]' : '[dir]/[name]');
|
|
468
|
+
const manifestPath = options.manifestPath || path.join(outdir, 'manifest.json');
|
|
469
|
+
const userPlugins = Array.isArray(options.plugins) ? options.plugins : [];
|
|
470
|
+
|
|
471
|
+
const manifestPlugin = createManifestPlugin({
|
|
472
|
+
outdir,
|
|
473
|
+
publicDir,
|
|
474
|
+
manifestPath,
|
|
475
|
+
logger: options.logger,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const buildConfig = {
|
|
479
|
+
entryPoints,
|
|
480
|
+
outdir,
|
|
481
|
+
bundle,
|
|
482
|
+
minify,
|
|
483
|
+
sourcemap,
|
|
484
|
+
target,
|
|
485
|
+
format,
|
|
486
|
+
entryNames,
|
|
487
|
+
outbase,
|
|
488
|
+
metafile: true,
|
|
489
|
+
plugins: [manifestPlugin, ...userPlugins],
|
|
490
|
+
...(options.esbuildOptions || {}),
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const result = await esbuild.build(buildConfig);
|
|
494
|
+
const manifest = generateManifest(result.metafile, {
|
|
495
|
+
outdir,
|
|
496
|
+
publicDir,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
result,
|
|
501
|
+
manifest,
|
|
502
|
+
manifestPath,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
module.exports = {
|
|
507
|
+
createDevBundler,
|
|
508
|
+
buildBundle,
|
|
509
|
+
generateManifest,
|
|
510
|
+
createManifestPlugin,
|
|
511
|
+
findDefaultEntryPoints,
|
|
512
|
+
findPageEntryPoints,
|
|
513
|
+
scanClientPages,
|
|
514
|
+
expandGlob,
|
|
515
|
+
resolveEntryPoints,
|
|
516
|
+
DEFAULT_ENTRYPOINTS,
|
|
517
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest Generator & esbuild Plugin for Webspresso Asset Management
|
|
3
|
+
*
|
|
4
|
+
* Generates production manifest.json from esbuild metafile for zero-config
|
|
5
|
+
* cache busting and seamless integration with Webspresso AssetManager (fsy.asset, fsy.js, fsy.css).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a Webspresso / Vite-compatible manifest object from an esbuild metafile
|
|
13
|
+
*
|
|
14
|
+
* @param {object} metafile - esbuild build result metafile
|
|
15
|
+
* @param {object} [options={}]
|
|
16
|
+
* @param {string} [options.outdir='public/dist']
|
|
17
|
+
* @param {string} [options.publicDir='public']
|
|
18
|
+
* @returns {Record<string, { file: string, src: string, bytes: number }>}
|
|
19
|
+
*/
|
|
20
|
+
function generateManifest(metafile, options = {}) {
|
|
21
|
+
const publicDir = (options.publicDir || 'public').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
22
|
+
const manifest = {};
|
|
23
|
+
|
|
24
|
+
if (!metafile || !metafile.outputs) {
|
|
25
|
+
return manifest;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const [outPathRaw, meta] of Object.entries(metafile.outputs)) {
|
|
29
|
+
// Skip sourcemap entries
|
|
30
|
+
if (outPathRaw.endsWith('.map')) continue;
|
|
31
|
+
|
|
32
|
+
const outPath = outPathRaw.replace(/\\/g, '/');
|
|
33
|
+
let publicUrl = outPath;
|
|
34
|
+
if (publicUrl.startsWith(publicDir + '/')) {
|
|
35
|
+
publicUrl = publicUrl.slice(publicDir.length + 1);
|
|
36
|
+
} else if (publicUrl.startsWith(publicDir)) {
|
|
37
|
+
publicUrl = publicUrl.slice(publicDir.length);
|
|
38
|
+
}
|
|
39
|
+
if (publicUrl.startsWith('/')) {
|
|
40
|
+
publicUrl = publicUrl.slice(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const entryPoint = meta.entryPoint ? meta.entryPoint.replace(/\\/g, '/') : null;
|
|
44
|
+
const baseOutName = path.basename(publicUrl);
|
|
45
|
+
|
|
46
|
+
const record = {
|
|
47
|
+
file: publicUrl,
|
|
48
|
+
src: entryPoint || baseOutName,
|
|
49
|
+
bytes: meta.bytes || 0,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (entryPoint) {
|
|
53
|
+
// 1. Exact entry point (e.g. "src/client/main.js")
|
|
54
|
+
manifest[entryPoint] = record;
|
|
55
|
+
|
|
56
|
+
// 2. Base entry name (e.g. "main.js")
|
|
57
|
+
const entryBase = path.basename(entryPoint);
|
|
58
|
+
if (!manifest[entryBase]) {
|
|
59
|
+
manifest[entryBase] = record;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Normalized without src/ (e.g. "client/main.js")
|
|
63
|
+
const trimmedSrc = entryPoint.replace(/^src\//, '');
|
|
64
|
+
if (!manifest[trimmedSrc]) {
|
|
65
|
+
manifest[trimmedSrc] = record;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. Normalized relative to client root (e.g. "pages/home.js")
|
|
69
|
+
let clientRel = '';
|
|
70
|
+
const srcClientIdx = entryPoint.indexOf('src/client/');
|
|
71
|
+
if (srcClientIdx !== -1) {
|
|
72
|
+
clientRel = entryPoint.slice(srcClientIdx + 'src/client/'.length);
|
|
73
|
+
} else {
|
|
74
|
+
const clientIdx = entryPoint.indexOf('client/');
|
|
75
|
+
if (clientIdx !== -1) {
|
|
76
|
+
clientRel = entryPoint.slice(clientIdx + 'client/'.length);
|
|
77
|
+
} else {
|
|
78
|
+
clientRel = entryPoint.replace(/^src\//, '');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (clientRel && !manifest[clientRel]) {
|
|
83
|
+
manifest[clientRel] = record;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 5. Conventional js/ prefix (e.g. "js/main.js" or "js/pages/home.js")
|
|
87
|
+
const jsPrefixed = 'js/' + clientRel;
|
|
88
|
+
if (!manifest[jsPrefixed]) {
|
|
89
|
+
manifest[jsPrefixed] = record;
|
|
90
|
+
}
|
|
91
|
+
const jsBasePrefixed = 'js/' + entryBase;
|
|
92
|
+
if (!manifest[jsBasePrefixed]) {
|
|
93
|
+
manifest[jsBasePrefixed] = record;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 6. Conventional css/ prefix for CSS inputs
|
|
97
|
+
if (entryBase.endsWith('.css')) {
|
|
98
|
+
const cssPrefixed = 'css/' + clientRel;
|
|
99
|
+
if (!manifest[cssPrefixed]) {
|
|
100
|
+
manifest[cssPrefixed] = record;
|
|
101
|
+
}
|
|
102
|
+
const cssBasePrefixed = 'css/' + entryBase;
|
|
103
|
+
if (!manifest[cssBasePrefixed]) {
|
|
104
|
+
manifest[cssBasePrefixed] = record;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 7. Key without file extension (e.g. "pages/home", "home")
|
|
109
|
+
const nameWithoutExt = clientRel.replace(/\.[a-z0-9]+$/i, '');
|
|
110
|
+
if (!manifest[nameWithoutExt]) {
|
|
111
|
+
manifest[nameWithoutExt] = record;
|
|
112
|
+
}
|
|
113
|
+
const jsNameWithoutExt = 'js/' + nameWithoutExt;
|
|
114
|
+
if (!manifest[jsNameWithoutExt]) {
|
|
115
|
+
manifest[jsNameWithoutExt] = record;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
manifest[baseOutName] = record;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return manifest;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates an esbuild plugin that writes out a production manifest.json upon build completion
|
|
127
|
+
*
|
|
128
|
+
* @param {object} [options={}]
|
|
129
|
+
* @param {string} [options.manifestPath]
|
|
130
|
+
* @param {string} [options.outdir='public/dist']
|
|
131
|
+
* @param {string} [options.publicDir='public']
|
|
132
|
+
* @param {object} [options.logger=console]
|
|
133
|
+
* @returns {object} esbuild plugin
|
|
134
|
+
*/
|
|
135
|
+
function createManifestPlugin(options = {}) {
|
|
136
|
+
const manifestPath = options.manifestPath || path.join(options.outdir || 'public/dist', 'manifest.json');
|
|
137
|
+
const logger = options.logger !== undefined ? options.logger : console;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
name: 'webspresso-manifest',
|
|
141
|
+
setup(build) {
|
|
142
|
+
build.onEnd((result) => {
|
|
143
|
+
if (!result.errors || result.errors.length === 0) {
|
|
144
|
+
if (result.metafile) {
|
|
145
|
+
const manifest = generateManifest(result.metafile, options);
|
|
146
|
+
const fullManifestPath = path.resolve(manifestPath);
|
|
147
|
+
const dir = path.dirname(fullManifestPath);
|
|
148
|
+
if (!fs.existsSync(dir)) {
|
|
149
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
fs.writeFileSync(fullManifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
152
|
+
if (logger && logger.log) {
|
|
153
|
+
logger.log(`📦 [Webspresso] Generated asset manifest: ${manifestPath}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
generateManifest,
|
|
164
|
+
createManifestPlugin,
|
|
165
|
+
};
|