@abreen/tada 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -33
- package/bin/tada.ts +356 -0
- package/bin/validators.test.ts +204 -0
- package/bin/validators.ts +83 -0
- package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +16 -7
- package/build/bundle.ts +117 -0
- package/{webpack/code.test.js → build/code.test.ts} +6 -7
- package/build/colors.ts +25 -0
- package/build/content-watch.ts +107 -0
- package/build/copy.ts +118 -0
- package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
- package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
- package/build/features.ts +11 -0
- package/build/generate-content-assets.ts +315 -0
- package/build/generate-favicon.ts +165 -0
- package/build/generate-fonts.ts +31 -0
- package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
- package/build/globals.test.ts +101 -0
- package/{webpack/globals.js → build/globals.ts} +28 -13
- package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
- package/build/json-schema.test.ts +57 -0
- package/build/json-schema.ts +33 -0
- package/build/log.test.ts +111 -0
- package/build/log.ts +167 -0
- package/{webpack/markdown-plugins.test.js → build/markdown-plugins.test.ts} +94 -9
- package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
- package/build/pagefind.ts +339 -0
- package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
- package/build/pipeline.ts +93 -0
- package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
- package/{webpack/reachability.js → build/reachability.ts} +77 -34
- package/build/serve.ts +112 -0
- package/{webpack/site-variables.js → build/site-variables.ts} +22 -15
- package/{webpack → build}/site.schema.json +3 -10
- package/{webpack/templates.js → build/templates.ts} +35 -33
- package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
- package/build/toc-plugin.test.ts +105 -0
- package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
- package/build/types.ts +172 -0
- package/build/util.ts +26 -0
- package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
- package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
- package/build/utils/derive-theme.test.ts +111 -0
- package/build/utils/derive-theme.ts +85 -0
- package/build/utils/file-types.test.ts +61 -0
- package/build/utils/file-types.ts +13 -0
- package/build/utils/front-matter.test.ts +80 -0
- package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
- package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
- package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +63 -34
- package/{webpack/utils/markdown.js → build/utils/markdown.ts} +94 -49
- package/build/utils/paths.test.ts +91 -0
- package/{webpack/utils/paths.js → build/utils/paths.ts} +14 -22
- package/{webpack/utils/render.js → build/utils/render.ts} +188 -123
- package/build/utils/shiki-highlighter.ts +29 -0
- package/build/validate-internal-links-plugin.test.ts +106 -0
- package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
- package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
- package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
- package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
- package/build/watch.ts +573 -0
- package/content/index.md +9 -3
- package/content/markdown.md +2 -1
- package/content/problem_sets/index.html +14 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable.woff2 +0 -0
- package/package.json +28 -19
- package/src/_alerts.scss +92 -0
- package/src/_base.scss +106 -0
- package/src/{layout.scss → _layout.scss} +0 -2
- package/src/anchor/style.scss +1 -9
- package/src/code/index.ts +3 -3
- package/src/code.scss +1 -1
- package/src/critical.scss +5 -0
- package/src/header/_base.scss +129 -0
- package/src/header/style.scss +3 -131
- package/src/index.ts +1 -2
- package/src/question/style.scss +1 -1
- package/src/search/index.ts +36 -15
- package/src/search/style.scss +9 -15
- package/src/style.scss +6 -269
- package/src/toc/style.scss +5 -39
- package/src/util.ts +8 -5
- package/templates/_theme.scss +38 -14
- package/tsconfig.json +10 -6
- package/types/file-system-access.d.ts +5 -0
- package/types/markdown-it-plugins.d.ts +11 -0
- package/types/untyped-modules.d.ts +40 -0
- package/bin/tada.js +0 -361
- package/content/problem_sets/index.md +0 -6
- package/webpack/build-state.js +0 -97
- package/webpack/colors.js +0 -15
- package/webpack/config.base.js +0 -151
- package/webpack/config.dev.js +0 -23
- package/webpack/config.prod.js +0 -32
- package/webpack/content-watch-plugin.js +0 -153
- package/webpack/features.js +0 -5
- package/webpack/generate-content-assets-plugin.js +0 -308
- package/webpack/generate-favicon-plugin.js +0 -198
- package/webpack/generate-fonts-plugin.js +0 -69
- package/webpack/json-schema.js +0 -19
- package/webpack/log.js +0 -143
- package/webpack/pagefind-plugin.js +0 -379
- package/webpack/print-flair-plugin.js +0 -22
- package/webpack/serve.js +0 -104
- package/webpack/util.js +0 -49
- package/webpack/utils/define-plugin.js +0 -20
- package/webpack/utils/file-types.js +0 -26
- package/webpack/utils/parse-hsl.js +0 -8
- package/webpack/utils/shiki-highlighter.js +0 -26
- package/webpack/watch.js +0 -166
- /package/{webpack → build}/flair.json +0 -0
- /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
- /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
- /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
- /package/types/{dev.ts → dev.d.ts} +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HighlighterGeneric, BundledLanguage, BundledTheme } from 'shiki';
|
|
2
|
+
import { makeLogger } from '../log.js';
|
|
3
|
+
|
|
4
|
+
const log = makeLogger(__filename);
|
|
5
|
+
|
|
6
|
+
let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null =
|
|
7
|
+
null;
|
|
8
|
+
|
|
9
|
+
export async function initHighlighter(langs: string[]): Promise<void> {
|
|
10
|
+
if (highlighter) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
log.debug`Initializing syntax highlighter`;
|
|
14
|
+
const { createHighlighter } = await import('shiki');
|
|
15
|
+
highlighter = await createHighlighter({
|
|
16
|
+
themes: ['github-light', 'github-dark'],
|
|
17
|
+
langs,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getHighlighter(): HighlighterGeneric<
|
|
22
|
+
BundledLanguage,
|
|
23
|
+
BundledTheme
|
|
24
|
+
> {
|
|
25
|
+
if (!highlighter) {
|
|
26
|
+
throw new Error('Shiki highlighter not initialized');
|
|
27
|
+
}
|
|
28
|
+
return highlighter;
|
|
29
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import MarkdownIt from 'markdown-it';
|
|
3
|
+
import validateInternalLinks from './validate-internal-links-plugin.js';
|
|
4
|
+
|
|
5
|
+
function createMd(
|
|
6
|
+
validTargets: string[],
|
|
7
|
+
options: { sourceUrlPath?: string; codeExtensions?: string[] } = {},
|
|
8
|
+
) {
|
|
9
|
+
const md = new MarkdownIt({ html: true });
|
|
10
|
+
md.use(validateInternalLinks, {
|
|
11
|
+
enabled: true,
|
|
12
|
+
filePath: 'test.md',
|
|
13
|
+
sourceUrlPath: options.sourceUrlPath ?? '/test.html',
|
|
14
|
+
validTargets: new Set(validTargets),
|
|
15
|
+
codeExtensions: options.codeExtensions ?? [],
|
|
16
|
+
});
|
|
17
|
+
return md;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('validateInternalLinks', () => {
|
|
21
|
+
test('does nothing when disabled', () => {
|
|
22
|
+
const md = new MarkdownIt();
|
|
23
|
+
md.use(validateInternalLinks, { enabled: false });
|
|
24
|
+
expect(() => md.render('[link](/missing.html)')).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('throws when required options are missing', () => {
|
|
28
|
+
const md = new MarkdownIt();
|
|
29
|
+
expect(() => {
|
|
30
|
+
md.use(validateInternalLinks, { enabled: true });
|
|
31
|
+
md.render('test');
|
|
32
|
+
}).toThrow('requires filePath');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('allows valid internal links', () => {
|
|
36
|
+
const md = createMd(['/about.html']);
|
|
37
|
+
expect(() => md.render('[About](/about.html)')).not.toThrow();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('allows external links', () => {
|
|
41
|
+
const md = createMd([]);
|
|
42
|
+
expect(() => md.render('[Google](https://google.com)')).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('allows anchor links', () => {
|
|
46
|
+
const md = createMd([]);
|
|
47
|
+
expect(() => md.render('[Section](#top)')).not.toThrow();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('allows mailto links', () => {
|
|
51
|
+
const md = createMd([]);
|
|
52
|
+
expect(() => md.render('[Email](mailto:test@example.com)')).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('throws for broken internal link', () => {
|
|
56
|
+
const md = createMd(['/about.html']);
|
|
57
|
+
expect(() => md.render('[Missing](/missing.html)')).toThrow(
|
|
58
|
+
'broken internal link',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('strips query and hash before validating', () => {
|
|
63
|
+
const md = createMd(['/page.html']);
|
|
64
|
+
expect(() => md.render('[Page](/page.html?v=1#section)')).not.toThrow();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('resolves relative links from source path', () => {
|
|
68
|
+
const md = createMd(['/docs/guide.html'], {
|
|
69
|
+
sourceUrlPath: '/docs/index.html',
|
|
70
|
+
});
|
|
71
|
+
expect(() => md.render('[Guide](guide.html)')).not.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('throws for broken relative link', () => {
|
|
75
|
+
const md = createMd(['/docs/guide.html'], {
|
|
76
|
+
sourceUrlPath: '/docs/index.html',
|
|
77
|
+
});
|
|
78
|
+
expect(() => md.render('[Missing](missing.html)')).toThrow(
|
|
79
|
+
'broken internal link',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('rewrites code extensions to .html', () => {
|
|
84
|
+
const md = createMd(['/src/App.html'], { codeExtensions: ['java'] });
|
|
85
|
+
expect(() => md.render('[App](/src/App.java)')).not.toThrow();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('detects directory links that should reference index.html', () => {
|
|
89
|
+
const md = createMd(['/docs/index.html']);
|
|
90
|
+
// The plugin logs the "directory link" message and throws a generic
|
|
91
|
+
// "broken internal link(s)" error, so match the thrown message.
|
|
92
|
+
expect(() => md.render('[Docs](/docs)')).toThrow('broken internal link');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('validates links in raw HTML blocks', () => {
|
|
96
|
+
const md = createMd(['/about.html']);
|
|
97
|
+
expect(() => md.render('<a href="/missing.html">link</a>')).toThrow(
|
|
98
|
+
'broken internal link',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('allows protocol-relative URLs', () => {
|
|
103
|
+
const md = createMd([]);
|
|
104
|
+
expect(() => md.render('[CDN](//cdn.example.com/lib.js)')).not.toThrow();
|
|
105
|
+
});
|
|
106
|
+
});
|
package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts}
RENAMED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type MarkdownIt from 'markdown-it';
|
|
2
|
+
import type Token from 'markdown-it/lib/token.mjs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { makeLogger } from './log.js';
|
|
3
5
|
|
|
4
6
|
const log = makeLogger(__filename);
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
interface ValidateInternalLinksOptions {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
filePath?: string;
|
|
11
|
+
sourceUrlPath?: string;
|
|
12
|
+
validTargets?: Set<string>;
|
|
13
|
+
codeExtensions?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripQueryAndHash(href: string): string {
|
|
7
17
|
return href.split('#')[0].split('?')[0];
|
|
8
18
|
}
|
|
9
19
|
|
|
10
|
-
function isExternalOrAnchor(href) {
|
|
20
|
+
function isExternalOrAnchor(href: string): boolean {
|
|
11
21
|
if (!href || href.startsWith('#') || href.startsWith('//')) {
|
|
12
22
|
return true;
|
|
13
23
|
}
|
|
@@ -15,7 +25,7 @@ function isExternalOrAnchor(href) {
|
|
|
15
25
|
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href);
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
function normalizePathname(pathname) {
|
|
28
|
+
function normalizePathname(pathname: string): string {
|
|
19
29
|
const normalized = path.posix.normalize(pathname);
|
|
20
30
|
if (normalized === '.') {
|
|
21
31
|
return '/';
|
|
@@ -23,7 +33,7 @@ function normalizePathname(pathname) {
|
|
|
23
33
|
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
function createCodeExtPattern(codeExtensions) {
|
|
36
|
+
function createCodeExtPattern(codeExtensions: string[]): RegExp | null {
|
|
27
37
|
const escaped = codeExtensions.map(ext =>
|
|
28
38
|
ext.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
|
29
39
|
);
|
|
@@ -33,7 +43,10 @@ function createCodeExtPattern(codeExtensions) {
|
|
|
33
43
|
return new RegExp(`\\.(${escaped.join('|')})$`, 'i');
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
function rewriteCodeLink(
|
|
46
|
+
function rewriteCodeLink(
|
|
47
|
+
pathname: string,
|
|
48
|
+
codeExtPattern: RegExp | null,
|
|
49
|
+
): string {
|
|
37
50
|
if (!codeExtPattern) {
|
|
38
51
|
return pathname;
|
|
39
52
|
}
|
|
@@ -41,7 +54,11 @@ function rewriteCodeLink(pathname, codeExtPattern) {
|
|
|
41
54
|
return pathname.replace(codeExtPattern, '.html');
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
function resolveLinkPath(
|
|
57
|
+
function resolveLinkPath(
|
|
58
|
+
sourceUrlPath: string,
|
|
59
|
+
rawHref: string,
|
|
60
|
+
codeExtPattern: RegExp | null,
|
|
61
|
+
): string | null {
|
|
45
62
|
const hrefPath = stripQueryAndHash(rawHref.trim());
|
|
46
63
|
if (!hrefPath) {
|
|
47
64
|
return null;
|
|
@@ -55,11 +72,14 @@ function resolveLinkPath(sourceUrlPath, rawHref, codeExtPattern) {
|
|
|
55
72
|
return rewriteCodeLink(resolved, codeExtPattern);
|
|
56
73
|
}
|
|
57
74
|
|
|
58
|
-
function getDirectoryIndexPath(pathname) {
|
|
75
|
+
function getDirectoryIndexPath(pathname: string): string {
|
|
59
76
|
return normalizePathname(path.posix.join(pathname, 'index.html'));
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
|
|
79
|
+
export default function validateInternalLinks(
|
|
80
|
+
md: MarkdownIt,
|
|
81
|
+
options: ValidateInternalLinksOptions = {},
|
|
82
|
+
): void {
|
|
63
83
|
const {
|
|
64
84
|
enabled = true,
|
|
65
85
|
filePath,
|
|
@@ -79,9 +99,9 @@ module.exports = function validateInternalLinks(md, options = {}) {
|
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
const codeExtPattern = createCodeExtPattern(codeExtensions);
|
|
82
|
-
const seenErrors = new Set();
|
|
102
|
+
const seenErrors = new Set<string>();
|
|
83
103
|
|
|
84
|
-
function reportBrokenLink(rawHref, resolvedPath) {
|
|
104
|
+
function reportBrokenLink(rawHref: string, resolvedPath: string): void {
|
|
85
105
|
const key = `${rawHref}|${resolvedPath}`;
|
|
86
106
|
if (seenErrors.has(key)) {
|
|
87
107
|
return;
|
|
@@ -91,7 +111,11 @@ module.exports = function validateInternalLinks(md, options = {}) {
|
|
|
91
111
|
log.error`${filePath}: broken internal link: "${rawHref}" (resolved to "${resolvedPath}")`;
|
|
92
112
|
}
|
|
93
113
|
|
|
94
|
-
function reportDirectoryLink(
|
|
114
|
+
function reportDirectoryLink(
|
|
115
|
+
rawHref: string,
|
|
116
|
+
resolvedPath: string,
|
|
117
|
+
indexPath: string,
|
|
118
|
+
): void {
|
|
95
119
|
const key = `${rawHref}|${resolvedPath}|directory`;
|
|
96
120
|
if (seenErrors.has(key)) {
|
|
97
121
|
return;
|
|
@@ -101,7 +125,7 @@ module.exports = function validateInternalLinks(md, options = {}) {
|
|
|
101
125
|
log.error`${filePath}: directory link must reference index.html explicitly: "${rawHref}" (resolved to "${resolvedPath}", expected "${indexPath}")`;
|
|
102
126
|
}
|
|
103
127
|
|
|
104
|
-
function validateHref(rawHref) {
|
|
128
|
+
function validateHref(rawHref: string): void {
|
|
105
129
|
if (!rawHref) {
|
|
106
130
|
return;
|
|
107
131
|
}
|
|
@@ -111,7 +135,7 @@ module.exports = function validateInternalLinks(md, options = {}) {
|
|
|
111
135
|
return;
|
|
112
136
|
}
|
|
113
137
|
|
|
114
|
-
const resolvedPath = resolveLinkPath(sourceUrlPath
|
|
138
|
+
const resolvedPath = resolveLinkPath(sourceUrlPath!, href, codeExtPattern);
|
|
115
139
|
if (!resolvedPath) {
|
|
116
140
|
return;
|
|
117
141
|
}
|
|
@@ -119,20 +143,23 @@ module.exports = function validateInternalLinks(md, options = {}) {
|
|
|
119
143
|
const directoryIndexPath = getDirectoryIndexPath(resolvedPath);
|
|
120
144
|
if (
|
|
121
145
|
directoryIndexPath !== resolvedPath &&
|
|
122
|
-
validTargets
|
|
146
|
+
validTargets!.has(directoryIndexPath)
|
|
123
147
|
) {
|
|
124
148
|
reportDirectoryLink(rawHref, resolvedPath, directoryIndexPath);
|
|
125
149
|
return;
|
|
126
150
|
}
|
|
127
151
|
|
|
128
|
-
if (!validTargets
|
|
152
|
+
if (!validTargets!.has(resolvedPath)) {
|
|
129
153
|
reportBrokenLink(rawHref, resolvedPath);
|
|
130
154
|
}
|
|
131
155
|
}
|
|
132
156
|
|
|
133
|
-
function validateToken(token) {
|
|
157
|
+
function validateToken(token: Token): void {
|
|
134
158
|
if (token.type === 'link_open') {
|
|
135
|
-
|
|
159
|
+
const href = token.attrGet('href');
|
|
160
|
+
if (href) {
|
|
161
|
+
validateHref(href);
|
|
162
|
+
}
|
|
136
163
|
} else if (token.type === 'html_block' || token.type === 'html_inline') {
|
|
137
164
|
token.content.replace(/<a\b[^>]*\bhref\s*=\s*"([^"]+)"/gi, (_, href) => {
|
|
138
165
|
validateHref(href);
|
|
@@ -152,4 +179,4 @@ module.exports = function validateInternalLinks(md, options = {}) {
|
|
|
152
179
|
);
|
|
153
180
|
}
|
|
154
181
|
});
|
|
155
|
-
}
|
|
182
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { collectReachableSiteAssets } from './reachability.js';
|
|
3
|
+
import WatchReachabilityState from './watch-reachability-state.js';
|
|
4
4
|
|
|
5
|
-
function createReader(htmlByPath) {
|
|
6
|
-
const calls = [];
|
|
5
|
+
function createReader(htmlByPath: Map<string, string>) {
|
|
6
|
+
const calls: string[] = [];
|
|
7
7
|
|
|
8
8
|
return {
|
|
9
9
|
calls,
|
|
10
|
-
read(assetPath) {
|
|
10
|
+
read(assetPath: string): string {
|
|
11
11
|
calls.push(assetPath);
|
|
12
12
|
if (!htmlByPath.has(assetPath)) {
|
|
13
13
|
throw new Error(`Missing HTML asset: ${assetPath}`);
|
|
14
14
|
}
|
|
15
|
-
return htmlByPath.get(assetPath)
|
|
15
|
+
return htmlByPath.get(assetPath)!;
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function createState(htmlEntries) {
|
|
20
|
+
function createState(htmlEntries: Record<string, string>) {
|
|
21
21
|
const htmlByPath = new Map(Object.entries(htmlEntries));
|
|
22
22
|
const reader = createReader(htmlByPath);
|
|
23
23
|
const state = new WatchReachabilityState();
|
|
@@ -1,17 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
import { collectDirectSiteAssetLinks } from './reachability.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type ReadHtmlForAsset = (assetPath: string) => string;
|
|
4
|
+
|
|
5
|
+
function cloneSet(values: Iterable<string> | undefined): Set<string> {
|
|
4
6
|
return new Set(values || []);
|
|
5
7
|
}
|
|
6
8
|
|
|
9
|
+
interface IncrementalOptions {
|
|
10
|
+
changedAssetPaths?: Iterable<string>;
|
|
11
|
+
removedAssetPaths?: Iterable<string>;
|
|
12
|
+
readHtmlForAsset: ReadHtmlForAsset;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
class WatchReachabilityState {
|
|
8
|
-
|
|
16
|
+
private rootPath: string;
|
|
17
|
+
private basePath: string;
|
|
18
|
+
private knownAssets: Set<string>;
|
|
19
|
+
private outgoingBySource: Map<string, Set<string>>;
|
|
20
|
+
private incomingByTarget: Map<string, Set<string>>;
|
|
21
|
+
private reachable: Set<string>;
|
|
22
|
+
private initialized: boolean;
|
|
23
|
+
|
|
24
|
+
constructor({
|
|
25
|
+
rootPath = 'index.html',
|
|
26
|
+
basePath = '/',
|
|
27
|
+
}: { rootPath?: string; basePath?: string } = {}) {
|
|
9
28
|
this.rootPath = rootPath;
|
|
10
29
|
this.basePath = basePath;
|
|
11
|
-
this.
|
|
30
|
+
this.knownAssets = new Set();
|
|
31
|
+
this.outgoingBySource = new Map();
|
|
32
|
+
this.incomingByTarget = new Map();
|
|
33
|
+
this.reachable = new Set();
|
|
34
|
+
this.initialized = false;
|
|
12
35
|
}
|
|
13
36
|
|
|
14
|
-
reset() {
|
|
37
|
+
reset(): void {
|
|
15
38
|
this.knownAssets = new Set();
|
|
16
39
|
this.outgoingBySource = new Map();
|
|
17
40
|
this.incomingByTarget = new Map();
|
|
@@ -19,13 +42,13 @@ class WatchReachabilityState {
|
|
|
19
42
|
this.initialized = false;
|
|
20
43
|
}
|
|
21
44
|
|
|
22
|
-
setKnownAssets(assetPaths) {
|
|
45
|
+
setKnownAssets(assetPaths: Iterable<string>): void {
|
|
23
46
|
this.knownAssets = cloneSet(assetPaths);
|
|
24
47
|
this.reachable = new Set(
|
|
25
48
|
[...this.reachable].filter(assetPath => this.knownAssets.has(assetPath)),
|
|
26
49
|
);
|
|
27
50
|
|
|
28
|
-
const nextOutgoingBySource = new Map();
|
|
51
|
+
const nextOutgoingBySource = new Map<string, Set<string>>();
|
|
29
52
|
for (const [sourcePath, targets] of this.outgoingBySource) {
|
|
30
53
|
if (!this.knownAssets.has(sourcePath)) {
|
|
31
54
|
continue;
|
|
@@ -43,7 +66,7 @@ class WatchReachabilityState {
|
|
|
43
66
|
this.rebuildIncomingByTarget();
|
|
44
67
|
}
|
|
45
68
|
|
|
46
|
-
rebuild(readHtmlForAsset) {
|
|
69
|
+
rebuild(readHtmlForAsset: ReadHtmlForAsset): void {
|
|
47
70
|
if (!this.knownAssets.has(this.rootPath)) {
|
|
48
71
|
throw new Error(`Pagefind reachability root not found: ${this.rootPath}`);
|
|
49
72
|
}
|
|
@@ -66,20 +89,24 @@ class WatchReachabilityState {
|
|
|
66
89
|
this.initialized = true;
|
|
67
90
|
}
|
|
68
91
|
|
|
69
|
-
applyIncremental({
|
|
92
|
+
applyIncremental({
|
|
93
|
+
changedAssetPaths,
|
|
94
|
+
removedAssetPaths,
|
|
95
|
+
readHtmlForAsset,
|
|
96
|
+
}: IncrementalOptions): void {
|
|
70
97
|
if (!this.initialized) {
|
|
71
98
|
this.rebuild(readHtmlForAsset);
|
|
72
99
|
return;
|
|
73
100
|
}
|
|
74
101
|
|
|
75
|
-
const normalizedChanged = new Set();
|
|
102
|
+
const normalizedChanged = new Set<string>();
|
|
76
103
|
for (const assetPath of changedAssetPaths || []) {
|
|
77
104
|
if (this.knownAssets.has(assetPath)) {
|
|
78
105
|
normalizedChanged.add(assetPath);
|
|
79
106
|
}
|
|
80
107
|
}
|
|
81
108
|
|
|
82
|
-
const normalizedRemoved = new Set();
|
|
109
|
+
const normalizedRemoved = new Set<string>();
|
|
83
110
|
for (const assetPath of removedAssetPaths || []) {
|
|
84
111
|
if (
|
|
85
112
|
!this.knownAssets.has(assetPath) &&
|
|
@@ -89,7 +116,7 @@ class WatchReachabilityState {
|
|
|
89
116
|
}
|
|
90
117
|
}
|
|
91
118
|
|
|
92
|
-
const previouslyReachableRoots = new Set();
|
|
119
|
+
const previouslyReachableRoots = new Set<string>();
|
|
93
120
|
for (const assetPath of normalizedChanged) {
|
|
94
121
|
if (this.reachable.has(assetPath)) {
|
|
95
122
|
previouslyReachableRoots.add(assetPath);
|
|
@@ -130,7 +157,7 @@ class WatchReachabilityState {
|
|
|
130
157
|
this.reachable.delete(assetPath);
|
|
131
158
|
}
|
|
132
159
|
|
|
133
|
-
const seedPaths = new Set();
|
|
160
|
+
const seedPaths = new Set<string>();
|
|
134
161
|
for (const assetPath of stillKnownRoots) {
|
|
135
162
|
if (affected.has(assetPath)) {
|
|
136
163
|
seedPaths.add(assetPath);
|
|
@@ -163,11 +190,14 @@ class WatchReachabilityState {
|
|
|
163
190
|
this.initialized = true;
|
|
164
191
|
}
|
|
165
192
|
|
|
166
|
-
getReachablePaths() {
|
|
193
|
+
getReachablePaths(): string[] {
|
|
167
194
|
return [...this.reachable].sort();
|
|
168
195
|
}
|
|
169
196
|
|
|
170
|
-
readOutgoingPaths(
|
|
197
|
+
private readOutgoingPaths(
|
|
198
|
+
sourcePath: string,
|
|
199
|
+
readHtmlForAsset: ReadHtmlForAsset,
|
|
200
|
+
): Set<string> {
|
|
171
201
|
if (!this.knownAssets.has(sourcePath)) {
|
|
172
202
|
return new Set();
|
|
173
203
|
}
|
|
@@ -182,7 +212,10 @@ class WatchReachabilityState {
|
|
|
182
212
|
return new Set(htmlAssetPaths);
|
|
183
213
|
}
|
|
184
214
|
|
|
185
|
-
addIncomingEdges(
|
|
215
|
+
private addIncomingEdges(
|
|
216
|
+
sourcePath: string,
|
|
217
|
+
outgoingPaths: Set<string>,
|
|
218
|
+
): void {
|
|
186
219
|
for (const targetPath of outgoingPaths) {
|
|
187
220
|
let incomingPaths = this.incomingByTarget.get(targetPath);
|
|
188
221
|
if (!incomingPaths) {
|
|
@@ -193,14 +226,14 @@ class WatchReachabilityState {
|
|
|
193
226
|
}
|
|
194
227
|
}
|
|
195
228
|
|
|
196
|
-
rebuildIncomingByTarget() {
|
|
229
|
+
private rebuildIncomingByTarget(): void {
|
|
197
230
|
this.incomingByTarget = new Map();
|
|
198
231
|
for (const [sourcePath, outgoingPaths] of this.outgoingBySource) {
|
|
199
232
|
this.addIncomingEdges(sourcePath, outgoingPaths);
|
|
200
233
|
}
|
|
201
234
|
}
|
|
202
235
|
|
|
203
|
-
removeOutgoingEdges(sourcePath) {
|
|
236
|
+
private removeOutgoingEdges(sourcePath: string): void {
|
|
204
237
|
const outgoingPaths = this.outgoingBySource.get(sourcePath);
|
|
205
238
|
if (!outgoingPaths) {
|
|
206
239
|
return;
|
|
@@ -219,7 +252,10 @@ class WatchReachabilityState {
|
|
|
219
252
|
}
|
|
220
253
|
}
|
|
221
254
|
|
|
222
|
-
replaceOutgoing(
|
|
255
|
+
private replaceOutgoing(
|
|
256
|
+
sourcePath: string,
|
|
257
|
+
outgoingPaths: Set<string>,
|
|
258
|
+
): void {
|
|
223
259
|
this.removeOutgoingEdges(sourcePath);
|
|
224
260
|
if (!this.knownAssets.has(sourcePath)) {
|
|
225
261
|
this.outgoingBySource.delete(sourcePath);
|
|
@@ -230,19 +266,22 @@ class WatchReachabilityState {
|
|
|
230
266
|
this.addIncomingEdges(sourcePath, outgoingPaths);
|
|
231
267
|
}
|
|
232
268
|
|
|
233
|
-
removeAsset(assetPath) {
|
|
269
|
+
private removeAsset(assetPath: string): void {
|
|
234
270
|
this.removeOutgoingEdges(assetPath);
|
|
235
271
|
this.outgoingBySource.delete(assetPath);
|
|
236
272
|
this.incomingByTarget.delete(assetPath);
|
|
237
273
|
this.reachable.delete(assetPath);
|
|
238
274
|
}
|
|
239
275
|
|
|
240
|
-
collectClosure(
|
|
276
|
+
private collectClosure(
|
|
277
|
+
rootPaths: Set<string>,
|
|
278
|
+
allowedPaths: Set<string> | null = null,
|
|
279
|
+
): Set<string> {
|
|
241
280
|
const pending = [...rootPaths];
|
|
242
|
-
const visited = new Set();
|
|
281
|
+
const visited = new Set<string>();
|
|
243
282
|
|
|
244
283
|
while (pending.length > 0) {
|
|
245
|
-
const currentPath = pending.pop()
|
|
284
|
+
const currentPath = pending.pop()!;
|
|
246
285
|
if (visited.has(currentPath)) {
|
|
247
286
|
continue;
|
|
248
287
|
}
|
|
@@ -270,4 +309,4 @@ class WatchReachabilityState {
|
|
|
270
309
|
}
|
|
271
310
|
}
|
|
272
311
|
|
|
273
|
-
|
|
312
|
+
export default WatchReachabilityState;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
declare const __WEBSOCKET_PORT__: number;
|
|
2
|
+
|
|
1
3
|
(function () {
|
|
2
4
|
const style = document.createElement('style');
|
|
3
5
|
style.textContent = [
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
].join('\n');
|
|
25
27
|
document.head.appendChild(style);
|
|
26
28
|
|
|
27
|
-
const ws = new WebSocket(
|
|
29
|
+
const ws = new WebSocket(`ws://localhost:${__WEBSOCKET_PORT__}`);
|
|
28
30
|
|
|
29
31
|
ws.onopen = () => {
|
|
30
32
|
console.log('[watch-reload] connected to watcher');
|