@abreen/tada 1.0.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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/bin/tada.js +361 -0
  4. package/config/authors.json +1 -0
  5. package/config/nav.json +28 -0
  6. package/content/index.md +19 -0
  7. package/content/lectures/01/Pair.java.md +296 -0
  8. package/content/lectures/01/Rectangle.java +80 -0
  9. package/content/lectures/01/demo.py +9 -0
  10. package/content/lectures/01/index.md +39 -0
  11. package/content/lectures/01/lecture1.pdf +0 -0
  12. package/content/lectures/index.md +25 -0
  13. package/content/markdown.md +379 -0
  14. package/content/problem_sets/index.md +6 -0
  15. package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
  16. package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
  17. package/fonts/google-sans-code/LICENSE.txt +93 -0
  18. package/fonts/inter/InterVariable-Italic.ttf +0 -0
  19. package/fonts/inter/InterVariable.ttf +0 -0
  20. package/fonts/inter/LICENSE.txt +92 -0
  21. package/package.json +70 -0
  22. package/public/avatars/alex.jpg +0 -0
  23. package/public/test.txt +1 -0
  24. package/src/_mixins.scss +4 -0
  25. package/src/anchor/README.md +6 -0
  26. package/src/anchor/index.ts +34 -0
  27. package/src/anchor/style.scss +48 -0
  28. package/src/code/README.md +5 -0
  29. package/src/code/index.ts +113 -0
  30. package/src/code/style.scss +101 -0
  31. package/src/code.scss +54 -0
  32. package/src/header/README.md +8 -0
  33. package/src/header/index.ts +43 -0
  34. package/src/header/style.scss +228 -0
  35. package/src/index.ts +73 -0
  36. package/src/layout.scss +144 -0
  37. package/src/literate/style.scss +60 -0
  38. package/src/print/README.md +4 -0
  39. package/src/print/index.ts +32 -0
  40. package/src/print/style.scss +82 -0
  41. package/src/question/README.md +3 -0
  42. package/src/question/index.ts +25 -0
  43. package/src/question/style.scss +116 -0
  44. package/src/search/README.md +6 -0
  45. package/src/search/index.ts +574 -0
  46. package/src/search/style.scss +217 -0
  47. package/src/style.scss +815 -0
  48. package/src/timezone/index.test.ts +100 -0
  49. package/src/timezone/index.ts +298 -0
  50. package/src/timezone/style.scss +16 -0
  51. package/src/timezone/timezones.json +58 -0
  52. package/src/toc/README.md +3 -0
  53. package/src/toc/index.ts +322 -0
  54. package/src/toc/style.scss +203 -0
  55. package/src/top/README.md +4 -0
  56. package/src/top/index.ts +75 -0
  57. package/src/util.ts +122 -0
  58. package/templates/_author.html +27 -0
  59. package/templates/_bottom.html +3 -0
  60. package/templates/_download.html +1 -0
  61. package/templates/_heading.html +19 -0
  62. package/templates/_nav.html +18 -0
  63. package/templates/_theme.scss +97 -0
  64. package/templates/_top.html +87 -0
  65. package/templates/authors.schema.json +13 -0
  66. package/templates/code.html +31 -0
  67. package/templates/default.html +13 -0
  68. package/templates/literate.html +16 -0
  69. package/templates/nav.schema.json +27 -0
  70. package/tsconfig.json +15 -0
  71. package/types/dev.ts +3 -0
  72. package/types/sass.d.ts +1 -0
  73. package/types/site-variables.d.ts +16 -0
  74. package/webpack/apply-base-path-plugin.js +78 -0
  75. package/webpack/build-state.js +97 -0
  76. package/webpack/code.test.js +162 -0
  77. package/webpack/colors.js +15 -0
  78. package/webpack/config.base.js +147 -0
  79. package/webpack/config.dev.js +23 -0
  80. package/webpack/config.prod.js +32 -0
  81. package/webpack/content-watch-plugin.js +153 -0
  82. package/webpack/deflist-id-plugin.js +62 -0
  83. package/webpack/external-links-plugin.js +37 -0
  84. package/webpack/features.js +5 -0
  85. package/webpack/flair.json +1 -0
  86. package/webpack/generate-content-assets-plugin.js +308 -0
  87. package/webpack/generate-favicon-plugin.js +198 -0
  88. package/webpack/generate-fonts-plugin.js +69 -0
  89. package/webpack/generate-manifest-plugin.js +116 -0
  90. package/webpack/globals.js +74 -0
  91. package/webpack/heading-subtitle-plugin.js +80 -0
  92. package/webpack/json-schema.js +19 -0
  93. package/webpack/log.js +143 -0
  94. package/webpack/markdown-plugins.test.js +203 -0
  95. package/webpack/pagefind-plugin.js +379 -0
  96. package/webpack/pagefind-plugin.test.js +131 -0
  97. package/webpack/pdf-text.js +163 -0
  98. package/webpack/print-flair-plugin.js +22 -0
  99. package/webpack/reachability.js +273 -0
  100. package/webpack/reachability.test.js +80 -0
  101. package/webpack/serve.js +104 -0
  102. package/webpack/site-variables.js +53 -0
  103. package/webpack/site.schema.json +67 -0
  104. package/webpack/templates.js +128 -0
  105. package/webpack/text-to-id.js +8 -0
  106. package/webpack/toc-plugin.js +167 -0
  107. package/webpack/util.js +49 -0
  108. package/webpack/utils/code.js +439 -0
  109. package/webpack/utils/content-files.js +147 -0
  110. package/webpack/utils/define-plugin.js +20 -0
  111. package/webpack/utils/file-types.js +26 -0
  112. package/webpack/utils/front-matter.js +57 -0
  113. package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
  114. package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
  115. package/webpack/utils/literate-java.js +153 -0
  116. package/webpack/utils/markdown.js +244 -0
  117. package/webpack/utils/parse-hsl.js +8 -0
  118. package/webpack/utils/paths.js +58 -0
  119. package/webpack/utils/render.js +466 -0
  120. package/webpack/utils/shiki-highlighter.js +26 -0
  121. package/webpack/validate-internal-links-plugin.js +155 -0
  122. package/webpack/watch-reachability-state.js +273 -0
  123. package/webpack/watch-reachability-state.test.js +198 -0
  124. package/webpack/watch-reload-client.js +54 -0
  125. package/webpack/watch.js +166 -0
@@ -0,0 +1,273 @@
1
+ const { collectDirectSiteAssetLinks } = require('./reachability');
2
+
3
+ function cloneSet(values) {
4
+ return new Set(values || []);
5
+ }
6
+
7
+ class WatchReachabilityState {
8
+ constructor({ rootPath = 'index.html', basePath = '/' } = {}) {
9
+ this.rootPath = rootPath;
10
+ this.basePath = basePath;
11
+ this.reset();
12
+ }
13
+
14
+ reset() {
15
+ this.knownAssets = new Set();
16
+ this.outgoingBySource = new Map();
17
+ this.incomingByTarget = new Map();
18
+ this.reachable = new Set();
19
+ this.initialized = false;
20
+ }
21
+
22
+ setKnownAssets(assetPaths) {
23
+ this.knownAssets = cloneSet(assetPaths);
24
+ this.reachable = new Set(
25
+ [...this.reachable].filter(assetPath => this.knownAssets.has(assetPath)),
26
+ );
27
+
28
+ const nextOutgoingBySource = new Map();
29
+ for (const [sourcePath, targets] of this.outgoingBySource) {
30
+ if (!this.knownAssets.has(sourcePath)) {
31
+ continue;
32
+ }
33
+
34
+ nextOutgoingBySource.set(
35
+ sourcePath,
36
+ new Set(
37
+ [...targets].filter(targetPath => this.knownAssets.has(targetPath)),
38
+ ),
39
+ );
40
+ }
41
+
42
+ this.outgoingBySource = nextOutgoingBySource;
43
+ this.rebuildIncomingByTarget();
44
+ }
45
+
46
+ rebuild(readHtmlForAsset) {
47
+ if (!this.knownAssets.has(this.rootPath)) {
48
+ throw new Error(`Pagefind reachability root not found: ${this.rootPath}`);
49
+ }
50
+
51
+ this.outgoingBySource = new Map();
52
+ this.incomingByTarget = new Map();
53
+ this.reachable = new Set();
54
+
55
+ const knownAssetPaths = [...this.knownAssets].sort();
56
+ for (const sourcePath of knownAssetPaths) {
57
+ const outgoingPaths = this.readOutgoingPaths(
58
+ sourcePath,
59
+ readHtmlForAsset,
60
+ );
61
+ this.outgoingBySource.set(sourcePath, outgoingPaths);
62
+ this.addIncomingEdges(sourcePath, outgoingPaths);
63
+ }
64
+
65
+ this.reachable = this.collectClosure(new Set([this.rootPath]));
66
+ this.initialized = true;
67
+ }
68
+
69
+ applyIncremental({ changedAssetPaths, removedAssetPaths, readHtmlForAsset }) {
70
+ if (!this.initialized) {
71
+ this.rebuild(readHtmlForAsset);
72
+ return;
73
+ }
74
+
75
+ const normalizedChanged = new Set();
76
+ for (const assetPath of changedAssetPaths || []) {
77
+ if (this.knownAssets.has(assetPath)) {
78
+ normalizedChanged.add(assetPath);
79
+ }
80
+ }
81
+
82
+ const normalizedRemoved = new Set();
83
+ for (const assetPath of removedAssetPaths || []) {
84
+ if (
85
+ !this.knownAssets.has(assetPath) &&
86
+ !normalizedChanged.has(assetPath)
87
+ ) {
88
+ normalizedRemoved.add(assetPath);
89
+ }
90
+ }
91
+
92
+ const previouslyReachableRoots = new Set();
93
+ for (const assetPath of normalizedChanged) {
94
+ if (this.reachable.has(assetPath)) {
95
+ previouslyReachableRoots.add(assetPath);
96
+ }
97
+ }
98
+ for (const assetPath of normalizedRemoved) {
99
+ if (this.reachable.has(assetPath)) {
100
+ previouslyReachableRoots.add(assetPath);
101
+ }
102
+ }
103
+
104
+ const oldAffectedClosure = this.collectClosure(previouslyReachableRoots);
105
+
106
+ for (const assetPath of normalizedRemoved) {
107
+ this.removeAsset(assetPath);
108
+ }
109
+
110
+ const changedAssetList = [...normalizedChanged].sort();
111
+ for (const assetPath of changedAssetList) {
112
+ const outgoingPaths = this.readOutgoingPaths(assetPath, readHtmlForAsset);
113
+ this.replaceOutgoing(assetPath, outgoingPaths);
114
+ }
115
+
116
+ const stillKnownRoots = new Set(
117
+ [...previouslyReachableRoots].filter(assetPath =>
118
+ this.knownAssets.has(assetPath),
119
+ ),
120
+ );
121
+ const newAffectedClosure = this.collectClosure(stillKnownRoots);
122
+ const affected = new Set([...oldAffectedClosure, ...newAffectedClosure]);
123
+
124
+ if (affected.size === 0) {
125
+ this.initialized = true;
126
+ return;
127
+ }
128
+
129
+ for (const assetPath of affected) {
130
+ this.reachable.delete(assetPath);
131
+ }
132
+
133
+ const seedPaths = new Set();
134
+ for (const assetPath of stillKnownRoots) {
135
+ if (affected.has(assetPath)) {
136
+ seedPaths.add(assetPath);
137
+ }
138
+ }
139
+
140
+ if (this.knownAssets.has(this.rootPath) && affected.has(this.rootPath)) {
141
+ seedPaths.add(this.rootPath);
142
+ }
143
+
144
+ for (const assetPath of affected) {
145
+ const incomingPaths = this.incomingByTarget.get(assetPath);
146
+ if (!incomingPaths) {
147
+ continue;
148
+ }
149
+
150
+ for (const sourcePath of incomingPaths) {
151
+ if (this.reachable.has(sourcePath) && !affected.has(sourcePath)) {
152
+ seedPaths.add(assetPath);
153
+ break;
154
+ }
155
+ }
156
+ }
157
+
158
+ const restoredReachable = this.collectClosure(seedPaths, affected);
159
+ for (const assetPath of restoredReachable) {
160
+ this.reachable.add(assetPath);
161
+ }
162
+
163
+ this.initialized = true;
164
+ }
165
+
166
+ getReachablePaths() {
167
+ return [...this.reachable].sort();
168
+ }
169
+
170
+ readOutgoingPaths(sourcePath, readHtmlForAsset) {
171
+ if (!this.knownAssets.has(sourcePath)) {
172
+ return new Set();
173
+ }
174
+
175
+ const html = readHtmlForAsset(sourcePath);
176
+ const { htmlAssetPaths } = collectDirectSiteAssetLinks({
177
+ html,
178
+ fromAssetPath: sourcePath,
179
+ knownAssets: this.knownAssets,
180
+ basePath: this.basePath,
181
+ });
182
+ return new Set(htmlAssetPaths);
183
+ }
184
+
185
+ addIncomingEdges(sourcePath, outgoingPaths) {
186
+ for (const targetPath of outgoingPaths) {
187
+ let incomingPaths = this.incomingByTarget.get(targetPath);
188
+ if (!incomingPaths) {
189
+ incomingPaths = new Set();
190
+ this.incomingByTarget.set(targetPath, incomingPaths);
191
+ }
192
+ incomingPaths.add(sourcePath);
193
+ }
194
+ }
195
+
196
+ rebuildIncomingByTarget() {
197
+ this.incomingByTarget = new Map();
198
+ for (const [sourcePath, outgoingPaths] of this.outgoingBySource) {
199
+ this.addIncomingEdges(sourcePath, outgoingPaths);
200
+ }
201
+ }
202
+
203
+ removeOutgoingEdges(sourcePath) {
204
+ const outgoingPaths = this.outgoingBySource.get(sourcePath);
205
+ if (!outgoingPaths) {
206
+ return;
207
+ }
208
+
209
+ for (const targetPath of outgoingPaths) {
210
+ const incomingPaths = this.incomingByTarget.get(targetPath);
211
+ if (!incomingPaths) {
212
+ continue;
213
+ }
214
+
215
+ incomingPaths.delete(sourcePath);
216
+ if (incomingPaths.size === 0) {
217
+ this.incomingByTarget.delete(targetPath);
218
+ }
219
+ }
220
+ }
221
+
222
+ replaceOutgoing(sourcePath, outgoingPaths) {
223
+ this.removeOutgoingEdges(sourcePath);
224
+ if (!this.knownAssets.has(sourcePath)) {
225
+ this.outgoingBySource.delete(sourcePath);
226
+ return;
227
+ }
228
+
229
+ this.outgoingBySource.set(sourcePath, outgoingPaths);
230
+ this.addIncomingEdges(sourcePath, outgoingPaths);
231
+ }
232
+
233
+ removeAsset(assetPath) {
234
+ this.removeOutgoingEdges(assetPath);
235
+ this.outgoingBySource.delete(assetPath);
236
+ this.incomingByTarget.delete(assetPath);
237
+ this.reachable.delete(assetPath);
238
+ }
239
+
240
+ collectClosure(rootPaths, allowedPaths = null) {
241
+ const pending = [...rootPaths];
242
+ const visited = new Set();
243
+
244
+ while (pending.length > 0) {
245
+ const currentPath = pending.pop();
246
+ if (visited.has(currentPath)) {
247
+ continue;
248
+ }
249
+ if (allowedPaths && !allowedPaths.has(currentPath)) {
250
+ continue;
251
+ }
252
+
253
+ visited.add(currentPath);
254
+ const outgoingPaths = this.outgoingBySource.get(currentPath);
255
+ if (!outgoingPaths) {
256
+ continue;
257
+ }
258
+
259
+ for (const targetPath of outgoingPaths) {
260
+ if (allowedPaths && !allowedPaths.has(targetPath)) {
261
+ continue;
262
+ }
263
+ if (!visited.has(targetPath)) {
264
+ pending.push(targetPath);
265
+ }
266
+ }
267
+ }
268
+
269
+ return visited;
270
+ }
271
+ }
272
+
273
+ module.exports = WatchReachabilityState;
@@ -0,0 +1,198 @@
1
+ const { describe, expect, test } = require('bun:test');
2
+ const { collectReachableSiteAssets } = require('./reachability');
3
+ const WatchReachabilityState = require('./watch-reachability-state');
4
+
5
+ function createReader(htmlByPath) {
6
+ const calls = [];
7
+
8
+ return {
9
+ calls,
10
+ read(assetPath) {
11
+ calls.push(assetPath);
12
+ if (!htmlByPath.has(assetPath)) {
13
+ throw new Error(`Missing HTML asset: ${assetPath}`);
14
+ }
15
+ return htmlByPath.get(assetPath);
16
+ },
17
+ };
18
+ }
19
+
20
+ function createState(htmlEntries) {
21
+ const htmlByPath = new Map(Object.entries(htmlEntries));
22
+ const reader = createReader(htmlByPath);
23
+ const state = new WatchReachabilityState();
24
+
25
+ state.setKnownAssets(new Set(htmlByPath.keys()));
26
+ state.rebuild(assetPath => reader.read(assetPath));
27
+
28
+ return { htmlByPath, reader, state };
29
+ }
30
+
31
+ describe('WatchReachabilityState', () => {
32
+ test('rebuild matches full traversal reachability', () => {
33
+ const htmlByPath = new Map([
34
+ ['index.html', '<a href="/about/">About</a>'],
35
+ ['about/index.html', '<a href="/deep/">Deep</a>'],
36
+ ['deep/index.html', '<p>Deep</p>'],
37
+ ['orphan/index.html', '<p>Orphan</p>'],
38
+ ]);
39
+ const reader = createReader(htmlByPath);
40
+ const state = new WatchReachabilityState();
41
+
42
+ state.setKnownAssets(new Set(htmlByPath.keys()));
43
+ state.rebuild(assetPath => reader.read(assetPath));
44
+
45
+ expect(state.getReachablePaths()).toEqual(
46
+ collectReachableSiteAssets({ htmlAssetsByPath: htmlByPath })
47
+ .reachableHtmlPaths,
48
+ );
49
+ });
50
+
51
+ test('incremental update adds a newly linked subtree', () => {
52
+ const { htmlByPath, reader, state } = createState({
53
+ 'index.html': '<a href="/about/">About</a>',
54
+ 'about/index.html': '<p>About</p>',
55
+ 'hidden/index.html': '<a href="/bonus/">Bonus</a>',
56
+ 'bonus/index.html': '<p>Bonus</p>',
57
+ });
58
+
59
+ reader.calls.length = 0;
60
+ htmlByPath.set(
61
+ 'about/index.html',
62
+ '<a href="/hidden/">Reveal hidden subtree</a>',
63
+ );
64
+ state.setKnownAssets(new Set(htmlByPath.keys()));
65
+ state.applyIncremental({
66
+ changedAssetPaths: new Set(['about/index.html']),
67
+ removedAssetPaths: new Set(),
68
+ readHtmlForAsset: assetPath => reader.read(assetPath),
69
+ });
70
+
71
+ expect(reader.calls).toEqual(['about/index.html']);
72
+ expect(state.getReachablePaths()).toEqual([
73
+ 'about/index.html',
74
+ 'bonus/index.html',
75
+ 'hidden/index.html',
76
+ 'index.html',
77
+ ]);
78
+ });
79
+
80
+ test('incremental update removes a subtree when its only parent stops linking to it', () => {
81
+ const { htmlByPath, reader, state } = createState({
82
+ 'index.html': '<a href="/about/">About</a>',
83
+ 'about/index.html': '<a href="/hidden/">Hidden</a>',
84
+ 'hidden/index.html': '<a href="/bonus/">Bonus</a>',
85
+ 'bonus/index.html': '<p>Bonus</p>',
86
+ });
87
+
88
+ reader.calls.length = 0;
89
+ htmlByPath.set('about/index.html', '<p>About</p>');
90
+ state.setKnownAssets(new Set(htmlByPath.keys()));
91
+ state.applyIncremental({
92
+ changedAssetPaths: new Set(['about/index.html']),
93
+ removedAssetPaths: new Set(),
94
+ readHtmlForAsset: assetPath => reader.read(assetPath),
95
+ });
96
+
97
+ expect(reader.calls).toEqual(['about/index.html']);
98
+ expect(state.getReachablePaths()).toEqual([
99
+ 'about/index.html',
100
+ 'index.html',
101
+ ]);
102
+ });
103
+
104
+ test('removing one of two parents keeps the child reachable', () => {
105
+ const { htmlByPath, reader, state } = createState({
106
+ 'index.html': '<a href="/about/">About</a><a href="/keep/">Keep</a>',
107
+ 'about/index.html': '<a href="/child/">Child</a>',
108
+ 'keep/index.html': '<a href="/child/">Child</a>',
109
+ 'child/index.html': '<p>Child</p>',
110
+ });
111
+
112
+ htmlByPath.delete('about/index.html');
113
+ state.setKnownAssets(new Set(htmlByPath.keys()));
114
+ state.applyIncremental({
115
+ changedAssetPaths: new Set(),
116
+ removedAssetPaths: new Set(['about/index.html']),
117
+ readHtmlForAsset: assetPath => reader.read(assetPath),
118
+ });
119
+
120
+ expect(state.getReachablePaths()).toEqual([
121
+ 'child/index.html',
122
+ 'index.html',
123
+ 'keep/index.html',
124
+ ]);
125
+ });
126
+
127
+ test('a supported cycle remains reachable and drops once external support is removed', () => {
128
+ const { htmlByPath, reader, state } = createState({
129
+ 'index.html': '<a href="/support/">Support</a>',
130
+ 'support/index.html': '<a href="/a/">A</a>',
131
+ 'a/index.html': '<a href="/b/">B</a>',
132
+ 'b/index.html': '<a href="/a/">A</a>',
133
+ });
134
+
135
+ expect(state.getReachablePaths()).toEqual([
136
+ 'a/index.html',
137
+ 'b/index.html',
138
+ 'index.html',
139
+ 'support/index.html',
140
+ ]);
141
+
142
+ reader.calls.length = 0;
143
+ htmlByPath.set('support/index.html', '<p>No links</p>');
144
+ state.setKnownAssets(new Set(htmlByPath.keys()));
145
+ state.applyIncremental({
146
+ changedAssetPaths: new Set(['support/index.html']),
147
+ removedAssetPaths: new Set(),
148
+ readHtmlForAsset: assetPath => reader.read(assetPath),
149
+ });
150
+
151
+ expect(reader.calls).toEqual(['support/index.html']);
152
+ expect(state.getReachablePaths()).toEqual([
153
+ 'index.html',
154
+ 'support/index.html',
155
+ ]);
156
+ });
157
+
158
+ test('changing an unreachable page updates its graph without reparsing the whole site', () => {
159
+ const { htmlByPath, reader, state } = createState({
160
+ 'index.html': '<a href="/about/">About</a>',
161
+ 'about/index.html': '<p>About</p>',
162
+ 'hidden/index.html': '<p>Hidden</p>',
163
+ 'bonus/index.html': '<p>Bonus</p>',
164
+ });
165
+
166
+ reader.calls.length = 0;
167
+ htmlByPath.set('hidden/index.html', '<a href="/bonus/">Bonus</a>');
168
+ state.setKnownAssets(new Set(htmlByPath.keys()));
169
+ state.applyIncremental({
170
+ changedAssetPaths: new Set(['hidden/index.html']),
171
+ removedAssetPaths: new Set(),
172
+ readHtmlForAsset: assetPath => reader.read(assetPath),
173
+ });
174
+
175
+ expect(reader.calls).toEqual(['hidden/index.html']);
176
+ expect(state.getReachablePaths()).toEqual([
177
+ 'about/index.html',
178
+ 'index.html',
179
+ ]);
180
+
181
+ reader.calls.length = 0;
182
+ htmlByPath.set('about/index.html', '<a href="/hidden/">Hidden</a>');
183
+ state.setKnownAssets(new Set(htmlByPath.keys()));
184
+ state.applyIncremental({
185
+ changedAssetPaths: new Set(['about/index.html']),
186
+ removedAssetPaths: new Set(),
187
+ readHtmlForAsset: assetPath => reader.read(assetPath),
188
+ });
189
+
190
+ expect(reader.calls).toEqual(['about/index.html']);
191
+ expect(state.getReachablePaths()).toEqual([
192
+ 'about/index.html',
193
+ 'bonus/index.html',
194
+ 'hidden/index.html',
195
+ 'index.html',
196
+ ]);
197
+ });
198
+ });
@@ -0,0 +1,54 @@
1
+ (function () {
2
+ const style = document.createElement('style');
3
+ style.textContent = [
4
+ '@keyframes tada-shimmer {',
5
+ ' 0%, 25% { background-position: 200% 0; }',
6
+ ' 100% { background-position: -200% 0; }',
7
+ '}',
8
+ 'header.tada-rebuilding {',
9
+ ' background-image: linear-gradient(',
10
+ ' 90deg,',
11
+ ' transparent 25%,',
12
+ ' var(--bg2-color) 50%,',
13
+ ' transparent 75%',
14
+ ' ) !important;',
15
+ ' background-color: var(--bg-color-translucent) !important;',
16
+ ' background-size: 200% 100% !important;',
17
+ ' background-repeat: no-repeat !important;',
18
+ ' animation: tada-shimmer 2s ease-in-out infinite !important;',
19
+ '}',
20
+ 'body.tada-rebuilding-cursor,',
21
+ 'body.tada-rebuilding-cursor * {',
22
+ ' cursor: wait !important;',
23
+ '}',
24
+ ].join('\n');
25
+ document.head.appendChild(style);
26
+
27
+ const ws = new WebSocket('ws://localhost:35729');
28
+
29
+ ws.onopen = () => {
30
+ console.log('[watch-reload] connected to watcher');
31
+ };
32
+
33
+ ws.onmessage = event => {
34
+ if (event.data === 'rebuilding') {
35
+ console.log('[watch-reload] Rebuilding...');
36
+ const header = document.querySelector('header');
37
+ if (header) {
38
+ header.classList.add('tada-rebuilding');
39
+ }
40
+ document.body.classList.add('tada-rebuilding-cursor');
41
+ } else if (event.data === 'reload') {
42
+ console.log('[watch-reload] Reloading page...');
43
+ window.location.reload();
44
+ }
45
+ };
46
+
47
+ ws.onclose = () => {
48
+ console.warn('[watch-reload] connection closed');
49
+ };
50
+
51
+ ws.onerror = err => {
52
+ console.error('[watch-reload] error:', err);
53
+ };
54
+ })();
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ const webpack = require('webpack');
3
+ const { fork } = require('child_process');
4
+ const path = require('path');
5
+ const WebSocket = require('ws');
6
+ const { B, G } = require('./colors');
7
+ const { makeLogger, getFlair } = require('./log');
8
+ const getConfig = require('./config.dev');
9
+ const ContentWatchPlugin = require('./content-watch-plugin');
10
+ const { getContentDir } = require('./util');
11
+
12
+ const WEBSOCKET_PORT = 35729;
13
+
14
+ const log = makeLogger(__filename);
15
+ const wslog = makeLogger('WebSocket');
16
+ const contentDir = getContentDir();
17
+
18
+ function broadcast(msg) {
19
+ if (wss == null || !webSocketsReady) {
20
+ return;
21
+ }
22
+ wslog.debug(`Broadcasting "${msg}" to WebSocket clients...`);
23
+ wss.clients.forEach(client => {
24
+ if (client.readyState === WebSocket.OPEN) {
25
+ client.send(msg);
26
+ }
27
+ });
28
+ }
29
+
30
+ function serve() {
31
+ const child = fork(path.join(__dirname, 'serve.js'), { stdio: 'inherit' });
32
+ child.on('close', code => {
33
+ webServerReady = false;
34
+ log.error`Web server exited with code ${code}`;
35
+ process.exit(2);
36
+ });
37
+ child.on('error', err => {
38
+ webServerReady = false;
39
+ log.error`Web server failed: ${err.message}`;
40
+ });
41
+ child.on('message', msg => {
42
+ if (msg.ready) {
43
+ webServerReady = true;
44
+ clearTimeout(webServerTimeout);
45
+ }
46
+ });
47
+
48
+ webServerTimeout = setTimeout(() => {
49
+ if (webServerReady) {
50
+ return;
51
+ }
52
+ log.error`Web server failed to report within 10 seconds, exiting`;
53
+ process.exit(3);
54
+ }, 10000);
55
+ }
56
+
57
+ function toContentMarkdownPath(filePath) {
58
+ if (!filePath) {
59
+ return null;
60
+ }
61
+ const ext = path.extname(filePath).toLowerCase();
62
+ if (!['.md', '.markdown'].includes(ext)) {
63
+ return null;
64
+ }
65
+
66
+ const normalizedContentDir = path.resolve(contentDir) + path.sep;
67
+ const normalizedFilePath = path.resolve(filePath);
68
+ if (!normalizedFilePath.startsWith(normalizedContentDir)) {
69
+ return null;
70
+ }
71
+
72
+ return path
73
+ .relative(contentDir, normalizedFilePath)
74
+ .split(path.sep)
75
+ .join(path.posix.sep);
76
+ }
77
+
78
+ function logChangedMarkdownFiles(files, { skip = new Set() } = {}) {
79
+ if (!files) {
80
+ return;
81
+ }
82
+
83
+ const markdownPaths = [...files]
84
+ .map(toContentMarkdownPath)
85
+ .filter(markdownPath => markdownPath && !skip.has(markdownPath))
86
+ .sort();
87
+
88
+ for (const markdownPath of markdownPaths) {
89
+ log.event`${B`${markdownPath}`} changed, rebuilding...`;
90
+ }
91
+ }
92
+
93
+ let webSocketsReady = false;
94
+ let webServerReady = false;
95
+ let webServerTimeout;
96
+ let serveStarted = false;
97
+ let currentWatcher = null;
98
+
99
+ let wss = null;
100
+ try {
101
+ wss = new WebSocket.Server({ port: WEBSOCKET_PORT });
102
+
103
+ wss.on('connection', conn => {
104
+ wslog.debug`WebSocket client connected`;
105
+ conn.on('close', () => {
106
+ wslog.debug`WebSocket client disconnected`;
107
+ });
108
+ });
109
+
110
+ wss.on('error', err => {
111
+ wslog.error`WebSocket server error: ${err.message}`;
112
+ });
113
+
114
+ wss.on('listening', () => {
115
+ wslog.debug`WebSocket server listening at ws://localhost:${WEBSOCKET_PORT}`;
116
+ webSocketsReady = true;
117
+ });
118
+ } catch (err) {
119
+ wslog.error`Failed to start WebSocket server on port ${WEBSOCKET_PORT}: ${err.message}`;
120
+ }
121
+
122
+ async function startWatching() {
123
+ const config = await getConfig({ watchMode: true });
124
+ const compiler = webpack(config);
125
+ const loggedInvalidationFiles = new Set();
126
+
127
+ compiler.hooks.invalid.tap('WatchChangedFileLog', fileName => {
128
+ broadcast('rebuilding');
129
+
130
+ const markdownPath = toContentMarkdownPath(fileName);
131
+ if (markdownPath) {
132
+ loggedInvalidationFiles.add(markdownPath);
133
+ log.event`${B`${markdownPath}`} changed, rebuilding...`;
134
+ }
135
+ });
136
+
137
+ currentWatcher = compiler.watch({ aggregateTimeout: 300 }, (err, stats) => {
138
+ logChangedMarkdownFiles(compiler.modifiedFiles, {
139
+ skip: loggedInvalidationFiles,
140
+ });
141
+ loggedInvalidationFiles.clear();
142
+
143
+ if (ContentWatchPlugin.needsRestart()) {
144
+ ContentWatchPlugin.clearRestart();
145
+ log.event`Content changed, restarting Webpack compiler...`;
146
+ currentWatcher.close(() => startWatching());
147
+ return;
148
+ }
149
+
150
+ if (err) {
151
+ log.error`Build failed: ${err.message}`;
152
+ } else if (stats.hasErrors()) {
153
+ process.stderr.write(stats.toString('errors-only') + '\n');
154
+ log.error`Build failed`;
155
+ } else {
156
+ log.event`${getFlair()} Webpack build completed ${G`successfully`}`;
157
+ broadcast('reload');
158
+ }
159
+ if (!serveStarted && !err && !stats.hasErrors()) {
160
+ serveStarted = true;
161
+ serve();
162
+ }
163
+ });
164
+ }
165
+
166
+ startWatching();