underpost 2.95.3 → 2.96.0
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 +2 -2
- package/baremetal/commission-workflows.json +44 -0
- package/baremetal/packer-workflows.json +13 -0
- package/bin/deploy.js +6 -26
- package/cli.md +40 -43
- package/conf.js +4 -1
- package/examples/{QUICK-REFERENCE.md → static-page/QUICK-REFERENCE.md} +0 -18
- package/examples/{README.md → static-page/README.md} +3 -44
- package/examples/{STATIC-GENERATOR-GUIDE.md → static-page/STATIC-GENERATOR-GUIDE.md} +0 -50
- package/examples/{ssr-components → static-page/ssr-components}/CustomPage.js +0 -13
- package/examples/{static-config-simple.json → static-page/static-config-example.json} +1 -1
- 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/packer/images/Rocky9Amd64/Makefile +62 -0
- package/packer/images/Rocky9Amd64/QUICKSTART.md +113 -0
- package/packer/images/Rocky9Amd64/README.md +122 -0
- package/packer/images/Rocky9Amd64/http/rocky9.ks.pkrtpl.hcl +114 -0
- package/packer/images/Rocky9Amd64/rocky9.pkr.hcl +160 -0
- package/packer/scripts/fuse-nbd +64 -0
- package/packer/scripts/fuse-tar-root +63 -0
- package/scripts/maas-setup.sh +13 -2
- package/scripts/maas-upload-boot-resource.sh +183 -0
- package/scripts/packer-init-vars-file.sh +30 -0
- package/scripts/packer-setup.sh +52 -0
- package/src/cli/baremetal.js +262 -65
- package/src/cli/cloud-init.js +11 -5
- package/src/cli/cron.js +161 -29
- package/src/cli/db.js +59 -92
- package/src/cli/env.js +24 -3
- package/src/cli/index.js +18 -58
- package/src/cli/repository.js +178 -0
- package/src/cli/run.js +2 -3
- package/src/cli/static.js +99 -194
- package/src/client/services/default/default.management.js +7 -0
- package/src/index.js +1 -1
- package/src/server/backup.js +4 -53
- package/src/server/conf.js +3 -4
- package/examples/static-config-example.json +0 -183
- package/src/client/ssr/pages/404.js +0 -12
- package/src/client/ssr/pages/500.js +0 -12
- package/src/client/ssr/pages/maintenance.js +0 -14
- package/src/client/ssr/pages/offline.js +0 -21
package/src/cli/index.js
CHANGED
|
@@ -172,59 +172,7 @@ program
|
|
|
172
172
|
.option('--dev', 'Sets the development cli context')
|
|
173
173
|
|
|
174
174
|
.description(`Manages static build of page, bundles, and documentation with comprehensive customization options.`)
|
|
175
|
-
.action(
|
|
176
|
-
// Handle config template generation
|
|
177
|
-
if (options.generateConfig) {
|
|
178
|
-
const configPath = typeof options.generateConfig === 'string' ? options.generateConfig : './static-config.json';
|
|
179
|
-
return UnderpostStatic.API.generateConfigTemplate(configPath);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Parse comma-separated options
|
|
183
|
-
if (options.keywords) {
|
|
184
|
-
options.keywords = options.keywords.split(',').map((k) => k.trim());
|
|
185
|
-
}
|
|
186
|
-
if (options.headScripts) {
|
|
187
|
-
options.scripts = options.scripts || {};
|
|
188
|
-
options.scripts.head = options.headScripts.split(',').map((s) => ({ src: s.trim() }));
|
|
189
|
-
}
|
|
190
|
-
if (options.bodyScripts) {
|
|
191
|
-
options.scripts = options.scripts || {};
|
|
192
|
-
options.scripts.body = options.bodyScripts.split(',').map((s) => ({ src: s.trim() }));
|
|
193
|
-
}
|
|
194
|
-
if (options.styles) {
|
|
195
|
-
options.styles = options.styles.split(',').map((s) => ({ href: s.trim() }));
|
|
196
|
-
}
|
|
197
|
-
if (options.headComponents) {
|
|
198
|
-
options.headComponents = options.headComponents.split(',').map((c) => c.trim());
|
|
199
|
-
}
|
|
200
|
-
if (options.bodyComponents) {
|
|
201
|
-
options.bodyComponents = options.bodyComponents.split(',').map((c) => c.trim());
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Build metadata object from individual options
|
|
205
|
-
options.metadata = {
|
|
206
|
-
...(options.title && { title: options.title }),
|
|
207
|
-
...(options.description && { description: options.description }),
|
|
208
|
-
...(options.keywords && { keywords: options.keywords }),
|
|
209
|
-
...(options.author && { author: options.author }),
|
|
210
|
-
...(options.themeColor && { themeColor: options.themeColor }),
|
|
211
|
-
...(options.canonicalUrl && { canonicalURL: options.canonicalUrl }),
|
|
212
|
-
...(options.thumbnail && { thumbnail: options.thumbnail }),
|
|
213
|
-
...(options.locale && { locale: options.locale }),
|
|
214
|
-
...(options.siteName && { siteName: options.siteName }),
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
// Build icons object
|
|
218
|
-
if (options.favicon || options.appleTouchIcon || options.manifest) {
|
|
219
|
-
options.icons = {
|
|
220
|
-
...(options.favicon && { favicon: options.favicon }),
|
|
221
|
-
...(options.appleTouchIcon && { appleTouchIcon: options.appleTouchIcon }),
|
|
222
|
-
...(options.manifest && { manifest: options.manifest }),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return UnderpostStatic.API.callback(options);
|
|
227
|
-
});
|
|
175
|
+
.action(UnderpostStatic.API.callback);
|
|
228
176
|
|
|
229
177
|
// 'config' command: Manage Underpost configurations
|
|
230
178
|
program
|
|
@@ -233,6 +181,7 @@ program
|
|
|
233
181
|
.argument('[key]', 'Optional: The specific configuration key to manage.')
|
|
234
182
|
.argument('[value]', 'Optional: The value to set for the configuration key.')
|
|
235
183
|
.option('--plain', 'Prints the configuration value in plain text.')
|
|
184
|
+
.option('--filter <keyword>', 'Filters the list by matching key or value (only for list operation).')
|
|
236
185
|
.description(`Manages Underpost configurations using various operators.`)
|
|
237
186
|
.action((...args) => Underpost.env[args[0]](args[1], args[2], args[3]));
|
|
238
187
|
|
|
@@ -417,8 +366,6 @@ program
|
|
|
417
366
|
'--pod-name <pod-name>',
|
|
418
367
|
'Comma-separated list of pod names or patterns (supports wildcards like "mariadb-*").',
|
|
419
368
|
)
|
|
420
|
-
.option('--node-name <node-name>', 'Comma-separated list of node names to filter pods by their node placement.')
|
|
421
|
-
.option('--label-selector <selector>', 'Kubernetes label selector for filtering pods (e.g., "app=mariadb").')
|
|
422
369
|
.option('--all-pods', 'Target all matching pods instead of just the first one.')
|
|
423
370
|
.option('--primary-pod', 'Automatically detect and use MongoDB primary pod (MongoDB only).')
|
|
424
371
|
.option('--stats', 'Display database statistics (collection/table names with document/row counts).')
|
|
@@ -479,9 +426,9 @@ program
|
|
|
479
426
|
', ',
|
|
480
427
|
)}. Defaults to all available jobs.`,
|
|
481
428
|
)
|
|
482
|
-
.option('--
|
|
483
|
-
.option('--init', 'Initializes cron jobs for the default deployment ID.')
|
|
429
|
+
.option('--init-pm2-cronjobs', 'Initializes PM2 cron jobs from configuration for the specified deployment IDs.')
|
|
484
430
|
.option('--git', 'Uploads cron job configurations to GitHub.')
|
|
431
|
+
.option('--update-package-scripts', 'Updates package.json start scripts for each deploy-id configuration.')
|
|
485
432
|
.description('Manages cron jobs, including initialization, execution, and configuration updates.')
|
|
486
433
|
.action(Underpost.cron.callback);
|
|
487
434
|
|
|
@@ -664,13 +611,26 @@ program
|
|
|
664
611
|
.option('--control-server-uninstall', 'Uninstalls the baremetal control server.')
|
|
665
612
|
.option('--control-server-db-install', 'Installs up the database for the baremetal control server.')
|
|
666
613
|
.option('--control-server-db-uninstall', 'Uninstalls the database for the baremetal control server.')
|
|
614
|
+
.option('--install-packer', 'Installs Packer CLI.')
|
|
615
|
+
.option(
|
|
616
|
+
'--packer-maas-image-template <template-path>',
|
|
617
|
+
'Creates a new image folder from canonical/packer-maas template path (requires workflow-id).',
|
|
618
|
+
)
|
|
619
|
+
.option('--packer-workflow-id <workflow-id>', 'Specifies the workflow ID for Packer MAAS image operations.')
|
|
620
|
+
.option(
|
|
621
|
+
'--packer-maas-image-build',
|
|
622
|
+
'Builds a MAAS image using Packer for the workflow specified by --packer-workflow-id.',
|
|
623
|
+
)
|
|
624
|
+
.option(
|
|
625
|
+
'--packer-maas-image-upload',
|
|
626
|
+
'Uploads an existing MAAS image artifact without rebuilding for the workflow specified by --packer-workflow-id.',
|
|
627
|
+
)
|
|
667
628
|
.option('--commission', 'Init workflow for commissioning a physical machine.')
|
|
668
629
|
.option('--nfs-build', 'Builds an NFS root filesystem for a workflow id config architecture using QEMU emulation.')
|
|
669
630
|
.option('--nfs-mount', 'Mounts the NFS root filesystem for a workflow id config architecture.')
|
|
670
631
|
.option('--nfs-unmount', 'Unmounts the NFS root filesystem for a workflow id config architecture.')
|
|
671
632
|
.option('--nfs-sh', 'Copies QEMU emulation root entrypoint shell command to the clipboard.')
|
|
672
633
|
.option('--cloud-init-update', 'Updates cloud init for a workflow id config architecture.')
|
|
673
|
-
.option('--cloud-init-reset', 'Resets cloud init for a workflow id config architecture.')
|
|
674
634
|
.option('--logs <log-id>', 'Displays logs for log id: dhcp, cloud, machine, cloud-config.')
|
|
675
635
|
.option('--dev', 'Sets the development context environment for baremetal operations.')
|
|
676
636
|
.option('--ls', 'Lists available boot resources and machines.')
|
package/src/cli/repository.js
CHANGED
|
@@ -587,6 +587,184 @@ Prevent build private config repo.`,
|
|
|
587
587
|
fs.writeFileSync(targetConfPath, confRawPaths.join(sepRender), 'utf8');
|
|
588
588
|
shellExec(`prettier --write ${targetConfPath}`);
|
|
589
589
|
},
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Cleans the specified paths in the repository by resetting, checking out, and cleaning untracked files.
|
|
593
|
+
* @param {object} [options={ paths: [''] }] - The options for cleaning.
|
|
594
|
+
* @param {string[]} [options.paths=['']] - The paths to clean.
|
|
595
|
+
* @memberof UnderpostRepository
|
|
596
|
+
*/
|
|
597
|
+
clean(options = { paths: [''] }) {
|
|
598
|
+
for (const path of options.paths) {
|
|
599
|
+
shellExec(`cd ${path} && git reset`, { silent: true });
|
|
600
|
+
shellExec(`cd ${path} && git checkout .`, { silent: true });
|
|
601
|
+
shellExec(`cd ${path} && git clean -f -d`, { silent: true });
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Copies files recursively from a Git repository URL directory path.
|
|
607
|
+
* @param {object} options - Configuration options for copying files.
|
|
608
|
+
* @param {string} options.gitUrl - The GitHub repository URL (e.g., 'https://github.com/canonical/packer-maas').
|
|
609
|
+
* @param {string} options.directoryPath - The directory path within the repository to copy (e.g., 'rocky-9').
|
|
610
|
+
* @param {string} options.targetPath - The local target path where files should be copied.
|
|
611
|
+
* @param {string} [options.branch='main'] - The git branch to use (default: 'main').
|
|
612
|
+
* @param {boolean} [options.overwrite=false] - Whether to overwrite existing target directory.
|
|
613
|
+
* @returns {Promise<object>} A promise that resolves with copied files information.
|
|
614
|
+
* @memberof UnderpostRepository
|
|
615
|
+
*/
|
|
616
|
+
async copyGitUrlDirectoryRecursive(options) {
|
|
617
|
+
const { gitUrl, directoryPath, targetPath, branch = 'main', overwrite = false } = options;
|
|
618
|
+
|
|
619
|
+
// Validate inputs
|
|
620
|
+
if (!gitUrl) {
|
|
621
|
+
throw new Error('gitUrl is required');
|
|
622
|
+
}
|
|
623
|
+
if (!directoryPath) {
|
|
624
|
+
throw new Error('directoryPath is required');
|
|
625
|
+
}
|
|
626
|
+
if (!targetPath) {
|
|
627
|
+
throw new Error('targetPath is required');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Parse GitHub URL to extract owner and repo
|
|
631
|
+
const urlMatch = gitUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
632
|
+
if (!urlMatch) {
|
|
633
|
+
throw new Error(`Invalid GitHub URL: ${gitUrl}`);
|
|
634
|
+
}
|
|
635
|
+
const [, owner, repo] = urlMatch;
|
|
636
|
+
|
|
637
|
+
logger.info(`Copying from ${owner}/${repo}/${directoryPath} to ${targetPath}`);
|
|
638
|
+
|
|
639
|
+
// Check if target directory exists
|
|
640
|
+
if (fs.existsSync(targetPath) && !overwrite) {
|
|
641
|
+
throw new Error(`Target directory already exists: ${targetPath}. Use overwrite option to replace.`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Create target directory
|
|
645
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
646
|
+
|
|
647
|
+
// GitHub API base URL
|
|
648
|
+
const githubApiBase = 'https://api.github.com/repos';
|
|
649
|
+
const apiUrl = `${githubApiBase}/${owner}/${repo}/contents/${directoryPath}`;
|
|
650
|
+
|
|
651
|
+
logger.info(`Fetching directory contents from: ${apiUrl}`);
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
// Fetch directory contents recursively
|
|
655
|
+
const copiedFiles = await this._fetchAndCopyGitHubDirectory({
|
|
656
|
+
apiUrl,
|
|
657
|
+
targetPath,
|
|
658
|
+
basePath: directoryPath,
|
|
659
|
+
branch,
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
logger.info(`Successfully copied ${copiedFiles.length} files to ${targetPath}`);
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
success: true,
|
|
666
|
+
filesCount: copiedFiles.length,
|
|
667
|
+
files: copiedFiles,
|
|
668
|
+
targetPath,
|
|
669
|
+
};
|
|
670
|
+
} catch (error) {
|
|
671
|
+
// Clean up on error
|
|
672
|
+
if (fs.existsSync(targetPath)) {
|
|
673
|
+
fs.removeSync(targetPath);
|
|
674
|
+
logger.warn(`Cleaned up target directory after error: ${targetPath}`);
|
|
675
|
+
}
|
|
676
|
+
throw new Error(`Failed to copy directory: ${error.message}`);
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Internal method to recursively fetch and copy files from GitHub API.
|
|
682
|
+
* @private
|
|
683
|
+
* @param {object} options - Fetch options.
|
|
684
|
+
* @param {string} options.apiUrl - The GitHub API URL.
|
|
685
|
+
* @param {string} options.targetPath - The local target path.
|
|
686
|
+
* @param {string} options.basePath - The base path in the repository.
|
|
687
|
+
* @param {string} options.branch - The git branch.
|
|
688
|
+
* @returns {Promise<array>} Array of copied file paths.
|
|
689
|
+
* @memberof UnderpostRepository
|
|
690
|
+
*/
|
|
691
|
+
async _fetchAndCopyGitHubDirectory(options) {
|
|
692
|
+
const { apiUrl, targetPath, basePath, branch } = options;
|
|
693
|
+
const copiedFiles = [];
|
|
694
|
+
|
|
695
|
+
const response = await fetch(apiUrl, {
|
|
696
|
+
headers: {
|
|
697
|
+
Accept: 'application/vnd.github.v3+json',
|
|
698
|
+
'User-Agent': 'underpost-cli',
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
if (!response.ok) {
|
|
703
|
+
const errorBody = await response.text();
|
|
704
|
+
logger.error(`GitHub API request failed for: ${apiUrl}`);
|
|
705
|
+
logger.error(`Status: ${response.status} ${response.statusText}`);
|
|
706
|
+
logger.error(`Response: ${errorBody}`);
|
|
707
|
+
throw new Error(`GitHub API request failed: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const contents = await response.json();
|
|
711
|
+
|
|
712
|
+
if (!Array.isArray(contents)) {
|
|
713
|
+
logger.error(`Expected directory but got: ${typeof contents}`);
|
|
714
|
+
logger.error(`API URL: ${apiUrl}`);
|
|
715
|
+
logger.error(`Response keys: ${Object.keys(contents).join(', ')}`);
|
|
716
|
+
if (contents.message) {
|
|
717
|
+
logger.error(`GitHub message: ${contents.message}`);
|
|
718
|
+
}
|
|
719
|
+
throw new Error(
|
|
720
|
+
`Path is not a directory: ${basePath}. Response: ${JSON.stringify(contents).substring(0, 200)}`,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
logger.info(`Found ${contents.length} items in directory: ${basePath}`);
|
|
725
|
+
|
|
726
|
+
// Process each item in the directory
|
|
727
|
+
for (const item of contents) {
|
|
728
|
+
const itemTargetPath = `${targetPath}/${item.name}`;
|
|
729
|
+
|
|
730
|
+
if (item.type === 'file') {
|
|
731
|
+
logger.info(`Downloading file: ${item.path}`);
|
|
732
|
+
|
|
733
|
+
// Download file content
|
|
734
|
+
const fileResponse = await fetch(item.download_url);
|
|
735
|
+
if (!fileResponse.ok) {
|
|
736
|
+
logger.error(`Failed to download: ${item.download_url}`);
|
|
737
|
+
throw new Error(`Failed to download file: ${item.path} (${fileResponse.status})`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const fileContent = await fileResponse.text();
|
|
741
|
+
fs.writeFileSync(itemTargetPath, fileContent);
|
|
742
|
+
|
|
743
|
+
logger.info(`✓ Saved: ${itemTargetPath}`);
|
|
744
|
+
copiedFiles.push(itemTargetPath);
|
|
745
|
+
} else if (item.type === 'dir') {
|
|
746
|
+
logger.info(`📁 Processing directory: ${item.path}`);
|
|
747
|
+
|
|
748
|
+
// Create subdirectory
|
|
749
|
+
fs.mkdirSync(itemTargetPath, { recursive: true });
|
|
750
|
+
|
|
751
|
+
// Recursively process subdirectory
|
|
752
|
+
const subFiles = await this._fetchAndCopyGitHubDirectory({
|
|
753
|
+
apiUrl: item.url,
|
|
754
|
+
targetPath: itemTargetPath,
|
|
755
|
+
basePath: item.path,
|
|
756
|
+
branch,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
copiedFiles.push(...subFiles);
|
|
760
|
+
logger.info(`✓ Completed directory: ${item.path} (${subFiles.length} files)`);
|
|
761
|
+
} else {
|
|
762
|
+
logger.warn(`Skipping unknown item type '${item.type}': ${item.path}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return copiedFiles;
|
|
767
|
+
},
|
|
590
768
|
};
|
|
591
769
|
}
|
|
592
770
|
|
package/src/cli/run.js
CHANGED
|
@@ -446,8 +446,7 @@ class UnderpostRun {
|
|
|
446
446
|
* @memberof UnderpostRun
|
|
447
447
|
*/
|
|
448
448
|
clean: (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
|
|
449
|
-
|
|
450
|
-
shellExec(`node bin/deploy clean-core-repo`);
|
|
449
|
+
Underpost.repo.clean({ paths: path ? path.split(',') : ['/home/dd/engine', '/home/dd/engine/engine-private'] });
|
|
451
450
|
},
|
|
452
451
|
/**
|
|
453
452
|
* @method pull
|
|
@@ -1187,7 +1186,7 @@ EOF
|
|
|
1187
1186
|
}
|
|
1188
1187
|
await timer(5000);
|
|
1189
1188
|
for (const deployId of deployList) {
|
|
1190
|
-
shellExec(`${baseCommand} db ${deployId} --import --git`);
|
|
1189
|
+
shellExec(`${baseCommand} db ${deployId} --import --git --drop --preserveUUID --primary-pod`);
|
|
1191
1190
|
}
|
|
1192
1191
|
await timer(5000);
|
|
1193
1192
|
shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --pull-image --valkey`);
|
package/src/cli/static.js
CHANGED
|
@@ -2,55 +2,6 @@
|
|
|
2
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
|
-
* });
|
|
54
5
|
*/
|
|
55
6
|
|
|
56
7
|
import fs from 'fs-extra';
|
|
@@ -140,6 +91,31 @@ const logger = loggerFactory(import.meta);
|
|
|
140
91
|
* @property {Object} [microdata=[]] - Structured data (JSON-LD)
|
|
141
92
|
*/
|
|
142
93
|
|
|
94
|
+
const DefaultStaticGenerationOptions = {
|
|
95
|
+
page: '',
|
|
96
|
+
title: '',
|
|
97
|
+
outputPath: '',
|
|
98
|
+
deployId: '',
|
|
99
|
+
buildHost: '',
|
|
100
|
+
buildPath: '/',
|
|
101
|
+
env: 'production',
|
|
102
|
+
build: false,
|
|
103
|
+
dev: false,
|
|
104
|
+
minify: true,
|
|
105
|
+
metadata: {},
|
|
106
|
+
scripts: {},
|
|
107
|
+
styles: [],
|
|
108
|
+
headComponents: [],
|
|
109
|
+
bodyComponents: [],
|
|
110
|
+
icons: {},
|
|
111
|
+
customPayload: {},
|
|
112
|
+
templateHelpers: {},
|
|
113
|
+
configFile: '',
|
|
114
|
+
lang: 'en',
|
|
115
|
+
dir: 'ltr',
|
|
116
|
+
microdata: [],
|
|
117
|
+
};
|
|
118
|
+
|
|
143
119
|
/**
|
|
144
120
|
* Template helper functions for common SSR patterns
|
|
145
121
|
* @namespace TemplateHelpers
|
|
@@ -150,19 +126,6 @@ const TemplateHelpers = {
|
|
|
150
126
|
* @param {ScriptOptions} options - Script options
|
|
151
127
|
* @returns {string} HTML script tag
|
|
152
128
|
* @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
129
|
*/
|
|
167
130
|
createScriptTag(options) {
|
|
168
131
|
const attrs = [];
|
|
@@ -194,16 +157,6 @@ const TemplateHelpers = {
|
|
|
194
157
|
* @param {StyleOptions} options - Style options
|
|
195
158
|
* @returns {string} HTML link or style tag
|
|
196
159
|
* @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
160
|
*/
|
|
208
161
|
createStyleTag(options) {
|
|
209
162
|
if (options.content) {
|
|
@@ -224,13 +177,6 @@ const TemplateHelpers = {
|
|
|
224
177
|
* @param {IconOptions} icons - Icon options
|
|
225
178
|
* @returns {string} HTML icon link tags
|
|
226
179
|
* @memberof TemplateHelpers
|
|
227
|
-
*
|
|
228
|
-
* @example
|
|
229
|
-
* TemplateHelpers.createIconTags({
|
|
230
|
-
* favicon: '/favicon.ico',
|
|
231
|
-
* appleTouchIcon: '/apple-touch-icon.png',
|
|
232
|
-
* manifest: '/manifest.json'
|
|
233
|
-
* })
|
|
234
180
|
*/
|
|
235
181
|
createIconTags(icons) {
|
|
236
182
|
const tags = [];
|
|
@@ -261,13 +207,6 @@ const TemplateHelpers = {
|
|
|
261
207
|
* @param {MetadataOptions} metadata - Metadata options
|
|
262
208
|
* @returns {string} HTML meta tags
|
|
263
209
|
* @memberof TemplateHelpers
|
|
264
|
-
*
|
|
265
|
-
* @example
|
|
266
|
-
* TemplateHelpers.createMetaTags({
|
|
267
|
-
* description: 'My page description',
|
|
268
|
-
* keywords: ['web', 'app'],
|
|
269
|
-
* author: 'John Doe'
|
|
270
|
-
* })
|
|
271
210
|
*/
|
|
272
211
|
createMetaTags(metadata) {
|
|
273
212
|
const tags = [];
|
|
@@ -329,16 +268,6 @@ const TemplateHelpers = {
|
|
|
329
268
|
* @param {Object[]} microdata - Array of structured data objects
|
|
330
269
|
* @returns {string} HTML script tags with JSON-LD
|
|
331
270
|
* @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
271
|
*/
|
|
343
272
|
createMicrodataTags(microdata) {
|
|
344
273
|
if (!microdata || !Array.isArray(microdata) || microdata.length === 0) {
|
|
@@ -410,19 +339,6 @@ const ConfigLoader = {
|
|
|
410
339
|
* @param {string} configPath - Path to config file
|
|
411
340
|
* @returns {Object} Configuration object
|
|
412
341
|
* @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
342
|
*/
|
|
427
343
|
load(configPath) {
|
|
428
344
|
try {
|
|
@@ -466,90 +382,83 @@ class UnderpostStatic {
|
|
|
466
382
|
* Generate static HTML file with enhanced customization options
|
|
467
383
|
*
|
|
468
384
|
* @param {StaticGenerationOptions} options - Options for static generation
|
|
385
|
+
* @param {string} [options.page] - Path to the SSR component to render
|
|
386
|
+
* @param {string} [options.title] - Page title (deprecated: use metadata.title)
|
|
387
|
+
* @param {string} [options.outputPath] - Output file path
|
|
388
|
+
* @param {string} [options.deployId] - Deployment identifier
|
|
389
|
+
* @param {string} [options.buildHost] - Build host URL
|
|
390
|
+
* @param {string} [options.buildPath='/'] - Build path
|
|
391
|
+
* @param {string} [options.env='production'] - Environment (development/production)
|
|
392
|
+
* @param {boolean} [options.build=false] - Whether to trigger build
|
|
393
|
+
* @param {boolean} [options.minify=true] - Minify HTML output
|
|
394
|
+
* @param {MetadataOptions} [options.metadata={}] - Comprehensive metadata options
|
|
395
|
+
* @param {Object} [options.scripts={}] - Script injection options
|
|
396
|
+
* @param {ScriptOptions[]} [options.scripts.head=[]] - Scripts for
|
|
397
|
+
* head section
|
|
398
|
+
* @param {ScriptOptions[]} [options.scripts.body=[]] - Scripts for body section
|
|
399
|
+
* @param {StyleOptions[]} [options.styles=[]] - Stylesheet options
|
|
400
|
+
* @param {string[]} [options.headComponents=[]] - Array of SSR head component paths
|
|
401
|
+
* @param {string[]} [options.bodyComponents=[]] - Array of SSR body component paths
|
|
402
|
+
* @param {IconOptions} [options.icons={}] - Icon configuration
|
|
403
|
+
* @param {Object} [options.customPayload={}] - Custom data to inject into renderPayload
|
|
404
|
+
* @param {string} [options.configFile=''] - Path to JSON config file
|
|
405
|
+
* @param {string} [options.lang='en'] - HTML lang attribute
|
|
406
|
+
* @param {string} [options.dir='ltr'] - HTML dir attribute
|
|
407
|
+
* @param {Object} [options.microdata=[]] - Structured data (JSON-LD)
|
|
469
408
|
* @returns {Promise<void>}
|
|
470
409
|
* @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
|
-
* });
|
|
551
410
|
*/
|
|
552
|
-
async callback(options =
|
|
411
|
+
async callback(options = DefaultStaticGenerationOptions) {
|
|
412
|
+
// Handle config template generation
|
|
413
|
+
if (options.generateConfig) {
|
|
414
|
+
const configPath = typeof options.generateConfig === 'string' ? options.generateConfig : './static-config.json';
|
|
415
|
+
return UnderpostStatic.API.generateConfigTemplate(configPath);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Parse comma-separated options
|
|
419
|
+
if (options.keywords) {
|
|
420
|
+
options.keywords = options.keywords.split(',').map((k) => k.trim());
|
|
421
|
+
}
|
|
422
|
+
if (options.headScripts) {
|
|
423
|
+
options.scripts = options.scripts || {};
|
|
424
|
+
options.scripts.head = options.headScripts.split(',').map((s) => ({ src: s.trim() }));
|
|
425
|
+
}
|
|
426
|
+
if (options.bodyScripts) {
|
|
427
|
+
options.scripts = options.scripts || {};
|
|
428
|
+
options.scripts.body = options.bodyScripts.split(',').map((s) => ({ src: s.trim() }));
|
|
429
|
+
}
|
|
430
|
+
if (options.styles) {
|
|
431
|
+
options.styles = options.styles.split(',').map((s) => ({ href: s.trim() }));
|
|
432
|
+
}
|
|
433
|
+
if (options.headComponents) {
|
|
434
|
+
options.headComponents = options.headComponents.split(',').map((c) => c.trim());
|
|
435
|
+
}
|
|
436
|
+
if (options.bodyComponents) {
|
|
437
|
+
options.bodyComponents = options.bodyComponents.split(',').map((c) => c.trim());
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Build metadata object from individual options
|
|
441
|
+
options.metadata = {
|
|
442
|
+
...(options.title && { title: options.title }),
|
|
443
|
+
...(options.description && { description: options.description }),
|
|
444
|
+
...(options.keywords && { keywords: options.keywords }),
|
|
445
|
+
...(options.author && { author: options.author }),
|
|
446
|
+
...(options.themeColor && { themeColor: options.themeColor }),
|
|
447
|
+
...(options.canonicalUrl && { canonicalURL: options.canonicalUrl }),
|
|
448
|
+
...(options.thumbnail && { thumbnail: options.thumbnail }),
|
|
449
|
+
...(options.locale && { locale: options.locale }),
|
|
450
|
+
...(options.siteName && { siteName: options.siteName }),
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Build icons object
|
|
454
|
+
if (options.favicon || options.appleTouchIcon || options.manifest) {
|
|
455
|
+
options.icons = {
|
|
456
|
+
...(options.favicon && { favicon: options.favicon }),
|
|
457
|
+
...(options.appleTouchIcon && { appleTouchIcon: options.appleTouchIcon }),
|
|
458
|
+
...(options.manifest && { manifest: options.manifest }),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
553
462
|
// Load config from file if specified
|
|
554
463
|
if (options.configFile) {
|
|
555
464
|
const fileConfig = ConfigLoader.load(options.configFile);
|
|
@@ -717,10 +626,6 @@ class UnderpostStatic {
|
|
|
717
626
|
* @param {string} outputPath - Where to save the template config
|
|
718
627
|
* @returns {void}
|
|
719
628
|
* @memberof UnderpostStatic
|
|
720
|
-
*
|
|
721
|
-
* @example
|
|
722
|
-
* // Generate a template configuration file
|
|
723
|
-
* UnderpostStatic.API.generateConfigTemplate('./my-static-config.json');
|
|
724
629
|
*/
|
|
725
630
|
generateConfigTemplate(outputPath = './static-config.json') {
|
|
726
631
|
const template = {
|