@abreen/tada 1.9.0 → 1.10.1
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 +24 -34
- package/bin/tada.ts +1 -1
- package/build/apply-base-path-plugin.ts +79 -17
- package/build/bundle.ts +9 -1
- package/build/content-watch.ts +3 -3
- package/build/copy.ts +30 -0
- package/build/generate-content-assets.ts +45 -59
- package/build/pagefind.ts +0 -7
- package/build/serve.ts +4 -0
- package/build/types.d.ts +19 -1
- package/build/util.ts +3 -0
- package/build/utils/content-files.ts +98 -0
- package/build/utils/include.ts +4 -0
- package/build/utils/python-runner/trace_runner.py +272 -0
- package/build/utils/render.ts +34 -8
- package/build/utils/trace-core.ts +203 -0
- package/build/utils/trace-java.ts +132 -0
- package/build/utils/trace-layout.ts +31 -3
- package/build/utils/trace-python.ts +26 -0
- package/build/utils/trace.ts +174 -294
- package/build/validate-internal-links-plugin.ts +16 -4
- package/build/watch/assets.ts +111 -0
- package/build/watch/build-full.ts +124 -0
- package/build/watch/build-incremental.ts +139 -0
- package/build/watch/compiler-types.d.ts +19 -0
- package/build/watch/compiler.ts +112 -0
- package/build/watch/index.ts +22 -0
- package/build/watch/mutations.ts +75 -0
- package/build/watch/planner.ts +230 -0
- package/build/watch/records.ts +190 -0
- package/build/watch/runtime.ts +143 -0
- package/build/watch/snapshot.ts +602 -0
- package/build/watch/validation.ts +36 -0
- package/build/watch-reload-client.ts +10 -5
- package/init/content/labs/01/index.md +6 -0
- package/init/content/labs/01/trace_demo.py +16 -0
- package/package.json +42 -23
- package/python/command.ts +95 -0
- package/python/module.ts +18 -0
- package/src/_alerts.scss +11 -8
- package/src/_base.scss +25 -29
- package/src/_layout.scss +17 -14
- package/src/_mixins.scss +10 -9
- package/src/anchor/style.scss +4 -2
- package/src/code/index.ts +9 -2
- package/src/code/style.scss +14 -13
- package/src/code.scss +14 -13
- package/src/header/_base.scss +21 -24
- package/src/header/style.scss +16 -12
- package/src/index.ts +3 -0
- package/src/literate/style.scss +4 -4
- package/src/navigate/index.ts +43 -279
- package/src/navigate/lifecycle.ts +1 -1
- package/src/navigate/runtime.ts +341 -0
- package/src/page-update/README.md +4 -0
- package/src/page-update/index.ts +280 -0
- package/src/page-update/style.scss +53 -0
- package/src/print/style.scss +4 -4
- package/src/question/style.scss +10 -6
- package/src/search/index.ts +9 -12
- package/src/search/style.scss +44 -42
- package/src/style.scss +63 -58
- package/src/timezone/style.scss +5 -5
- package/src/toc/style.scss +17 -16
- package/src/trace/style.scss +27 -27
- package/src/validators.ts +45 -0
- package/templates/_footer.html +1 -1
- package/templates/_theme.scss +8 -19
- package/watch/engine.ts +296 -0
- package/watch/events.ts +10 -0
- package/watch/fs-commit.ts +102 -0
- package/watch/types.d.ts +103 -0
- package/build/watch.ts +0 -493
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface ResponseValidators {
|
|
2
|
+
etag: string | null;
|
|
3
|
+
lastModified: string | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const EMPTY_RESPONSE_VALIDATORS: ResponseValidators = Object.freeze({
|
|
7
|
+
etag: null,
|
|
8
|
+
lastModified: null,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function getResponseValidators(source: {
|
|
12
|
+
headers: { get(name: string): string | null };
|
|
13
|
+
}): ResponseValidators {
|
|
14
|
+
return {
|
|
15
|
+
etag: source.headers.get('ETag'),
|
|
16
|
+
lastModified: source.headers.get('Last-Modified'),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function hasUsableResponseValidators(
|
|
21
|
+
validators: ResponseValidators,
|
|
22
|
+
): boolean {
|
|
23
|
+
return validators.etag !== null || validators.lastModified !== null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function hasResponseValidatorsChanged(
|
|
27
|
+
previous: ResponseValidators,
|
|
28
|
+
current: ResponseValidators,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (previous.etag !== null && current.etag !== null) {
|
|
31
|
+
return current.etag !== previous.etag;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (previous.lastModified !== null && current.lastModified !== null) {
|
|
35
|
+
return current.lastModified !== previous.lastModified;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getPreferredValidatorKey(
|
|
42
|
+
validators: ResponseValidators,
|
|
43
|
+
): string | null {
|
|
44
|
+
return validators.etag ?? validators.lastModified;
|
|
45
|
+
}
|
package/templates/_footer.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<% if (site.features.footer) { %>
|
|
2
2
|
<footer data-pagefind-ignore>
|
|
3
|
-
<p>
|
|
3
|
+
<p>Built using <a href="https://github.com/abreen/Tada" target="_blank">Tada</a> <%= tadaVersion %></p>
|
|
4
4
|
</footer>
|
|
5
5
|
<% } %>
|
package/templates/_theme.scss
CHANGED
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
// Background & foreground color tint
|
|
7
7
|
--tint-hue: <%= tintHue %>deg;
|
|
8
8
|
--tint-amount: <%= tintAmount / 100 %>;
|
|
9
|
-
|
|
10
9
|
--link-active-color: #000;
|
|
11
|
-
|
|
12
|
-
--
|
|
13
|
-
--note-bg-color: hsl(209 95% 75%);
|
|
10
|
+
--warning-bg-color: hsl(36deg 95% 70%);
|
|
11
|
+
--note-bg-color: hsl(209deg 95% 75%);
|
|
14
12
|
|
|
15
13
|
// Main foreground color (the color of most text)
|
|
16
14
|
--fg-color: hsl(var(--tint-hue) calc(8% * var(--tint-amount)) 8%);
|
|
@@ -20,7 +18,6 @@
|
|
|
20
18
|
|
|
21
19
|
// Secondary foreground color (lighter)
|
|
22
20
|
--fg2-color: hsl(var(--tint-hue) calc(6% * var(--tint-amount)) 45%);
|
|
23
|
-
|
|
24
21
|
--link-color: <%= linkColor %>;
|
|
25
22
|
--link-color-hover: <%= linkColorHover %>;
|
|
26
23
|
|
|
@@ -35,24 +32,20 @@
|
|
|
35
32
|
--bg2-color-translucent: hsl(
|
|
36
33
|
var(--tint-hue) calc(12% * var(--tint-amount)) 92% / 66%
|
|
37
34
|
);
|
|
38
|
-
|
|
39
35
|
--box-shadow: 0 2px 9px
|
|
40
36
|
hsl(var(--tint-hue) calc(85% * var(--tint-amount)) 10% / 25%);
|
|
41
37
|
--backdrop-filter: blur(10px);
|
|
42
|
-
|
|
43
38
|
--font:
|
|
44
|
-
'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
45
|
-
Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
|
39
|
+
'Inter', -apple-system, 'BlinkMacSystemFont', 'Segoe UI', 'Roboto',
|
|
40
|
+
'Helvetica', 'Arial', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
|
41
|
+
'Segoe UI Symbol';
|
|
46
42
|
--heading-font: var(--font);
|
|
47
|
-
--mono-font: 'Google Sans Code', Consolas, Courier, monospace;
|
|
48
|
-
|
|
43
|
+
--mono-font: 'Google Sans Code', 'Consolas', 'Courier', monospace;
|
|
49
44
|
--font-size: 16px;
|
|
50
45
|
--font-size-smaller: 15px;
|
|
51
46
|
--font-size-small: 13px;
|
|
52
|
-
|
|
53
47
|
--font-weight: 400;
|
|
54
48
|
--line-height: 1.7;
|
|
55
|
-
|
|
56
49
|
--mono-font-size: 0.94em;
|
|
57
50
|
--mono-line-height: 1.5;
|
|
58
51
|
--mono-border-radius: 0.375em;
|
|
@@ -77,7 +70,6 @@
|
|
|
77
70
|
--theme-color: <%= themeColorDark %>;
|
|
78
71
|
--theme-color-text: <%= themeColorTextDark %>;
|
|
79
72
|
--text-on-theme: <%= textOnThemeDark %>;
|
|
80
|
-
|
|
81
73
|
--fg-color: hsl(var(--tint-hue) calc(10% * var(--tint-amount)) 92%);
|
|
82
74
|
--fg-color-translucent: hsl(
|
|
83
75
|
var(--tint-hue) calc(85% * var(--tint-amount)) 90% / 33%
|
|
@@ -93,14 +85,11 @@
|
|
|
93
85
|
--bg2-color-translucent: hsl(
|
|
94
86
|
var(--tint-hue) calc(12% * var(--tint-amount)) 12% / 66%
|
|
95
87
|
);
|
|
96
|
-
|
|
97
88
|
--box-shadow: 0 3px 8px
|
|
98
89
|
hsl(var(--tint-hue) calc(33% * var(--tint-amount)) 8% / 70%);
|
|
99
|
-
|
|
100
90
|
--link-active-color: #fff;
|
|
101
|
-
|
|
102
|
-
--
|
|
103
|
-
--note-bg-color: hsl(209 90% 25%);
|
|
91
|
+
--warning-bg-color: hsl(36deg 77% 27%);
|
|
92
|
+
--note-bg-color: hsl(209deg 90% 25%);
|
|
104
93
|
|
|
105
94
|
// SVG icons (dark overrides)
|
|
106
95
|
--icon-search: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><circle cx='11' cy='11' r='7' stroke='<%= iconColorDark %>' stroke-width='2' fill='none'/><line x1='16.5' y1='16.5' x2='22' y2='22' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
package/watch/engine.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import { applyCommitPlan } from './fs-commit';
|
|
3
|
+
import { emitWatchEvent } from './events';
|
|
4
|
+
import type {
|
|
5
|
+
ChangeBatch,
|
|
6
|
+
CompilerBuildResult,
|
|
7
|
+
CompilerPlanResult,
|
|
8
|
+
WatchEngineOptions,
|
|
9
|
+
WatchEventKind,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
function normalizeBatch(changes: Map<string, WatchEventKind>): ChangeBatch {
|
|
13
|
+
return {
|
|
14
|
+
changes: [...changes.entries()]
|
|
15
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
16
|
+
.map(([path, kind]) => ({ path, kind })),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mergeChangeKind(
|
|
21
|
+
previous: WatchEventKind | undefined,
|
|
22
|
+
next: WatchEventKind,
|
|
23
|
+
): WatchEventKind {
|
|
24
|
+
if (!previous) {
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
27
|
+
if (previous === 'add' && next === 'unlink') {
|
|
28
|
+
return 'change';
|
|
29
|
+
}
|
|
30
|
+
if (previous === 'unlink' && next === 'add') {
|
|
31
|
+
return 'change';
|
|
32
|
+
}
|
|
33
|
+
if (previous === 'add' || next === 'add') {
|
|
34
|
+
return 'add';
|
|
35
|
+
}
|
|
36
|
+
if (previous === 'unlink' || next === 'unlink') {
|
|
37
|
+
return 'unlink';
|
|
38
|
+
}
|
|
39
|
+
return 'change';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toDiagnostics(error: unknown): { message: string }[] {
|
|
43
|
+
if (error instanceof Error) {
|
|
44
|
+
return [{ message: error.message }];
|
|
45
|
+
}
|
|
46
|
+
return [{ message: String(error) }];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isNoopCommit(commit: {
|
|
50
|
+
kind: string;
|
|
51
|
+
mutations?: unknown[];
|
|
52
|
+
}): boolean {
|
|
53
|
+
return (
|
|
54
|
+
commit.kind === 'apply-mutations' && (commit.mutations?.length ?? 0) === 0
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type BuildAttemptOutcome<Snapshot, Meta> =
|
|
59
|
+
| { kind: 'skipped'; batch: ChangeBatch; initial: boolean }
|
|
60
|
+
| {
|
|
61
|
+
kind: 'succeeded';
|
|
62
|
+
batch?: ChangeBatch;
|
|
63
|
+
initial: boolean;
|
|
64
|
+
result: Extract<CompilerBuildResult<Snapshot, Meta>, { ok: true }>;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
kind: 'failed';
|
|
68
|
+
batch?: ChangeBatch;
|
|
69
|
+
initial: boolean;
|
|
70
|
+
diagnostics: { message: string }[];
|
|
71
|
+
meta?: Meta;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export async function runWatchEngine<Snapshot, Plan, Meta>(
|
|
75
|
+
options: WatchEngineOptions<Snapshot, Plan, Meta>,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const debounceMs = options.debounceMs ?? 300;
|
|
78
|
+
let snapshot: Snapshot | undefined;
|
|
79
|
+
let pending = new Map<string, WatchEventKind>();
|
|
80
|
+
let blocked = new Map<string, WatchEventKind>();
|
|
81
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
let running = false;
|
|
83
|
+
|
|
84
|
+
async function waitForQuietPeriod(): Promise<void> {
|
|
85
|
+
await new Promise(resolve => setTimeout(resolve, debounceMs));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function runBuild(
|
|
89
|
+
batch?: ChangeBatch,
|
|
90
|
+
initial: boolean = false,
|
|
91
|
+
): Promise<BuildAttemptOutcome<Snapshot, Meta>> {
|
|
92
|
+
await emitWatchEvent(options, { kind: 'build-started', batch, initial });
|
|
93
|
+
try {
|
|
94
|
+
let result: CompilerBuildResult<Snapshot, Meta> | null = null;
|
|
95
|
+
if (initial) {
|
|
96
|
+
result = await options.compiler.buildInitial();
|
|
97
|
+
} else if (batch) {
|
|
98
|
+
const nextPlan: CompilerPlanResult<Plan> = await options.compiler.plan(
|
|
99
|
+
snapshot,
|
|
100
|
+
batch,
|
|
101
|
+
);
|
|
102
|
+
if (nextPlan.kind === 'skip') {
|
|
103
|
+
return { kind: 'skipped', batch, initial };
|
|
104
|
+
}
|
|
105
|
+
result = await options.compiler.run(nextPlan.plan, snapshot);
|
|
106
|
+
}
|
|
107
|
+
if (!result) {
|
|
108
|
+
return { kind: 'skipped', batch: batch!, initial };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
return { kind: 'succeeded', batch, initial, result };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
kind: 'failed',
|
|
117
|
+
batch,
|
|
118
|
+
initial,
|
|
119
|
+
diagnostics: result.diagnostics,
|
|
120
|
+
meta: result.meta,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
kind: 'failed',
|
|
125
|
+
batch,
|
|
126
|
+
initial,
|
|
127
|
+
diagnostics: toDiagnostics(error),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function flush(): Promise<void> {
|
|
133
|
+
if (running) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
running = true;
|
|
137
|
+
try {
|
|
138
|
+
let finalSuccess:
|
|
139
|
+
| {
|
|
140
|
+
batch?: ChangeBatch;
|
|
141
|
+
initial: boolean;
|
|
142
|
+
result: Extract<CompilerBuildResult<Snapshot, Meta>, { ok: true }>;
|
|
143
|
+
changed: boolean;
|
|
144
|
+
}
|
|
145
|
+
| undefined;
|
|
146
|
+
|
|
147
|
+
while (pending.size > 0) {
|
|
148
|
+
const batch = normalizeBatch(pending);
|
|
149
|
+
pending = new Map();
|
|
150
|
+
const outcome = await runBuild(batch, false);
|
|
151
|
+
|
|
152
|
+
if (outcome.kind === 'skipped') {
|
|
153
|
+
if (pending.size === 0 && !finalSuccess) {
|
|
154
|
+
await emitWatchEvent(options, {
|
|
155
|
+
kind: 'build-skipped',
|
|
156
|
+
batch: outcome.batch,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (outcome.kind === 'failed') {
|
|
163
|
+
await emitWatchEvent(options, {
|
|
164
|
+
kind: 'build-failed',
|
|
165
|
+
initial: outcome.initial,
|
|
166
|
+
batch: outcome.batch,
|
|
167
|
+
diagnostics: outcome.diagnostics,
|
|
168
|
+
meta: outcome.meta,
|
|
169
|
+
});
|
|
170
|
+
if (outcome.batch) {
|
|
171
|
+
for (const change of outcome.batch.changes) {
|
|
172
|
+
blocked.set(
|
|
173
|
+
change.path,
|
|
174
|
+
mergeChangeKind(blocked.get(change.path), change.kind),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const changed = !isNoopCommit(outcome.result.commit);
|
|
182
|
+
if (changed) {
|
|
183
|
+
applyCommitPlan(outcome.result.commit);
|
|
184
|
+
}
|
|
185
|
+
snapshot = outcome.result.snapshot;
|
|
186
|
+
finalSuccess = {
|
|
187
|
+
batch: outcome.batch,
|
|
188
|
+
initial: outcome.initial,
|
|
189
|
+
result: outcome.result,
|
|
190
|
+
changed: (finalSuccess?.changed ?? false) || changed,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (pending.size === 0) {
|
|
194
|
+
await waitForQuietPeriod();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (finalSuccess) {
|
|
199
|
+
await emitWatchEvent(options, {
|
|
200
|
+
kind: 'build-succeeded',
|
|
201
|
+
initial: finalSuccess.initial,
|
|
202
|
+
batch: finalSuccess.batch,
|
|
203
|
+
changed: finalSuccess.changed,
|
|
204
|
+
snapshot: finalSuccess.result.snapshot,
|
|
205
|
+
meta: finalSuccess.result.meta,
|
|
206
|
+
diagnostics: finalSuccess.result.diagnostics || [],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
running = false;
|
|
211
|
+
if (pending.size > 0) {
|
|
212
|
+
schedule();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function schedule(): void {
|
|
218
|
+
if (timer) {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
timer = setTimeout(() => {
|
|
222
|
+
timer = null;
|
|
223
|
+
void flush();
|
|
224
|
+
}, debounceMs);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function onFileChange(filePath: string, kind: WatchEventKind): void {
|
|
228
|
+
if (blocked.size > 0) {
|
|
229
|
+
for (const [blockedPath, blockedKind] of blocked) {
|
|
230
|
+
pending.set(
|
|
231
|
+
blockedPath,
|
|
232
|
+
mergeChangeKind(pending.get(blockedPath), blockedKind),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
blocked = new Map();
|
|
236
|
+
}
|
|
237
|
+
pending.set(filePath, mergeChangeKind(pending.get(filePath), kind));
|
|
238
|
+
schedule();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const initialOutcome = await runBuild(undefined, true);
|
|
242
|
+
if (initialOutcome.kind === 'failed') {
|
|
243
|
+
await emitWatchEvent(options, {
|
|
244
|
+
kind: 'build-failed',
|
|
245
|
+
initial: true,
|
|
246
|
+
diagnostics: initialOutcome.diagnostics,
|
|
247
|
+
meta: initialOutcome.meta,
|
|
248
|
+
});
|
|
249
|
+
} else if (initialOutcome.kind === 'succeeded') {
|
|
250
|
+
applyCommitPlan(initialOutcome.result.commit);
|
|
251
|
+
snapshot = initialOutcome.result.snapshot;
|
|
252
|
+
await emitWatchEvent(options, {
|
|
253
|
+
kind: 'build-succeeded',
|
|
254
|
+
initial: true,
|
|
255
|
+
changed: !isNoopCommit(initialOutcome.result.commit),
|
|
256
|
+
snapshot: initialOutcome.result.snapshot,
|
|
257
|
+
meta: initialOutcome.result.meta,
|
|
258
|
+
diagnostics: initialOutcome.result.diagnostics || [],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const watcherEntries = options.compiler
|
|
263
|
+
.getWatchTargets()
|
|
264
|
+
.map(target => ({
|
|
265
|
+
target,
|
|
266
|
+
watcher: chokidar.watch(target.path, {
|
|
267
|
+
ignoreInitial: true,
|
|
268
|
+
atomic: true,
|
|
269
|
+
awaitWriteFinish: { stabilityThreshold: 100 },
|
|
270
|
+
depth: target.chokidar?.depth,
|
|
271
|
+
}),
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
let readyCount = 0;
|
|
275
|
+
const readyGoal = watcherEntries.length;
|
|
276
|
+
const onReady = async () => {
|
|
277
|
+
readyCount += 1;
|
|
278
|
+
if (readyCount === readyGoal) {
|
|
279
|
+
await emitWatchEvent(options, { kind: 'watching' });
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
for (const { watcher, target } of watcherEntries) {
|
|
284
|
+
const emitIfIncluded = (filePath: string, kind: WatchEventKind) => {
|
|
285
|
+
if (!target.filter || target.filter(filePath)) {
|
|
286
|
+
onFileChange(filePath, kind);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
watcher.on('add', filePath => emitIfIncluded(filePath, 'add'));
|
|
290
|
+
watcher.on('change', filePath => emitIfIncluded(filePath, 'change'));
|
|
291
|
+
watcher.on('unlink', filePath => emitIfIncluded(filePath, 'unlink'));
|
|
292
|
+
watcher.on('ready', () => void onReady());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return new Promise(() => {});
|
|
296
|
+
}
|
package/watch/events.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WatchEngineOptions, WatchLifecycleEvent } from './types';
|
|
2
|
+
|
|
3
|
+
export async function emitWatchEvent<Snapshot, Plan, Meta>(
|
|
4
|
+
options: WatchEngineOptions<Snapshot, Plan, Meta>,
|
|
5
|
+
event: WatchLifecycleEvent<Snapshot, Meta>,
|
|
6
|
+
): Promise<void> {
|
|
7
|
+
if (options.onEvent) {
|
|
8
|
+
await options.onEvent(event);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type {
|
|
4
|
+
ApplyMutationsCommitPlan,
|
|
5
|
+
CommitPlan,
|
|
6
|
+
FileMutation,
|
|
7
|
+
ReplaceRootCommitPlan,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
function removeDirIfExists(dir: string): void {
|
|
11
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureParentDir(filePath: string): void {
|
|
15
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pruneEmptyDirs(rootDir: string, filePath: string): void {
|
|
19
|
+
let currentDir = path.dirname(filePath);
|
|
20
|
+
const root = path.resolve(rootDir);
|
|
21
|
+
while (currentDir !== root) {
|
|
22
|
+
try {
|
|
23
|
+
fs.rmdirSync(currentDir);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
26
|
+
if (code === 'ENOTEMPTY' || code === 'ENOENT') {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
currentDir = path.dirname(currentDir);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function applyFileMutation(
|
|
36
|
+
rootDir: string,
|
|
37
|
+
mutation: FileMutation,
|
|
38
|
+
stagedRoot: string,
|
|
39
|
+
): void {
|
|
40
|
+
const targetPath = path.join(rootDir, mutation.path);
|
|
41
|
+
const stagedPath = path.join(stagedRoot, mutation.path);
|
|
42
|
+
|
|
43
|
+
if (mutation.kind === 'write') {
|
|
44
|
+
ensureParentDir(stagedPath);
|
|
45
|
+
fs.writeFileSync(stagedPath, mutation.content);
|
|
46
|
+
ensureParentDir(targetPath);
|
|
47
|
+
fs.renameSync(stagedPath, targetPath);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.rmSync(targetPath, { force: true });
|
|
52
|
+
pruneEmptyDirs(rootDir, targetPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function applyMutations(plan: ApplyMutationsCommitPlan): void {
|
|
56
|
+
const stagedRoot = fs.mkdtempSync(path.join(plan.rootDir, '.watch-stage-'));
|
|
57
|
+
try {
|
|
58
|
+
for (const mutation of plan.mutations) {
|
|
59
|
+
if (mutation.kind === 'write') {
|
|
60
|
+
const stagedPath = path.join(stagedRoot, mutation.path);
|
|
61
|
+
ensureParentDir(stagedPath);
|
|
62
|
+
fs.writeFileSync(stagedPath, mutation.content);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const mutation of plan.mutations) {
|
|
67
|
+
applyFileMutation(plan.rootDir, mutation, stagedRoot);
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
removeDirIfExists(stagedRoot);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function replaceRoot(plan: ReplaceRootCommitPlan): void {
|
|
75
|
+
const { targetPath, stagedPath } = plan;
|
|
76
|
+
const backupDir = `${targetPath}.bak-${process.pid}-${Date.now()}`;
|
|
77
|
+
const targetExists = fs.existsSync(targetPath);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (targetExists) {
|
|
81
|
+
fs.renameSync(targetPath, backupDir);
|
|
82
|
+
}
|
|
83
|
+
fs.renameSync(stagedPath, targetPath);
|
|
84
|
+
if (targetExists) {
|
|
85
|
+
removeDirIfExists(backupDir);
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (!fs.existsSync(targetPath) && fs.existsSync(backupDir)) {
|
|
89
|
+
fs.renameSync(backupDir, targetPath);
|
|
90
|
+
}
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function applyCommitPlan(plan: CommitPlan): void {
|
|
96
|
+
if (plan.kind === 'replace-root') {
|
|
97
|
+
replaceRoot(plan);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
applyMutations(plan);
|
|
102
|
+
}
|
package/watch/types.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export type WatchEventKind = 'add' | 'change' | 'unlink';
|
|
2
|
+
|
|
3
|
+
export interface FileChange {
|
|
4
|
+
path: string;
|
|
5
|
+
kind: WatchEventKind;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ChangeBatch {
|
|
9
|
+
changes: FileChange[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WatchTarget {
|
|
13
|
+
path: string;
|
|
14
|
+
chokidar?: { depth?: number };
|
|
15
|
+
filter?: (filePath: string) => boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WatchDiagnostic {
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WriteFileMutation {
|
|
23
|
+
kind: 'write';
|
|
24
|
+
path: string;
|
|
25
|
+
content: string | Buffer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DeleteFileMutation {
|
|
29
|
+
kind: 'delete';
|
|
30
|
+
path: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type FileMutation = WriteFileMutation | DeleteFileMutation;
|
|
34
|
+
|
|
35
|
+
export interface ReplaceRootCommitPlan {
|
|
36
|
+
kind: 'replace-root';
|
|
37
|
+
stagedPath: string;
|
|
38
|
+
targetPath: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ApplyMutationsCommitPlan {
|
|
42
|
+
kind: 'apply-mutations';
|
|
43
|
+
rootDir: string;
|
|
44
|
+
mutations: FileMutation[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type CommitPlan = ReplaceRootCommitPlan | ApplyMutationsCommitPlan;
|
|
48
|
+
|
|
49
|
+
export type CompilerPlanResult<Plan> =
|
|
50
|
+
| { kind: 'skip' }
|
|
51
|
+
| { kind: 'build'; plan: Plan };
|
|
52
|
+
|
|
53
|
+
export type CompilerBuildResult<Snapshot, Meta> =
|
|
54
|
+
| {
|
|
55
|
+
ok: true;
|
|
56
|
+
snapshot: Snapshot;
|
|
57
|
+
commit: CommitPlan;
|
|
58
|
+
diagnostics?: WatchDiagnostic[];
|
|
59
|
+
meta?: Meta;
|
|
60
|
+
}
|
|
61
|
+
| { ok: false; diagnostics: WatchDiagnostic[]; meta?: Meta };
|
|
62
|
+
|
|
63
|
+
export interface WatchCompiler<Snapshot, Plan, Meta> {
|
|
64
|
+
getWatchTargets(): WatchTarget[];
|
|
65
|
+
buildInitial(): Promise<CompilerBuildResult<Snapshot, Meta>>;
|
|
66
|
+
plan(
|
|
67
|
+
snapshot: Snapshot | undefined,
|
|
68
|
+
batch: ChangeBatch,
|
|
69
|
+
): Promise<CompilerPlanResult<Plan>>;
|
|
70
|
+
run(
|
|
71
|
+
plan: Plan,
|
|
72
|
+
snapshot: Snapshot | undefined,
|
|
73
|
+
): Promise<CompilerBuildResult<Snapshot, Meta>>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type WatchLifecycleEvent<Snapshot, Meta> =
|
|
77
|
+
| { kind: 'watching' }
|
|
78
|
+
| { kind: 'build-started'; batch?: ChangeBatch; initial: boolean }
|
|
79
|
+
| {
|
|
80
|
+
kind: 'build-succeeded';
|
|
81
|
+
initial: boolean;
|
|
82
|
+
batch?: ChangeBatch;
|
|
83
|
+
changed: boolean;
|
|
84
|
+
snapshot: Snapshot;
|
|
85
|
+
meta?: Meta;
|
|
86
|
+
diagnostics: WatchDiagnostic[];
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
kind: 'build-failed';
|
|
90
|
+
initial: boolean;
|
|
91
|
+
batch?: ChangeBatch;
|
|
92
|
+
diagnostics: WatchDiagnostic[];
|
|
93
|
+
meta?: Meta;
|
|
94
|
+
}
|
|
95
|
+
| { kind: 'build-skipped'; batch: ChangeBatch };
|
|
96
|
+
|
|
97
|
+
export interface WatchEngineOptions<Snapshot, Plan, Meta> {
|
|
98
|
+
compiler: WatchCompiler<Snapshot, Plan, Meta>;
|
|
99
|
+
debounceMs?: number;
|
|
100
|
+
onEvent?: (
|
|
101
|
+
event: WatchLifecycleEvent<Snapshot, Meta>,
|
|
102
|
+
) => void | Promise<void>;
|
|
103
|
+
}
|