rclnodejs 1.8.3 → 1.9.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.
Files changed (90) hide show
  1. package/README.md +46 -37
  2. package/index.js +39 -0
  3. package/lib/action/client.js +61 -3
  4. package/lib/action/server_goal_handle.js +26 -1
  5. package/lib/message_info.js +94 -0
  6. package/lib/node.js +260 -13
  7. package/lib/parameter_event_handler.js +566 -0
  8. package/lib/parameter_watcher.js +12 -12
  9. package/lib/qos_overriding_options.js +358 -0
  10. package/lib/subscription.js +38 -5
  11. package/lib/timer.js +3 -2
  12. package/lib/wait_for_message.js +111 -0
  13. package/package.json +7 -4
  14. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  15. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  16. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  17. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  18. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  19. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  20. package/scripts/run_asan_test.sh +118 -0
  21. package/src/executor.cpp +36 -2
  22. package/src/executor.h +11 -0
  23. package/src/rcl_action_client_bindings.cpp +70 -1
  24. package/src/rcl_context_bindings.cpp +3 -3
  25. package/src/rcl_graph_bindings.cpp +2 -2
  26. package/src/rcl_subscription_bindings.cpp +70 -2
  27. package/src/rcl_timer_bindings.cpp +21 -2
  28. package/src/rcl_utilities.cpp +2 -2
  29. package/tools/jsdoc/Makefile +5 -0
  30. package/tools/jsdoc/README.md +96 -0
  31. package/tools/jsdoc/build-index.js +610 -0
  32. package/tools/jsdoc/publish.js +854 -0
  33. package/tools/jsdoc/regenerate-published-docs.js +605 -0
  34. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  35. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
  36. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  37. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  38. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  39. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  40. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  41. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
  42. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  43. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
  44. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
  45. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
  46. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  47. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  48. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  49. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  50. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
  51. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  52. package/tools/jsdoc/static/scripts/linenumber.js +25 -0
  53. package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  54. package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
  55. package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
  56. package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
  57. package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
  58. package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
  59. package/tools/jsdoc/tmpl/augments.tmpl +10 -0
  60. package/tools/jsdoc/tmpl/container.tmpl +193 -0
  61. package/tools/jsdoc/tmpl/details.tmpl +143 -0
  62. package/tools/jsdoc/tmpl/example.tmpl +2 -0
  63. package/tools/jsdoc/tmpl/examples.tmpl +13 -0
  64. package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
  65. package/tools/jsdoc/tmpl/layout.tmpl +83 -0
  66. package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
  67. package/tools/jsdoc/tmpl/members.tmpl +43 -0
  68. package/tools/jsdoc/tmpl/method.tmpl +124 -0
  69. package/tools/jsdoc/tmpl/params.tmpl +133 -0
  70. package/tools/jsdoc/tmpl/properties.tmpl +110 -0
  71. package/tools/jsdoc/tmpl/returns.tmpl +12 -0
  72. package/tools/jsdoc/tmpl/source.tmpl +8 -0
  73. package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
  74. package/tools/jsdoc/tmpl/type.tmpl +7 -0
  75. package/types/action_client.d.ts +8 -0
  76. package/types/index.d.ts +34 -0
  77. package/types/message_info.d.ts +72 -0
  78. package/types/node.d.ts +90 -5
  79. package/types/parameter_event_handler.d.ts +150 -0
  80. package/types/qos.d.ts +55 -0
  81. package/types/subscription.d.ts +14 -2
  82. package/types/timer.d.ts +3 -2
  83. package/test_data_integrity.js +0 -108
  84. package/test_repro_exact.js +0 -57
  85. package/test_repro_hz.js +0 -86
  86. package/test_repro_pub.js +0 -36
  87. package/test_repro_stress.js +0 -83
  88. package/test_repro_sub.js +0 -64
  89. package/test_xproc_data.js +0 -64
  90. package/types/interfaces.d.ts +0 -8895
@@ -0,0 +1,610 @@
1
+ // Copyright (c) 2026 The Robot Web Tools Contributors. All rights reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ 'use strict';
16
+
17
+ const childProcess = require('child_process');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const repoRoot = path.resolve(__dirname, '../..');
22
+ const docsRoot = path.resolve(
23
+ process.env.RCLNODEJS_DOCS_ROOT || path.join(repoRoot, 'docs')
24
+ );
25
+ const packageJsonPath = path.resolve(
26
+ process.env.RCLNODEJS_PACKAGE_JSON_PATH || path.join(repoRoot, 'package.json')
27
+ );
28
+ const localIndexPath = process.env.RCLNODEJS_LOCAL_INDEX_PATH
29
+ ? path.resolve(process.env.RCLNODEJS_LOCAL_INDEX_PATH)
30
+ : null;
31
+ const docsIndexPath = path.resolve(
32
+ process.env.RCLNODEJS_DOCS_INDEX_PATH || path.join(docsRoot, 'index.html')
33
+ );
34
+ const gitCwd = path.resolve(process.env.RCLNODEJS_GIT_CWD || repoRoot);
35
+
36
+ function readPackageInfo() {
37
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
38
+ }
39
+
40
+ function isVersionDirectory(entryName) {
41
+ return /^\d+\.\d+\.\d+$/.test(entryName);
42
+ }
43
+
44
+ function compareVersionsDesc(left, right) {
45
+ const leftParts = left.split('.').map(Number);
46
+ const rightParts = right.split('.').map(Number);
47
+ const maxLength = Math.max(leftParts.length, rightParts.length);
48
+
49
+ for (let index = 0; index < maxLength; index += 1) {
50
+ const leftValue = leftParts[index] || 0;
51
+ const rightValue = rightParts[index] || 0;
52
+
53
+ if (leftValue !== rightValue) {
54
+ return rightValue - leftValue;
55
+ }
56
+ }
57
+
58
+ return 0;
59
+ }
60
+
61
+ function getAvailableVersions() {
62
+ return fs
63
+ .readdirSync(docsRoot, { withFileTypes: true })
64
+ .filter((entry) => entry.isDirectory() && isVersionDirectory(entry.name))
65
+ .map((entry) => entry.name)
66
+ .sort(compareVersionsDesc);
67
+ }
68
+
69
+ function runGitCommand(args) {
70
+ try {
71
+ return childProcess
72
+ .execFileSync('git', args, {
73
+ cwd: gitCwd,
74
+ encoding: 'utf8',
75
+ stdio: ['ignore', 'pipe', 'ignore'],
76
+ })
77
+ .trim();
78
+ } catch {
79
+ return '';
80
+ }
81
+ }
82
+
83
+ function getGitTags() {
84
+ const tagsOutput = runGitCommand(['tag', '--list']);
85
+
86
+ return new Set(tagsOutput ? tagsOutput.split('\n').filter(Boolean) : []);
87
+ }
88
+
89
+ function getTagReleaseDate(tagName) {
90
+ const commitSha = runGitCommand(['rev-list', '-n', '1', tagName]);
91
+
92
+ if (!commitSha) {
93
+ return null;
94
+ }
95
+
96
+ return (
97
+ runGitCommand(['show', '-s', '--format=%ad', '--date=short', commitSha]) ||
98
+ null
99
+ );
100
+ }
101
+
102
+ function buildVersionEntries(packageInfo, versions) {
103
+ const gitTags = getGitTags();
104
+
105
+ return versions.map((version) => {
106
+ const hasTag = gitTags.has(version);
107
+
108
+ return {
109
+ version,
110
+ releasedOn: hasTag ? getTagReleaseDate(version) : null,
111
+ docsUrl: `./${version}/index.html`,
112
+ };
113
+ });
114
+ }
115
+
116
+ function escapeHtml(value) {
117
+ return String(value)
118
+ .replace(/&/g, '&amp;')
119
+ .replace(/</g, '&lt;')
120
+ .replace(/>/g, '&gt;')
121
+ .replace(/"/g, '&quot;')
122
+ .replace(/'/g, '&#39;');
123
+ }
124
+
125
+ function renderVersionCards(versionEntries, latestVersion) {
126
+ return versionEntries
127
+ .map((entry) => {
128
+ const latestBadge =
129
+ entry.version === latestVersion
130
+ ? '<span class="version-badge">Latest</span>'
131
+ : '';
132
+ const releaseMeta = entry.releasedOn
133
+ ? `<p class="version-meta">Released ${escapeHtml(entry.releasedOn)}</p>`
134
+ : '';
135
+
136
+ return `
137
+ <a class="version-card" href="${escapeHtml(entry.docsUrl)}">
138
+ <div class="version-card-header">
139
+ <span class="version-label">Release</span>
140
+ ${latestBadge}
141
+ </div>
142
+ <strong class="version-number">v${escapeHtml(entry.version)}</strong>
143
+ ${releaseMeta}
144
+ <p class="version-copy">Browse the generated API reference for rclnodejs ${escapeHtml(entry.version)}.</p>
145
+ </a>`;
146
+ })
147
+ .join('');
148
+ }
149
+
150
+ function renderIndexHtml(packageInfo, versionEntries) {
151
+ const latestEntry = versionEntries[0] || {
152
+ version: packageInfo.version,
153
+ releasedOn: null,
154
+ };
155
+ const latestVersion = latestEntry.version;
156
+ const versionCards = renderVersionCards(versionEntries, latestVersion);
157
+ const versionCount = versionEntries.length;
158
+ const latestReleaseDate = latestEntry.releasedOn
159
+ ? `<p>Released on <strong>${escapeHtml(latestEntry.releasedOn)}</strong></p>`
160
+ : '<p>Release date unavailable for this version.</p>';
161
+
162
+ return `<!DOCTYPE html>
163
+ <html lang="en">
164
+ <head>
165
+ <meta charset="utf-8">
166
+ <meta name="viewport" content="width=device-width, initial-scale=1">
167
+ <title>rclnodejs Documentation</title>
168
+ <style>
169
+ :root {
170
+ --bg: #f3f7fb;
171
+ --bg-deep: #dfeaf4;
172
+ --surface: rgba(255, 255, 255, 0.86);
173
+ --surface-strong: rgba(255, 255, 255, 0.96);
174
+ --border: rgba(28, 53, 77, 0.12);
175
+ --border-strong: rgba(28, 53, 77, 0.18);
176
+ --text: #12263a;
177
+ --text-soft: #486277;
178
+ --text-muted: #667f92;
179
+ --accent: #0b84c9;
180
+ --accent-strong: #08679b;
181
+ --accent-soft: rgba(11, 132, 201, 0.12);
182
+ --shadow-lg: 0 24px 60px rgba(14, 31, 53, 0.14);
183
+ --shadow-md: 0 12px 30px rgba(14, 31, 53, 0.1);
184
+ --radius-xl: 28px;
185
+ --radius-lg: 20px;
186
+ --radius-md: 14px;
187
+ }
188
+
189
+ * {
190
+ box-sizing: border-box;
191
+ }
192
+
193
+ html {
194
+ background: linear-gradient(180deg, #eff5fa 0%, #f7fbff 100%);
195
+ color: var(--text);
196
+ font-size: 16px;
197
+ }
198
+
199
+ body {
200
+ margin: 0;
201
+ font-family: "Open Sans", "Segoe UI", sans-serif;
202
+ line-height: 1.6;
203
+ color: var(--text);
204
+ background: transparent;
205
+ }
206
+
207
+ a {
208
+ color: inherit;
209
+ text-decoration: none;
210
+ }
211
+
212
+ .site-backdrop {
213
+ position: fixed;
214
+ inset: 0;
215
+ background:
216
+ radial-gradient(circle at top left, rgba(11, 132, 201, 0.18), transparent 32%),
217
+ radial-gradient(circle at right 10%, rgba(0, 128, 106, 0.12), transparent 24%),
218
+ linear-gradient(180deg, rgba(255, 255, 255, 0.6), rgba(243, 247, 251, 0.95));
219
+ pointer-events: none;
220
+ z-index: -2;
221
+ }
222
+
223
+ .site-grid {
224
+ position: fixed;
225
+ inset: 0;
226
+ background-image:
227
+ linear-gradient(rgba(18, 38, 58, 0.03) 1px, transparent 1px),
228
+ linear-gradient(90deg, rgba(18, 38, 58, 0.03) 1px, transparent 1px);
229
+ background-size: 32px 32px;
230
+ mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.28), transparent 75%);
231
+ pointer-events: none;
232
+ z-index: -1;
233
+ }
234
+
235
+ .page-shell {
236
+ max-width: 1320px;
237
+ margin: 0 auto;
238
+ padding: 24px;
239
+ }
240
+
241
+ .hero,
242
+ .panel,
243
+ .footer {
244
+ background: var(--surface-strong);
245
+ border: 1px solid var(--border);
246
+ border-radius: var(--radius-xl);
247
+ box-shadow: var(--shadow-lg);
248
+ backdrop-filter: blur(18px);
249
+ -webkit-backdrop-filter: blur(18px);
250
+ }
251
+
252
+ .hero {
253
+ display: grid;
254
+ grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.8fr);
255
+ gap: 24px;
256
+ padding: 34px;
257
+ overflow: hidden;
258
+ position: relative;
259
+ background:
260
+ linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(231, 243, 250, 0.9)),
261
+ linear-gradient(120deg, rgba(11, 132, 201, 0.16), transparent 42%);
262
+ }
263
+
264
+ .hero::after {
265
+ content: "";
266
+ position: absolute;
267
+ top: -50px;
268
+ right: -50px;
269
+ width: 200px;
270
+ height: 200px;
271
+ border-radius: 50%;
272
+ background: radial-gradient(circle, rgba(11, 132, 201, 0.16), transparent 68%);
273
+ pointer-events: none;
274
+ }
275
+
276
+ .eyebrow {
277
+ margin: 0 0 10px;
278
+ text-transform: uppercase;
279
+ letter-spacing: 0.18em;
280
+ font-size: 0.74rem;
281
+ font-weight: 700;
282
+ color: var(--accent-strong);
283
+ }
284
+
285
+ .hero h1 {
286
+ margin: 0;
287
+ font-size: clamp(2.8rem, 5vw, 4.8rem);
288
+ line-height: 0.95;
289
+ letter-spacing: -0.07em;
290
+ font-weight: 300;
291
+ }
292
+
293
+ .hero h1 span {
294
+ display: block;
295
+ margin-top: 8px;
296
+ color: var(--text-muted);
297
+ font-size: 0.42em;
298
+ font-weight: 700;
299
+ letter-spacing: 0;
300
+ text-transform: uppercase;
301
+ }
302
+
303
+ .hero-copy {
304
+ position: relative;
305
+ z-index: 1;
306
+ }
307
+
308
+ .hero-description,
309
+ .panel p,
310
+ .summary-card p,
311
+ .version-copy,
312
+ .version-meta,
313
+ .footer {
314
+ color: var(--text-soft);
315
+ }
316
+
317
+ .hero-description {
318
+ max-width: 48rem;
319
+ margin: 18px 0 0;
320
+ font-size: 1.02rem;
321
+ }
322
+
323
+ .hero-actions {
324
+ display: flex;
325
+ flex-wrap: wrap;
326
+ gap: 12px;
327
+ margin-top: 22px;
328
+ }
329
+
330
+ .hero-action {
331
+ display: inline-flex;
332
+ align-items: center;
333
+ justify-content: center;
334
+ min-height: 48px;
335
+ padding: 0 16px;
336
+ border-radius: 14px;
337
+ border: 1px solid var(--border-strong);
338
+ background: rgba(255, 255, 255, 0.72);
339
+ color: var(--text);
340
+ font-weight: 700;
341
+ transition: transform 160ms ease, background-color 160ms ease, border-color 160ms ease;
342
+ }
343
+
344
+ .hero-action:hover {
345
+ transform: translateY(-1px);
346
+ background: rgba(255, 255, 255, 0.94);
347
+ border-color: rgba(11, 132, 201, 0.22);
348
+ }
349
+
350
+ .hero-action.primary {
351
+ background: linear-gradient(135deg, var(--accent), var(--accent-strong));
352
+ border-color: transparent;
353
+ color: #fff;
354
+ }
355
+
356
+ .panel {
357
+ padding: 22px;
358
+ position: relative;
359
+ z-index: 1;
360
+ background: rgba(248, 252, 255, 0.82);
361
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
362
+ }
363
+
364
+ .panel-title {
365
+ margin: 0 0 14px;
366
+ color: var(--text);
367
+ font-size: 0.92rem;
368
+ font-weight: 700;
369
+ text-transform: uppercase;
370
+ letter-spacing: 0.12em;
371
+ }
372
+
373
+ .summary-grid,
374
+ .versions-grid {
375
+ display: grid;
376
+ gap: 18px;
377
+ margin-top: 24px;
378
+ }
379
+
380
+ .summary-grid {
381
+ grid-template-columns: repeat(2, minmax(0, 1fr));
382
+ }
383
+
384
+ .summary-card,
385
+ .version-card {
386
+ background: var(--surface-strong);
387
+ border: 1px solid var(--border);
388
+ border-radius: var(--radius-lg);
389
+ box-shadow: var(--shadow-md);
390
+ }
391
+
392
+ .summary-card {
393
+ padding: 22px;
394
+ }
395
+
396
+ .summary-number {
397
+ display: block;
398
+ font-size: 2.2rem;
399
+ line-height: 1;
400
+ letter-spacing: -0.06em;
401
+ font-weight: 300;
402
+ }
403
+
404
+ .summary-label {
405
+ display: block;
406
+ margin-top: 8px;
407
+ font-weight: 700;
408
+ color: var(--text);
409
+ }
410
+
411
+ .versions {
412
+ margin-top: 24px;
413
+ padding: 28px 30px;
414
+ }
415
+
416
+ .section-heading {
417
+ margin: 0;
418
+ font-size: clamp(1.8rem, 2.6vw, 2.6rem);
419
+ line-height: 1.04;
420
+ letter-spacing: -0.05em;
421
+ font-weight: 300;
422
+ }
423
+
424
+ .section-copy {
425
+ margin: 12px 0 0;
426
+ max-width: 46rem;
427
+ color: var(--text-soft);
428
+ }
429
+
430
+ .versions-grid {
431
+ grid-template-columns: repeat(3, minmax(0, 1fr));
432
+ margin-top: 22px;
433
+ }
434
+
435
+ .version-card {
436
+ padding: 22px;
437
+ transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
438
+ }
439
+
440
+ .version-card:hover {
441
+ transform: translateY(-2px);
442
+ border-color: rgba(11, 132, 201, 0.24);
443
+ box-shadow: 0 18px 36px rgba(14, 31, 53, 0.12);
444
+ }
445
+
446
+ .version-card-header {
447
+ display: flex;
448
+ align-items: center;
449
+ justify-content: space-between;
450
+ gap: 10px;
451
+ }
452
+
453
+ .version-label,
454
+ .version-badge {
455
+ display: inline-flex;
456
+ align-items: center;
457
+ padding: 0.35rem 0.75rem;
458
+ border-radius: 999px;
459
+ font-size: 0.78rem;
460
+ font-weight: 700;
461
+ text-transform: uppercase;
462
+ letter-spacing: 0.12em;
463
+ }
464
+
465
+ .version-label {
466
+ background: var(--accent-soft);
467
+ color: var(--accent-strong);
468
+ }
469
+
470
+ .version-badge {
471
+ background: rgba(0, 128, 106, 0.12);
472
+ color: #0d6a59;
473
+ }
474
+
475
+ .version-number {
476
+ display: block;
477
+ margin-top: 18px;
478
+ font-size: 2rem;
479
+ line-height: 1;
480
+ letter-spacing: -0.06em;
481
+ font-weight: 300;
482
+ color: var(--text);
483
+ }
484
+
485
+ .version-meta {
486
+ margin: 10px 0 0;
487
+ font-size: 0.92rem;
488
+ font-weight: 700;
489
+ color: var(--text-muted);
490
+ }
491
+
492
+ .version-copy {
493
+ margin: 12px 0 0;
494
+ }
495
+
496
+ .footer {
497
+ margin-top: 24px;
498
+ padding: 18px 22px;
499
+ font-size: 0.92rem;
500
+ }
501
+
502
+ @media (max-width: 1100px) {
503
+ .hero,
504
+ .versions-grid,
505
+ .summary-grid {
506
+ grid-template-columns: 1fr 1fr;
507
+ }
508
+
509
+ .hero > :first-child {
510
+ grid-column: 1 / -1;
511
+ }
512
+ }
513
+
514
+ @media (max-width: 720px) {
515
+ .page-shell {
516
+ padding: 14px;
517
+ }
518
+
519
+ .hero,
520
+ .versions-grid,
521
+ .summary-grid {
522
+ grid-template-columns: 1fr;
523
+ }
524
+
525
+ .hero,
526
+ .versions,
527
+ .panel,
528
+ .footer,
529
+ .summary-card,
530
+ .version-card {
531
+ border-radius: 22px;
532
+ }
533
+
534
+ .hero,
535
+ .versions {
536
+ padding: 22px 20px;
537
+ }
538
+ }
539
+ </style>
540
+ </head>
541
+ <body>
542
+ <div class="site-backdrop"></div>
543
+ <div class="site-grid"></div>
544
+ <div class="page-shell">
545
+ <section class="hero">
546
+ <div class="hero-copy">
547
+ <p class="eyebrow">Versioned API Reference</p>
548
+ <h1>rclnodejs <span>ROS 2 JavaScript Client</span></h1>
549
+ <p class="hero-description">Browse published API references across released versions. The latest version is highlighted, and every docs build refreshes this landing page automatically.</p>
550
+ <div class="hero-actions">
551
+ <a class="hero-action primary" href="./${escapeHtml(latestVersion)}/index.html">Open Latest Docs</a>
552
+ <a class="hero-action" href="#versions">Browse Versions</a>
553
+ </div>
554
+ </div>
555
+
556
+ <aside class="panel">
557
+ <p class="panel-title">Current Release</p>
558
+ <h2 class="section-heading">v${escapeHtml(latestVersion)}</h2>
559
+ <p>Package version from package.json: <strong>${escapeHtml(packageInfo.version)}</strong></p>
560
+ ${latestReleaseDate}
561
+ </aside>
562
+ </section>
563
+
564
+ <section class="summary-grid">
565
+ <article class="summary-card">
566
+ <strong class="summary-number">${versionCount}</strong>
567
+ <span class="summary-label">Published Versions</span>
568
+ <p>All discovered version directories under the docs output root.</p>
569
+ </article>
570
+
571
+ <article class="summary-card">
572
+ <strong class="summary-number">v${escapeHtml(latestVersion)}</strong>
573
+ <span class="summary-label">Latest Version</span>
574
+ <p>The newest semantic version discovered during index generation, with release date information when available.</p>
575
+ </article>
576
+ </section>
577
+
578
+ <section class="panel versions" id="versions">
579
+ <p class="eyebrow">Releases</p>
580
+ <h2 class="section-heading">Choose a documentation snapshot</h2>
581
+ <p class="section-copy">Version entries are generated automatically from the directories currently present in the docs output, so new releases appear after <code>npm run docs</code> without manual HTML edits.</p>
582
+ <div class="versions-grid">${versionCards}</div>
583
+ </section>
584
+
585
+ <footer class="footer">
586
+ Documentation index for ${escapeHtml(packageInfo.name)}. Generated automatically by <code>tools/jsdoc/build-index.js</code>.
587
+ </footer>
588
+ </div>
589
+ </body>
590
+ </html>
591
+ `;
592
+ }
593
+
594
+ function writeIndexFiles(html) {
595
+ if (localIndexPath) {
596
+ fs.writeFileSync(localIndexPath, html, 'utf8');
597
+ }
598
+ fs.writeFileSync(docsIndexPath, html, 'utf8');
599
+ }
600
+
601
+ function main() {
602
+ const packageInfo = readPackageInfo();
603
+ const versions = getAvailableVersions();
604
+ const versionEntries = buildVersionEntries(packageInfo, versions);
605
+ const html = renderIndexHtml(packageInfo, versionEntries);
606
+
607
+ writeIndexFiles(html);
608
+ }
609
+
610
+ main();