@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
@@ -9,7 +9,7 @@ export const frontendPathSchema = z.object({
9
9
  content: z.string(),
10
10
  images: z.string(),
11
11
  fonts: z.string(),
12
- media: z.string()
12
+ media: z.string(),
13
13
  }),
14
14
  build: z.object({
15
15
  root: z.string(),
@@ -19,7 +19,7 @@ export const frontendPathSchema = z.object({
19
19
  content: z.string(),
20
20
  images: z.string(),
21
21
  fonts: z.string(),
22
- media: z.string()
22
+ media: z.string(),
23
23
  }),
24
24
  dist: z.object({
25
25
  root: z.string(),
@@ -29,16 +29,17 @@ export const frontendPathSchema = z.object({
29
29
  content: z.string(),
30
30
  images: z.string(),
31
31
  fonts: z.string(),
32
- media: z.string()
33
- })
32
+ media: z.string(),
33
+ }),
34
34
  });
35
35
  export const frontendFeatureFlagsSchema = z.object({
36
36
  htmlSecurity: z.boolean().default(true),
37
+ externalResourceIntegrity: z.boolean().default(false),
37
38
  imageOptimization: z.boolean().default(true),
38
- precompression: z.boolean().default(true)
39
+ precompression: z.boolean().default(true),
39
40
  });
40
41
  export const frontendConfigSchema = z.object({
41
42
  version: z.literal(1),
42
43
  paths: frontendPathSchema,
43
- features: frontendFeatureFlagsSchema
44
+ features: frontendFeatureFlagsSchema,
44
45
  });
@@ -6,7 +6,7 @@ export async function prepareWorkspaceConfig(workspaceRoot) {
6
6
  await ensureWebstirDirectory(workspaceRoot);
7
7
  await writeConfigManifest({
8
8
  outputPath: resolveManifestPath(workspaceRoot),
9
- data: config
9
+ data: config,
10
10
  });
11
11
  return config;
12
12
  }
@@ -1,11 +1,12 @@
1
1
  import fs from 'node:fs';
2
- import path from 'path';
2
+ import path from 'node:path';
3
3
  import { FOLDERS } from '../core/constants.js';
4
4
  import { frontendFeatureFlagsSchema } from './schema.js';
5
5
  const DEFAULT_FEATURE_FLAGS = {
6
6
  htmlSecurity: true,
7
+ externalResourceIntegrity: false,
7
8
  imageOptimization: true,
8
- precompression: true
9
+ precompression: true,
9
10
  };
10
11
  export function buildConfig(workspaceRoot) {
11
12
  const srcRoot = path.join(workspaceRoot, FOLDERS.src);
@@ -27,7 +28,7 @@ export function buildConfig(workspaceRoot) {
27
28
  content: srcContentRoot,
28
29
  images: path.join(frontendRoot, FOLDERS.images),
29
30
  fonts: path.join(frontendRoot, FOLDERS.fonts),
30
- media: path.join(frontendRoot, FOLDERS.media)
31
+ media: path.join(frontendRoot, FOLDERS.media),
31
32
  },
32
33
  build: {
33
34
  root: buildRoot,
@@ -37,7 +38,7 @@ export function buildConfig(workspaceRoot) {
37
38
  content: path.join(buildFrontend, FOLDERS.pages, 'docs'),
38
39
  images: path.join(buildFrontend, FOLDERS.images),
39
40
  fonts: path.join(buildFrontend, FOLDERS.fonts),
40
- media: path.join(buildFrontend, FOLDERS.media)
41
+ media: path.join(buildFrontend, FOLDERS.media),
41
42
  },
42
43
  dist: {
43
44
  root: distRoot,
@@ -47,10 +48,10 @@ export function buildConfig(workspaceRoot) {
47
48
  content: path.join(distFrontend, FOLDERS.pages, 'docs'),
48
49
  images: path.join(distFrontend, FOLDERS.images),
49
50
  fonts: path.join(distFrontend, FOLDERS.fonts),
50
- media: path.join(distFrontend, FOLDERS.media)
51
- }
51
+ media: path.join(distFrontend, FOLDERS.media),
52
+ },
52
53
  },
53
- features: loadFeatureFlags(frontendRoot)
54
+ features: loadFeatureFlags(frontendRoot),
54
55
  };
55
56
  }
56
57
  function resolveContentRoot(workspaceRoot, frontendRoot) {
@@ -111,8 +112,9 @@ function loadFeatureFlags(frontendRoot) {
111
112
  const overrides = frontendFeatureFlagsSchema.parse(overridesSource);
112
113
  return {
113
114
  htmlSecurity: overrides.htmlSecurity,
115
+ externalResourceIntegrity: overrides.externalResourceIntegrity,
114
116
  imageOptimization: overrides.imageOptimization,
115
- precompression: overrides.precompression
117
+ precompression: overrides.precompression,
116
118
  };
117
119
  }
118
120
  catch (error) {
@@ -127,5 +129,5 @@ function extractOverrideSource(value) {
127
129
  return container;
128
130
  }
129
131
  }
130
- return (value && typeof value === 'object') ? value : {};
132
+ return value && typeof value === 'object' ? value : {};
131
133
  }
@@ -24,7 +24,7 @@ export declare const FOLDERS: {
24
24
  };
25
25
  export declare const FILES: {
26
26
  readonly packageJson: "package.json";
27
- readonly packageLockJson: "package-lock.json";
27
+ readonly bunLock: "bun.lock";
28
28
  readonly tsBuildInfo: ".tsbuildinfo";
29
29
  readonly baseTsConfigJson: "base.tsconfig.json";
30
30
  readonly manifestJson: "manifest.json";
@@ -20,11 +20,11 @@ export const FOLDERS = {
20
20
  nodeModules: 'node_modules',
21
21
  seed: 'seed',
22
22
  demo: 'demo',
23
- temp: 'temp'
23
+ temp: 'temp',
24
24
  };
25
25
  export const FILES = {
26
26
  packageJson: 'package.json',
27
- packageLockJson: 'package-lock.json',
27
+ bunLock: 'bun.lock',
28
28
  tsBuildInfo: '.tsbuildinfo',
29
29
  baseTsConfigJson: 'base.tsconfig.json',
30
30
  manifestJson: 'manifest.json',
@@ -33,7 +33,7 @@ export const FILES = {
33
33
  indexHtml: 'index.html',
34
34
  refreshJs: 'refresh.js',
35
35
  hmrJs: 'hmr.js',
36
- robotsTxt: 'robots.txt'
36
+ robotsTxt: 'robots.txt',
37
37
  };
38
38
  export const EXTENSIONS = {
39
39
  html: '.html',
@@ -63,8 +63,8 @@ export const EXTENSIONS = {
63
63
  ogg: '.ogg',
64
64
  mp4: '.mp4',
65
65
  webm: '.webm',
66
- mov: '.mov'
66
+ mov: '.mov',
67
67
  };
68
68
  export const FILE_NAMES = {
69
- htmlAppTemplate: 'app.html'
69
+ htmlAppTemplate: 'app.html',
70
70
  };
@@ -2,7 +2,7 @@ export const STRUCTURED_DIAGNOSTIC_PREFIX = 'WEBSTIR_DIAGNOSTIC ';
2
2
  export function emitDiagnostic(event) {
3
3
  const payload = {
4
4
  type: 'diagnostic',
5
- ...event
5
+ ...event,
6
6
  };
7
7
  const logMessage = `[webstir-frontend][${event.code}] ${event.message}`;
8
8
  switch (event.severity) {
@@ -1,23 +1,23 @@
1
1
  import path from 'node:path';
2
- import { glob } from 'glob';
3
2
  import { pathExists } from '../utils/fs.js';
3
+ import { scanDirectories } from '../utils/glob.js';
4
4
  export async function getPages(root) {
5
5
  const directories = await getPageDirectories(root);
6
6
  return directories.map((entry) => ({
7
7
  name: entry.name,
8
- directory: entry.directory
8
+ directory: entry.directory,
9
9
  }));
10
10
  }
11
11
  export async function getPageDirectories(root) {
12
12
  if (!(await pathExists(root))) {
13
13
  return [];
14
14
  }
15
- const entries = await glob('*/', { cwd: root, absolute: false, withFileTypes: false });
15
+ const entries = await scanDirectories('*/', { cwd: root, absolute: false });
16
16
  return entries.map((entry) => {
17
17
  const name = entry.endsWith('/') ? entry.slice(0, -1) : entry;
18
18
  return {
19
19
  name,
20
- directory: path.join(root, name)
20
+ directory: path.join(root, name),
21
21
  };
22
22
  });
23
23
  }
package/dist/hooks.js CHANGED
@@ -6,7 +6,7 @@ const EMPTY_HOOKS = {
6
6
  pipelineBefore: [],
7
7
  pipelineAfter: [],
8
8
  builderBefore: new Map(),
9
- builderAfter: new Map()
9
+ builderAfter: new Map(),
10
10
  };
11
11
  export async function loadHooks(workspaceRoot, cacheBust) {
12
12
  const configPath = findConfigPath(workspaceRoot);
@@ -39,7 +39,7 @@ export function createHookContext(config, mode, changedFile, builderName) {
39
39
  mode,
40
40
  workspaceRoot: config.paths.workspace,
41
41
  builderName,
42
- changedFile
42
+ changedFile,
43
43
  };
44
44
  }
45
45
  export async function executeHooks(label, handlers, context) {
@@ -87,7 +87,7 @@ function normalizeHooks(raw, configPath) {
87
87
  pipelineBefore,
88
88
  pipelineAfter,
89
89
  builderBefore,
90
- builderAfter
90
+ builderAfter,
91
91
  };
92
92
  }
93
93
  function normalizeHandlerSet(value, label) {
@@ -4,7 +4,8 @@ import { EXTENSIONS } from '../core/constants.js';
4
4
  import { pathExists, readFile, stat } from '../utils/fs.js';
5
5
  import { resolvePageAssetUrl } from '../utils/pagePaths.js';
6
6
  const INLINE_THRESHOLD_BYTES = 6 * 1024;
7
- const csso = (cssoModule.default ?? cssoModule);
7
+ const csso = (cssoModule.default ??
8
+ cssoModule);
8
9
  function minifyCriticalCss(css) {
9
10
  return csso.minify(css).css;
10
11
  }
@@ -178,10 +179,12 @@ export function ensureDocsShellCriticalCss(document) {
178
179
  if (existing.length > 0) {
179
180
  return;
180
181
  }
181
- const docsStylesheet = document('link[rel="stylesheet"]').filter((_, element) => {
182
+ const docsStylesheet = document('link[rel="stylesheet"]')
183
+ .filter((_, element) => {
182
184
  const href = document(element).attr('href');
183
185
  return typeof href === 'string' && href.includes('/docs/');
184
- }).first();
186
+ })
187
+ .first();
185
188
  const styleTag = `<style data-critical="docs">\n${DOCS_SHELL_CRITICAL_CSS}\n</style>`;
186
189
  if (docsStylesheet.length > 0) {
187
190
  docsStylesheet.before(styleTag);
@@ -1,5 +1,10 @@
1
1
  import type { CheerioAPI } from 'cheerio';
2
+ export interface SubresourceIntegrityOptions {
3
+ readonly allowExternalFetch?: boolean;
4
+ readonly fetcher?: typeof fetch;
5
+ }
2
6
  export interface SubresourceIntegrityResult {
3
7
  readonly failures: string[];
8
+ readonly skippedExternalResources: string[];
4
9
  }
5
- export declare function addSubresourceIntegrity(document: CheerioAPI): Promise<SubresourceIntegrityResult>;
10
+ export declare function addSubresourceIntegrity(document: CheerioAPI, options?: SubresourceIntegrityOptions): Promise<SubresourceIntegrityResult>;
@@ -1,22 +1,29 @@
1
- import { createHash } from 'node:crypto';
2
1
  const HTTP_TIMEOUT_MS = 5000;
3
- export async function addSubresourceIntegrity(document) {
2
+ export async function addSubresourceIntegrity(document, options = {}) {
4
3
  const failures = [];
4
+ const skippedExternalResources = [];
5
5
  await Promise.all([
6
- processScripts(document, failures),
7
- processStylesheets(document, failures)
6
+ processScripts(document, failures, skippedExternalResources, options),
7
+ processStylesheets(document, failures, skippedExternalResources, options),
8
8
  ]);
9
- return { failures };
9
+ return { failures, skippedExternalResources };
10
10
  }
11
- async function processScripts(document, failures) {
11
+ async function processScripts(document, failures, skippedExternalResources, options) {
12
12
  const scripts = document('script[src]').toArray();
13
13
  await Promise.all(scripts.map(async (element) => {
14
14
  const script = document(element);
15
15
  const src = script.attr('src');
16
- if (!src || !isExternal(src) || script.attr('integrity')) {
16
+ if (!src || script.attr('integrity')) {
17
17
  return;
18
18
  }
19
- const sri = await fetchIntegrity(src);
19
+ if (!isExternal(src)) {
20
+ return;
21
+ }
22
+ if (!options.allowExternalFetch) {
23
+ skippedExternalResources.push(src);
24
+ return;
25
+ }
26
+ const sri = await fetchIntegrity(src, options.fetcher ?? fetch);
20
27
  if (!sri) {
21
28
  failures.push(src);
22
29
  return;
@@ -27,15 +34,22 @@ async function processScripts(document, failures) {
27
34
  }
28
35
  }));
29
36
  }
30
- async function processStylesheets(document, failures) {
37
+ async function processStylesheets(document, failures, skippedExternalResources, options) {
31
38
  const links = document('link[rel="stylesheet"][href]').toArray();
32
39
  await Promise.all(links.map(async (element) => {
33
40
  const link = document(element);
34
41
  const href = link.attr('href');
35
- if (!href || !isExternal(href) || link.attr('integrity')) {
42
+ if (!href || link.attr('integrity')) {
43
+ return;
44
+ }
45
+ if (!isExternal(href)) {
46
+ return;
47
+ }
48
+ if (!options.allowExternalFetch) {
49
+ skippedExternalResources.push(href);
36
50
  return;
37
51
  }
38
- const sri = await fetchIntegrity(href);
52
+ const sri = await fetchIntegrity(href, options.fetcher ?? fetch);
39
53
  if (!sri) {
40
54
  failures.push(href);
41
55
  return;
@@ -49,18 +63,18 @@ async function processStylesheets(document, failures) {
49
63
  function isExternal(url) {
50
64
  return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//');
51
65
  }
52
- async function fetchIntegrity(url) {
66
+ async function fetchIntegrity(url, fetcher) {
53
67
  try {
54
68
  const normalizedUrl = url.startsWith('//') ? `https:${url}` : url;
55
69
  const controller = new AbortController();
56
70
  const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
57
71
  try {
58
- const response = await fetch(normalizedUrl, { signal: controller.signal });
72
+ const response = await fetcher(normalizedUrl, { signal: controller.signal });
59
73
  if (!response.ok) {
60
74
  return null;
61
75
  }
62
76
  const arrayBuffer = await response.arrayBuffer();
63
- const hash = createHash('sha384').update(Buffer.from(arrayBuffer)).digest('base64');
77
+ const hash = new Bun.CryptoHasher('sha384').update(Buffer.from(arrayBuffer)).digest('base64');
64
78
  return `sha384-${hash}`;
65
79
  }
66
80
  finally {
@@ -1,5 +1,5 @@
1
1
  const DEFAULT_OPTIONS = {
2
- skip: 1
2
+ skip: 1,
3
3
  };
4
4
  export function applyLazyLoading(document, options = DEFAULT_OPTIONS) {
5
5
  const { skip } = options;
@@ -10,7 +10,7 @@ export async function createPageScaffold(options) {
10
10
  const mode = options.mode ?? 'standard';
11
11
  const writes = [
12
12
  writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.html}`), buildHtmlTemplate(options.pageName, mode)),
13
- writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.css}`), buildCssTemplate(options.pageName))
13
+ writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.css}`), buildCssTemplate(options.pageName)),
14
14
  ];
15
15
  if (mode === 'standard') {
16
16
  writes.push(writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.ts}`), buildScriptTemplate()));
@@ -6,7 +6,7 @@ export function injectResourceHints(document, currentPage, pagesUrlPrefix, useRo
6
6
  return {
7
7
  added: 0,
8
8
  candidates: pages,
9
- missingHead: pages.length > 0
9
+ missingHead: pages.length > 0,
10
10
  };
11
11
  }
12
12
  if (pages.length === 0) {
@@ -35,7 +35,10 @@ function normalizePageName(href, pagesUrlPrefix) {
35
35
  return null;
36
36
  }
37
37
  const lower = href.toLowerCase();
38
- if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('mailto:') || lower.startsWith('#')) {
38
+ if (lower.startsWith('http://') ||
39
+ lower.startsWith('https://') ||
40
+ lower.startsWith('mailto:') ||
41
+ lower.startsWith('#')) {
39
42
  return null;
40
43
  }
41
44
  let path = href.split('#')[0]?.split('?')[0] ?? '';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './operations.js';
2
+ export * from './inspect.js';
2
3
  export * from './config/manifest.js';
3
4
  export * from './config/schema.js';
5
+ export * from './runtime/index.js';
4
6
  export * from './types.js';
5
7
  export { frontendProvider } from './provider.js';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './operations.js';
2
+ export * from './inspect.js';
2
3
  export * from './config/manifest.js';
3
4
  export * from './config/schema.js';
5
+ export * from './runtime/index.js';
4
6
  export * from './types.js';
5
7
  export { frontendProvider } from './provider.js';
@@ -0,0 +1,2 @@
1
+ import type { FrontendWorkspaceInspectResult } from './types.js';
2
+ export declare function inspectFrontendWorkspace(workspaceRoot: string): Promise<FrontendWorkspaceInspectResult>;
@@ -0,0 +1,110 @@
1
+ import path from 'node:path';
2
+ import { FILES, FILE_NAMES } from './core/constants.js';
3
+ import { getPageDirectories } from './core/pages.js';
4
+ import { buildConfig } from './config/workspace.js';
5
+ import { pathExists, readJson } from './utils/fs.js';
6
+ const PAGE_SCRIPT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
7
+ export async function inspectFrontendWorkspace(workspaceRoot) {
8
+ const config = buildConfig(workspaceRoot);
9
+ const packageJson = await readWorkspacePackageInspect(workspaceRoot);
10
+ const appShell = await inspectAppShell(config.paths.src.app);
11
+ const pages = await inspectPages(config.paths.src.pages);
12
+ const content = await inspectContent(config.paths.src.content);
13
+ return {
14
+ workspaceRoot,
15
+ config,
16
+ packageJson,
17
+ appShell,
18
+ pages,
19
+ content,
20
+ };
21
+ }
22
+ async function readWorkspacePackageInspect(workspaceRoot) {
23
+ const packagePath = path.join(workspaceRoot, FILES.packageJson);
24
+ const pkg = await readJson(packagePath);
25
+ const enable = pkg?.webstir?.enable;
26
+ return {
27
+ path: packagePath,
28
+ exists: pkg !== null,
29
+ mode: pkg?.webstir?.mode,
30
+ enable: {
31
+ raw: enable,
32
+ known: normalizeKnownEnableFlags(enable),
33
+ },
34
+ };
35
+ }
36
+ async function inspectAppShell(appRoot) {
37
+ const templatePath = path.join(appRoot, FILE_NAMES.htmlAppTemplate);
38
+ const stylesheetPath = path.join(appRoot, 'app.css');
39
+ const scriptPath = await resolveFirstExistingPath(appRoot, 'app', PAGE_SCRIPT_EXTENSIONS);
40
+ const [exists, templateExists, stylesheetExists, scriptExists] = await Promise.all([
41
+ pathExists(appRoot),
42
+ pathExists(templatePath),
43
+ pathExists(stylesheetPath),
44
+ pathExists(scriptPath),
45
+ ]);
46
+ return {
47
+ root: appRoot,
48
+ exists,
49
+ templatePath,
50
+ templateExists,
51
+ stylesheetPath,
52
+ stylesheetExists,
53
+ scriptPath,
54
+ scriptExists,
55
+ };
56
+ }
57
+ async function inspectPages(pagesRoot) {
58
+ const pages = await getPageDirectories(pagesRoot);
59
+ return await Promise.all(pages.map(async (page) => {
60
+ const htmlPath = path.join(page.directory, FILES.indexHtml);
61
+ const stylesheetPath = path.join(page.directory, `${FILES.index}.css`);
62
+ const scriptPath = await resolveFirstExistingPath(page.directory, FILES.index, PAGE_SCRIPT_EXTENSIONS);
63
+ const [htmlExists, stylesheetExists, scriptExists] = await Promise.all([
64
+ pathExists(htmlPath),
65
+ pathExists(stylesheetPath),
66
+ pathExists(scriptPath),
67
+ ]);
68
+ return {
69
+ name: page.name,
70
+ directory: page.directory,
71
+ htmlPath,
72
+ htmlExists,
73
+ stylesheetPath,
74
+ stylesheetExists,
75
+ scriptPath,
76
+ scriptExists,
77
+ };
78
+ }));
79
+ }
80
+ async function inspectContent(contentRoot) {
81
+ const sidebarOverridePath = path.join(contentRoot, '_sidebar.json');
82
+ const [exists, sidebarOverrideExists] = await Promise.all([
83
+ pathExists(contentRoot),
84
+ pathExists(sidebarOverridePath),
85
+ ]);
86
+ return {
87
+ root: contentRoot,
88
+ exists,
89
+ sidebarOverridePath,
90
+ sidebarOverrideExists,
91
+ };
92
+ }
93
+ async function resolveFirstExistingPath(root, baseName, extensions) {
94
+ for (const extension of extensions) {
95
+ const candidate = path.join(root, `${baseName}${extension}`);
96
+ if (await pathExists(candidate)) {
97
+ return candidate;
98
+ }
99
+ }
100
+ return path.join(root, `${baseName}${extensions[0] ?? ''}`);
101
+ }
102
+ function normalizeKnownEnableFlags(value) {
103
+ return {
104
+ spa: value?.spa === true,
105
+ clientNav: value?.clientNav === true,
106
+ backend: value?.backend === true,
107
+ search: value?.search === true,
108
+ contentNav: value?.contentNav === true,
109
+ };
110
+ }
@@ -22,7 +22,7 @@ export async function ensureSsgViewMetadataForPage(options) {
22
22
  name: viewName,
23
23
  path: viewPath,
24
24
  renderMode: 'ssg',
25
- staticPaths: [viewPath]
25
+ staticPaths: [viewPath],
26
26
  };
27
27
  if (existingIndex >= 0) {
28
28
  existingViews[existingIndex] = nextView;
@@ -36,9 +36,9 @@ export async function ensureSsgViewMetadataForPage(options) {
36
36
  ...webstir,
37
37
  moduleManifest: {
38
38
  ...moduleConfig,
39
- views: existingViews
40
- }
41
- }
39
+ views: existingViews,
40
+ },
41
+ },
42
42
  };
43
43
  await writeJson(pkgPath, nextPkg);
44
44
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
- import { glob } from 'glob';
3
2
  import { FOLDERS, FILES } from '../../core/constants.js';
4
3
  import { copy, ensureDir, pathExists, readJson } from '../../utils/fs.js';
4
+ import { scanGlob } from '../../utils/glob.js';
5
5
  import { getPageDirectories } from '../../core/pages.js';
6
6
  import { assertNoSsgRoutesInModuleConfig } from './validation.js';
7
7
  import { runSsgSeo } from './seo.js';
@@ -53,10 +53,7 @@ async function applyDocsContentAliases(distRoot, distPagesRoot) {
53
53
  if (!(await pathExists(docsRoot))) {
54
54
  return;
55
55
  }
56
- const indexes = await glob('docs/**/index.html', {
57
- cwd: distPagesRoot,
58
- nodir: true
59
- });
56
+ const indexes = await scanGlob('docs/**/index.html', { cwd: distPagesRoot });
60
57
  for (const relativeIndex of indexes) {
61
58
  const sourceIndex = path.join(distPagesRoot, relativeIndex);
62
59
  if (!(await pathExists(sourceIndex))) {
@@ -1,8 +1,8 @@
1
1
  import path from 'node:path';
2
- import { glob } from 'glob';
3
2
  import { load } from 'cheerio';
4
3
  import { ensureDir, pathExists, readFile, writeFile } from '../../utils/fs.js';
5
4
  import { FILES } from '../../core/constants.js';
5
+ import { scanGlob } from '../../utils/glob.js';
6
6
  export async function runSsgSeo(distRoot, options = {}) {
7
7
  const pages = await discoverHtmlPages(distRoot);
8
8
  await validateInternalLinks(pages, distRoot);
@@ -13,14 +13,14 @@ async function discoverHtmlPages(distRoot) {
13
13
  if (!(await pathExists(distRoot))) {
14
14
  return [];
15
15
  }
16
- const files = await glob('**/index.html', { cwd: distRoot, nodir: true, ignore: ['pages/**'] });
16
+ const files = (await scanGlob('**/index.html', { cwd: distRoot })).filter((relative) => !relative.split(path.sep).join('/').startsWith('pages/'));
17
17
  const pages = files
18
18
  .map((relative) => {
19
19
  const normalized = relative.split(path.sep).join('/');
20
20
  const urlPath = toUrlPath(normalized);
21
21
  return {
22
22
  filePath: path.join(distRoot, relative),
23
- urlPath
23
+ urlPath,
24
24
  };
25
25
  })
26
26
  .filter((page) => Boolean(page.urlPath));
@@ -165,10 +165,10 @@ async function writeSitemap(distRoot, pages, siteUrl) {
165
165
  .join('\n');
166
166
  const xml = [
167
167
  '<?xml version="1.0" encoding="UTF-8"?>',
168
- comment + '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
168
+ `${comment}<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
169
169
  entries,
170
170
  '</urlset>',
171
- ''
171
+ '',
172
172
  ].join('\n');
173
173
  const outputPath = path.join(distRoot, 'sitemap.xml');
174
174
  await ensureDir(path.dirname(outputPath));