webspresso 0.0.8 → 0.0.9
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/package.json +3 -2
- package/plugins/analytics.js +270 -0
- package/plugins/dashboard/app.js +267 -0
- package/plugins/dashboard/index.js +142 -0
- package/plugins/dashboard/styles.js +384 -0
- package/plugins/index.js +17 -0
- package/plugins/schema-explorer.js +398 -0
- package/plugins/sitemap.js +221 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"bin/",
|
|
34
34
|
"src/",
|
|
35
35
|
"utils/",
|
|
36
|
-
"core/"
|
|
36
|
+
"core/",
|
|
37
|
+
"plugins/"
|
|
37
38
|
],
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"commander": "^11.1.0",
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Analytics Plugin
|
|
3
|
+
* Adds tracking scripts and verification meta tags for Google, Yandex, and Bing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate Google Analytics (gtag.js) script
|
|
8
|
+
*/
|
|
9
|
+
function generateGtagScript(config) {
|
|
10
|
+
if (!config || !config.measurementId) return '';
|
|
11
|
+
|
|
12
|
+
const { measurementId, adsId } = config;
|
|
13
|
+
|
|
14
|
+
let script = `<!-- Google Analytics -->
|
|
15
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=${measurementId}"></script>
|
|
16
|
+
<script>
|
|
17
|
+
window.dataLayer = window.dataLayer || [];
|
|
18
|
+
function gtag(){dataLayer.push(arguments);}
|
|
19
|
+
gtag('js', new Date());
|
|
20
|
+
gtag('config', '${measurementId}');`;
|
|
21
|
+
|
|
22
|
+
if (adsId) {
|
|
23
|
+
script += `
|
|
24
|
+
gtag('config', '${adsId}');`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
script += `
|
|
28
|
+
</script>`;
|
|
29
|
+
|
|
30
|
+
return script;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate Google Tag Manager script
|
|
35
|
+
*/
|
|
36
|
+
function generateGtmScript(config) {
|
|
37
|
+
if (!config || !config.containerId) return '';
|
|
38
|
+
|
|
39
|
+
const { containerId } = config;
|
|
40
|
+
|
|
41
|
+
return `<!-- Google Tag Manager -->
|
|
42
|
+
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
43
|
+
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
44
|
+
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
45
|
+
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
46
|
+
})(window,document,'script','dataLayer','${containerId}');</script>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate Google Tag Manager noscript fallback
|
|
51
|
+
*/
|
|
52
|
+
function generateGtmNoscript(config) {
|
|
53
|
+
if (!config || !config.containerId) return '';
|
|
54
|
+
|
|
55
|
+
return `<!-- Google Tag Manager (noscript) -->
|
|
56
|
+
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${config.containerId}"
|
|
57
|
+
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate Yandex.Metrika script
|
|
62
|
+
*/
|
|
63
|
+
function generateYandexScript(config) {
|
|
64
|
+
if (!config || !config.counterId) return '';
|
|
65
|
+
|
|
66
|
+
const { counterId, clickmap = true, trackLinks = true, accurateTrackBounce = true, webvisor = false } = config;
|
|
67
|
+
|
|
68
|
+
return `<!-- Yandex.Metrika -->
|
|
69
|
+
<script>
|
|
70
|
+
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
|
71
|
+
m[i].l=1*new Date();
|
|
72
|
+
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
|
73
|
+
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
|
74
|
+
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
|
|
75
|
+
|
|
76
|
+
ym(${counterId}, "init", {
|
|
77
|
+
clickmap:${clickmap},
|
|
78
|
+
trackLinks:${trackLinks},
|
|
79
|
+
accurateTrackBounce:${accurateTrackBounce}${webvisor ? ',\n webvisor:true' : ''}
|
|
80
|
+
});
|
|
81
|
+
</script>
|
|
82
|
+
<noscript><div><img src="https://mc.yandex.ru/watch/${counterId}" style="position:absolute; left:-9999px;" alt="" /></div></noscript>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate Microsoft/Bing UET script
|
|
87
|
+
*/
|
|
88
|
+
function generateBingScript(config) {
|
|
89
|
+
if (!config || !config.uetId) return '';
|
|
90
|
+
|
|
91
|
+
const { uetId } = config;
|
|
92
|
+
|
|
93
|
+
return `<!-- Microsoft UET -->
|
|
94
|
+
<script>
|
|
95
|
+
(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"${uetId}",enableAutoSpaTracking:true};
|
|
96
|
+
o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){
|
|
97
|
+
var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)
|
|
98
|
+
})(window,document,"script","//bat.bing.com/bat.js","uetq");
|
|
99
|
+
</script>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate Facebook Pixel script
|
|
104
|
+
*/
|
|
105
|
+
function generateFacebookScript(config) {
|
|
106
|
+
if (!config || !config.pixelId) return '';
|
|
107
|
+
|
|
108
|
+
const { pixelId } = config;
|
|
109
|
+
|
|
110
|
+
return `<!-- Facebook Pixel -->
|
|
111
|
+
<script>
|
|
112
|
+
!function(f,b,e,v,n,t,s)
|
|
113
|
+
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
|
114
|
+
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
|
115
|
+
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
|
116
|
+
n.queue=[];t=b.createElement(e);t.async=!0;
|
|
117
|
+
t.src=v;s=b.getElementsByTagName(e)[0];
|
|
118
|
+
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
|
119
|
+
'https://connect.facebook.net/en_US/fbevents.js');
|
|
120
|
+
fbq('init', '${pixelId}');
|
|
121
|
+
fbq('track', 'PageView');
|
|
122
|
+
</script>
|
|
123
|
+
<noscript><img height="1" width="1" style="display:none"
|
|
124
|
+
src="https://www.facebook.com/tr?id=${pixelId}&ev=PageView&noscript=1"/></noscript>`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate verification meta tags
|
|
129
|
+
*/
|
|
130
|
+
function generateVerificationTags(options) {
|
|
131
|
+
const tags = [];
|
|
132
|
+
|
|
133
|
+
if (options.google?.verificationCode) {
|
|
134
|
+
tags.push(`<meta name="google-site-verification" content="${options.google.verificationCode}">`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options.yandex?.verificationCode) {
|
|
138
|
+
tags.push(`<meta name="yandex-verification" content="${options.yandex.verificationCode}">`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.bing?.verificationCode) {
|
|
142
|
+
tags.push(`<meta name="msvalidate.01" content="${options.bing.verificationCode}">`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.facebook?.domainVerification) {
|
|
146
|
+
tags.push(`<meta name="facebook-domain-verification" content="${options.facebook.domainVerification}">`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options.pinterest?.verificationCode) {
|
|
150
|
+
tags.push(`<meta name="p:domain_verify" content="${options.pinterest.verificationCode}">`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return tags.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create the analytics plugin
|
|
158
|
+
* @param {Object} options - Plugin options
|
|
159
|
+
* @param {Object} options.google - Google Analytics config
|
|
160
|
+
* @param {string} options.google.measurementId - GA4 Measurement ID (G-XXXXXXXXXX)
|
|
161
|
+
* @param {string} options.google.adsId - Google Ads ID (AW-XXXXXXXXXX)
|
|
162
|
+
* @param {string} options.google.verificationCode - Search Console verification
|
|
163
|
+
* @param {Object} options.gtm - Google Tag Manager config
|
|
164
|
+
* @param {string} options.gtm.containerId - GTM Container ID (GTM-XXXXXXX)
|
|
165
|
+
* @param {Object} options.yandex - Yandex.Metrika config
|
|
166
|
+
* @param {string} options.yandex.counterId - Yandex counter ID
|
|
167
|
+
* @param {string} options.yandex.verificationCode - Yandex Webmaster verification
|
|
168
|
+
* @param {boolean} options.yandex.webvisor - Enable Webvisor
|
|
169
|
+
* @param {Object} options.bing - Microsoft/Bing config
|
|
170
|
+
* @param {string} options.bing.uetId - UET tag ID
|
|
171
|
+
* @param {string} options.bing.verificationCode - Bing Webmaster verification
|
|
172
|
+
* @param {Object} options.facebook - Facebook/Meta config
|
|
173
|
+
* @param {string} options.facebook.pixelId - Facebook Pixel ID
|
|
174
|
+
* @param {string} options.facebook.domainVerification - Domain verification code
|
|
175
|
+
*/
|
|
176
|
+
function analyticsPlugin(options = {}) {
|
|
177
|
+
const { google, gtm, yandex, bing, facebook, pinterest } = options;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
name: 'analytics',
|
|
181
|
+
version: '1.0.0',
|
|
182
|
+
_options: options,
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Public API for other plugins
|
|
186
|
+
*/
|
|
187
|
+
api: {
|
|
188
|
+
/**
|
|
189
|
+
* Get configuration
|
|
190
|
+
*/
|
|
191
|
+
getConfig() {
|
|
192
|
+
return { ...options };
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if a tracker is configured
|
|
197
|
+
*/
|
|
198
|
+
hasTracker(name) {
|
|
199
|
+
return !!options[name];
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Register helpers
|
|
205
|
+
*/
|
|
206
|
+
register(ctx) {
|
|
207
|
+
// Google Analytics (gtag.js)
|
|
208
|
+
ctx.addHelper('gtag', () => generateGtagScript(google));
|
|
209
|
+
|
|
210
|
+
// Google Tag Manager
|
|
211
|
+
ctx.addHelper('gtm', () => generateGtmScript(gtm));
|
|
212
|
+
ctx.addHelper('gtmNoscript', () => generateGtmNoscript(gtm));
|
|
213
|
+
|
|
214
|
+
// Yandex.Metrika
|
|
215
|
+
ctx.addHelper('yandexMetrika', () => generateYandexScript(yandex));
|
|
216
|
+
|
|
217
|
+
// Microsoft/Bing UET
|
|
218
|
+
ctx.addHelper('bingUET', () => generateBingScript(bing));
|
|
219
|
+
|
|
220
|
+
// Facebook Pixel
|
|
221
|
+
ctx.addHelper('facebookPixel', () => generateFacebookScript(facebook));
|
|
222
|
+
|
|
223
|
+
// Verification meta tags
|
|
224
|
+
ctx.addHelper('verificationTags', () => generateVerificationTags(options));
|
|
225
|
+
|
|
226
|
+
// All analytics scripts combined
|
|
227
|
+
ctx.addHelper('allAnalytics', () => {
|
|
228
|
+
const scripts = [];
|
|
229
|
+
|
|
230
|
+
if (google) scripts.push(generateGtagScript(google));
|
|
231
|
+
if (gtm) scripts.push(generateGtmScript(gtm));
|
|
232
|
+
if (yandex) scripts.push(generateYandexScript(yandex));
|
|
233
|
+
if (bing) scripts.push(generateBingScript(bing));
|
|
234
|
+
if (facebook) scripts.push(generateFacebookScript(facebook));
|
|
235
|
+
|
|
236
|
+
return scripts.filter(Boolean).join('\n\n');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Head scripts (verification + analytics)
|
|
240
|
+
ctx.addHelper('analyticsHead', () => {
|
|
241
|
+
const parts = [];
|
|
242
|
+
|
|
243
|
+
// Verification tags first
|
|
244
|
+
const verificationTags = generateVerificationTags(options);
|
|
245
|
+
if (verificationTags) parts.push(verificationTags);
|
|
246
|
+
|
|
247
|
+
// GTM should be as early as possible
|
|
248
|
+
if (gtm) parts.push(generateGtmScript(gtm));
|
|
249
|
+
|
|
250
|
+
// Other analytics
|
|
251
|
+
if (google) parts.push(generateGtagScript(google));
|
|
252
|
+
if (yandex) parts.push(generateYandexScript(yandex));
|
|
253
|
+
if (bing) parts.push(generateBingScript(bing));
|
|
254
|
+
if (facebook) parts.push(generateFacebookScript(facebook));
|
|
255
|
+
|
|
256
|
+
return parts.filter(Boolean).join('\n\n');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Body open scripts (GTM noscript)
|
|
260
|
+
ctx.addHelper('analyticsBodyOpen', () => {
|
|
261
|
+
return generateGtmNoscript(gtm);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = analyticsPlugin;
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Mithril.js Application
|
|
3
|
+
* Components and logic for the dashboard SPA
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = `
|
|
7
|
+
// State
|
|
8
|
+
const state = {
|
|
9
|
+
activeTab: 'routes',
|
|
10
|
+
filter: 'all',
|
|
11
|
+
search: '',
|
|
12
|
+
routes: window.__DASHBOARD_DATA__.routes || [],
|
|
13
|
+
plugins: window.__DASHBOARD_DATA__.plugins || [],
|
|
14
|
+
config: window.__DASHBOARD_DATA__.config || {}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Icons
|
|
18
|
+
const Icons = {
|
|
19
|
+
logo: () => m('svg', { viewBox: '0 0 24 24', fill: 'none' }, [
|
|
20
|
+
m('path', { d: 'M12 2L2 7L12 12L22 7L12 2Z', fill: 'currentColor', opacity: '0.3' }),
|
|
21
|
+
m('path', { d: 'M2 17L12 22L22 17', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }),
|
|
22
|
+
m('path', { d: 'M2 12L12 17L22 12', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' })
|
|
23
|
+
]),
|
|
24
|
+
empty: () => m('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.5' }, [
|
|
25
|
+
m('path', { d: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z' })
|
|
26
|
+
])
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Components
|
|
30
|
+
const Header = {
|
|
31
|
+
view: () => m('.header', [
|
|
32
|
+
m('h1', [
|
|
33
|
+
Icons.logo(),
|
|
34
|
+
'Webspresso Dashboard'
|
|
35
|
+
]),
|
|
36
|
+
m('span.dev-badge', 'development')
|
|
37
|
+
])
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const Tabs = {
|
|
41
|
+
view: () => m('.tabs', [
|
|
42
|
+
['routes', 'Routes'],
|
|
43
|
+
['plugins', 'Plugins'],
|
|
44
|
+
['config', 'Config']
|
|
45
|
+
].map(([id, label]) =>
|
|
46
|
+
m('button.tab', {
|
|
47
|
+
class: state.activeTab === id ? 'active' : '',
|
|
48
|
+
onclick: () => { state.activeTab = id; }
|
|
49
|
+
}, label)
|
|
50
|
+
))
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const FilterBar = {
|
|
54
|
+
view: () => m('.filter-bar', [
|
|
55
|
+
m('.filter-group', [
|
|
56
|
+
['all', 'All'],
|
|
57
|
+
['ssr', 'SSR'],
|
|
58
|
+
['api', 'API']
|
|
59
|
+
].map(([id, label]) =>
|
|
60
|
+
m('button.filter-btn', {
|
|
61
|
+
class: state.filter === id ? 'active' : '',
|
|
62
|
+
onclick: () => { state.filter = id; }
|
|
63
|
+
}, label)
|
|
64
|
+
)),
|
|
65
|
+
m('input.search-input', {
|
|
66
|
+
type: 'text',
|
|
67
|
+
placeholder: 'Search routes...',
|
|
68
|
+
value: state.search,
|
|
69
|
+
oninput: (e) => { state.search = e.target.value; }
|
|
70
|
+
})
|
|
71
|
+
])
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const MethodBadge = {
|
|
75
|
+
view: (vnode) => m('span.method-badge.method-' + vnode.attrs.method,
|
|
76
|
+
vnode.attrs.method.toUpperCase()
|
|
77
|
+
)
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const TypeBadge = {
|
|
81
|
+
view: (vnode) => m('span.type-badge.type-' + vnode.attrs.type,
|
|
82
|
+
vnode.attrs.type.toUpperCase()
|
|
83
|
+
)
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const RoutesTable = {
|
|
87
|
+
view: () => {
|
|
88
|
+
let routes = state.routes;
|
|
89
|
+
|
|
90
|
+
// Apply filter
|
|
91
|
+
if (state.filter !== 'all') {
|
|
92
|
+
routes = routes.filter(r => r.type === state.filter);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Apply search
|
|
96
|
+
if (state.search) {
|
|
97
|
+
const search = state.search.toLowerCase();
|
|
98
|
+
routes = routes.filter(r =>
|
|
99
|
+
r.pattern.toLowerCase().includes(search) ||
|
|
100
|
+
r.file.toLowerCase().includes(search)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (routes.length === 0) {
|
|
105
|
+
return m('.table-container',
|
|
106
|
+
m('.empty-state', [
|
|
107
|
+
Icons.empty(),
|
|
108
|
+
m('p', 'No routes found')
|
|
109
|
+
])
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return m('.table-container',
|
|
114
|
+
m('table', [
|
|
115
|
+
m('thead',
|
|
116
|
+
m('tr', [
|
|
117
|
+
m('th', 'Method'),
|
|
118
|
+
m('th', 'Path'),
|
|
119
|
+
m('th', 'File'),
|
|
120
|
+
m('th', 'Type')
|
|
121
|
+
])
|
|
122
|
+
),
|
|
123
|
+
m('tbody',
|
|
124
|
+
routes.map(route =>
|
|
125
|
+
m('tr', [
|
|
126
|
+
m('td', m(MethodBadge, { method: route.method })),
|
|
127
|
+
m('td', [
|
|
128
|
+
m('span.code', route.pattern),
|
|
129
|
+
route.isDynamic ? m('span.dynamic-indicator', '⚡ dynamic') : null
|
|
130
|
+
]),
|
|
131
|
+
m('td', m('span.file-path', route.file)),
|
|
132
|
+
m('td', m(TypeBadge, { type: route.type }))
|
|
133
|
+
])
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
])
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const StatsGrid = {
|
|
142
|
+
view: () => {
|
|
143
|
+
const ssrCount = state.routes.filter(r => r.type === 'ssr').length;
|
|
144
|
+
const apiCount = state.routes.filter(r => r.type === 'api').length;
|
|
145
|
+
const dynamicCount = state.routes.filter(r => r.isDynamic).length;
|
|
146
|
+
|
|
147
|
+
return m('.stats-grid', [
|
|
148
|
+
m('.card', [
|
|
149
|
+
m('.card-title', 'Total Routes'),
|
|
150
|
+
m('.card-value', state.routes.length)
|
|
151
|
+
]),
|
|
152
|
+
m('.card', [
|
|
153
|
+
m('.card-title', 'SSR Pages'),
|
|
154
|
+
m('.card-value', ssrCount)
|
|
155
|
+
]),
|
|
156
|
+
m('.card', [
|
|
157
|
+
m('.card-title', 'API Endpoints'),
|
|
158
|
+
m('.card-value', apiCount)
|
|
159
|
+
]),
|
|
160
|
+
m('.card', [
|
|
161
|
+
m('.card-title', 'Dynamic Routes'),
|
|
162
|
+
m('.card-value', dynamicCount)
|
|
163
|
+
])
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const RoutesView = {
|
|
169
|
+
view: () => m('div', [
|
|
170
|
+
m(StatsGrid),
|
|
171
|
+
m(FilterBar),
|
|
172
|
+
m(RoutesTable)
|
|
173
|
+
])
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const PluginsView = {
|
|
177
|
+
view: () => {
|
|
178
|
+
if (state.plugins.length === 0) {
|
|
179
|
+
return m('.table-container',
|
|
180
|
+
m('.empty-state', [
|
|
181
|
+
Icons.empty(),
|
|
182
|
+
m('p', 'No plugins loaded')
|
|
183
|
+
])
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return m('.table-container',
|
|
188
|
+
state.plugins.map(plugin =>
|
|
189
|
+
m('.plugin-item', [
|
|
190
|
+
m('div', [
|
|
191
|
+
m('.plugin-name', plugin.name),
|
|
192
|
+
plugin.description ? m('p', { style: 'color: var(--text-secondary); font-size: 13px; margin-top: 4px;' }, plugin.description) : null
|
|
193
|
+
]),
|
|
194
|
+
m('.plugin-version', 'v' + plugin.version)
|
|
195
|
+
])
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const ConfigView = {
|
|
202
|
+
view: () => {
|
|
203
|
+
const config = state.config;
|
|
204
|
+
|
|
205
|
+
return m('div', [
|
|
206
|
+
// Environment
|
|
207
|
+
m('.config-section', [
|
|
208
|
+
m('h3', 'Environment'),
|
|
209
|
+
m('.table-container',
|
|
210
|
+
Object.entries(config.env || {}).map(([key, value]) =>
|
|
211
|
+
m('.config-item', [
|
|
212
|
+
m('.config-key', key),
|
|
213
|
+
m('.config-value', String(value))
|
|
214
|
+
])
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
]),
|
|
218
|
+
|
|
219
|
+
// i18n
|
|
220
|
+
m('.config-section', [
|
|
221
|
+
m('h3', 'Internationalization'),
|
|
222
|
+
m('.table-container', [
|
|
223
|
+
m('.config-item', [
|
|
224
|
+
m('.config-key', 'Default Locale'),
|
|
225
|
+
m('.config-value', config.i18n?.defaultLocale || 'en')
|
|
226
|
+
]),
|
|
227
|
+
m('.config-item', [
|
|
228
|
+
m('.config-key', 'Supported Locales'),
|
|
229
|
+
m('.config-value', (config.i18n?.supportedLocales || ['en']).join(', '))
|
|
230
|
+
])
|
|
231
|
+
])
|
|
232
|
+
]),
|
|
233
|
+
|
|
234
|
+
// Server
|
|
235
|
+
m('.config-section', [
|
|
236
|
+
m('h3', 'Server'),
|
|
237
|
+
m('.table-container', [
|
|
238
|
+
m('.config-item', [
|
|
239
|
+
m('.config-key', 'Port'),
|
|
240
|
+
m('.config-value', config.server?.port || '3000')
|
|
241
|
+
]),
|
|
242
|
+
m('.config-item', [
|
|
243
|
+
m('.config-key', 'Base URL'),
|
|
244
|
+
m('.config-value', config.server?.baseUrl || 'http://localhost:3000')
|
|
245
|
+
])
|
|
246
|
+
])
|
|
247
|
+
])
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const App = {
|
|
253
|
+
view: () => m('.dashboard', [
|
|
254
|
+
m(Header),
|
|
255
|
+
m(Tabs),
|
|
256
|
+
state.activeTab === 'routes' ? m(RoutesView) : null,
|
|
257
|
+
state.activeTab === 'plugins' ? m(PluginsView) : null,
|
|
258
|
+
state.activeTab === 'config' ? m(ConfigView) : null
|
|
259
|
+
])
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Mount app
|
|
263
|
+
m.mount(document.getElementById('app'), App);
|
|
264
|
+
`;
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Dashboard Plugin
|
|
3
|
+
* Development dashboard for monitoring routes, plugins, and configuration
|
|
4
|
+
* Uses Mithril.js for the SPA interface
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const styles = require('./styles');
|
|
8
|
+
const appScript = require('./app');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Filter sensitive environment variables
|
|
12
|
+
*/
|
|
13
|
+
function filterSensitiveEnv(env) {
|
|
14
|
+
const sensitiveKeys = ['SECRET', 'PASSWORD', 'KEY', 'TOKEN', 'CREDENTIAL', 'PRIVATE'];
|
|
15
|
+
const filtered = {};
|
|
16
|
+
|
|
17
|
+
for (const [key, value] of Object.entries(env)) {
|
|
18
|
+
const isSensitive = sensitiveKeys.some(s => key.toUpperCase().includes(s));
|
|
19
|
+
filtered[key] = isSensitive ? '••••••••' : value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return filtered;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate the dashboard HTML
|
|
27
|
+
*/
|
|
28
|
+
function generateDashboardHtml(data) {
|
|
29
|
+
return `<!DOCTYPE html>
|
|
30
|
+
<html lang="en">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="UTF-8">
|
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
34
|
+
<title>Webspresso Dashboard</title>
|
|
35
|
+
<script src="https://unpkg.com/mithril/mithril.js"></script>
|
|
36
|
+
<style>${styles}</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div id="app"></div>
|
|
40
|
+
<script>
|
|
41
|
+
window.__DASHBOARD_DATA__ = ${JSON.stringify(data)};
|
|
42
|
+
</script>
|
|
43
|
+
<script>${appScript}</script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create the dashboard plugin
|
|
50
|
+
* @param {Object} options - Plugin options
|
|
51
|
+
* @param {string} options.path - Dashboard path (default: '/_webspresso')
|
|
52
|
+
* @param {boolean} options.enabled - Force enable/disable (default: auto based on NODE_ENV)
|
|
53
|
+
*/
|
|
54
|
+
function dashboardPlugin(options = {}) {
|
|
55
|
+
const {
|
|
56
|
+
path: dashboardPath = '/_webspresso',
|
|
57
|
+
enabled
|
|
58
|
+
} = options;
|
|
59
|
+
|
|
60
|
+
// Determine if enabled (default: only in development)
|
|
61
|
+
const isEnabled = enabled !== undefined
|
|
62
|
+
? enabled
|
|
63
|
+
: process.env.NODE_ENV !== 'production';
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
name: 'dashboard',
|
|
67
|
+
version: '1.0.0',
|
|
68
|
+
description: 'Development dashboard for monitoring routes and configuration',
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Called after routes are mounted
|
|
72
|
+
*/
|
|
73
|
+
onRoutesReady(ctx) {
|
|
74
|
+
// Skip if disabled
|
|
75
|
+
if (!isEnabled) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { app, routes, options: appOptions } = ctx;
|
|
80
|
+
|
|
81
|
+
// Collect plugin info
|
|
82
|
+
const pluginManager = ctx.pluginManager || (appOptions && appOptions.pluginManager);
|
|
83
|
+
const plugins = [];
|
|
84
|
+
|
|
85
|
+
// Get plugins from plugin manager if available
|
|
86
|
+
if (pluginManager && pluginManager.plugins) {
|
|
87
|
+
for (const [name, plugin] of pluginManager.plugins) {
|
|
88
|
+
plugins.push({
|
|
89
|
+
name: plugin.name || name,
|
|
90
|
+
version: plugin.version || '0.0.0',
|
|
91
|
+
description: plugin.description || ''
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build config data
|
|
97
|
+
const config = {
|
|
98
|
+
env: filterSensitiveEnv({
|
|
99
|
+
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
100
|
+
PORT: process.env.PORT || '3000',
|
|
101
|
+
BASE_URL: process.env.BASE_URL || 'http://localhost:3000'
|
|
102
|
+
}),
|
|
103
|
+
i18n: {
|
|
104
|
+
defaultLocale: process.env.DEFAULT_LOCALE || 'en',
|
|
105
|
+
supportedLocales: (process.env.SUPPORTED_LOCALES || 'en').split(',')
|
|
106
|
+
},
|
|
107
|
+
server: {
|
|
108
|
+
port: process.env.PORT || '3000',
|
|
109
|
+
baseUrl: process.env.BASE_URL || 'http://localhost:3000'
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Dashboard HTML endpoint
|
|
114
|
+
ctx.addRoute('get', dashboardPath, (req, res) => {
|
|
115
|
+
const data = { routes, plugins, config };
|
|
116
|
+
res.type('text/html');
|
|
117
|
+
res.send(generateDashboardHtml(data));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// JSON API endpoints
|
|
121
|
+
ctx.addRoute('get', dashboardPath + '/api/routes', (req, res) => {
|
|
122
|
+
res.json(routes);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
ctx.addRoute('get', dashboardPath + '/api/plugins', (req, res) => {
|
|
126
|
+
res.json(plugins);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
ctx.addRoute('get', dashboardPath + '/api/config', (req, res) => {
|
|
130
|
+
res.json(config);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Log dashboard URL
|
|
134
|
+
console.log(`\n📊 Dashboard available at: http://localhost:${process.env.PORT || 3000}${dashboardPath}\n`);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = dashboardPlugin;
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|