@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.
- package/README.md +124 -60
- package/dist/assets/imageOptimizer.js +10 -15
- package/dist/assets/precompression.js +1 -1
- package/dist/builders/contentBuilder.js +102 -90
- package/dist/builders/cssBuilder.js +25 -19
- package/dist/builders/htmlBuilder.js +57 -42
- package/dist/builders/index.js +1 -1
- package/dist/builders/jsBuilder.js +219 -76
- package/dist/builders/staticAssetsBuilder.js +27 -9
- package/dist/builders/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -30
- package/dist/config/manifest.js +7 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -6
- package/dist/config/setup.js +1 -1
- package/dist/config/workspace.js +11 -9
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +5 -5
- package/dist/core/diagnostics.js +1 -1
- package/dist/core/pages.js +4 -4
- package/dist/hooks.js +3 -3
- package/dist/html/criticalCss.js +6 -3
- package/dist/html/htmlSecurity.d.ts +6 -1
- package/dist/html/htmlSecurity.js +28 -14
- package/dist/html/lazyLoad.js +1 -1
- package/dist/html/pageScaffold.js +1 -1
- package/dist/html/resourceHints.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +110 -0
- package/dist/modes/ssg/metadata.js +4 -4
- package/dist/modes/ssg/routing.js +2 -5
- package/dist/modes/ssg/seo.js +5 -5
- package/dist/modes/ssg/views.js +17 -11
- package/dist/operations.js +18 -10
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.js +6 -1
- package/dist/provider.js +28 -24
- package/dist/runtime/boundary.d.ts +28 -0
- package/dist/runtime/boundary.js +247 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils/fs.d.ts +11 -10
- package/dist/utils/fs.js +48 -20
- package/dist/utils/glob.d.ts +8 -0
- package/dist/utils/glob.js +21 -0
- package/dist/utils/hash.js +1 -2
- package/dist/utils/pagePaths.js +2 -2
- package/package.json +19 -14
- package/scripts/publish.sh +2 -94
- package/scripts/update-contract.sh +12 -10
- package/src/assets/assetManifest.ts +39 -29
- package/src/assets/imageOptimizer.ts +91 -82
- package/src/assets/precompression.ts +22 -16
- package/src/builders/contentBuilder.ts +1224 -1149
- package/src/builders/cssBuilder.ts +466 -417
- package/src/builders/htmlBuilder.ts +511 -448
- package/src/builders/index.ts +7 -7
- package/src/builders/jsBuilder.ts +538 -280
- package/src/builders/staticAssetsBuilder.ts +166 -135
- package/src/builders/types.ts +7 -6
- package/src/cli.ts +66 -90
- package/src/config/manifest.ts +16 -14
- package/src/config/paths.ts +5 -5
- package/src/config/schema.ts +38 -37
- package/src/config/setup.ts +7 -7
- package/src/config/workspace.ts +118 -116
- package/src/config/workspaceManifest.ts +14 -14
- package/src/core/constants.ts +62 -62
- package/src/core/diagnostics.ts +26 -26
- package/src/core/pages.ts +19 -19
- package/src/hooks.ts +128 -118
- package/src/html/criticalCss.ts +84 -77
- package/src/html/htmlSecurity.ts +107 -66
- package/src/html/lazyLoad.ts +22 -19
- package/src/html/pageScaffold.ts +37 -28
- package/src/html/resourceHints.ts +83 -74
- package/src/index.ts +2 -0
- package/src/inspect.ts +158 -0
- package/src/modes/ssg/metadata.ts +53 -51
- package/src/modes/ssg/routing.ts +177 -177
- package/src/modes/ssg/seo.ts +208 -200
- package/src/modes/ssg/validation.ts +31 -25
- package/src/modes/ssg/views.ts +257 -238
- package/src/operations.ts +105 -95
- package/src/pipeline.ts +81 -69
- package/src/provider.ts +184 -176
- package/src/runtime/boundary.ts +325 -0
- package/src/runtime/index.ts +1 -0
- package/src/types.ts +107 -48
- package/src/utils/changedFile.ts +22 -22
- package/src/utils/fs.ts +73 -26
- package/src/utils/glob.ts +38 -0
- package/src/utils/hash.ts +2 -4
- package/src/utils/pagePaths.ts +35 -23
- package/src/utils/pathMatch.ts +26 -23
- package/tests/add-page-defaults.test.js +44 -39
- package/tests/bundlerParity.test.js +252 -0
- package/tests/cli.contract.test.js +13 -0
- package/tests/content-pages.test.js +108 -13
- package/tests/css-app-imports.test.js +22 -11
- package/tests/css-page-imports.test.js +26 -13
- package/tests/diagnostics.test.js +39 -36
- package/tests/features.test.js +48 -43
- package/tests/hooks.test.js +58 -42
- package/tests/htmlSecurity.test.js +66 -0
- package/tests/inspect.test.js +148 -0
- package/tests/provider.integration.test.js +71 -20
- package/tests/runtime.test.js +493 -0
- package/tests/ssg-defaults.test.js +284 -177
- package/tests/ssg-guardrails.test.js +51 -51
- package/tsconfig.json +3 -10
- package/dist/watch/frontendFiles.d.ts +0 -3
- package/dist/watch/frontendFiles.js +0 -25
- package/dist/watch/hotUpdateTracker.d.ts +0 -51
- package/dist/watch/hotUpdateTracker.js +0 -205
- package/dist/watch/pipelineHelpers.d.ts +0 -26
- package/dist/watch/pipelineHelpers.js +0 -177
- package/dist/watch/types.d.ts +0 -27
- package/dist/watch/types.js +0 -1
- package/dist/watch/watchCoordinator.d.ts +0 -36
- package/dist/watch/watchCoordinator.js +0 -551
- package/dist/watch/watchDaemon.d.ts +0 -17
- package/dist/watch/watchDaemon.js +0 -127
- package/dist/watch/watchReporter.d.ts +0 -21
- package/dist/watch/watchReporter.js +0 -64
- package/scripts/smoke.mjs +0 -35
- package/src/watch/frontendFiles.ts +0 -32
- package/src/watch/hotUpdateTracker.ts +0 -285
- package/src/watch/pipelineHelpers.ts +0 -242
- package/src/watch/types.ts +0 -23
- package/src/watch/watchCoordinator.ts +0 -666
- package/src/watch/watchDaemon.ts +0 -144
- package/src/watch/watchReporter.ts +0 -98
package/src/html/htmlSecurity.ts
CHANGED
|
@@ -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
|
-
|
|
11
|
+
readonly failures: string[];
|
|
12
|
+
readonly skippedExternalResources: string[];
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
export async function addSubresourceIntegrity(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
}
|
package/src/html/lazyLoad.ts
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import type { CheerioAPI } from 'cheerio';
|
|
2
2
|
|
|
3
3
|
interface LazyOptions {
|
|
4
|
-
|
|
4
|
+
readonly skip: number;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
const DEFAULT_OPTIONS: LazyOptions = {
|
|
8
|
-
|
|
8
|
+
skip: 1,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export function applyLazyLoading(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
index += 1;
|
|
24
|
+
if (index <= skip) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
img.attr('loading', 'lazy');
|
|
29
|
+
if (!img.attr('fetchpriority')) {
|
|
30
|
+
img.attr('fetchpriority', 'low');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
30
33
|
}
|
package/src/html/pageScaffold.ts
CHANGED
|
@@ -1,44 +1,53 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
+
await ensureDir(pageDir);
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
if (mode === 'standard') {
|
|
36
|
+
writes.push(
|
|
37
|
+
writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.ts}`), buildScriptTemplate()),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
await Promise.all(writes);
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function buildHtmlTemplate(pageName: string, mode: 'standard' | 'ssg'): string {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
readonly added: number;
|
|
6
|
+
readonly candidates: string[];
|
|
7
|
+
readonly missingHead: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function injectResourceHints(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
document: CheerioAPI,
|
|
12
|
+
currentPage: string,
|
|
13
|
+
pagesUrlPrefix: string,
|
|
14
|
+
useRootIndex: boolean,
|
|
15
15
|
): ResourceHintResult {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
99
|
+
return value.replace(/^\/+|\/+$/g, '');
|
|
91
100
|
}
|
package/src/index.ts
CHANGED