@underpostnet/underpost 3.1.1 → 3.1.3

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.
@@ -0,0 +1,285 @@
1
+ const swaggerDarkCss = css`
2
+ /* ── Toggle button ── */
3
+ .swagger-ui .topbar-wrapper {
4
+ display: flex;
5
+ align-items: center;
6
+ }
7
+ .swagger-theme-toggle-btn {
8
+ background: rgba(255, 255, 255, 0.12);
9
+ border: 1px solid rgba(255, 255, 255, 0.3);
10
+ color: #fff;
11
+ padding: 5px 14px;
12
+ border-radius: 4px;
13
+ cursor: pointer;
14
+ font-size: 13px;
15
+ margin-left: auto;
16
+ margin-right: 16px;
17
+ white-space: nowrap;
18
+ transition: background 0.2s ease;
19
+ }
20
+ .swagger-theme-toggle-btn:hover {
21
+ background: rgba(255, 255, 255, 0.25);
22
+ }
23
+
24
+ /* ── Dark mode overrides — black/gray gradients ── */
25
+ body.swagger-dark {
26
+ background: #0f0f0f;
27
+ }
28
+ body.swagger-dark .swagger-ui {
29
+ background: #0f0f0f;
30
+ }
31
+ body.swagger-dark .swagger-ui .wrapper {
32
+ background: #0f0f0f;
33
+ }
34
+
35
+ /* Info block */
36
+ body.swagger-dark .swagger-ui .info .title,
37
+ body.swagger-dark .swagger-ui .info h1,
38
+ body.swagger-dark .swagger-ui .info h2,
39
+ body.swagger-dark .swagger-ui .info h3,
40
+ body.swagger-dark .swagger-ui .info p,
41
+ body.swagger-dark .swagger-ui .info li,
42
+ body.swagger-dark .swagger-ui .info a {
43
+ color: #e8e8e8;
44
+ }
45
+
46
+ /* Scheme / server selector bar */
47
+ body.swagger-dark .swagger-ui .scheme-container {
48
+ background: linear-gradient(180deg, #1a1a1a 0%, #222222 100%);
49
+ box-shadow: 0 1px 0 #383838;
50
+ }
51
+ body.swagger-dark .swagger-ui select {
52
+ background: #2a2a2a;
53
+ color: #e8e8e8;
54
+ border-color: #383838;
55
+ }
56
+
57
+ /* Operation tags */
58
+ body.swagger-dark .swagger-ui .opblock-tag {
59
+ color: #e8e8e8;
60
+ border-color: #383838;
61
+ }
62
+ body.swagger-dark .swagger-ui .opblock-tag small {
63
+ color: #a8a8a8;
64
+ }
65
+ body.swagger-dark .swagger-ui .opblock-tag:hover {
66
+ background: rgba(255, 255, 255, 0.04);
67
+ }
68
+
69
+ /* Operation blocks */
70
+ body.swagger-dark .swagger-ui .opblock {
71
+ background: linear-gradient(180deg, #161616 0%, #1e1e1e 100%);
72
+ border-color: #383838;
73
+ }
74
+ body.swagger-dark .swagger-ui .opblock .opblock-summary {
75
+ border-color: #383838;
76
+ }
77
+ body.swagger-dark .swagger-ui .opblock .opblock-summary-description,
78
+ body.swagger-dark .swagger-ui .opblock .opblock-summary-operation-id {
79
+ color: #a8a8a8;
80
+ }
81
+ body.swagger-dark .swagger-ui .opblock-body {
82
+ background: #0f0f0f;
83
+ }
84
+ body.swagger-dark .swagger-ui .opblock-description-wrapper p,
85
+ body.swagger-dark .swagger-ui .opblock-external-docs-wrapper p {
86
+ color: #a8a8a8;
87
+ }
88
+ body.swagger-dark .swagger-ui .opblock-section-header {
89
+ background: linear-gradient(180deg, #1a1a1a 0%, #222222 100%);
90
+ border-color: #383838;
91
+ }
92
+ body.swagger-dark .swagger-ui .opblock-section-header h4 {
93
+ color: #e8e8e8;
94
+ }
95
+
96
+ /* Models section */
97
+ body.swagger-dark .swagger-ui section.models {
98
+ background: linear-gradient(180deg, #161616 0%, #1e1e1e 100%);
99
+ border-color: #383838;
100
+ }
101
+ body.swagger-dark .swagger-ui section.models h4,
102
+ body.swagger-dark .swagger-ui section.models h5 {
103
+ color: #e8e8e8;
104
+ border-color: #383838;
105
+ }
106
+ body.swagger-dark .swagger-ui .model-container {
107
+ background: #0f0f0f;
108
+ border-color: #2a2a2a;
109
+ }
110
+ body.swagger-dark .swagger-ui .model-box {
111
+ background: #1a1a1a;
112
+ }
113
+ body.swagger-dark .swagger-ui .model,
114
+ body.swagger-dark .swagger-ui .model-title,
115
+ body.swagger-dark .swagger-ui .model span,
116
+ body.swagger-dark .swagger-ui .model .property {
117
+ color: #e8e8e8;
118
+ }
119
+ body.swagger-dark .swagger-ui .model .property.primitive {
120
+ color: #a8a8a8;
121
+ }
122
+
123
+ /* Tables */
124
+ body.swagger-dark .swagger-ui table thead tr th {
125
+ background: linear-gradient(180deg, #222222 0%, #2a2a2a 100%);
126
+ color: #e8e8e8;
127
+ border-color: #383838;
128
+ }
129
+ body.swagger-dark .swagger-ui table tbody tr td {
130
+ color: #a8a8a8;
131
+ border-color: #2a2a2a;
132
+ }
133
+
134
+ /* Parameters */
135
+ body.swagger-dark .swagger-ui .parameter__name,
136
+ body.swagger-dark .swagger-ui .parameter__type,
137
+ body.swagger-dark .swagger-ui .parameter__in,
138
+ body.swagger-dark .swagger-ui .parameter__extension,
139
+ body.swagger-dark .swagger-ui .parameter__empty_value_toggle {
140
+ color: #a8a8a8;
141
+ }
142
+ body.swagger-dark .swagger-ui .parameter__name.required:after {
143
+ color: #ff6b6b;
144
+ }
145
+
146
+ /* Inputs & textareas */
147
+ body.swagger-dark .swagger-ui input[type='text'],
148
+ body.swagger-dark .swagger-ui input[type='email'],
149
+ body.swagger-dark .swagger-ui input[type='password'],
150
+ body.swagger-dark .swagger-ui input[type='search'],
151
+ body.swagger-dark .swagger-ui input[type='number'],
152
+ body.swagger-dark .swagger-ui textarea {
153
+ background: #2a2a2a;
154
+ color: #e8e8e8;
155
+ border-color: #383838;
156
+ }
157
+
158
+ /* Buttons */
159
+ body.swagger-dark .swagger-ui .btn {
160
+ color: #e8e8e8;
161
+ border-color: #383838;
162
+ background: #222222;
163
+ }
164
+ body.swagger-dark .swagger-ui .btn:hover {
165
+ background: #2a2a2a;
166
+ }
167
+ body.swagger-dark .swagger-ui .btn.authorize {
168
+ color: #49cc90;
169
+ border-color: #49cc90;
170
+ background: transparent;
171
+ }
172
+ body.swagger-dark .swagger-ui .btn.execute {
173
+ background: linear-gradient(180deg, #333333 0%, #262626 100%);
174
+ border-color: #484848;
175
+ color: #e8e8e8;
176
+ }
177
+ body.swagger-dark .swagger-ui .btn.cancel {
178
+ color: #ff6b6b;
179
+ border-color: #ff6b6b;
180
+ }
181
+
182
+ /* Responses */
183
+ body.swagger-dark .swagger-ui .responses-inner h4,
184
+ body.swagger-dark .swagger-ui .responses-inner h5 {
185
+ color: #a8a8a8;
186
+ }
187
+ body.swagger-dark .swagger-ui .response-col_status {
188
+ color: #e8e8e8;
189
+ }
190
+ body.swagger-dark .swagger-ui .response-col_description__inner p {
191
+ color: #a8a8a8;
192
+ }
193
+ body.swagger-dark .swagger-ui .response {
194
+ border-color: #2a2a2a;
195
+ }
196
+
197
+ /* Code / highlight */
198
+ body.swagger-dark .swagger-ui .highlight-code,
199
+ body.swagger-dark .swagger-ui .microlight {
200
+ background: #1a1a1a;
201
+ border: 1px solid #2a2a2a;
202
+ }
203
+
204
+ /* Markdown */
205
+ body.swagger-dark .swagger-ui .markdown p,
206
+ body.swagger-dark .swagger-ui .markdown li,
207
+ body.swagger-dark .swagger-ui .markdown a {
208
+ color: #a8a8a8;
209
+ }
210
+
211
+ /* Tabs */
212
+ body.swagger-dark .swagger-ui .tab li {
213
+ color: #a8a8a8;
214
+ }
215
+ body.swagger-dark .swagger-ui .tab li.active {
216
+ color: #e8e8e8;
217
+ }
218
+
219
+ /* Auth / modal dialog */
220
+ body.swagger-dark .swagger-ui .dialog-ux .modal-ux {
221
+ background: linear-gradient(180deg, #1a1a1a 0%, #222222 100%);
222
+ border: 1px solid #383838;
223
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.7);
224
+ }
225
+ body.swagger-dark .swagger-ui .dialog-ux .modal-ux-header {
226
+ border-bottom: 1px solid #383838;
227
+ background: linear-gradient(180deg, #222222 0%, #1a1a1a 100%);
228
+ }
229
+ body.swagger-dark .swagger-ui .dialog-ux .modal-ux-header h3 {
230
+ color: #e8e8e8;
231
+ }
232
+ body.swagger-dark .swagger-ui .dialog-ux .modal-ux-content p,
233
+ body.swagger-dark .swagger-ui .dialog-ux .modal-ux-content label,
234
+ body.swagger-dark .swagger-ui .dialog-ux .modal-ux-content h4 {
235
+ color: #e8e8e8;
236
+ }
237
+ body.swagger-dark .swagger-ui .auth-container .errors {
238
+ color: #ff6b6b;
239
+ }
240
+
241
+ /* Info / description */
242
+ body.swagger-dark .swagger-ui .info .base-url,
243
+ body.swagger-dark .swagger-ui .servers-title,
244
+ body.swagger-dark .swagger-ui .servers > label {
245
+ color: #a8a8a8;
246
+ }
247
+
248
+ /* Expand/collapse arrows */
249
+ body.swagger-dark .swagger-ui svg.arrow path {
250
+ fill: #a8a8a8;
251
+ }
252
+ `;
253
+
254
+ const swaggerDarkJs = `(function () {
255
+ function injectThemeToggle() {
256
+ var topbarWrapper = document.querySelector('.swagger-ui .topbar-wrapper');
257
+ if (!topbarWrapper || document.getElementById('swagger-theme-toggle')) return;
258
+
259
+ var savedTheme = localStorage.getItem('swagger-theme') || 'light';
260
+ if (savedTheme === 'dark') document.body.classList.add('swagger-dark');
261
+
262
+ var btn = document.createElement('button');
263
+ btn.id = 'swagger-theme-toggle';
264
+ btn.className = 'swagger-theme-toggle-btn';
265
+ btn.setAttribute('title', 'Toggle dark / light mode');
266
+ btn.textContent = savedTheme === 'dark' ? '\u2600\uFE0F Light Mode' : '\uD83C\uDF19 Dark Mode';
267
+
268
+ btn.addEventListener('click', function () {
269
+ var isDark = document.body.classList.toggle('swagger-dark');
270
+ localStorage.setItem('swagger-theme', isDark ? 'dark' : 'light');
271
+ btn.textContent = isDark ? '\u2600\uFE0F Light Mode' : '\uD83C\uDF19 Dark Mode';
272
+ });
273
+
274
+ topbarWrapper.appendChild(btn);
275
+ }
276
+
277
+ var poll = setInterval(function () {
278
+ if (document.querySelector('.swagger-ui .topbar-wrapper')) {
279
+ injectThemeToggle();
280
+ clearInterval(poll);
281
+ }
282
+ }, 100);
283
+ })();`;
284
+
285
+ SrrComponent = () => ({ css: swaggerDarkCss, js: swaggerDarkJs });
@@ -51,17 +51,18 @@ const main = () => {
51
51
  <br />
52
52
  <br />${Translate.Render('no-internet-connection')} <br />
53
53
  <br />
54
- <a href="${location.origin}">${Translate.Render('back')}</a>
54
+ <a target="_top" href="${location.origin}">${Translate.Render('back')}</a>
55
55
  </div>`,
56
56
  );
57
57
  };
58
58
 
59
- SrrComponent = () => html`<script>
60
- {
61
- const s = ${s};
62
- const append = ${append};
63
- const getLang = ${getLang};
64
- const main = ${main};
65
- window.onload = main;
66
- }
67
- </script>`;
59
+ SrrComponent = () =>
60
+ html`<script>
61
+ {
62
+ const s = ${s};
63
+ const append = ${append};
64
+ const getLang = ${getLang};
65
+ const main = ${main};
66
+ window.onload = main;
67
+ }
68
+ </script>`;
@@ -182,17 +182,18 @@ const main = () => {
182
182
  <span class="bold">Test Page</span>
183
183
  <br />
184
184
  <br />
185
- <a href="${location.origin}">${Translate.Render('back')}</a>
185
+ <a target="_top" href="${location.origin}">${Translate.Render('back')}</a>
186
186
  </div>`,
187
187
  );
188
188
  };
189
189
 
190
- SrrComponent = () => html`<script>
191
- {
192
- const s = ${s};
193
- const append = ${append};
194
- const getLang = ${getLang};
195
- const main = ${main};
196
- window.onload = main;
197
- }
198
- </script>`;
190
+ SrrComponent = () =>
191
+ html`<script>
192
+ {
193
+ const s = ${s};
194
+ const append = ${append};
195
+ const getLang = ${getLang};
196
+ const main = ${main};
197
+ window.onload = main;
198
+ }
199
+ </script>`;
package/src/index.js CHANGED
@@ -42,7 +42,7 @@ class Underpost {
42
42
  * @type {String}
43
43
  * @memberof Underpost
44
44
  */
45
- static version = 'v3.1.1';
45
+ static version = 'v3.1.3';
46
46
 
47
47
  /**
48
48
  * Required Node.js major version
@@ -165,14 +165,7 @@ class ExpressService {
165
165
  });
166
166
  }
167
167
 
168
- // Swagger UI setup
169
- if (fs.existsSync(swaggerJsonPath)) {
170
- const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
171
- const swaggerUiOptions = await buildSwaggerUiOptions();
172
- app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc, swaggerUiOptions));
173
- }
174
-
175
- // Security and CORS
168
+ // Security and CORS — must run before swagger-ui so CSP headers are included in swagger responses
176
169
  if (process.env.NODE_ENV === 'development' && useLocalSsl)
177
170
  origins = origins.map((origin) => origin.replace('http', 'https'));
178
171
 
@@ -180,6 +173,13 @@ class ExpressService {
180
173
  origin: origins,
181
174
  });
182
175
 
176
+ // Swagger UI setup (after security middleware so responses carry correct CSP headers)
177
+ if (fs.existsSync(swaggerJsonPath)) {
178
+ const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
179
+ const swaggerUiOptions = await buildSwaggerUiOptions();
180
+ app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc, swaggerUiOptions));
181
+ }
182
+
183
183
  // Database and Valkey connections
184
184
  if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
185
185
 
@@ -613,13 +613,13 @@ function applySecurity(app, opts = {}) {
613
613
  frameAncestors: frameAncestors,
614
614
  imgSrc: ["'self'", 'data:', httpDirective, 'https:', 'blob:'],
615
615
  objectSrc: ["'none'"],
616
- // script-src and script-src-elem include dynamic nonce
617
- scriptSrc: [
616
+ // script-src and script-src-elem: use 'unsafe-inline' for swagger (no nonce, otherwise
617
+ // the nonce causes 'unsafe-inline' to be ignored per CSP3 spec), nonce for everything else.
618
+ scriptSrc: ["'self'", (req, res) => (res.locals.isSwagger ? "'unsafe-inline'" : `'nonce-${res.locals.nonce}'`)],
619
+ scriptSrcElem: [
618
620
  "'self'",
619
- (req, res) => `'nonce-${res.locals.nonce}'`,
620
- (req, res) => (res.locals.isSwagger ? "'unsafe-inline'" : ''),
621
+ (req, res) => (res.locals.isSwagger ? "'unsafe-inline'" : `'nonce-${res.locals.nonce}'`),
621
622
  ],
622
- scriptSrcElem: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
623
623
  // style-src: avoid 'unsafe-inline' when possible; if you must inline styles,
624
624
  // use a nonce for them too (or hash).
625
625
  styleSrc: [
@@ -627,6 +627,7 @@ function applySecurity(app, opts = {}) {
627
627
  httpDirective,
628
628
  (req, res) => (res.locals.isSwagger ? "'unsafe-inline'" : `'nonce-${res.locals.nonce}'`),
629
629
  ],
630
+ styleSrcAttr: [(req, res) => (res.locals.isSwagger ? "'unsafe-inline'" : "'none'")],
630
631
  // deny plugins
631
632
  objectSrc: ["'none'"],
632
633
  },
@@ -332,56 +332,17 @@ const buildApiDocs = async ({
332
332
  * @param {string} options.path - The base path for the documentation
333
333
  * @param {Object} options.metadata - Metadata for the documentation
334
334
  * @param {string} options.publicClientId - Client ID used to resolve the tutorials/references directory
335
+ * @param {Object} options.docs - Documentation config from server conf
336
+ * @param {string} options.docs.jsJsonPath - Path to the JSDoc JSON config file
335
337
  */
336
- const buildJsDocs = async ({ host, path, metadata = {}, publicClientId }) => {
338
+ const buildJsDocs = async ({ host, path, metadata = {}, publicClientId, docs }) => {
337
339
  const logger = loggerFactory(import.meta);
338
340
 
339
- // Detect custom jsdoc.<deployId>.json by matching host against deploy server configs
340
- let customJsDocPath = '';
341
- const privateConfBase = `./engine-private/conf`;
342
- if (fs.existsSync(privateConfBase)) {
343
- for (const deployId of fs.readdirSync(privateConfBase)) {
344
- const candidatePath = `./jsdoc.${deployId}.json`;
345
- if (!fs.existsSync(candidatePath)) continue;
346
-
347
- // Check if this deployId's server config contains the current host
348
- const serverConfPath = `${privateConfBase}/${deployId}/conf.server.json`;
349
- if (fs.existsSync(serverConfPath)) {
350
- try {
351
- const serverConf = JSON.parse(fs.readFileSync(serverConfPath, 'utf8'));
352
- if (serverConf[host]) {
353
- customJsDocPath = candidatePath;
354
- logger.info('detected custom jsdoc config', { deployId, host, path: candidatePath });
355
- break;
356
- }
357
- } catch (e) {
358
- // skip invalid JSON
359
- }
360
- }
361
-
362
- // Fallback: also check dev server configs
363
- if (!customJsDocPath) {
364
- const devConfFiles = fs
365
- .readdirSync(`${privateConfBase}/${deployId}`)
366
- .filter((f) => f.match(/^conf\.server\.dev\..*\.json$/));
367
- for (const devFile of devConfFiles) {
368
- try {
369
- const devConf = JSON.parse(fs.readFileSync(`${privateConfBase}/${deployId}/${devFile}`, 'utf8'));
370
- if (devConf[host]) {
371
- customJsDocPath = candidatePath;
372
- logger.info('detected custom jsdoc config (dev)', { deployId, host, path: candidatePath });
373
- break;
374
- }
375
- } catch (e) {
376
- // skip invalid JSON
377
- }
378
- }
379
- }
380
- if (customJsDocPath) break;
381
- }
341
+ const jsDocSourcePath = docs.jsJsonPath;
342
+ if (!fs.existsSync(jsDocSourcePath)) {
343
+ logger.warn('jsdoc config not found, skipping', jsDocSourcePath);
344
+ return;
382
345
  }
383
-
384
- const jsDocSourcePath = customJsDocPath || `./jsdoc.json`;
385
346
  const jsDocsConfig = JSON.parse(fs.readFileSync(jsDocSourcePath, 'utf8'));
386
347
  logger.info('using jsdoc config', jsDocSourcePath);
387
348
 
@@ -391,22 +352,14 @@ const buildJsDocs = async ({ host, path, metadata = {}, publicClientId }) => {
391
352
 
392
353
  const tutorialsPath = `./src/client/public/${publicClientId}/docs/references`;
393
354
 
394
- // Auto-prepare hardhat references when jsdoc config includes hardhat source files
395
- const includesHardhat =
396
- jsDocsConfig.source &&
397
- Array.isArray(jsDocsConfig.source.include) &&
398
- jsDocsConfig.source.include.some((p) => p.includes('hardhat/'));
399
- if (includesHardhat && fs.existsSync(`./hardhat`)) {
355
+ if (Array.isArray(docs.references) && docs.references.length > 0) {
400
356
  fs.mkdirSync(tutorialsPath, { recursive: true });
401
- const hardhatReadmePath = `./hardhat/README.md`;
402
- const hardhatWhitePaperPath = `./hardhat/WHITE-PAPER.md`;
403
- if (fs.existsSync(hardhatReadmePath)) {
404
- fs.copySync(hardhatReadmePath, `${tutorialsPath}/Hardhat Module.md`);
405
- logger.info('copied hardhat README.md to tutorials references');
406
- }
407
- if (fs.existsSync(hardhatWhitePaperPath)) {
408
- fs.copySync(hardhatWhitePaperPath, `${tutorialsPath}/White Paper.md`);
409
- logger.info('copied hardhat WHITE-PAPER.md to tutorials references');
357
+ for (const refPath of docs.references) {
358
+ if (fs.existsSync(refPath)) {
359
+ const fileName = refPath.split('/').pop();
360
+ fs.copySync(refPath, `${tutorialsPath}/${fileName}`);
361
+ logger.info('copied reference to tutorials', refPath);
362
+ }
410
363
  }
411
364
  }
412
365
 
@@ -420,10 +373,10 @@ const buildJsDocs = async ({ host, path, metadata = {}, publicClientId }) => {
420
373
  delete jsDocsConfig.opts.tutorials;
421
374
  }
422
375
 
423
- fs.writeFileSync(`./jsdoc.json`, JSON.stringify(jsDocsConfig, null, 4), 'utf8');
376
+ fs.writeFileSync(jsDocSourcePath, JSON.stringify(jsDocsConfig, null, 4), 'utf8');
424
377
  logger.warn('build jsdoc view', jsDocsConfig.opts.destination);
425
378
 
426
- shellExec(`npm run docs`, { silent: true });
379
+ shellExec(`npx jsdoc -c ${jsDocSourcePath}`, { silent: true });
427
380
  };
428
381
 
429
382
  /**
@@ -433,58 +386,37 @@ const buildJsDocs = async ({ host, path, metadata = {}, publicClientId }) => {
433
386
  * @param {Object} options - Coverage build options
434
387
  * @param {string} options.host - The hostname for the coverage
435
388
  * @param {string} options.path - The base path for the coverage
389
+ * @param {Object} options.docs - Documentation config from server conf
390
+ * @param {string} options.docs.coveragePath - Directory where to run npm run coverage
436
391
  */
437
- const buildCoverage = async ({ host, path }) => {
392
+ const buildCoverage = async ({ host, path, docs }) => {
438
393
  const logger = loggerFactory(import.meta);
439
- const jsDocsConfig = JSON.parse(fs.readFileSync(`./jsdoc.json`, 'utf8'));
440
-
441
- if (!fs.existsSync(`./coverage`)) {
442
- shellExec(`npm test`);
443
- }
444
-
445
- const coverageBuildPath = `${jsDocsConfig.opts.destination}coverage`;
446
- fs.mkdirSync(coverageBuildPath, { recursive: true });
447
- fs.copySync(`./coverage`, coverageBuildPath);
448
-
449
- logger.warn('build coverage', coverageBuildPath);
394
+ const jsDocSourcePath = docs.jsJsonPath;
395
+ const jsDocsConfig = JSON.parse(fs.readFileSync(jsDocSourcePath, 'utf8'));
396
+ const coveragePath = docs.coveragePath;
450
397
 
451
- // Include hardhat coverage for cyberia-related builds
452
- const hardhatCoveragePath = `./hardhat/coverage`;
453
- if (fs.existsSync(hardhatCoveragePath) && fs.readdirSync(hardhatCoveragePath).length > 0) {
454
- const hardhatCoverageBuildPath = `${jsDocsConfig.opts.destination}hardhat-coverage`;
455
- fs.mkdirSync(hardhatCoverageBuildPath, { recursive: true });
456
- fs.copySync(hardhatCoveragePath, hardhatCoverageBuildPath);
457
- logger.warn('build hardhat coverage', hardhatCoverageBuildPath);
458
- } else if (fs.existsSync(`./hardhat/package.json`)) {
459
- // Attempt to generate hardhat coverage if the hardhat project exists
460
- try {
461
- const hardhatPkg = JSON.parse(fs.readFileSync(`./hardhat/package.json`, 'utf8'));
462
- if (hardhatPkg.scripts && hardhatPkg.scripts.coverage) {
463
- logger.info('generating hardhat coverage report');
464
- shellExec(`cd ./hardhat && npx hardhat coverage`, { silent: true });
465
- if (fs.existsSync(hardhatCoveragePath) && fs.readdirSync(hardhatCoveragePath).length > 0) {
466
- const hardhatCoverageBuildPath = `${jsDocsConfig.opts.destination}hardhat-coverage`;
467
- fs.mkdirSync(hardhatCoverageBuildPath, { recursive: true });
468
- fs.copySync(hardhatCoveragePath, hardhatCoverageBuildPath);
469
- logger.warn('build hardhat coverage (generated)', hardhatCoverageBuildPath);
470
- }
398
+ const coverageOutputPath = `${coveragePath}/coverage`;
399
+ if (!fs.existsSync(coverageOutputPath)) {
400
+ const pkgPath = `${coveragePath}/package.json`;
401
+ if (fs.existsSync(pkgPath)) {
402
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
403
+ if (pkg.scripts && pkg.scripts.coverage) {
404
+ logger.info('generating coverage report', coveragePath);
405
+ shellExec(`cd ${coveragePath} && npm run coverage`, { silent: true });
406
+ } else if (pkg.scripts && pkg.scripts.test) {
407
+ logger.info('generating coverage via test', coveragePath);
408
+ shellExec(`cd ${coveragePath} && npm test`, { silent: true });
471
409
  }
472
- } catch (e) {
473
- logger.warn('hardhat coverage generation skipped', e.message);
474
410
  }
475
411
  }
476
412
 
477
- // Copy hardhat README and WHITE-PAPER as markdown docs
478
- const docsDestBase = jsDocsConfig.opts.destination;
479
- const hardhatReadmePath = `./hardhat/README.md`;
480
- const hardhatWhitePaperPath = `./hardhat/WHITE-PAPER.md`;
481
- if (fs.existsSync(hardhatReadmePath)) {
482
- fs.copySync(hardhatReadmePath, `${docsDestBase}hardhat-README.md`);
483
- logger.info('copied hardhat README.md to docs');
484
- }
485
- if (fs.existsSync(hardhatWhitePaperPath)) {
486
- fs.copySync(hardhatWhitePaperPath, `${docsDestBase}hardhat-WHITE-PAPER.md`);
487
- logger.info('copied hardhat WHITE-PAPER.md to docs');
413
+ if (fs.existsSync(coverageOutputPath) && fs.readdirSync(coverageOutputPath).length > 0) {
414
+ const coverageBuildPath = `${jsDocsConfig.opts.destination}coverage`;
415
+ fs.mkdirSync(coverageBuildPath, { recursive: true });
416
+ fs.copySync(coverageOutputPath, coverageBuildPath);
417
+ logger.warn('build coverage', coverageBuildPath);
418
+ } else {
419
+ logger.warn('no coverage output found, skipping', coverageOutputPath);
488
420
  }
489
421
  };
490
422
 
@@ -501,6 +433,7 @@ const buildCoverage = async ({ host, path }) => {
501
433
  * @param {string} options.publicClientId - Client ID for the public documentation
502
434
  * @param {string} options.rootClientPath - Root path for client files
503
435
  * @param {Object} options.packageData - Package.json data
436
+ * @param {Object} options.docs - Documentation config from server conf
504
437
  */
505
438
  const buildDocs = async ({
506
439
  host,
@@ -511,9 +444,10 @@ const buildDocs = async ({
511
444
  publicClientId,
512
445
  rootClientPath,
513
446
  packageData,
447
+ docs,
514
448
  }) => {
515
- await buildJsDocs({ host, path, metadata, publicClientId });
516
- await buildCoverage({ host, path });
449
+ await buildJsDocs({ host, path, metadata, publicClientId, docs });
450
+ await buildCoverage({ host, path, docs });
517
451
  await buildApiDocs({
518
452
  host,
519
453
  path,