@webstir-io/webstir-frontend 0.1.40 → 0.1.41

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 (138) hide show
  1. package/README.md +124 -60
  2. package/dist/assets/imageOptimizer.js +10 -15
  3. package/dist/assets/precompression.js +1 -1
  4. package/dist/builders/contentBuilder.js +102 -90
  5. package/dist/builders/cssBuilder.js +25 -19
  6. package/dist/builders/htmlBuilder.js +57 -42
  7. package/dist/builders/index.js +1 -1
  8. package/dist/builders/jsBuilder.js +219 -76
  9. package/dist/builders/staticAssetsBuilder.js +27 -9
  10. package/dist/builders/types.d.ts +1 -0
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +6 -30
  13. package/dist/config/manifest.js +7 -6
  14. package/dist/config/paths.js +2 -2
  15. package/dist/config/schema.d.ts +8 -0
  16. package/dist/config/schema.js +7 -6
  17. package/dist/config/setup.js +1 -1
  18. package/dist/config/workspace.js +11 -9
  19. package/dist/core/constants.d.ts +1 -1
  20. package/dist/core/constants.js +5 -5
  21. package/dist/core/diagnostics.js +1 -1
  22. package/dist/core/pages.js +4 -4
  23. package/dist/hooks.js +3 -3
  24. package/dist/html/criticalCss.js +6 -3
  25. package/dist/html/htmlSecurity.d.ts +6 -1
  26. package/dist/html/htmlSecurity.js +28 -14
  27. package/dist/html/lazyLoad.js +1 -1
  28. package/dist/html/pageScaffold.js +1 -1
  29. package/dist/html/resourceHints.js +5 -2
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +2 -0
  32. package/dist/inspect.d.ts +2 -0
  33. package/dist/inspect.js +110 -0
  34. package/dist/modes/ssg/metadata.js +4 -4
  35. package/dist/modes/ssg/routing.js +2 -5
  36. package/dist/modes/ssg/seo.js +5 -5
  37. package/dist/modes/ssg/views.js +17 -11
  38. package/dist/operations.js +18 -10
  39. package/dist/pipeline.d.ts +1 -0
  40. package/dist/pipeline.js +6 -1
  41. package/dist/provider.js +28 -24
  42. package/dist/runtime/boundary.d.ts +28 -0
  43. package/dist/runtime/boundary.js +247 -0
  44. package/dist/runtime/index.d.ts +1 -0
  45. package/dist/runtime/index.js +1 -0
  46. package/dist/types.d.ts +52 -0
  47. package/dist/utils/fs.d.ts +11 -10
  48. package/dist/utils/fs.js +48 -20
  49. package/dist/utils/glob.d.ts +8 -0
  50. package/dist/utils/glob.js +21 -0
  51. package/dist/utils/hash.js +1 -2
  52. package/dist/utils/pagePaths.js +2 -2
  53. package/package.json +19 -14
  54. package/scripts/publish.sh +2 -94
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/assets/assetManifest.ts +39 -29
  57. package/src/assets/imageOptimizer.ts +91 -82
  58. package/src/assets/precompression.ts +22 -16
  59. package/src/builders/contentBuilder.ts +1224 -1149
  60. package/src/builders/cssBuilder.ts +466 -417
  61. package/src/builders/htmlBuilder.ts +511 -448
  62. package/src/builders/index.ts +7 -7
  63. package/src/builders/jsBuilder.ts +538 -280
  64. package/src/builders/staticAssetsBuilder.ts +166 -135
  65. package/src/builders/types.ts +7 -6
  66. package/src/cli.ts +66 -90
  67. package/src/config/manifest.ts +16 -14
  68. package/src/config/paths.ts +5 -5
  69. package/src/config/schema.ts +38 -37
  70. package/src/config/setup.ts +7 -7
  71. package/src/config/workspace.ts +118 -116
  72. package/src/config/workspaceManifest.ts +14 -14
  73. package/src/core/constants.ts +62 -62
  74. package/src/core/diagnostics.ts +26 -26
  75. package/src/core/pages.ts +19 -19
  76. package/src/hooks.ts +128 -118
  77. package/src/html/criticalCss.ts +84 -77
  78. package/src/html/htmlSecurity.ts +107 -66
  79. package/src/html/lazyLoad.ts +22 -19
  80. package/src/html/pageScaffold.ts +37 -28
  81. package/src/html/resourceHints.ts +83 -74
  82. package/src/index.ts +2 -0
  83. package/src/inspect.ts +158 -0
  84. package/src/modes/ssg/metadata.ts +53 -51
  85. package/src/modes/ssg/routing.ts +177 -177
  86. package/src/modes/ssg/seo.ts +208 -200
  87. package/src/modes/ssg/validation.ts +31 -25
  88. package/src/modes/ssg/views.ts +257 -238
  89. package/src/operations.ts +105 -95
  90. package/src/pipeline.ts +81 -69
  91. package/src/provider.ts +184 -176
  92. package/src/runtime/boundary.ts +325 -0
  93. package/src/runtime/index.ts +1 -0
  94. package/src/types.ts +107 -48
  95. package/src/utils/changedFile.ts +22 -22
  96. package/src/utils/fs.ts +73 -26
  97. package/src/utils/glob.ts +38 -0
  98. package/src/utils/hash.ts +2 -4
  99. package/src/utils/pagePaths.ts +35 -23
  100. package/src/utils/pathMatch.ts +26 -23
  101. package/tests/add-page-defaults.test.js +44 -39
  102. package/tests/bundlerParity.test.js +252 -0
  103. package/tests/cli.contract.test.js +13 -0
  104. package/tests/content-pages.test.js +108 -13
  105. package/tests/css-app-imports.test.js +22 -11
  106. package/tests/css-page-imports.test.js +26 -13
  107. package/tests/diagnostics.test.js +39 -36
  108. package/tests/features.test.js +48 -43
  109. package/tests/hooks.test.js +58 -42
  110. package/tests/htmlSecurity.test.js +66 -0
  111. package/tests/inspect.test.js +148 -0
  112. package/tests/provider.integration.test.js +71 -20
  113. package/tests/runtime.test.js +493 -0
  114. package/tests/ssg-defaults.test.js +284 -177
  115. package/tests/ssg-guardrails.test.js +51 -51
  116. package/tsconfig.json +3 -10
  117. package/dist/watch/frontendFiles.d.ts +0 -3
  118. package/dist/watch/frontendFiles.js +0 -25
  119. package/dist/watch/hotUpdateTracker.d.ts +0 -51
  120. package/dist/watch/hotUpdateTracker.js +0 -205
  121. package/dist/watch/pipelineHelpers.d.ts +0 -26
  122. package/dist/watch/pipelineHelpers.js +0 -177
  123. package/dist/watch/types.d.ts +0 -27
  124. package/dist/watch/types.js +0 -1
  125. package/dist/watch/watchCoordinator.d.ts +0 -36
  126. package/dist/watch/watchCoordinator.js +0 -551
  127. package/dist/watch/watchDaemon.d.ts +0 -17
  128. package/dist/watch/watchDaemon.js +0 -127
  129. package/dist/watch/watchReporter.d.ts +0 -21
  130. package/dist/watch/watchReporter.js +0 -64
  131. package/scripts/smoke.mjs +0 -35
  132. package/src/watch/frontendFiles.ts +0 -32
  133. package/src/watch/hotUpdateTracker.ts +0 -285
  134. package/src/watch/pipelineHelpers.ts +0 -242
  135. package/src/watch/types.ts +0 -23
  136. package/src/watch/watchCoordinator.ts +0 -666
  137. package/src/watch/watchDaemon.ts +0 -144
  138. package/src/watch/watchReporter.ts +0 -98
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
- import { glob } from 'glob';
3
2
  import { marked } from 'marked';
4
3
  import { load } from 'cheerio';
5
4
  import hljs from 'highlight.js/lib/common';
6
5
  import { FOLDERS, FILES, FILE_NAMES, EXTENSIONS } from '../core/constants.js';
7
6
  import { ensureDir, pathExists, readFile, readJson, remove, writeFile } from '../utils/fs.js';
7
+ import { scanGlob } from '../utils/glob.js';
8
8
  import { shouldProcess } from '../utils/changedFile.js';
9
9
  import { getPageDirectories } from '../core/pages.js';
10
10
  import { readPageManifest, readSharedAssets } from '../assets/assetManifest.js';
@@ -20,7 +20,7 @@ export function createContentBuilder(context) {
20
20
  async publish() {
21
21
  await publishContentPages(context);
22
22
  await publishContentManifests(context);
23
- }
23
+ },
24
24
  };
25
25
  }
26
26
  async function buildContentPages(context) {
@@ -29,13 +29,11 @@ async function buildContentPages(context) {
29
29
  if (!(await pathExists(contentRoot))) {
30
30
  return;
31
31
  }
32
- if (!shouldProcess(context, [{ directory: contentRoot, extensions: ['.md'] }])) {
32
+ if (!isSidebarOverrideChange(context, contentRoot) &&
33
+ !shouldProcess(context, [{ directory: contentRoot, extensions: ['.md'] }])) {
33
34
  return;
34
35
  }
35
- const files = await glob('**/*.md', {
36
- cwd: contentRoot,
37
- nodir: true
38
- });
36
+ const files = await scanGlob('**/*.md', { cwd: contentRoot });
39
37
  if (files.length === 0) {
40
38
  return;
41
39
  }
@@ -46,9 +44,7 @@ async function buildContentPages(context) {
46
44
  const templateHtml = await readFile(appTemplatePath);
47
45
  validateAppTemplate(templateHtml, appTemplatePath);
48
46
  const buildPagesUrlPrefix = resolvePagesUrlPrefix(config.paths.build.frontend, config.paths.build.pages);
49
- const navEntries = context.enable?.contentNav === true
50
- ? await collectContentManifests(context)
51
- : [];
47
+ const navEntries = context.enable?.contentNav === true ? await collectContentManifests(context) : [];
52
48
  await removeStaleContentOutputs(context, files, buildPagesUrlPrefix);
53
49
  for (const relative of files) {
54
50
  const sourcePath = path.join(contentRoot, relative);
@@ -57,7 +53,7 @@ async function buildContentPages(context) {
57
53
  const htmlBody = (await renderMarkdownDoc(content)).html;
58
54
  const segments = resolveDocsSegments(relative);
59
55
  const pagePath = path.join(...segments);
60
- const href = '/' + segments.join('/') + '/';
56
+ const href = `/${segments.join('/')}/`;
61
57
  const pageTitle = resolveTitle(frontmatter, content, segments);
62
58
  const mergedHtml = mergeContentIntoTemplate(templateHtml, pageTitle, htmlBody, frontmatter.description, context.enable?.contentNav === true, buildPagesUrlPrefix, navEntries, href);
63
59
  const mergedWithOptIn = injectGlobalOptInScripts(mergedHtml, context.enable);
@@ -74,10 +70,7 @@ async function publishContentPages(context) {
74
70
  if (!(await pathExists(contentRoot))) {
75
71
  return;
76
72
  }
77
- const files = await glob('**/*.md', {
78
- cwd: contentRoot,
79
- nodir: true
80
- });
73
+ const files = await scanGlob('**/*.md', { cwd: contentRoot });
81
74
  if (files.length === 0) {
82
75
  return;
83
76
  }
@@ -91,9 +84,7 @@ async function publishContentPages(context) {
91
84
  const buildPagesUrlPrefix = resolvePagesUrlPrefix(config.paths.build.frontend, config.paths.build.pages);
92
85
  await removeStaleContentOutputsForRoot(config.paths.dist.content, files, pagesUrlPrefix);
93
86
  const shared = await readSharedAssets(config.paths.dist.frontend);
94
- const navEntries = context.enable?.contentNav === true
95
- ? await collectContentManifests(context)
96
- : [];
87
+ const navEntries = context.enable?.contentNav === true ? await collectContentManifests(context) : [];
97
88
  const docsManifestRoot = path.join(config.paths.dist.pages, 'docs');
98
89
  const docsManifest = await readPageManifest(docsManifestRoot, 'docs');
99
90
  if (!docsManifest.css || !docsManifest.js) {
@@ -106,7 +97,7 @@ async function publishContentPages(context) {
106
97
  const { frontmatter, content } = extractFrontmatter(markdown);
107
98
  const segments = resolveDocsSegments(relative);
108
99
  const pagePath = path.join(...segments);
109
- const href = '/' + segments.join('/') + '/';
100
+ const href = `/${segments.join('/')}/`;
110
101
  const pageTitle = resolveTitle(frontmatter, content, segments);
111
102
  const rendered = await renderMarkdownDoc(content);
112
103
  const htmlBody = rendered.html;
@@ -114,7 +105,7 @@ async function publishContentPages(context) {
114
105
  const mergedWithOptIn = injectGlobalOptInScripts(mergedHtml, context.enable);
115
106
  const rewritten = await rewriteContentForPublish(mergedWithOptIn, shared, docsManifest, {
116
107
  pagesUrlPrefix,
117
- buildPagesUrlPrefix
108
+ buildPagesUrlPrefix,
118
109
  });
119
110
  const distDir = path.join(config.paths.dist.pages, pagePath);
120
111
  const distPath = path.join(distDir, FILES.indexHtml);
@@ -124,7 +115,7 @@ async function publishContentPages(context) {
124
115
  outputPath: distPath,
125
116
  html: rewritten,
126
117
  headingIds: rendered.headingIds,
127
- sourcePath
118
+ sourcePath,
128
119
  });
129
120
  }
130
121
  validateRenderedContentPages(renderedPages);
@@ -145,10 +136,7 @@ async function removeStaleContentOutputsForRoot(docsRoot, contentFiles, pagesUrl
145
136
  const segments = resolveDocsSegments(relative);
146
137
  expected.add(path.join(...segments.slice(1)));
147
138
  }
148
- const candidateIndexes = await glob('**/index.html', {
149
- cwd: docsRoot,
150
- nodir: true
151
- });
139
+ const candidateIndexes = await scanGlob('**/index.html', { cwd: docsRoot });
152
140
  const docsPrefix = resolvePageAssetUrl(pagesUrlPrefix, 'docs', '');
153
141
  const docsAssetToken = docsPrefix.endsWith('/') ? docsPrefix : `${docsPrefix}/`;
154
142
  for (const relativeIndex of candidateIndexes) {
@@ -163,8 +151,7 @@ async function removeStaleContentOutputsForRoot(docsRoot, contentFiles, pagesUrl
163
151
  const absoluteIndex = path.join(docsRoot, relativeIndex);
164
152
  const html = await readFile(absoluteIndex);
165
153
  // Only remove pages that were generated by the content pipeline (avoid deleting user-owned pages under /docs).
166
- const looksLikeContentOutput = html.includes('class="docs-article"')
167
- && html.includes(docsAssetToken);
154
+ const looksLikeContentOutput = html.includes('class="docs-article"') && html.includes(docsAssetToken);
168
155
  if (!looksLikeContentOutput) {
169
156
  continue;
170
157
  }
@@ -184,11 +171,12 @@ async function buildContentManifests(context) {
184
171
  }
185
172
  return;
186
173
  }
187
- if (!shouldProcess(context, [
188
- { directory: contentRoot, extensions: ['.md'] },
189
- // `webstir enable search` updates package.json and should emit the index immediately.
190
- { directory: config.paths.workspace, extensions: ['.json'] }
191
- ])) {
174
+ if (!isSidebarOverrideChange(context, contentRoot) &&
175
+ !shouldProcess(context, [
176
+ { directory: contentRoot, extensions: ['.md'] },
177
+ // `webstir enable search` updates package.json and should emit the index immediately.
178
+ { directory: config.paths.workspace, extensions: ['.json'] },
179
+ ])) {
192
180
  return;
193
181
  }
194
182
  const navEntries = await collectContentManifests(context);
@@ -199,7 +187,7 @@ async function buildContentManifests(context) {
199
187
  if (context.enable?.search === true) {
200
188
  const [docEntries, pageEntries] = await Promise.all([
201
189
  collectContentSearchEntries(context),
202
- collectPageSearchEntries(context)
190
+ collectPageSearchEntries(context),
203
191
  ]);
204
192
  const searchEntries = [...docEntries, ...pageEntries];
205
193
  if (searchEntries.length > 0) {
@@ -218,7 +206,7 @@ async function publishContentManifests(context) {
218
206
  if (context.enable?.search === true) {
219
207
  const [docEntries, pageEntries] = await Promise.all([
220
208
  hasContent ? collectContentSearchEntries(context) : Promise.resolve([]),
221
- collectPageSearchEntries(context)
209
+ collectPageSearchEntries(context),
222
210
  ]);
223
211
  const searchEntries = [...docEntries, ...pageEntries];
224
212
  if (searchEntries.length > 0) {
@@ -230,10 +218,7 @@ async function collectContentManifests(context) {
230
218
  const { config } = context;
231
219
  const contentRoot = config.paths.src.content;
232
220
  const overrides = await loadSidebarOverrides(contentRoot);
233
- const files = await glob('**/*.md', {
234
- cwd: contentRoot,
235
- nodir: true
236
- });
221
+ const files = await scanGlob('**/*.md', { cwd: contentRoot });
237
222
  if (files.length === 0) {
238
223
  return [];
239
224
  }
@@ -244,17 +229,15 @@ async function collectContentManifests(context) {
244
229
  const { frontmatter, content } = extractFrontmatter(markdown);
245
230
  const segments = resolveDocsSegments(relative);
246
231
  const parsed = path.parse(relative);
247
- const section = parsed.dir && parsed.dir.trim().length > 0
248
- ? parsed.dir.split(path.sep)[0]
249
- : undefined;
250
- const href = '/' + segments.join('/') + '/';
232
+ const section = parsed.dir && parsed.dir.trim().length > 0 ? parsed.dir.split(path.sep)[0] : undefined;
233
+ const href = `/${segments.join('/')}/`;
251
234
  const title = resolveTitle(frontmatter, content, segments);
252
235
  const order = frontmatter.order;
253
236
  const baseEntry = {
254
237
  path: href,
255
238
  title,
256
239
  section,
257
- order
240
+ order,
258
241
  };
259
242
  const merged = applySidebarOverride(baseEntry, overrides);
260
243
  if (merged) {
@@ -287,10 +270,7 @@ async function collectContentSearchEntries(context) {
287
270
  const { config } = context;
288
271
  const contentRoot = config.paths.src.content;
289
272
  const overrides = await loadSidebarOverrides(contentRoot);
290
- const files = await glob('**/*.md', {
291
- cwd: contentRoot,
292
- nodir: true
293
- });
273
+ const files = await scanGlob('**/*.md', { cwd: contentRoot });
294
274
  if (files.length === 0) {
295
275
  return [];
296
276
  }
@@ -300,7 +280,7 @@ async function collectContentSearchEntries(context) {
300
280
  const markdown = await readFile(sourcePath);
301
281
  const { frontmatter, content } = extractFrontmatter(markdown);
302
282
  const segments = resolveDocsSegments(relative);
303
- const href = '/' + segments.join('/') + '/';
283
+ const href = `/${segments.join('/')}/`;
304
284
  const rawTitle = resolveTitle(frontmatter, content, segments);
305
285
  const title = applySidebarTitleOverride(href, rawTitle, overrides);
306
286
  if (!title) {
@@ -320,7 +300,7 @@ async function collectContentSearchEntries(context) {
320
300
  description: frontmatter.description?.trim() ? frontmatter.description.trim() : undefined,
321
301
  headings,
322
302
  excerpt,
323
- kind: 'docs'
303
+ kind: 'docs',
324
304
  });
325
305
  }
326
306
  entries.sort((a, b) => a.path.localeCompare(b.path));
@@ -357,7 +337,7 @@ async function loadSidebarOverrides(contentRoot) {
357
337
  map.set(normalized, {
358
338
  ...entry,
359
339
  path: normalized,
360
- order: defaultOrder
340
+ order: defaultOrder,
361
341
  });
362
342
  }
363
343
  return map;
@@ -367,14 +347,22 @@ async function loadSidebarOverrides(contentRoot) {
367
347
  if (!value || typeof value !== 'object') {
368
348
  continue;
369
349
  }
370
- const rawPath = typeof value.path === 'string' ? String(value.path) : key;
350
+ const rawPath = typeof value.path === 'string'
351
+ ? String(value.path)
352
+ : key;
371
353
  const normalized = normalizeDocsOverrideHref(rawPath);
372
354
  if (!normalized) {
373
355
  continue;
374
356
  }
375
- const title = typeof value.title === 'string' ? String(value.title) : undefined;
376
- const section = typeof value.section === 'string' ? String(value.section) : undefined;
377
- const hidden = typeof value.hidden === 'boolean' ? Boolean(value.hidden) : undefined;
357
+ const title = typeof value.title === 'string'
358
+ ? String(value.title)
359
+ : undefined;
360
+ const section = typeof value.section === 'string'
361
+ ? String(value.section)
362
+ : undefined;
363
+ const hidden = typeof value.hidden === 'boolean'
364
+ ? Boolean(value.hidden)
365
+ : undefined;
378
366
  const orderValue = value.order;
379
367
  const order = typeof orderValue === 'number' && Number.isFinite(orderValue) ? orderValue : undefined;
380
368
  map.set(normalized, { path: normalized, title, section, hidden, order });
@@ -382,6 +370,12 @@ async function loadSidebarOverrides(contentRoot) {
382
370
  }
383
371
  return map;
384
372
  }
373
+ function isSidebarOverrideChange(context, contentRoot) {
374
+ if (!context.changedFile) {
375
+ return false;
376
+ }
377
+ return (path.resolve(context.changedFile) === path.join(path.resolve(contentRoot), '_sidebar.json'));
378
+ }
385
379
  function applySidebarOverride(entry, overrides) {
386
380
  const key = normalizeDocsOverrideHref(entry.path);
387
381
  const override = key ? overrides.get(key) : undefined;
@@ -391,14 +385,20 @@ function applySidebarOverride(entry, overrides) {
391
385
  if (override.hidden === true) {
392
386
  return null;
393
387
  }
394
- const title = typeof override.title === 'string' && override.title.trim().length > 0 ? override.title.trim() : entry.title;
395
- const section = typeof override.section === 'string' && override.section.trim().length > 0 ? override.section.trim() : entry.section;
396
- const order = typeof override.order === 'number' && Number.isFinite(override.order) ? override.order : entry.order;
388
+ const title = typeof override.title === 'string' && override.title.trim().length > 0
389
+ ? override.title.trim()
390
+ : entry.title;
391
+ const section = typeof override.section === 'string' && override.section.trim().length > 0
392
+ ? override.section.trim()
393
+ : entry.section;
394
+ const order = typeof override.order === 'number' && Number.isFinite(override.order)
395
+ ? override.order
396
+ : entry.order;
397
397
  return {
398
398
  path: entry.path,
399
399
  title,
400
400
  section,
401
- order
401
+ order,
402
402
  };
403
403
  }
404
404
  function applySidebarTitleOverride(href, fallbackTitle, overrides) {
@@ -410,7 +410,9 @@ function applySidebarTitleOverride(href, fallbackTitle, overrides) {
410
410
  if (override.hidden === true) {
411
411
  return null;
412
412
  }
413
- const title = typeof override.title === 'string' && override.title.trim().length > 0 ? override.title.trim() : fallbackTitle;
413
+ const title = typeof override.title === 'string' && override.title.trim().length > 0
414
+ ? override.title.trim()
415
+ : fallbackTitle;
414
416
  return title;
415
417
  }
416
418
  function normalizeDocsOverrideHref(value) {
@@ -444,13 +446,14 @@ async function collectPageSearchEntries(context) {
444
446
  const titleFromTag = document('title').first().text().trim();
445
447
  const titleFromH1 = document('h1').first().text().trim();
446
448
  const title = titleFromTag || titleFromH1 || toTitleCase(page.name);
447
- const description = document('meta[name="description"]').first().attr('content')?.trim()
448
- || undefined;
449
+ const description = document('meta[name="description"]').first().attr('content')?.trim() || undefined;
449
450
  const headings = document('h2, h3')
450
451
  .toArray()
451
452
  .map((element) => document(element).text().trim())
452
453
  .filter((text) => text.length > 0);
453
- const mainText = (document('main').first().text() || document.text()).replace(/\s+/g, ' ').trim();
454
+ const mainText = (document('main').first().text() || document.text())
455
+ .replace(/\s+/g, ' ')
456
+ .trim();
454
457
  const excerpt = mainText.length > 240 ? `${mainText.slice(0, 240).trim()}…` : mainText;
455
458
  entries.push({
456
459
  path: resolvePageHref(page.name),
@@ -458,7 +461,7 @@ async function collectPageSearchEntries(context) {
458
461
  description,
459
462
  headings,
460
463
  excerpt,
461
- kind: 'page'
464
+ kind: 'page',
462
465
  });
463
466
  }
464
467
  entries.sort((a, b) => a.path.localeCompare(b.path));
@@ -534,7 +537,7 @@ function extractFrontmatter(markdown) {
534
537
  return { frontmatter, content };
535
538
  }
536
539
  function resolveTitle(frontmatter, content, segments) {
537
- if (frontmatter.title && frontmatter.title.trim()) {
540
+ if (frontmatter.title?.trim()) {
538
541
  return frontmatter.title.trim();
539
542
  }
540
543
  const headingMatch = content.match(/^#\s+(.+)$/m);
@@ -568,7 +571,7 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
568
571
  if (main.length === 0 || head.length === 0) {
569
572
  throw new Error('Base application template for content pages must include <head> and <main> elements.');
570
573
  }
571
- if (description && description.trim()) {
574
+ if (description?.trim()) {
572
575
  const meta = head.find('meta[name="description"]').first();
573
576
  if (meta.length > 0) {
574
577
  meta.attr('content', description.trim());
@@ -581,8 +584,11 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
581
584
  const effectiveDescription = (description ?? '').trim() || defaultDescription;
582
585
  // Ensure content pages load the shared app styles.
583
586
  const cssHref = `/${FOLDERS.app}/app.css`;
584
- const existingStylesheet = head.find(`link[rel="stylesheet"][href="${cssHref}"]`).first().length > 0
585
- || head.find('link[rel="stylesheet"]').toArray().some((element) => {
587
+ const existingStylesheet = head.find(`link[rel="stylesheet"][href="${cssHref}"]`).first().length > 0 ||
588
+ head
589
+ .find('link[rel="stylesheet"]')
590
+ .toArray()
591
+ .some((element) => {
586
592
  const href = document(element).attr('href');
587
593
  return typeof href === 'string' && href.includes('/app/app.css');
588
594
  });
@@ -591,8 +597,11 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
591
597
  }
592
598
  // Ensure docs pages load the docs layout styles.
593
599
  const docsCssHref = resolvePageAssetUrl(pagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.css}`);
594
- const existingDocsStylesheet = head.find(`link[rel="stylesheet"][href="${docsCssHref}"]`).first().length > 0
595
- || head.find('link[rel="stylesheet"]').toArray().some((element) => {
600
+ const existingDocsStylesheet = head.find(`link[rel="stylesheet"][href="${docsCssHref}"]`).first().length > 0 ||
601
+ head
602
+ .find('link[rel="stylesheet"]')
603
+ .toArray()
604
+ .some((element) => {
596
605
  const href = document(element).attr('href');
597
606
  return typeof href === 'string' && href.includes('/docs/index.css');
598
607
  });
@@ -646,7 +655,7 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
646
655
  ' </div>',
647
656
  ' </div>',
648
657
  ' </div>',
649
- '</section>'
658
+ '</section>',
650
659
  ].join('\n')
651
660
  : [
652
661
  '<section class="docs-layout" data-scope="docs">',
@@ -655,7 +664,7 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
655
664
  ` <article class="docs-article ws-markdown">${bodyHtml}</article>`,
656
665
  ' </div>',
657
666
  ' </div>',
658
- '</section>'
667
+ '</section>',
659
668
  ].join('\n');
660
669
  main.html(docsLayoutHtml);
661
670
  return document.root().html() ?? '';
@@ -689,7 +698,7 @@ function buildContentNavTree(entries) {
689
698
  title: 'Docs',
690
699
  children: [],
691
700
  isPage: false,
692
- position: position++
701
+ position: position++,
693
702
  };
694
703
  for (const entry of entries) {
695
704
  const normalizedPath = normalizeDocsPath(entry.path);
@@ -709,7 +718,7 @@ function buildContentNavTree(entries) {
709
718
  title: toTitleCase(segment.replace(/[-_]/g, ' ')),
710
719
  children: [],
711
720
  isPage: false,
712
- position: position++
721
+ position: position++,
713
722
  };
714
723
  current.children.push(child);
715
724
  }
@@ -734,9 +743,7 @@ function renderContentNavList(nodes, currentPath, depth = 0) {
734
743
  const label = node.isPage
735
744
  ? `<a class="docs-nav__link" href="${node.path}"${isActive ? ' aria-current="page"' : ''}>${escapeHtml(node.title)}</a>`
736
745
  : `<span class="docs-nav__label">${escapeHtml(node.title)}</span>`;
737
- const nested = node.children.length > 0
738
- ? renderContentNavList(node.children, currentPath, depth + 1)
739
- : '';
746
+ const nested = node.children.length > 0 ? renderContentNavList(node.children, currentPath, depth + 1) : '';
740
747
  return `<li class="docs-nav__item"${activeAttr}>${label}${nested}</li>`;
741
748
  });
742
749
  return `<ol class="${listClass}">${items.join('')}</ol>`;
@@ -748,7 +755,10 @@ function renderContentBreadcrumb(titleByPath, currentPath) {
748
755
  const crumbs = [];
749
756
  const rootTitle = titleByPath.get('/docs/') ?? 'Docs';
750
757
  crumbs.push({ title: rootTitle, href: '/docs/' });
751
- const segments = currentPath.replace(/^\/docs\/?/, '').split('/').filter(Boolean);
758
+ const segments = currentPath
759
+ .replace(/^\/docs\/?/, '')
760
+ .split('/')
761
+ .filter(Boolean);
752
762
  let href = '/docs/';
753
763
  for (const segment of segments) {
754
764
  href = `${href}${segment}/`;
@@ -784,7 +794,7 @@ function ensureMetaName(head, name, content) {
784
794
  async function renderMarkdownDoc(markdown) {
785
795
  const renderer = getMarkdownRenderer();
786
796
  const expanded = await expandAdmonitions(markdown, renderer);
787
- const rawHtml = await marked.parse(expanded, { renderer: renderer });
797
+ const rawHtml = await marked.parse(expanded, { renderer });
788
798
  const linked = rewriteMarkdownLinks(rawHtml);
789
799
  const { html, headingIds } = ensureHeadingIds(linked);
790
800
  return { html, headingIds };
@@ -821,7 +831,7 @@ const ADMONITION_TITLES = {
821
831
  tip: 'Tip',
822
832
  info: 'Info',
823
833
  warning: 'Warning',
824
- danger: 'Danger'
834
+ danger: 'Danger',
825
835
  };
826
836
  async function expandAdmonitions(markdown, renderer) {
827
837
  const lines = markdown.split(/\r?\n/);
@@ -856,18 +866,22 @@ async function expandAdmonitions(markdown, renderer) {
856
866
  break;
857
867
  }
858
868
  const bodyMarkdown = inner.join('\n').trim();
859
- const bodyHtml = bodyMarkdown.length > 0 ? await marked.parse(bodyMarkdown, { renderer: renderer }) : '';
869
+ const bodyHtml = bodyMarkdown.length > 0 ? await marked.parse(bodyMarkdown, { renderer }) : '';
860
870
  out.push([
861
871
  `<aside class="docs-callout docs-callout--${kindRaw}">`,
862
872
  ` <div class="docs-callout__title">${escapeHtml(title)}</div>`,
863
873
  ` <div class="docs-callout__body">${bodyHtml}</div>`,
864
- `</aside>`
874
+ `</aside>`,
865
875
  ].join('\n'));
866
876
  }
867
877
  return out.join('\n');
868
878
  }
869
879
  function isAdmonitionKind(value) {
870
- return value === 'note' || value === 'tip' || value === 'info' || value === 'warning' || value === 'danger';
880
+ return (value === 'note' ||
881
+ value === 'tip' ||
882
+ value === 'info' ||
883
+ value === 'warning' ||
884
+ value === 'danger');
871
885
  }
872
886
  function ensureHeadingIds(html) {
873
887
  const document = load(html);
@@ -928,9 +942,9 @@ function validateRenderedContentPages(pages) {
928
942
  if (!resolved) {
929
943
  continue;
930
944
  }
931
- const isDocsPage = resolved.pathname === '/docs'
932
- || resolved.pathname === '/docs/'
933
- || resolved.pathname.startsWith('/docs/');
945
+ const isDocsPage = resolved.pathname === '/docs' ||
946
+ resolved.pathname === '/docs/' ||
947
+ resolved.pathname.startsWith('/docs/');
934
948
  if (!isDocsPage) {
935
949
  continue;
936
950
  }
@@ -995,21 +1009,19 @@ async function rewriteContentForPublish(html, shared, docsManifest, options) {
995
1009
  document(`link[href="/app/app.css"]`).attr('href', `/app/${shared.css}`);
996
1010
  }
997
1011
  if (shared?.js) {
998
- document(`script[src="/app/app.js"]`)
999
- .attr('src', `/app/${shared.js}`)
1000
- .attr('type', 'module');
1012
+ document(`script[src="/app/app.js"]`).attr('src', `/app/${shared.js}`).attr('type', 'module');
1001
1013
  }
1002
1014
  if (docsManifest.css) {
1003
1015
  const selector = [
1004
1016
  `link[href="${resolvePageAssetUrl(pagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.css}`)}"]`,
1005
- `link[href="${resolvePageAssetUrl(buildPagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.css}`)}"]`
1017
+ `link[href="${resolvePageAssetUrl(buildPagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.css}`)}"]`,
1006
1018
  ].join(', ');
1007
1019
  document(selector).attr('href', resolvePageAssetUrl(pagesUrlPrefix, 'docs', docsManifest.css));
1008
1020
  }
1009
1021
  if (docsManifest.js) {
1010
1022
  const selector = [
1011
1023
  `script[src="${resolvePageAssetUrl(pagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.js}`)}"]`,
1012
- `script[src="${resolvePageAssetUrl(buildPagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.js}`)}"]`
1024
+ `script[src="${resolvePageAssetUrl(buildPagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.js}`)}"]`,
1013
1025
  ].join(', ');
1014
1026
  document(selector)
1015
1027
  .attr('src', resolvePageAssetUrl(pagesUrlPrefix, 'docs', docsManifest.js))