@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,86 +1,127 @@
1
- import { createHash } from 'node:crypto';
2
1
  import type { CheerioAPI } from 'cheerio';
3
2
 
4
3
  const HTTP_TIMEOUT_MS = 5000;
5
4
 
5
+ export interface SubresourceIntegrityOptions {
6
+ readonly allowExternalFetch?: boolean;
7
+ readonly fetcher?: typeof fetch;
8
+ }
9
+
6
10
  export interface SubresourceIntegrityResult {
7
- readonly failures: string[];
11
+ readonly failures: string[];
12
+ readonly skippedExternalResources: string[];
8
13
  }
9
14
 
10
- export async function addSubresourceIntegrity(document: CheerioAPI): Promise<SubresourceIntegrityResult> {
11
- const failures: string[] = [];
12
- await Promise.all([
13
- processScripts(document, failures),
14
- processStylesheets(document, failures)
15
- ]);
16
- return { failures };
15
+ export async function addSubresourceIntegrity(
16
+ document: CheerioAPI,
17
+ options: SubresourceIntegrityOptions = {},
18
+ ): Promise<SubresourceIntegrityResult> {
19
+ const failures: string[] = [];
20
+ const skippedExternalResources: string[] = [];
21
+ await Promise.all([
22
+ processScripts(document, failures, skippedExternalResources, options),
23
+ processStylesheets(document, failures, skippedExternalResources, options),
24
+ ]);
25
+ return { failures, skippedExternalResources };
17
26
  }
18
27
 
19
- async function processScripts(document: CheerioAPI, failures: string[]): Promise<void> {
20
- const scripts = document('script[src]').toArray();
21
- await Promise.all(scripts.map(async (element) => {
22
- const script = document(element);
23
- const src = script.attr('src');
24
- if (!src || !isExternal(src) || script.attr('integrity')) {
25
- return;
26
- }
27
-
28
- const sri = await fetchIntegrity(src);
29
- if (!sri) {
30
- failures.push(src);
31
- return;
32
- }
33
-
34
- script.attr('integrity', sri);
35
- if (!script.attr('crossorigin')) {
36
- script.attr('crossorigin', 'anonymous');
37
- }
38
- }));
28
+ async function processScripts(
29
+ document: CheerioAPI,
30
+ failures: string[],
31
+ skippedExternalResources: string[],
32
+ options: SubresourceIntegrityOptions,
33
+ ): Promise<void> {
34
+ const scripts = document('script[src]').toArray();
35
+ await Promise.all(
36
+ scripts.map(async (element) => {
37
+ const script = document(element);
38
+ const src = script.attr('src');
39
+ if (!src || script.attr('integrity')) {
40
+ return;
41
+ }
42
+
43
+ if (!isExternal(src)) {
44
+ return;
45
+ }
46
+
47
+ if (!options.allowExternalFetch) {
48
+ skippedExternalResources.push(src);
49
+ return;
50
+ }
51
+
52
+ const sri = await fetchIntegrity(src, options.fetcher ?? fetch);
53
+ if (!sri) {
54
+ failures.push(src);
55
+ return;
56
+ }
57
+
58
+ script.attr('integrity', sri);
59
+ if (!script.attr('crossorigin')) {
60
+ script.attr('crossorigin', 'anonymous');
61
+ }
62
+ }),
63
+ );
39
64
  }
40
65
 
41
- async function processStylesheets(document: CheerioAPI, failures: string[]): Promise<void> {
42
- const links = document('link[rel="stylesheet"][href]').toArray();
43
- await Promise.all(links.map(async (element) => {
44
- const link = document(element);
45
- const href = link.attr('href');
46
- if (!href || !isExternal(href) || link.attr('integrity')) {
47
- return;
48
- }
49
-
50
- const sri = await fetchIntegrity(href);
51
- if (!sri) {
52
- failures.push(href);
53
- return;
54
- }
55
-
56
- link.attr('integrity', sri);
57
- if (!link.attr('crossorigin')) {
58
- link.attr('crossorigin', 'anonymous');
59
- }
60
- }));
66
+ async function processStylesheets(
67
+ document: CheerioAPI,
68
+ failures: string[],
69
+ skippedExternalResources: string[],
70
+ options: SubresourceIntegrityOptions,
71
+ ): Promise<void> {
72
+ const links = document('link[rel="stylesheet"][href]').toArray();
73
+ await Promise.all(
74
+ links.map(async (element) => {
75
+ const link = document(element);
76
+ const href = link.attr('href');
77
+ if (!href || link.attr('integrity')) {
78
+ return;
79
+ }
80
+
81
+ if (!isExternal(href)) {
82
+ return;
83
+ }
84
+
85
+ if (!options.allowExternalFetch) {
86
+ skippedExternalResources.push(href);
87
+ return;
88
+ }
89
+
90
+ const sri = await fetchIntegrity(href, options.fetcher ?? fetch);
91
+ if (!sri) {
92
+ failures.push(href);
93
+ return;
94
+ }
95
+
96
+ link.attr('integrity', sri);
97
+ if (!link.attr('crossorigin')) {
98
+ link.attr('crossorigin', 'anonymous');
99
+ }
100
+ }),
101
+ );
61
102
  }
62
103
 
63
104
  function isExternal(url: string): boolean {
64
- return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//');
105
+ return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//');
65
106
  }
66
107
 
67
- async function fetchIntegrity(url: string): Promise<string | null> {
108
+ async function fetchIntegrity(url: string, fetcher: typeof fetch): Promise<string | null> {
109
+ try {
110
+ const normalizedUrl = url.startsWith('//') ? `https:${url}` : url;
111
+ const controller = new AbortController();
112
+ const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
68
113
  try {
69
- const normalizedUrl = url.startsWith('//') ? `https:${url}` : url;
70
- const controller = new AbortController();
71
- const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
72
- try {
73
- const response = await fetch(normalizedUrl, { signal: controller.signal });
74
- if (!response.ok) {
75
- return null;
76
- }
77
- const arrayBuffer = await response.arrayBuffer();
78
- const hash = createHash('sha384').update(Buffer.from(arrayBuffer)).digest('base64');
79
- return `sha384-${hash}`;
80
- } finally {
81
- clearTimeout(timeout);
82
- }
83
- } catch {
114
+ const response = await fetcher(normalizedUrl, { signal: controller.signal });
115
+ if (!response.ok) {
84
116
  return null;
117
+ }
118
+ const arrayBuffer = await response.arrayBuffer();
119
+ const hash = new Bun.CryptoHasher('sha384').update(Buffer.from(arrayBuffer)).digest('base64');
120
+ return `sha384-${hash}`;
121
+ } finally {
122
+ clearTimeout(timeout);
85
123
  }
124
+ } catch {
125
+ return null;
126
+ }
86
127
  }
@@ -1,30 +1,33 @@
1
1
  import type { CheerioAPI } from 'cheerio';
2
2
 
3
3
  interface LazyOptions {
4
- readonly skip: number;
4
+ readonly skip: number;
5
5
  }
6
6
 
7
7
  const DEFAULT_OPTIONS: LazyOptions = {
8
- skip: 1
8
+ skip: 1,
9
9
  };
10
10
 
11
- export function applyLazyLoading(document: CheerioAPI, options: LazyOptions = DEFAULT_OPTIONS): void {
12
- const { skip } = options;
13
- let index = 0;
14
- document('img').each((_i, element) => {
15
- const img = document(element);
16
- if (img.attr('loading')) {
17
- return;
18
- }
11
+ export function applyLazyLoading(
12
+ document: CheerioAPI,
13
+ options: LazyOptions = DEFAULT_OPTIONS,
14
+ ): void {
15
+ const { skip } = options;
16
+ let index = 0;
17
+ document('img').each((_i, element) => {
18
+ const img = document(element);
19
+ if (img.attr('loading')) {
20
+ return;
21
+ }
19
22
 
20
- index += 1;
21
- if (index <= skip) {
22
- return;
23
- }
23
+ index += 1;
24
+ if (index <= skip) {
25
+ return;
26
+ }
24
27
 
25
- img.attr('loading', 'lazy');
26
- if (!img.attr('fetchpriority')) {
27
- img.attr('fetchpriority', 'low');
28
- }
29
- });
28
+ img.attr('loading', 'lazy');
29
+ if (!img.attr('fetchpriority')) {
30
+ img.attr('fetchpriority', 'low');
31
+ }
32
+ });
30
33
  }
@@ -1,44 +1,53 @@
1
1
  import path from 'node:path';
2
- import { FOLDERS, FILES, EXTENSIONS } from '../core/constants.js';
2
+ import { FILES, EXTENSIONS } from '../core/constants.js';
3
3
  import { ensureDir, pathExists, writeFile } from '../utils/fs.js';
4
4
 
5
5
  export interface PageScaffoldOptions {
6
- readonly workspaceRoot: string;
7
- readonly pageName: string;
8
- readonly mode?: 'standard' | 'ssg';
9
- readonly paths: {
10
- readonly pages: string;
11
- readonly app: string;
12
- };
6
+ readonly workspaceRoot: string;
7
+ readonly pageName: string;
8
+ readonly mode?: 'standard' | 'ssg';
9
+ readonly paths: {
10
+ readonly pages: string;
11
+ readonly app: string;
12
+ };
13
13
  }
14
14
 
15
15
  export async function createPageScaffold(options: PageScaffoldOptions): Promise<void> {
16
- const pageDir = path.join(options.paths.pages, options.pageName);
17
- if (await pathExists(pageDir)) {
18
- throw new Error(`Page '${options.pageName}' already exists.`);
19
- }
16
+ const pageDir = path.join(options.paths.pages, options.pageName);
17
+ if (await pathExists(pageDir)) {
18
+ throw new Error(`Page '${options.pageName}' already exists.`);
19
+ }
20
20
 
21
- await ensureDir(pageDir);
21
+ await ensureDir(pageDir);
22
22
 
23
- const mode = options.mode ?? 'standard';
24
- const writes: Promise<void>[] = [
25
- writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.html}`), buildHtmlTemplate(options.pageName, mode)),
26
- writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.css}`), buildCssTemplate(options.pageName))
27
- ];
23
+ const mode = options.mode ?? 'standard';
24
+ const writes: Promise<void>[] = [
25
+ writeFile(
26
+ path.join(pageDir, `${FILES.index}${EXTENSIONS.html}`),
27
+ buildHtmlTemplate(options.pageName, mode),
28
+ ),
29
+ writeFile(
30
+ path.join(pageDir, `${FILES.index}${EXTENSIONS.css}`),
31
+ buildCssTemplate(options.pageName),
32
+ ),
33
+ ];
28
34
 
29
- if (mode === 'standard') {
30
- writes.push(writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.ts}`), buildScriptTemplate()));
31
- }
35
+ if (mode === 'standard') {
36
+ writes.push(
37
+ writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.ts}`), buildScriptTemplate()),
38
+ );
39
+ }
32
40
 
33
- await Promise.all(writes);
41
+ await Promise.all(writes);
34
42
  }
35
43
 
36
44
  function buildHtmlTemplate(pageName: string, mode: 'standard' | 'ssg'): string {
37
- const script = mode === 'standard'
38
- ? ` <script type="module" src="${FILES.index}${EXTENSIONS.js}" async></script>`
39
- : ` <!-- Add ${FILES.index}${EXTENSIONS.ts} to enable JS on this page. -->`;
45
+ const script =
46
+ mode === 'standard'
47
+ ? ` <script type="module" src="${FILES.index}${EXTENSIONS.js}" async></script>`
48
+ : ` <!-- Add ${FILES.index}${EXTENSIONS.ts} to enable JS on this page. -->`;
40
49
 
41
- return `<head>
50
+ return `<head>
42
51
  <meta charset="utf-8">
43
52
  <title>${pageName}</title>
44
53
  <link rel="stylesheet" href="${FILES.index}${EXTENSIONS.css}">
@@ -54,7 +63,7 @@ ${script}
54
63
  }
55
64
 
56
65
  function buildCssTemplate(pageName: string): string {
57
- return `/* ${pageName} Page Styles */
66
+ return `/* ${pageName} Page Styles */
58
67
  @import "@app/app.css";
59
68
 
60
69
  /* Add your page-specific styles here */
@@ -62,7 +71,7 @@ function buildCssTemplate(pageName: string): string {
62
71
  }
63
72
 
64
73
  function buildScriptTemplate(): string {
65
- return `// Page entry point
74
+ return `// Page entry point
66
75
  import '../../app/app';
67
76
 
68
77
  // Add page-specific logic here
@@ -2,90 +2,99 @@ import type { CheerioAPI } from 'cheerio';
2
2
  import { resolvePageHtmlUrl } from '../utils/pagePaths.js';
3
3
 
4
4
  export interface ResourceHintResult {
5
- readonly added: number;
6
- readonly candidates: string[];
7
- readonly missingHead: boolean;
5
+ readonly added: number;
6
+ readonly candidates: string[];
7
+ readonly missingHead: boolean;
8
8
  }
9
9
 
10
10
  export function injectResourceHints(
11
- document: CheerioAPI,
12
- currentPage: string,
13
- pagesUrlPrefix: string,
14
- useRootIndex: boolean
11
+ document: CheerioAPI,
12
+ currentPage: string,
13
+ pagesUrlPrefix: string,
14
+ useRootIndex: boolean,
15
15
  ): ResourceHintResult {
16
- const head = document('head').first();
17
- const pages = [...collectInternalPages(document, currentPage, pagesUrlPrefix)];
18
-
19
- if (head.length === 0) {
20
- return {
21
- added: 0,
22
- candidates: pages,
23
- missingHead: pages.length > 0
24
- };
25
- }
26
-
27
- if (pages.length === 0) {
28
- return { added: 0, candidates: [], missingHead: false };
29
- }
30
-
31
- for (const page of pages) {
32
- const href = resolvePageHtmlUrl(pagesUrlPrefix, page, useRootIndex);
33
- head.append(`\n<link rel="prefetch" href="${href}" as="document">`);
34
- }
35
-
36
- return { added: pages.length, candidates: pages, missingHead: false };
16
+ const head = document('head').first();
17
+ const pages = [...collectInternalPages(document, currentPage, pagesUrlPrefix)];
18
+
19
+ if (head.length === 0) {
20
+ return {
21
+ added: 0,
22
+ candidates: pages,
23
+ missingHead: pages.length > 0,
24
+ };
25
+ }
26
+
27
+ if (pages.length === 0) {
28
+ return { added: 0, candidates: [], missingHead: false };
29
+ }
30
+
31
+ for (const page of pages) {
32
+ const href = resolvePageHtmlUrl(pagesUrlPrefix, page, useRootIndex);
33
+ head.append(`\n<link rel="prefetch" href="${href}" as="document">`);
34
+ }
35
+
36
+ return { added: pages.length, candidates: pages, missingHead: false };
37
37
  }
38
38
 
39
- function collectInternalPages(document: CheerioAPI, currentPage: string, pagesUrlPrefix: string): Set<string> {
40
- const pages = new Set<string>();
41
- document('a[href]').each((_index, element) => {
42
- const href = document(element).attr('href');
43
- const pageName = normalizePageName(href, pagesUrlPrefix);
44
- if (!pageName || pageName === currentPage) {
45
- return;
46
- }
47
- pages.add(pageName);
48
- });
49
- return pages;
39
+ function collectInternalPages(
40
+ document: CheerioAPI,
41
+ currentPage: string,
42
+ pagesUrlPrefix: string,
43
+ ): Set<string> {
44
+ const pages = new Set<string>();
45
+ document('a[href]').each((_index, element) => {
46
+ const href = document(element).attr('href');
47
+ const pageName = normalizePageName(href, pagesUrlPrefix);
48
+ if (!pageName || pageName === currentPage) {
49
+ return;
50
+ }
51
+ pages.add(pageName);
52
+ });
53
+ return pages;
50
54
  }
51
55
 
52
56
  function normalizePageName(href: string | undefined, pagesUrlPrefix: string): string | null {
53
- if (!href || href.length === 0) {
54
- return null;
55
- }
56
-
57
- const lower = href.toLowerCase();
58
- if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('mailto:') || lower.startsWith('#')) {
59
- return null;
60
- }
61
-
62
- let path = href.split('#')[0]?.split('?')[0] ?? '';
63
- if (path.length === 0) {
64
- return null;
65
- }
66
-
67
- if (path.startsWith('/')) {
68
- path = path.slice(1);
69
- }
70
-
71
- const prefix = trimSlashes(pagesUrlPrefix);
72
- if (prefix && path.startsWith(`${prefix}/`)) {
73
- path = path.slice(prefix.length + 1);
74
- }
75
-
76
- if (path.endsWith('/')) {
77
- path = path.slice(0, -1);
78
- }
79
-
80
- const segments = path.split('/');
81
- const candidate = segments[0];
82
- if (!candidate) {
83
- return null;
84
- }
85
-
86
- return candidate;
57
+ if (!href || href.length === 0) {
58
+ return null;
59
+ }
60
+
61
+ const lower = href.toLowerCase();
62
+ if (
63
+ lower.startsWith('http://') ||
64
+ lower.startsWith('https://') ||
65
+ lower.startsWith('mailto:') ||
66
+ lower.startsWith('#')
67
+ ) {
68
+ return null;
69
+ }
70
+
71
+ let path = href.split('#')[0]?.split('?')[0] ?? '';
72
+ if (path.length === 0) {
73
+ return null;
74
+ }
75
+
76
+ if (path.startsWith('/')) {
77
+ path = path.slice(1);
78
+ }
79
+
80
+ const prefix = trimSlashes(pagesUrlPrefix);
81
+ if (prefix && path.startsWith(`${prefix}/`)) {
82
+ path = path.slice(prefix.length + 1);
83
+ }
84
+
85
+ if (path.endsWith('/')) {
86
+ path = path.slice(0, -1);
87
+ }
88
+
89
+ const segments = path.split('/');
90
+ const candidate = segments[0];
91
+ if (!candidate) {
92
+ return null;
93
+ }
94
+
95
+ return candidate;
87
96
  }
88
97
 
89
98
  function trimSlashes(value: string): string {
90
- return value.replace(/^\/+|\/+$/g, '');
99
+ return value.replace(/^\/+|\/+$/g, '');
91
100
  }
package/src/index.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';