@abreen/tada 1.9.0 → 1.10.2
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/globals.ts +1 -1
- 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 +47 -24
- 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/index.ts +13 -3
- package/src/timezone/style.scss +11 -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
package/src/trace/style.scss
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
1
|
.trace-body {
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
|
+
gap: var(--gap);
|
|
4
5
|
height: calc(100dvh - var(--header-collapsed-height) - 3 * var(--gap));
|
|
5
6
|
overflow: hidden;
|
|
6
|
-
gap: var(--gap);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.trace-content {
|
|
10
10
|
display: flex;
|
|
11
|
-
flex-direction: column;
|
|
12
11
|
flex: 1;
|
|
13
|
-
|
|
12
|
+
flex-direction: column;
|
|
14
13
|
gap: var(--gap);
|
|
14
|
+
min-height: 0;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
.trace-content > .trace-diagram {
|
|
18
18
|
flex: 1 1 100%;
|
|
19
19
|
min-height: 8rem;
|
|
20
|
+
overflow: auto;
|
|
20
21
|
border: 1px solid var(--fg2-color);
|
|
21
22
|
border-radius: var(--border-radius);
|
|
22
|
-
overflow: auto;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
.trace-source-wrapper {
|
|
26
|
+
position: relative;
|
|
26
27
|
min-height: calc(
|
|
27
28
|
3 * var(--font-size-smaller) * var(--mono-line-height) + 1em
|
|
28
29
|
);
|
|
29
30
|
max-height: calc(
|
|
30
31
|
5 * var(--font-size-smaller) * var(--mono-line-height) + 1em
|
|
31
32
|
);
|
|
32
|
-
position: relative;
|
|
33
33
|
|
|
34
34
|
&:has(:focus-visible)::after {
|
|
35
|
-
content: '';
|
|
36
35
|
position: absolute;
|
|
37
36
|
inset: 0;
|
|
37
|
+
z-index: 1;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
content: '';
|
|
38
40
|
border: 2px solid var(--theme-color);
|
|
39
41
|
border-radius: var(--mono-border-radius);
|
|
40
|
-
pointer-events: none;
|
|
41
|
-
z-index: 1;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
& > .trace-source {
|
|
@@ -48,64 +48,64 @@
|
|
|
48
48
|
|
|
49
49
|
.trace-toolbar {
|
|
50
50
|
display: flex;
|
|
51
|
+
flex-shrink: 0;
|
|
51
52
|
align-items: center;
|
|
52
53
|
justify-content: center;
|
|
53
|
-
flex-shrink: 0;
|
|
54
54
|
padding: 3px;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
.trace-controls {
|
|
58
58
|
display: flex;
|
|
59
|
-
align-items: center;
|
|
60
59
|
gap: 0.5rem;
|
|
60
|
+
align-items: center;
|
|
61
|
+
width: 100%;
|
|
62
|
+
max-width: 386px;
|
|
61
63
|
padding: 6px;
|
|
62
|
-
border-radius: calc(var(--border-radius) + 4px);
|
|
63
64
|
background: var(--bg2-color);
|
|
65
|
+
border-radius: calc(var(--border-radius) + 4px);
|
|
64
66
|
|
|
65
67
|
&:focus-within {
|
|
66
68
|
outline: 2px solid var(--fg-color);
|
|
67
69
|
outline-offset: 1px;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
width
|
|
71
|
-
max-width: 386px;
|
|
72
|
-
|
|
73
|
-
@media (max-width: 410px) {
|
|
72
|
+
@media (width <= 410px) {
|
|
74
73
|
max-width: none;
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
.trace-btn {
|
|
79
78
|
display: inline-flex;
|
|
79
|
+
flex: 1;
|
|
80
80
|
align-items: center;
|
|
81
81
|
justify-content: center;
|
|
82
|
-
flex: 1;
|
|
83
82
|
min-width: 2.5em;
|
|
84
83
|
padding: 0.5em 0.75em;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
.trace-step-counter {
|
|
88
|
-
text-align: center;
|
|
89
|
-
font-variant-numeric: tabular-nums;
|
|
90
87
|
font-size: var(--font-size-smaller);
|
|
88
|
+
font-variant-numeric: tabular-nums;
|
|
89
|
+
text-align: center;
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
.trace-source {
|
|
94
93
|
--code-gutter-width: 4rem;
|
|
94
|
+
|
|
95
95
|
overflow: auto;
|
|
96
|
-
clip-path: inset(0 round var(--mono-border-radius));
|
|
97
96
|
scroll-behavior: smooth;
|
|
97
|
+
clip-path: inset(0 round var(--mono-border-radius));
|
|
98
98
|
|
|
99
99
|
&:focus-visible {
|
|
100
100
|
outline: none;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
& > pre {
|
|
104
|
-
margin: 0;
|
|
105
|
-
padding: 0;
|
|
106
|
-
overflow: visible;
|
|
107
104
|
width: max-content;
|
|
108
105
|
min-width: 100%;
|
|
106
|
+
padding: 0;
|
|
107
|
+
margin: 0;
|
|
108
|
+
overflow: visible;
|
|
109
109
|
|
|
110
110
|
code {
|
|
111
111
|
padding-right: var(--gap);
|
|
@@ -117,11 +117,11 @@
|
|
|
117
117
|
color: var(--fg-color);
|
|
118
118
|
|
|
119
119
|
&::before {
|
|
120
|
-
content: '\25B6';
|
|
121
120
|
position: absolute;
|
|
122
|
-
margin-right: 0.25em;
|
|
123
121
|
left: 0;
|
|
122
|
+
margin-right: 0.25em;
|
|
124
123
|
line-height: 1.3;
|
|
124
|
+
content: '\25B6';
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -133,18 +133,18 @@
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
.trace-output::before {
|
|
136
|
-
content: 'Output';
|
|
137
136
|
display: block;
|
|
138
137
|
font-size: var(--font-size-small);
|
|
139
138
|
font-style: italic;
|
|
140
139
|
color: var(--fg2-color);
|
|
140
|
+
content: 'Output';
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
.trace-memory {
|
|
144
|
-
font-family: var(--mono-font, monospace);
|
|
145
|
-
font-size: var(--font-size-smaller, 15px);
|
|
146
144
|
max-width: 100%;
|
|
147
145
|
height: auto;
|
|
146
|
+
font-family: var(--mono-font, monospace);
|
|
147
|
+
font-size: var(--font-size-smaller, 15px);
|
|
148
148
|
|
|
149
149
|
.trace-separator {
|
|
150
150
|
stroke: var(--fg2-color);
|
|
@@ -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
|
+
}
|