underpost 2.90.1 → 2.90.4
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/README.md +3 -3
- package/bin/deploy.js +16 -0
- package/cli.md +31 -5
- package/examples/QUICK-REFERENCE.md +499 -0
- package/examples/README.md +447 -0
- package/examples/STATIC-GENERATOR-GUIDE.md +807 -0
- package/examples/ssr-components/CustomPage.js +579 -0
- package/examples/static-config-example.json +183 -0
- package/examples/static-config-simple.json +57 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/src/api/document/document.model.js +7 -0
- package/src/api/document/document.service.js +4 -1
- package/src/cli/index.js +91 -4
- package/src/cli/run.js +1 -1
- package/src/cli/static.js +785 -49
- package/src/client/components/core/Input.js +6 -4
- package/src/client/components/core/Modal.js +13 -18
- package/src/client/components/core/Panel.js +26 -6
- package/src/client/components/core/PanelForm.js +67 -52
- package/src/index.js +1 -1
package/src/cli/static.js
CHANGED
|
@@ -1,84 +1,820 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Static site generation module
|
|
2
|
+
* Static site generation module with enhanced customization capabilities
|
|
3
3
|
* @module src/cli/static.js
|
|
4
4
|
* @namespace UnderpostStatic
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // Basic usage - generate a simple page
|
|
8
|
+
* import UnderpostStatic from './static.js';
|
|
9
|
+
*
|
|
10
|
+
* await UnderpostStatic.API.callback({
|
|
11
|
+
* page: './src/client/ssr/body/DefaultSplashScreen.js',
|
|
12
|
+
* title: 'My App',
|
|
13
|
+
* outputPath: './dist/index.html'
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Advanced usage - full customization
|
|
18
|
+
* await UnderpostStatic.API.callback({
|
|
19
|
+
* page: './src/client/ssr/body/CustomPage.js',
|
|
20
|
+
* outputPath: './dist/custom.html',
|
|
21
|
+
* metadata: {
|
|
22
|
+
* title: 'My Custom Page',
|
|
23
|
+
* description: 'A fully customized static page',
|
|
24
|
+
* keywords: ['static', 'generator', 'custom'],
|
|
25
|
+
* author: 'John Doe',
|
|
26
|
+
* themeColor: '#007bff',
|
|
27
|
+
* canonicalURL: 'https://example.com/custom',
|
|
28
|
+
* thumbnail: 'https://example.com/thumb.png'
|
|
29
|
+
* },
|
|
30
|
+
* scripts: {
|
|
31
|
+
* head: [
|
|
32
|
+
* { src: '/vendor/library.js', async: true },
|
|
33
|
+
* { content: 'console.log("Inline script");', type: 'module' }
|
|
34
|
+
* ],
|
|
35
|
+
* body: [
|
|
36
|
+
* { src: '/app.js', defer: true }
|
|
37
|
+
* ]
|
|
38
|
+
* },
|
|
39
|
+
* styles: [
|
|
40
|
+
* { href: '/custom.css' },
|
|
41
|
+
* { content: 'body { margin: 0; }' }
|
|
42
|
+
* ],
|
|
43
|
+
* headComponents: [
|
|
44
|
+
* './src/client/ssr/head/Seo.js',
|
|
45
|
+
* './src/client/ssr/head/Pwa.js'
|
|
46
|
+
* ],
|
|
47
|
+
* icons: {
|
|
48
|
+
* favicon: '/custom-favicon.ico',
|
|
49
|
+
* appleTouchIcon: '/apple-touch-icon.png'
|
|
50
|
+
* },
|
|
51
|
+
* env: 'production',
|
|
52
|
+
* minify: true
|
|
53
|
+
* });
|
|
5
54
|
*/
|
|
6
55
|
|
|
7
56
|
import fs from 'fs-extra';
|
|
57
|
+
import path from 'path';
|
|
8
58
|
import { ssrFactory } from '../server/ssr.js';
|
|
9
59
|
import { shellExec } from '../server/process.js';
|
|
10
60
|
import Underpost from '../index.js';
|
|
11
61
|
import { JSONweb } from '../server/client-formatted.js';
|
|
62
|
+
import { loggerFactory } from '../server/logger.js';
|
|
63
|
+
|
|
64
|
+
const logger = loggerFactory(import.meta);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {Object} MetadataOptions
|
|
68
|
+
* @memberof UnderpostStatic
|
|
69
|
+
* @property {string} [title='Home'] - Page title
|
|
70
|
+
* @property {string} [description=''] - Page description for SEO
|
|
71
|
+
* @property {string[]} [keywords=[]] - Keywords for SEO
|
|
72
|
+
* @property {string} [author=''] - Page author
|
|
73
|
+
* @property {string} [themeColor='#ffffff'] - Theme color for mobile browsers
|
|
74
|
+
* @property {string} [canonicalURL=''] - Canonical URL for SEO
|
|
75
|
+
* @property {string} [thumbnail=''] - Open Graph thumbnail image URL
|
|
76
|
+
* @property {string} [locale='en-US'] - Page locale
|
|
77
|
+
* @property {string} [siteName=''] - Site name for Open Graph
|
|
78
|
+
* @property {Object} [openGraph={}] - Additional Open Graph metadata
|
|
79
|
+
* @property {Object} [twitter={}] - Twitter card metadata
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {Object} ScriptOptions
|
|
84
|
+
* @memberof UnderpostStatic
|
|
85
|
+
* @property {string} [src] - External script source URL
|
|
86
|
+
* @property {string} [content] - Inline script content
|
|
87
|
+
* @property {string} [type='text/javascript'] - Script type
|
|
88
|
+
* @property {boolean} [async=false] - Async loading flag
|
|
89
|
+
* @property {boolean} [defer=false] - Defer loading flag
|
|
90
|
+
* @property {string} [integrity] - Subresource integrity hash
|
|
91
|
+
* @property {string} [crossorigin] - CORS settings
|
|
92
|
+
* @property {Object} [attributes={}] - Additional HTML attributes
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @typedef {Object} StyleOptions
|
|
97
|
+
* @memberof UnderpostStatic
|
|
98
|
+
* @property {string} [href] - External stylesheet URL
|
|
99
|
+
* @property {string} [content] - Inline style content
|
|
100
|
+
* @property {string} [media='all'] - Media query
|
|
101
|
+
* @property {string} [integrity] - Subresource integrity hash
|
|
102
|
+
* @property {string} [crossorigin] - CORS settings
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @typedef {Object} IconOptions
|
|
107
|
+
* @memberof UnderpostStatic
|
|
108
|
+
* @property {string} [favicon] - Favicon path
|
|
109
|
+
* @property {string} [appleTouchIcon] - Apple touch icon path
|
|
110
|
+
* @property {string} [manifest] - Web manifest path
|
|
111
|
+
* @property {Object[]} [additional=[]] - Additional icon definitions
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @typedef {Object} StaticGenerationOptions
|
|
116
|
+
* @memberof UnderpostStatic
|
|
117
|
+
* @property {string} [page=''] - SSR component path to render
|
|
118
|
+
* @property {string} [title='Home'] - Page title (deprecated: use metadata.title)
|
|
119
|
+
* @property {string} [outputPath='.'] - Output file path
|
|
120
|
+
* @property {string} [deployId=''] - Deployment identifier
|
|
121
|
+
* @property {string} [buildHost=''] - Build host URL
|
|
122
|
+
* @property {string} [buildPath='/'] - Build path
|
|
123
|
+
* @property {string} [env='production'] - Environment (development/production)
|
|
124
|
+
* @property {boolean} [build=false] - Whether to trigger build
|
|
125
|
+
* @property {boolean} [dev=false] - Development mode flag
|
|
126
|
+
* @property {boolean} [minify=true] - Minify HTML output
|
|
127
|
+
* @property {MetadataOptions} [metadata={}] - Comprehensive metadata options
|
|
128
|
+
* @property {Object} [scripts={}] - Script injection options
|
|
129
|
+
* @property {ScriptOptions[]} [scripts.head=[]] - Scripts for head section
|
|
130
|
+
* @property {ScriptOptions[]} [scripts.body=[]] - Scripts for body section
|
|
131
|
+
* @property {StyleOptions[]} [styles=[]] - Stylesheet options
|
|
132
|
+
* @property {string[]} [headComponents=[]] - Array of SSR head component paths
|
|
133
|
+
* @property {string[]} [bodyComponents=[]] - Array of SSR body component paths
|
|
134
|
+
* @property {IconOptions} [icons={}] - Icon configuration
|
|
135
|
+
* @property {Object} [customPayload={}] - Custom data to inject into renderPayload
|
|
136
|
+
* @property {Object} [templateHelpers={}] - Custom helper functions for templates
|
|
137
|
+
* @property {string} [configFile=''] - Path to JSON config file
|
|
138
|
+
* @property {string} [lang='en'] - HTML lang attribute
|
|
139
|
+
* @property {string} [dir='ltr'] - HTML dir attribute
|
|
140
|
+
* @property {Object} [microdata=[]] - Structured data (JSON-LD)
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Template helper functions for common SSR patterns
|
|
145
|
+
* @namespace TemplateHelpers
|
|
146
|
+
*/
|
|
147
|
+
const TemplateHelpers = {
|
|
148
|
+
/**
|
|
149
|
+
* Generates a script tag from options
|
|
150
|
+
* @param {ScriptOptions} options - Script options
|
|
151
|
+
* @returns {string} HTML script tag
|
|
152
|
+
* @memberof TemplateHelpers
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* // External script with async
|
|
156
|
+
* TemplateHelpers.createScriptTag({ src: '/app.js', async: true })
|
|
157
|
+
* // Returns: <script async src="/app.js"></script>
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* // Inline module script
|
|
161
|
+
* TemplateHelpers.createScriptTag({
|
|
162
|
+
* content: 'console.log("Hello");',
|
|
163
|
+
* type: 'module'
|
|
164
|
+
* })
|
|
165
|
+
* // Returns: <script type="module">console.log("Hello");</script>
|
|
166
|
+
*/
|
|
167
|
+
createScriptTag(options) {
|
|
168
|
+
const attrs = [];
|
|
169
|
+
|
|
170
|
+
if (options.type && options.type !== 'text/javascript') {
|
|
171
|
+
attrs.push(`type="${options.type}"`);
|
|
172
|
+
}
|
|
173
|
+
if (options.async) attrs.push('async');
|
|
174
|
+
if (options.defer) attrs.push('defer');
|
|
175
|
+
if (options.src) attrs.push(`src="${options.src}"`);
|
|
176
|
+
if (options.integrity) attrs.push(`integrity="${options.integrity}"`);
|
|
177
|
+
if (options.crossorigin) attrs.push(`crossorigin="${options.crossorigin}"`);
|
|
178
|
+
|
|
179
|
+
// Add custom attributes
|
|
180
|
+
if (options.attributes) {
|
|
181
|
+
Object.entries(options.attributes).forEach(([key, value]) => {
|
|
182
|
+
attrs.push(`${key}="${value}"`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const attrString = attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
|
|
187
|
+
const content = options.content || '';
|
|
188
|
+
|
|
189
|
+
return `<script${attrString}>${content}</script>`;
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Generates a link/style tag from options
|
|
194
|
+
* @param {StyleOptions} options - Style options
|
|
195
|
+
* @returns {string} HTML link or style tag
|
|
196
|
+
* @memberof TemplateHelpers
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* // External stylesheet
|
|
200
|
+
* TemplateHelpers.createStyleTag({ href: '/styles.css' })
|
|
201
|
+
* // Returns: <link rel="stylesheet" href="/styles.css" media="all">
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* // Inline styles
|
|
205
|
+
* TemplateHelpers.createStyleTag({ content: 'body { margin: 0; }' })
|
|
206
|
+
* // Returns: <style>body { margin: 0; }</style>
|
|
207
|
+
*/
|
|
208
|
+
createStyleTag(options) {
|
|
209
|
+
if (options.content) {
|
|
210
|
+
return `<style>${options.content}</style>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const attrs = [`rel="stylesheet"`];
|
|
214
|
+
if (options.href) attrs.push(`href="${options.href}"`);
|
|
215
|
+
if (options.media) attrs.push(`media="${options.media}"`);
|
|
216
|
+
if (options.integrity) attrs.push(`integrity="${options.integrity}"`);
|
|
217
|
+
if (options.crossorigin) attrs.push(`crossorigin="${options.crossorigin}"`);
|
|
218
|
+
|
|
219
|
+
return `<link ${attrs.join(' ')}>`;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Generates icon link tags
|
|
224
|
+
* @param {IconOptions} icons - Icon options
|
|
225
|
+
* @returns {string} HTML icon link tags
|
|
226
|
+
* @memberof TemplateHelpers
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* TemplateHelpers.createIconTags({
|
|
230
|
+
* favicon: '/favicon.ico',
|
|
231
|
+
* appleTouchIcon: '/apple-touch-icon.png',
|
|
232
|
+
* manifest: '/manifest.json'
|
|
233
|
+
* })
|
|
234
|
+
*/
|
|
235
|
+
createIconTags(icons) {
|
|
236
|
+
const tags = [];
|
|
237
|
+
|
|
238
|
+
if (icons.favicon) {
|
|
239
|
+
tags.push(`<link rel="icon" type="image/x-icon" href="${icons.favicon}">`);
|
|
240
|
+
}
|
|
241
|
+
if (icons.appleTouchIcon) {
|
|
242
|
+
tags.push(`<link rel="apple-touch-icon" href="${icons.appleTouchIcon}">`);
|
|
243
|
+
}
|
|
244
|
+
if (icons.manifest) {
|
|
245
|
+
tags.push(`<link rel="manifest" href="${icons.manifest}">`);
|
|
246
|
+
}
|
|
247
|
+
if (icons.additional && Array.isArray(icons.additional)) {
|
|
248
|
+
icons.additional.forEach((icon) => {
|
|
249
|
+
const attrs = Object.entries(icon)
|
|
250
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
251
|
+
.join(' ');
|
|
252
|
+
tags.push(`<link ${attrs}>`);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return tags.join('\n');
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Generates meta tags from metadata object
|
|
261
|
+
* @param {MetadataOptions} metadata - Metadata options
|
|
262
|
+
* @returns {string} HTML meta tags
|
|
263
|
+
* @memberof TemplateHelpers
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* TemplateHelpers.createMetaTags({
|
|
267
|
+
* description: 'My page description',
|
|
268
|
+
* keywords: ['web', 'app'],
|
|
269
|
+
* author: 'John Doe'
|
|
270
|
+
* })
|
|
271
|
+
*/
|
|
272
|
+
createMetaTags(metadata) {
|
|
273
|
+
const tags = [];
|
|
274
|
+
|
|
275
|
+
if (metadata.description) {
|
|
276
|
+
tags.push(`<meta name="description" content="${metadata.description}">`);
|
|
277
|
+
}
|
|
278
|
+
if (metadata.keywords && metadata.keywords.length > 0) {
|
|
279
|
+
tags.push(`<meta name="keywords" content="${metadata.keywords.join(', ')}">`);
|
|
280
|
+
}
|
|
281
|
+
if (metadata.author) {
|
|
282
|
+
tags.push(`<meta name="author" content="${metadata.author}">`);
|
|
283
|
+
}
|
|
284
|
+
if (metadata.themeColor) {
|
|
285
|
+
tags.push(`<meta name="theme-color" content="${metadata.themeColor}">`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Open Graph
|
|
289
|
+
if (metadata.title) {
|
|
290
|
+
tags.push(`<meta property="og:title" content="${metadata.title}">`);
|
|
291
|
+
}
|
|
292
|
+
if (metadata.description) {
|
|
293
|
+
tags.push(`<meta property="og:description" content="${metadata.description}">`);
|
|
294
|
+
}
|
|
295
|
+
if (metadata.thumbnail) {
|
|
296
|
+
tags.push(`<meta property="og:image" content="${metadata.thumbnail}">`);
|
|
297
|
+
}
|
|
298
|
+
if (metadata.canonicalURL) {
|
|
299
|
+
tags.push(`<meta property="og:url" content="${metadata.canonicalURL}">`);
|
|
300
|
+
tags.push(`<link rel="canonical" href="${metadata.canonicalURL}">`);
|
|
301
|
+
}
|
|
302
|
+
if (metadata.siteName) {
|
|
303
|
+
tags.push(`<meta property="og:site_name" content="${metadata.siteName}">`);
|
|
304
|
+
}
|
|
305
|
+
if (metadata.locale) {
|
|
306
|
+
tags.push(`<meta property="og:locale" content="${metadata.locale}">`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Twitter Card
|
|
310
|
+
tags.push(`<meta name="twitter:card" content="summary_large_image">`);
|
|
311
|
+
if (metadata.twitter) {
|
|
312
|
+
Object.entries(metadata.twitter).forEach(([key, value]) => {
|
|
313
|
+
tags.push(`<meta name="twitter:${key}" content="${value}">`);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Additional Open Graph
|
|
318
|
+
if (metadata.openGraph) {
|
|
319
|
+
Object.entries(metadata.openGraph).forEach(([key, value]) => {
|
|
320
|
+
tags.push(`<meta property="og:${key}" content="${value}">`);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return tags.join('\n');
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Generates JSON-LD structured data script tags
|
|
329
|
+
* @param {Object[]} microdata - Array of structured data objects
|
|
330
|
+
* @returns {string} HTML script tags with JSON-LD
|
|
331
|
+
* @memberof TemplateHelpers
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* TemplateHelpers.createMicrodataTags([
|
|
335
|
+
* {
|
|
336
|
+
* '@context': 'https://schema.org',
|
|
337
|
+
* '@type': 'WebSite',
|
|
338
|
+
* 'name': 'My Site',
|
|
339
|
+
* 'url': 'https://example.com'
|
|
340
|
+
* }
|
|
341
|
+
* ])
|
|
342
|
+
*/
|
|
343
|
+
createMicrodataTags(microdata) {
|
|
344
|
+
if (!microdata || !Array.isArray(microdata) || microdata.length === 0) {
|
|
345
|
+
return '';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return microdata
|
|
349
|
+
.map((data) => `<script type="application/ld+json">\n${JSON.stringify(data, null, 2)}\n</script>`)
|
|
350
|
+
.join('\n');
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Configuration validator
|
|
356
|
+
* @namespace ConfigValidator
|
|
357
|
+
*/
|
|
358
|
+
const ConfigValidator = {
|
|
359
|
+
/**
|
|
360
|
+
* Validates static generation options
|
|
361
|
+
* @param {StaticGenerationOptions} options - Options to validate
|
|
362
|
+
* @returns {Object} Validation result with isValid flag and errors array
|
|
363
|
+
* @memberof ConfigValidator
|
|
364
|
+
*/
|
|
365
|
+
validate(options) {
|
|
366
|
+
const errors = [];
|
|
367
|
+
|
|
368
|
+
// Validate page path
|
|
369
|
+
if (options.page && !fs.existsSync(options.page)) {
|
|
370
|
+
errors.push(`Page component does not exist: ${options.page}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Validate head components
|
|
374
|
+
if (options.headComponents && Array.isArray(options.headComponents)) {
|
|
375
|
+
options.headComponents.forEach((comp) => {
|
|
376
|
+
if (!fs.existsSync(comp)) {
|
|
377
|
+
errors.push(`Head component does not exist: ${comp}`);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Validate body components
|
|
383
|
+
if (options.bodyComponents && Array.isArray(options.bodyComponents)) {
|
|
384
|
+
options.bodyComponents.forEach((comp) => {
|
|
385
|
+
if (!fs.existsSync(comp)) {
|
|
386
|
+
errors.push(`Body component does not exist: ${comp}`);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Validate environment
|
|
392
|
+
if (options.env && !['development', 'production'].includes(options.env)) {
|
|
393
|
+
logger.warn(`Invalid environment: ${options.env}. Using 'production' as default.`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
isValid: errors.length === 0,
|
|
398
|
+
errors,
|
|
399
|
+
};
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Configuration file loader
|
|
405
|
+
* @namespace ConfigLoader
|
|
406
|
+
*/
|
|
407
|
+
const ConfigLoader = {
|
|
408
|
+
/**
|
|
409
|
+
* Loads configuration from a JSON file
|
|
410
|
+
* @param {string} configPath - Path to config file
|
|
411
|
+
* @returns {Object} Configuration object
|
|
412
|
+
* @memberof ConfigLoader
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* // static-config.json
|
|
416
|
+
* {
|
|
417
|
+
* "metadata": {
|
|
418
|
+
* "title": "My App",
|
|
419
|
+
* "description": "My application description"
|
|
420
|
+
* },
|
|
421
|
+
* "env": "production"
|
|
422
|
+
* }
|
|
423
|
+
*
|
|
424
|
+
* // Usage
|
|
425
|
+
* const config = ConfigLoader.load('./static-config.json');
|
|
426
|
+
*/
|
|
427
|
+
load(configPath) {
|
|
428
|
+
try {
|
|
429
|
+
if (!fs.existsSync(configPath)) {
|
|
430
|
+
logger.error(`Config file not found: ${configPath}`);
|
|
431
|
+
return {};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
435
|
+
return JSON.parse(content);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
logger.error(`Error loading config file: ${error.message}`);
|
|
438
|
+
return {};
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Saves configuration to a JSON file
|
|
444
|
+
* @param {string} configPath - Path to save config
|
|
445
|
+
* @param {Object} config - Configuration object
|
|
446
|
+
* @memberof ConfigLoader
|
|
447
|
+
*/
|
|
448
|
+
save(configPath, config) {
|
|
449
|
+
try {
|
|
450
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
451
|
+
logger.info(`Config saved to: ${configPath}`);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
logger.error(`Error saving config file: ${error.message}`);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
};
|
|
12
457
|
|
|
13
458
|
/**
|
|
14
459
|
* @class UnderpostStatic
|
|
15
|
-
* @description
|
|
460
|
+
* @description Enhanced static site generation class with comprehensive customization
|
|
16
461
|
* @memberof UnderpostStatic
|
|
17
462
|
*/
|
|
18
463
|
class UnderpostStatic {
|
|
19
464
|
static API = {
|
|
20
465
|
/**
|
|
21
|
-
* Generate static HTML file
|
|
22
|
-
*
|
|
23
|
-
* @param {
|
|
24
|
-
* @param {string} options.title - Page title
|
|
25
|
-
* @param {string} options.outputPath - Output file path
|
|
26
|
-
* @param {string} options.deployId - Deployment identifier
|
|
27
|
-
* @param {string} options.buildHost - Build host
|
|
28
|
-
* @param {string} options.buildPath - Build path
|
|
29
|
-
* @param {string} options.env - Environment (development/production)
|
|
30
|
-
* @param {boolean} options.build - Whether to trigger build
|
|
31
|
-
* @param {boolean} options.dev - Development mode flag
|
|
32
|
-
* @memberof UnderpostStatic
|
|
466
|
+
* Generate static HTML file with enhanced customization options
|
|
467
|
+
*
|
|
468
|
+
* @param {StaticGenerationOptions} options - Options for static generation
|
|
33
469
|
* @returns {Promise<void>}
|
|
470
|
+
* @memberof UnderpostStatic
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* // Minimal usage
|
|
474
|
+
* await UnderpostStatic.API.callback({
|
|
475
|
+
* page: './src/client/ssr/body/DefaultSplashScreen.js',
|
|
476
|
+
* outputPath: './dist/index.html'
|
|
477
|
+
* });
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* // Full customization with metadata and scripts
|
|
481
|
+
* await UnderpostStatic.API.callback({
|
|
482
|
+
* page: './src/client/ssr/body/CustomPage.js',
|
|
483
|
+
* outputPath: './dist/page.html',
|
|
484
|
+
* metadata: {
|
|
485
|
+
* title: 'My Custom Page',
|
|
486
|
+
* description: 'A fully customized page',
|
|
487
|
+
* keywords: ['custom', 'static', 'page'],
|
|
488
|
+
* author: 'Jane Developer',
|
|
489
|
+
* themeColor: '#4CAF50',
|
|
490
|
+
* canonicalURL: 'https://example.com/page',
|
|
491
|
+
* thumbnail: 'https://example.com/images/thumbnail.png',
|
|
492
|
+
* locale: 'en-US',
|
|
493
|
+
* siteName: 'My Website'
|
|
494
|
+
* },
|
|
495
|
+
* scripts: {
|
|
496
|
+
* head: [
|
|
497
|
+
* { src: 'https://cdn.example.com/analytics.js', async: true },
|
|
498
|
+
* { content: 'window.config = { apiUrl: "https://api.example.com" };' }
|
|
499
|
+
* ],
|
|
500
|
+
* body: [
|
|
501
|
+
* { src: '/app.js', type: 'module', defer: true }
|
|
502
|
+
* ]
|
|
503
|
+
* },
|
|
504
|
+
* styles: [
|
|
505
|
+
* { href: '/main.css' },
|
|
506
|
+
* { content: 'body { font-family: sans-serif; }' }
|
|
507
|
+
* ],
|
|
508
|
+
* icons: {
|
|
509
|
+
* favicon: '/favicon.ico',
|
|
510
|
+
* appleTouchIcon: '/apple-touch-icon.png',
|
|
511
|
+
* manifest: '/manifest.json'
|
|
512
|
+
* },
|
|
513
|
+
* headComponents: [
|
|
514
|
+
* './src/client/ssr/head/Seo.js',
|
|
515
|
+
* './src/client/ssr/head/Pwa.js'
|
|
516
|
+
* ],
|
|
517
|
+
* microdata: [
|
|
518
|
+
* {
|
|
519
|
+
* '@context': 'https://schema.org',
|
|
520
|
+
* '@type': 'WebPage',
|
|
521
|
+
* 'name': 'My Custom Page',
|
|
522
|
+
* 'url': 'https://example.com/page'
|
|
523
|
+
* }
|
|
524
|
+
* ],
|
|
525
|
+
* customPayload: {
|
|
526
|
+
* apiEndpoint: 'https://api.example.com',
|
|
527
|
+
* features: ['feature1', 'feature2']
|
|
528
|
+
* },
|
|
529
|
+
* env: 'production',
|
|
530
|
+
* minify: true
|
|
531
|
+
* });
|
|
532
|
+
*
|
|
533
|
+
* @example
|
|
534
|
+
* // Using a config file
|
|
535
|
+
* await UnderpostStatic.API.callback({
|
|
536
|
+
* configFile: './static-config.json',
|
|
537
|
+
* outputPath: './dist/index.html'
|
|
538
|
+
* });
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* // Generate with build trigger
|
|
542
|
+
* await UnderpostStatic.API.callback({
|
|
543
|
+
* page: './src/client/ssr/body/DefaultSplashScreen.js',
|
|
544
|
+
* outputPath: './public/index.html',
|
|
545
|
+
* deployId: 'production-v1',
|
|
546
|
+
* buildHost: 'example.com',
|
|
547
|
+
* buildPath: '/',
|
|
548
|
+
* build: true,
|
|
549
|
+
* env: 'production'
|
|
550
|
+
* });
|
|
34
551
|
*/
|
|
35
|
-
async callback(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
env: '',
|
|
44
|
-
build: false,
|
|
45
|
-
dev: false,
|
|
46
|
-
},
|
|
47
|
-
) {
|
|
552
|
+
async callback(options = {}) {
|
|
553
|
+
// Load config from file if specified
|
|
554
|
+
if (options.configFile) {
|
|
555
|
+
const fileConfig = ConfigLoader.load(options.configFile);
|
|
556
|
+
options = { ...fileConfig, ...options }; // CLI options override file config
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Set defaults
|
|
48
560
|
if (!options.outputPath) options.outputPath = '.';
|
|
49
561
|
if (!options.buildPath) options.buildPath = '/';
|
|
50
562
|
if (!options.env) options.env = 'production';
|
|
563
|
+
if (options.minify === undefined) options.minify = options.env === 'production';
|
|
564
|
+
if (!options.metadata) options.metadata = {};
|
|
565
|
+
if (!options.lang) options.lang = 'en';
|
|
566
|
+
if (!options.dir) options.dir = 'ltr';
|
|
567
|
+
|
|
568
|
+
// Merge title for backwards compatibility
|
|
569
|
+
if (options.title && !options.metadata.title) {
|
|
570
|
+
options.metadata.title = options.title;
|
|
571
|
+
}
|
|
572
|
+
if (!options.metadata.title) {
|
|
573
|
+
options.metadata.title = 'Home';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Validate options
|
|
577
|
+
const validation = ConfigValidator.validate(options);
|
|
578
|
+
if (!validation.isValid) {
|
|
579
|
+
logger.error('Validation errors:');
|
|
580
|
+
validation.errors.forEach((err) => logger.error(` - ${err}`));
|
|
581
|
+
if (validation.errors.some((err) => err.includes('does not exist'))) {
|
|
582
|
+
return; // Exit if critical path errors
|
|
583
|
+
}
|
|
584
|
+
}
|
|
51
585
|
|
|
586
|
+
// Generate page HTML
|
|
52
587
|
if (options.page) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
588
|
+
try {
|
|
589
|
+
logger.info(`Generating static page: ${options.page}`);
|
|
590
|
+
|
|
591
|
+
const Render = await ssrFactory();
|
|
592
|
+
const SsrComponent = await ssrFactory(options.page);
|
|
593
|
+
|
|
594
|
+
// Build head components
|
|
595
|
+
let ssrHeadComponents = '';
|
|
596
|
+
|
|
597
|
+
// Add custom meta tags
|
|
598
|
+
if (options.metadata) {
|
|
599
|
+
ssrHeadComponents += TemplateHelpers.createMetaTags(options.metadata);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Add custom icons
|
|
603
|
+
if (options.icons) {
|
|
604
|
+
ssrHeadComponents += '\n' + TemplateHelpers.createIconTags(options.icons);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Add custom styles
|
|
608
|
+
if (options.styles && Array.isArray(options.styles)) {
|
|
609
|
+
ssrHeadComponents += '\n' + options.styles.map((style) => TemplateHelpers.createStyleTag(style)).join('\n');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Add custom head scripts
|
|
613
|
+
if (options.scripts?.head && Array.isArray(options.scripts.head)) {
|
|
614
|
+
ssrHeadComponents +=
|
|
615
|
+
'\n' + options.scripts.head.map((script) => TemplateHelpers.createScriptTag(script)).join('\n');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Add microdata/structured data
|
|
619
|
+
if (options.microdata && Array.isArray(options.microdata)) {
|
|
620
|
+
ssrHeadComponents += '\n' + TemplateHelpers.createMicrodataTags(options.microdata);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Load additional head components
|
|
624
|
+
if (options.headComponents && Array.isArray(options.headComponents)) {
|
|
625
|
+
for (const compPath of options.headComponents) {
|
|
626
|
+
try {
|
|
627
|
+
const HeadComponent = await ssrFactory(compPath);
|
|
628
|
+
// Pass metadata and other options to component
|
|
629
|
+
const componentData = {
|
|
630
|
+
...options.metadata,
|
|
631
|
+
ssrPath: options.buildPath === '/' ? '/' : `${options.buildPath}/`,
|
|
632
|
+
microdata: options.microdata || [],
|
|
633
|
+
};
|
|
634
|
+
ssrHeadComponents += '\n' + HeadComponent(componentData);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
logger.error(`Error loading head component ${compPath}: ${error.message}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Build body components
|
|
642
|
+
let ssrBodyComponents = SsrComponent();
|
|
643
|
+
|
|
644
|
+
// Load additional body components
|
|
645
|
+
if (options.bodyComponents && Array.isArray(options.bodyComponents)) {
|
|
646
|
+
for (const compPath of options.bodyComponents) {
|
|
647
|
+
try {
|
|
648
|
+
const BodyComponent = await ssrFactory(compPath);
|
|
649
|
+
ssrBodyComponents += '\n' + BodyComponent();
|
|
650
|
+
} catch (error) {
|
|
651
|
+
logger.error(`Error loading body component ${compPath}: ${error.message}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Add custom body scripts
|
|
657
|
+
if (options.scripts?.body && Array.isArray(options.scripts.body)) {
|
|
658
|
+
ssrBodyComponents +=
|
|
659
|
+
'\n' + options.scripts.body.map((script) => TemplateHelpers.createScriptTag(script)).join('\n');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Build render payload
|
|
663
|
+
const renderPayload = {
|
|
65
664
|
version: Underpost.version,
|
|
66
665
|
...(options.env === 'development' ? { dev: true } : undefined),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
666
|
+
...options.customPayload,
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// Generate HTML
|
|
670
|
+
const htmlSrc = Render({
|
|
671
|
+
title: options.metadata.title,
|
|
672
|
+
ssrPath: options.buildPath === '/' ? '/' : `${options.buildPath}/`,
|
|
673
|
+
ssrHeadComponents,
|
|
674
|
+
ssrBodyComponents,
|
|
675
|
+
renderPayload,
|
|
676
|
+
renderApi: {
|
|
677
|
+
JSONweb,
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// Write output file
|
|
682
|
+
const outputDir = path.dirname(options.outputPath);
|
|
683
|
+
if (!fs.existsSync(outputDir)) {
|
|
684
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
fs.writeFileSync(options.outputPath, htmlSrc, 'utf8');
|
|
688
|
+
logger.info(`Static page generated: ${options.outputPath}`);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
logger.error(`Error generating static page: ${error.message}`);
|
|
691
|
+
logger.error(error.stack);
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
73
694
|
}
|
|
695
|
+
|
|
696
|
+
// Trigger build if requested
|
|
74
697
|
if (options.deployId && options.build) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
698
|
+
try {
|
|
699
|
+
logger.info(`Triggering build for deployment: ${options.deployId}`);
|
|
700
|
+
|
|
701
|
+
shellExec(`underpost env ${options.deployId} ${options.env}`);
|
|
702
|
+
shellExec(
|
|
703
|
+
`npm run build ${options.deployId}${options.buildHost ? ` ${options.buildHost} ${options.buildPath}` : ``}`,
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
logger.info('Build completed successfully');
|
|
707
|
+
} catch (error) {
|
|
708
|
+
logger.error(`Build error: ${error.message}`);
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
79
711
|
}
|
|
80
712
|
},
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Helper method to generate a config template file
|
|
716
|
+
*
|
|
717
|
+
* @param {string} outputPath - Where to save the template config
|
|
718
|
+
* @returns {void}
|
|
719
|
+
* @memberof UnderpostStatic
|
|
720
|
+
*
|
|
721
|
+
* @example
|
|
722
|
+
* // Generate a template configuration file
|
|
723
|
+
* UnderpostStatic.API.generateConfigTemplate('./my-static-config.json');
|
|
724
|
+
*/
|
|
725
|
+
generateConfigTemplate(outputPath = './static-config.json') {
|
|
726
|
+
const template = {
|
|
727
|
+
page: './src/client/ssr/body/DefaultSplashScreen.js',
|
|
728
|
+
outputPath: './dist/index.html',
|
|
729
|
+
env: 'production',
|
|
730
|
+
minify: true,
|
|
731
|
+
lang: 'en',
|
|
732
|
+
dir: 'ltr',
|
|
733
|
+
metadata: {
|
|
734
|
+
title: 'My Application',
|
|
735
|
+
description: 'A description of my application',
|
|
736
|
+
keywords: ['keyword1', 'keyword2', 'keyword3'],
|
|
737
|
+
author: 'Your Name',
|
|
738
|
+
themeColor: '#ffffff',
|
|
739
|
+
canonicalURL: 'https://example.com',
|
|
740
|
+
thumbnail: 'https://example.com/images/thumbnail.png',
|
|
741
|
+
locale: 'en-US',
|
|
742
|
+
siteName: 'My Site',
|
|
743
|
+
},
|
|
744
|
+
scripts: {
|
|
745
|
+
head: [
|
|
746
|
+
{
|
|
747
|
+
src: 'https://example.com/analytics.js',
|
|
748
|
+
async: true,
|
|
749
|
+
comment: 'Analytics script',
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
content: 'window.config = { apiUrl: "https://api.example.com" };',
|
|
753
|
+
comment: 'App configuration',
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
body: [
|
|
757
|
+
{
|
|
758
|
+
src: '/app.js',
|
|
759
|
+
type: 'module',
|
|
760
|
+
defer: true,
|
|
761
|
+
},
|
|
762
|
+
],
|
|
763
|
+
},
|
|
764
|
+
styles: [
|
|
765
|
+
{
|
|
766
|
+
href: '/styles/main.css',
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
content: 'body { margin: 0; padding: 0; }',
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
icons: {
|
|
773
|
+
favicon: '/favicon.ico',
|
|
774
|
+
appleTouchIcon: '/apple-touch-icon.png',
|
|
775
|
+
manifest: '/manifest.json',
|
|
776
|
+
},
|
|
777
|
+
headComponents: ['./src/client/ssr/head/Seo.js', './src/client/ssr/head/Pwa.js'],
|
|
778
|
+
microdata: [
|
|
779
|
+
{
|
|
780
|
+
'@context': 'https://schema.org',
|
|
781
|
+
'@type': 'WebSite',
|
|
782
|
+
name: 'My Site',
|
|
783
|
+
url: 'https://example.com',
|
|
784
|
+
},
|
|
785
|
+
],
|
|
786
|
+
customPayload: {
|
|
787
|
+
apiEndpoint: 'https://api.example.com',
|
|
788
|
+
customFeature: true,
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
ConfigLoader.save(outputPath, template);
|
|
793
|
+
logger.info(`Config template generated: ${outputPath}`);
|
|
794
|
+
},
|
|
81
795
|
};
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Export template helpers for external use
|
|
799
|
+
* @static
|
|
800
|
+
* @memberof UnderpostStatic
|
|
801
|
+
*/
|
|
802
|
+
static TemplateHelpers = TemplateHelpers;
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Export config validator for external use
|
|
806
|
+
* @static
|
|
807
|
+
* @memberof UnderpostStatic
|
|
808
|
+
*/
|
|
809
|
+
static ConfigValidator = ConfigValidator;
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Export config loader for external use
|
|
813
|
+
* @static
|
|
814
|
+
* @memberof UnderpostStatic
|
|
815
|
+
*/
|
|
816
|
+
static ConfigLoader = ConfigLoader;
|
|
82
817
|
}
|
|
83
818
|
|
|
84
819
|
export default UnderpostStatic;
|
|
820
|
+
export { TemplateHelpers, ConfigValidator, ConfigLoader };
|