@abreen/tada 1.17.0 → 1.18.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 +41 -7
- package/bin/tada.ts +23 -9
- package/bin/validators.ts +34 -1
- package/build/branding.ts +41 -0
- package/build/build-manifest.ts +10 -5
- package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
- package/build/build-validation.ts +176 -0
- package/build/bundle.ts +75 -65
- package/build/columns-plugin.ts +27 -1
- package/build/config-loader.ts +3 -3
- package/build/custom-fonts.ts +8 -5
- package/build/deflist-id-plugin.ts +2 -2
- package/build/generate-favicon.ts +7 -6
- package/build/generate-fonts.ts +8 -15
- package/build/generate-katex-assets.ts +19 -17
- package/build/generate-web-app-manifest.ts +3 -11
- package/build/lodash-template.ts +14 -0
- package/build/output-publication.ts +238 -0
- package/build/pagefind.ts +49 -21
- package/build/pdf-text.ts +41 -1
- package/build/pipeline.ts +95 -101
- package/build/site-assets.ts +46 -0
- package/build/site-build.ts +267 -0
- package/build/source-model.ts +223 -229
- package/build/source-records.ts +69 -81
- package/build/template-globals.ts +3 -0
- package/build/templates.ts +12 -8
- package/build/toc-plugin.ts +21 -21
- package/build/types.d.ts +38 -38
- package/build/util.ts +0 -4
- package/build/utils/code.ts +51 -117
- package/build/utils/final-html.ts +171 -47
- package/build/utils/jdi-runner/LiterateRunner.java +9 -1
- package/build/utils/literate-java.ts +11 -7
- package/build/utils/markdown-partials.ts +4 -3
- package/build/utils/markdown.ts +6 -17
- package/build/utils/paths.ts +3 -0
- package/build/utils/plain-text.ts +18 -0
- package/build/utils/render.ts +197 -154
- package/build/utils/shiki-highlighter.ts +11 -0
- package/build/utils/source-template.ts +2 -2
- package/build/utils/trace-core.ts +8 -83
- package/build/utils/trace.ts +38 -95
- package/build/validate-config-links.ts +39 -6
- package/build/watch/build-incremental.ts +46 -103
- package/build/watch/compiler.ts +158 -37
- package/build/watch/engine.ts +207 -162
- package/build/watch/index.ts +9 -2
- package/build/watch/planner.ts +88 -144
- package/build/watch/runtime.ts +21 -34
- package/build/watch/types.d.ts +32 -64
- package/package.json +4 -5
- package/schema/site.schema.json +18 -2
- package/src/_content.scss +447 -0
- package/src/_fonts.scss +90 -0
- package/src/_layout.scss +50 -35
- package/src/appearance-picker/{style.scss → _index.scss} +1 -1
- package/src/code/{style.scss → _index.scss} +61 -0
- package/src/header/{_base.scss → _index.scss} +98 -6
- package/src/index.ts +1 -17
- package/src/navigate/{style.scss → _index.scss} +20 -0
- package/src/navigate/eligible.ts +0 -40
- package/src/navigate/index.ts +50 -5
- package/src/navigate/lifecycle.ts +19 -2
- package/src/navigate/runtime.ts +20 -4
- package/src/page-update/index.ts +33 -20
- package/src/print/{style.scss → _index.scss} +3 -1
- package/src/search/index.ts +30 -2
- package/src/slides/{style.scss → _index.scss} +5 -0
- package/src/style.scss +24 -536
- package/src/timezone/index.ts +29 -3
- package/src/timezone/time-format.ts +4 -0
- package/src/toc/index.ts +6 -5
- package/src/trace/index.ts +11 -1
- package/templates/_author.html +2 -2
- package/templates/_download.html +1 -1
- package/templates/_footer.html +1 -1
- package/templates/_heading.html +3 -3
- package/templates/_nav.html +2 -2
- package/templates/_page-bottom.html +6 -0
- package/templates/_top.html +18 -14
- package/templates/code.html +22 -21
- package/templates/default.html +12 -11
- package/templates/literate.html +6 -5
- package/build/copy.ts +0 -82
- package/build/generate-content-assets.ts +0 -121
- package/build/utils/content-files.ts +0 -170
- package/build/watch/assets.ts +0 -103
- package/build/watch/build-full.ts +0 -97
- package/build/watch/build-helpers.ts +0 -66
- package/build/watch/build-result.ts +0 -19
- package/build/watch/fs-commit.ts +0 -153
- package/build/watch/mutations.ts +0 -73
- package/build/watch/snapshot.ts +0 -201
- package/build/watch/validation.ts +0 -50
- package/src/code.scss +0 -60
- package/src/critical.scss +0 -5
- package/src/header/style.scss +0 -91
- /package/src/{literate.scss → _literate.scss} +0 -0
- /package/src/{material-symbols.scss → _material-symbols.scss} +0 -0
- /package/src/anchor/{style.scss → _index.scss} +0 -0
- /package/src/page-update/{style.scss → _index.scss} +0 -0
- /package/src/question/{style.scss → _index.scss} +0 -0
- /package/src/search/{style.scss → _index.scss} +0 -0
- /package/src/timezone/{style.scss → _index.scss} +0 -0
- /package/src/toc/{style.scss → _index.scss} +0 -0
- /package/src/trace/{style.scss → _index.scss} +0 -0
package/build/watch/compiler.ts
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { getProjectConfigDir } from '../templates';
|
|
3
|
-
import { getContentDir, getPublicDir } from '../
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import { getContentDir, getDistDir, getPublicDir } from '../utils/paths';
|
|
4
|
+
import type { CompileOutcome, WatchTarget, WatchBuildResult } from './types';
|
|
5
|
+
import type { TadaBuildMeta, TraceCache } from '../build-types';
|
|
6
|
+
import type { TraceToolAvailability } from '../types';
|
|
7
|
+
import {
|
|
8
|
+
applyMutations,
|
|
9
|
+
computeMutations,
|
|
10
|
+
planFullWrite,
|
|
11
|
+
} from '../output-publication';
|
|
12
|
+
import { loadProjectConfig } from '../config-loader';
|
|
13
|
+
import { getKeptOutputPrefixes } from '../pagefind';
|
|
14
|
+
import { getDevSiteVariables } from '../site-variables';
|
|
15
|
+
import { scanProject } from '../source-model';
|
|
16
|
+
import { buildSite, createBuildMeta, type TadaSnapshot } from '../site-build';
|
|
11
17
|
import { buildIncremental } from './build-incremental';
|
|
12
|
-
import type {
|
|
18
|
+
import type { TadaSourceRecord } from '../source-records';
|
|
13
19
|
import { checkTraceToolAvailability, isTraceSourceFile } from '../utils/trace';
|
|
14
|
-
import { createTadaWatchPlan } from './planner';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
import { createTadaWatchPlan, diffAuthorKeys } from './planner';
|
|
21
|
+
import {
|
|
22
|
+
getWatchConfigFilePaths,
|
|
23
|
+
classifyWatchConfigPath,
|
|
24
|
+
} from './config-paths';
|
|
18
25
|
|
|
19
26
|
export function invalidateTraceCacheForBatch(
|
|
20
27
|
traceCache: TraceCache,
|
|
21
|
-
|
|
28
|
+
paths: ReadonlySet<string>,
|
|
22
29
|
): void {
|
|
23
|
-
for (const
|
|
24
|
-
const sourcePath = path.resolve(change.path);
|
|
30
|
+
for (const sourcePath of paths) {
|
|
25
31
|
if (isTraceSourceFile(sourcePath)) {
|
|
26
32
|
for (const [cacheKey, entry] of traceCache) {
|
|
27
33
|
if (sourcePath in entry.sourceMtims) {
|
|
@@ -32,13 +38,39 @@ export function invalidateTraceCacheForBatch(
|
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
/** Drops cached traces whose source files no page in the build traces. */
|
|
42
|
+
export function pruneTraceCache(
|
|
43
|
+
traceCache: TraceCache,
|
|
44
|
+
records: ReadonlyMap<string, TadaSourceRecord>,
|
|
45
|
+
): void {
|
|
46
|
+
const traced = new Set<string>();
|
|
47
|
+
for (const record of records.values()) {
|
|
48
|
+
for (const sourcePath of record.traceDeps) {
|
|
49
|
+
traced.add(sourcePath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const [cacheKey, entry] of traceCache) {
|
|
53
|
+
if (!Object.keys(entry.sourceMtims).every(file => traced.has(file))) {
|
|
54
|
+
traceCache.delete(cacheKey);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class TadaWatchCompiler {
|
|
36
60
|
private traceCache: TraceCache;
|
|
37
|
-
private
|
|
61
|
+
private traceToolAvailability: TraceToolAvailability;
|
|
62
|
+
|
|
63
|
+
readonly build = createBuildSession(async (snapshot, paths) => {
|
|
64
|
+
const outcome = await this.compile(snapshot, paths);
|
|
65
|
+
if (outcome.ok) {
|
|
66
|
+
pruneTraceCache(this.traceCache, outcome.snapshot.records);
|
|
67
|
+
}
|
|
68
|
+
return outcome;
|
|
69
|
+
});
|
|
38
70
|
|
|
39
71
|
constructor() {
|
|
40
72
|
this.traceCache = new Map();
|
|
41
|
-
this.
|
|
73
|
+
this.traceToolAvailability = checkTraceToolAvailability();
|
|
42
74
|
}
|
|
43
75
|
|
|
44
76
|
getWatchTargets(): WatchTarget[] {
|
|
@@ -58,35 +90,124 @@ export class TadaWatchCompiler implements WatchCompiler {
|
|
|
58
90
|
];
|
|
59
91
|
}
|
|
60
92
|
|
|
61
|
-
async
|
|
93
|
+
private async buildFull(
|
|
94
|
+
reuse?: Pick<TadaSnapshot, 'siteVariables' | 'scan'>,
|
|
95
|
+
): Promise<CompileOutcome> {
|
|
96
|
+
const result = await buildSite({
|
|
97
|
+
siteVariables: reuse?.siteVariables ?? getDevSiteVariables(),
|
|
98
|
+
scan: reuse?.scan,
|
|
99
|
+
mode: 'development',
|
|
100
|
+
isWatchMode: true,
|
|
101
|
+
traceCache: this.traceCache,
|
|
102
|
+
traceToolAvailability: this.traceToolAvailability,
|
|
103
|
+
});
|
|
104
|
+
return result.ok ? { ...result, full: true } : result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async compile(
|
|
62
108
|
snapshot: TadaSnapshot | undefined,
|
|
63
|
-
|
|
64
|
-
): Promise<
|
|
65
|
-
if (
|
|
66
|
-
invalidateTraceCacheForBatch(this.traceCache,
|
|
109
|
+
paths?: ReadonlySet<string>,
|
|
110
|
+
): Promise<CompileOutcome> {
|
|
111
|
+
if (paths) {
|
|
112
|
+
invalidateTraceCacheForBatch(this.traceCache, paths);
|
|
67
113
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
114
|
+
const configKinds = new Set(
|
|
115
|
+
[...(paths ?? [])].map(classifyWatchConfigPath),
|
|
116
|
+
);
|
|
117
|
+
if (
|
|
118
|
+
!snapshot ||
|
|
119
|
+
!paths ||
|
|
120
|
+
configKinds.has('site') ||
|
|
121
|
+
configKinds.has('nav')
|
|
122
|
+
) {
|
|
123
|
+
return this.buildFull();
|
|
74
124
|
}
|
|
75
125
|
|
|
76
|
-
const
|
|
77
|
-
const
|
|
126
|
+
const authorsChanged = configKinds.has('authors');
|
|
127
|
+
const nextAuthors = authorsChanged
|
|
128
|
+
? loadProjectConfig(
|
|
129
|
+
getProjectConfigDir(),
|
|
130
|
+
'authors',
|
|
131
|
+
snapshot.siteVariables,
|
|
132
|
+
)?.value
|
|
133
|
+
: snapshot.authorsData;
|
|
134
|
+
// Every build rescans the source directories instead of reconciling
|
|
135
|
+
// individual file and directory events.
|
|
136
|
+
const scan = scanProject(snapshot.siteVariables);
|
|
137
|
+
const plan = createTadaWatchPlan({
|
|
138
|
+
snapshot,
|
|
139
|
+
paths,
|
|
140
|
+
scan,
|
|
141
|
+
full:
|
|
142
|
+
authorsChanged &&
|
|
143
|
+
(snapshot.authorsData === undefined || nextAuthors === undefined),
|
|
144
|
+
changedAuthors: authorsChanged
|
|
145
|
+
? diffAuthorKeys(snapshot.authorsData, nextAuthors)
|
|
146
|
+
: new Set(),
|
|
147
|
+
});
|
|
78
148
|
if (plan.kind === 'full') {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
traceOptions: this.traceOptions,
|
|
82
|
-
});
|
|
149
|
+
// Only the authors file changed, so the site config and scan still hold.
|
|
150
|
+
return this.buildFull({ siteVariables: snapshot.siteVariables, scan });
|
|
83
151
|
}
|
|
84
152
|
|
|
85
153
|
return buildIncremental({
|
|
86
154
|
plan,
|
|
87
155
|
snapshot,
|
|
88
156
|
traceCache: this.traceCache,
|
|
89
|
-
|
|
157
|
+
traceToolAvailability: this.traceToolAvailability,
|
|
90
158
|
});
|
|
91
159
|
}
|
|
92
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Writes a successful build into `dist/` in place. Full builds compare
|
|
164
|
+
* against the files on disk; incremental builds against the previous build.
|
|
165
|
+
*/
|
|
166
|
+
function writeBuild(
|
|
167
|
+
outcome: Extract<CompileOutcome, { ok: true }>,
|
|
168
|
+
previous: TadaSnapshot | undefined,
|
|
169
|
+
distDir: string = getDistDir(),
|
|
170
|
+
): void {
|
|
171
|
+
const keep = getKeptOutputPrefixes(outcome.snapshot.siteVariables);
|
|
172
|
+
applyMutations(
|
|
173
|
+
distDir,
|
|
174
|
+
outcome.full || !previous
|
|
175
|
+
? planFullWrite(distDir, outcome.snapshot.outputs, keep)
|
|
176
|
+
: computeMutations(
|
|
177
|
+
previous.outputs,
|
|
178
|
+
outcome.snapshot.outputs,
|
|
179
|
+
outcome.forceSourcePaths,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function createBuildSession(
|
|
185
|
+
compile: (
|
|
186
|
+
snapshot: TadaSnapshot | undefined,
|
|
187
|
+
paths?: ReadonlySet<string>,
|
|
188
|
+
) => Promise<CompileOutcome>,
|
|
189
|
+
write: typeof writeBuild = writeBuild,
|
|
190
|
+
): (paths?: ReadonlySet<string>) => Promise<WatchBuildResult<TadaBuildMeta>> {
|
|
191
|
+
let snapshot: TadaSnapshot | undefined;
|
|
192
|
+
return async (
|
|
193
|
+
paths?: ReadonlySet<string>,
|
|
194
|
+
): Promise<WatchBuildResult<TadaBuildMeta>> => {
|
|
195
|
+
const outcome = await compile(snapshot, paths);
|
|
196
|
+
if (!outcome.ok) {
|
|
197
|
+
return outcome;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
write(outcome, snapshot);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
// The next change rebuilds everything against what is on disk.
|
|
203
|
+
snapshot = undefined;
|
|
204
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
diagnostics: [{ message: `Failed to write output: ${message}` }],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
snapshot = outcome.snapshot;
|
|
211
|
+
return { ok: true, meta: createBuildMeta(outcome.snapshot) };
|
|
212
|
+
};
|
|
213
|
+
}
|
package/build/watch/engine.ts
CHANGED
|
@@ -1,199 +1,244 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
1
3
|
import chokidar from 'chokidar';
|
|
2
|
-
import { applyCommitPlan } from './fs-commit';
|
|
3
4
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
WatchBuildResult,
|
|
6
|
+
WatchClock,
|
|
6
7
|
WatchEngineOptions,
|
|
7
|
-
|
|
8
|
+
WatchHandle,
|
|
8
9
|
WatchLifecycleEvent,
|
|
9
10
|
} from './types';
|
|
10
|
-
import type { TadaSnapshot } from './snapshot';
|
|
11
11
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
12
|
+
export function runWatchEngine<Meta>(
|
|
13
|
+
options: WatchEngineOptions<Meta>,
|
|
14
|
+
dependencies = {
|
|
15
|
+
watch: chokidar.watch,
|
|
16
|
+
stat: (filePath: string): Pick<fs.Stats, 'isFile'> | undefined =>
|
|
17
|
+
fs.statSync(filePath, { throwIfNoEntry: false }),
|
|
18
|
+
clock: { setTimeout, clearTimeout } as WatchClock,
|
|
19
|
+
},
|
|
20
|
+
): WatchHandle {
|
|
21
|
+
const { clock } = dependencies;
|
|
22
|
+
const debounceMs = options.debounceMs ?? 300;
|
|
23
|
+
const watchers: ReturnType<typeof chokidar.watch>[] = [];
|
|
24
|
+
const pending = new Set<string>();
|
|
25
|
+
let uncommitted = new Set<string>();
|
|
26
|
+
let closed = false;
|
|
27
|
+
let fatal: { error: unknown } | undefined;
|
|
28
|
+
let closing: Promise<PromiseSettledResult<void>[]> | undefined;
|
|
29
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
30
|
+
let wake: (() => void) | undefined;
|
|
31
|
+
const stopped = Promise.withResolvers<void>();
|
|
32
|
+
|
|
33
|
+
function stop(): void {
|
|
34
|
+
closed = true;
|
|
35
|
+
pending.clear();
|
|
36
|
+
if (timer !== undefined) {
|
|
37
|
+
clock.clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
timer = undefined;
|
|
40
|
+
wake?.();
|
|
41
|
+
stopped.resolve();
|
|
42
|
+
closing ??= Promise.allSettled(
|
|
43
|
+
watchers.map(async watcher => watcher.close()),
|
|
44
|
+
);
|
|
42
45
|
}
|
|
43
|
-
return 'change';
|
|
44
|
-
}
|
|
45
46
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
function fail(error: unknown): void {
|
|
48
|
+
fatal ??= { error };
|
|
49
|
+
stop();
|
|
49
50
|
}
|
|
50
|
-
return [{ message: String(error) }];
|
|
51
|
-
}
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
function wait(quiet = false): Promise<void> {
|
|
53
|
+
if (closed) {
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
}
|
|
56
|
+
return new Promise(resolve => {
|
|
57
|
+
wake = () => {
|
|
58
|
+
wake = undefined;
|
|
59
|
+
timer = undefined;
|
|
60
|
+
resolve();
|
|
61
|
+
};
|
|
62
|
+
if (quiet) {
|
|
63
|
+
timer = clock.setTimeout(() => wake?.(), debounceMs);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
function changed(filePath: string): void {
|
|
69
|
+
if (closed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
pending.add(path.resolve(filePath));
|
|
73
|
+
if (timer !== undefined) {
|
|
74
|
+
clock.clearTimeout(timer);
|
|
75
|
+
timer = clock.setTimeout(() => wake?.(), debounceMs);
|
|
76
|
+
} else {
|
|
77
|
+
wake?.();
|
|
78
|
+
}
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
async function
|
|
68
|
-
|
|
81
|
+
async function emit(event: WatchLifecycleEvent<Meta>): Promise<void> {
|
|
82
|
+
if (!closed) {
|
|
83
|
+
await options.onEvent?.(event);
|
|
84
|
+
}
|
|
69
85
|
}
|
|
70
86
|
|
|
71
|
-
async function
|
|
72
|
-
|
|
87
|
+
async function build(
|
|
88
|
+
paths?: ReadonlySet<string>,
|
|
89
|
+
): Promise<WatchBuildResult<Meta> | undefined> {
|
|
90
|
+
await emit({ kind: 'build-started', paths });
|
|
91
|
+
if (closed) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let outcome: WatchBuildResult<Meta>;
|
|
73
95
|
try {
|
|
74
|
-
|
|
96
|
+
outcome = await options.build(paths);
|
|
75
97
|
} catch (error) {
|
|
76
|
-
|
|
98
|
+
outcome = {
|
|
99
|
+
ok: false,
|
|
100
|
+
diagnostics: [
|
|
101
|
+
{ message: error instanceof Error ? error.message : String(error) },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!outcome.ok) {
|
|
106
|
+
await emit({
|
|
107
|
+
kind: 'build-failed',
|
|
108
|
+
paths,
|
|
109
|
+
diagnostics: outcome.diagnostics,
|
|
110
|
+
});
|
|
77
111
|
}
|
|
112
|
+
return outcome;
|
|
78
113
|
}
|
|
79
114
|
|
|
80
|
-
async function
|
|
81
|
-
if (
|
|
115
|
+
async function run(): Promise<void> {
|
|
116
|
+
if (closed) {
|
|
82
117
|
return;
|
|
83
118
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
const ready = options.targets.map(target => {
|
|
120
|
+
if (closed) {
|
|
121
|
+
return Promise.resolve();
|
|
122
|
+
}
|
|
123
|
+
const watcher = dependencies.watch(target.path, {
|
|
124
|
+
ignoreInitial: true,
|
|
125
|
+
atomic: true,
|
|
126
|
+
awaitWriteFinish: { stabilityThreshold: 100 },
|
|
127
|
+
...target.chokidar,
|
|
128
|
+
});
|
|
129
|
+
watchers.push(watcher);
|
|
130
|
+
const included = (filePath: string) => {
|
|
131
|
+
if (closed) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if (!target.filter || target.filter(filePath)) {
|
|
136
|
+
changed(filePath);
|
|
90
137
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
while (pending.size > 0) {
|
|
94
|
-
const changes = new Map(uncommitted);
|
|
95
|
-
for (const [filePath, kind] of pending) {
|
|
96
|
-
changes.set(filePath, mergeChangeKind(changes.get(filePath), kind));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
fail(error);
|
|
97
140
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!outcome.ok) {
|
|
104
|
-
uncommitted = mapBatch(batch);
|
|
105
|
-
await emit({
|
|
106
|
-
kind: 'build-failed',
|
|
107
|
-
batch,
|
|
108
|
-
diagnostics: outcome.diagnostics,
|
|
109
|
-
});
|
|
141
|
+
};
|
|
142
|
+
watcher.on('add', included).on('unlink', included);
|
|
143
|
+
watcher.on('change', (filePath, stats) => {
|
|
144
|
+
if (closed) {
|
|
110
145
|
return;
|
|
111
146
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
// A polling file subscription does not become recursive on its own.
|
|
148
|
+
try {
|
|
149
|
+
if (stats?.isDirectory()) {
|
|
150
|
+
watcher.add(filePath);
|
|
151
|
+
}
|
|
152
|
+
included(filePath);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
fail(error);
|
|
120
155
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
});
|
|
157
|
+
watcher.on('addDir', included);
|
|
158
|
+
watcher.on('unlinkDir', filePath => {
|
|
159
|
+
included(filePath);
|
|
160
|
+
// Wait until Chokidar finishes closing the old directory subscription.
|
|
161
|
+
queueMicrotask(() => {
|
|
162
|
+
if (closed) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
if (dependencies.stat(filePath)?.isFile()) {
|
|
167
|
+
watcher.add(filePath);
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOTDIR') {
|
|
171
|
+
fail(error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
128
174
|
});
|
|
175
|
+
});
|
|
176
|
+
watcher.on('error', fail);
|
|
177
|
+
return new Promise<void>(resolve => watcher.once('ready', resolve));
|
|
178
|
+
});
|
|
179
|
+
await Promise.race([Promise.all(ready), stopped.promise]);
|
|
180
|
+
if (closed) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const startup = await build();
|
|
184
|
+
if (startup?.ok) {
|
|
185
|
+
await emit({ kind: 'build-succeeded', meta: startup.meta });
|
|
186
|
+
}
|
|
187
|
+
await emit({ kind: 'watching' });
|
|
188
|
+
while (!closed) {
|
|
189
|
+
if (!pending.size) {
|
|
190
|
+
await wait();
|
|
129
191
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
192
|
+
if (closed) {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
await wait(true);
|
|
196
|
+
let success: { paths: ReadonlySet<string>; meta: Meta } | undefined;
|
|
197
|
+
while (!closed && pending.size) {
|
|
198
|
+
const paths = new Set([...uncommitted, ...pending]);
|
|
199
|
+
pending.clear();
|
|
200
|
+
const outcome = await build(paths);
|
|
201
|
+
if (!outcome?.ok) {
|
|
202
|
+
uncommitted = paths;
|
|
203
|
+
success = undefined;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
uncommitted.clear();
|
|
207
|
+
success = { paths, meta: outcome.meta };
|
|
208
|
+
if (!pending.size) {
|
|
209
|
+
await wait(true);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (success) {
|
|
213
|
+
await emit({ kind: 'build-succeeded', ...success });
|
|
134
214
|
}
|
|
135
215
|
}
|
|
136
216
|
}
|
|
137
217
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
218
|
+
const done = (async () => {
|
|
219
|
+
// Defer setup so callers can install their completion handler first.
|
|
220
|
+
await Promise.resolve();
|
|
221
|
+
try {
|
|
222
|
+
await run();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
fatal ??= { error };
|
|
225
|
+
} finally {
|
|
226
|
+
stop();
|
|
227
|
+
const results = await closing!;
|
|
228
|
+
const failure = results.find(result => result.status === 'rejected');
|
|
229
|
+
if (failure?.status === 'rejected') {
|
|
230
|
+
fatal ??= { error: failure.reason };
|
|
231
|
+
}
|
|
141
232
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
void flush();
|
|
145
|
-
}, debounceMs);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function onFileChange(filePath: string, kind: WatchEventKind): void {
|
|
149
|
-
pending.set(filePath, mergeChangeKind(pending.get(filePath), kind));
|
|
150
|
-
schedule();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const startupOutcome = await runBuild();
|
|
154
|
-
if (!startupOutcome.ok) {
|
|
155
|
-
await emit({
|
|
156
|
-
kind: 'build-failed',
|
|
157
|
-
diagnostics: startupOutcome.diagnostics,
|
|
158
|
-
});
|
|
159
|
-
} else {
|
|
160
|
-
applyCommitPlan(startupOutcome.commit);
|
|
161
|
-
snapshot = startupOutcome.snapshot;
|
|
162
|
-
await emit({ kind: 'build-succeeded', meta: startupOutcome.meta });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const watcherEntries = options.compiler
|
|
166
|
-
.getWatchTargets()
|
|
167
|
-
.map(target => ({
|
|
168
|
-
target,
|
|
169
|
-
watcher: chokidar.watch(target.path, {
|
|
170
|
-
ignoreInitial: true,
|
|
171
|
-
atomic: true,
|
|
172
|
-
awaitWriteFinish: { stabilityThreshold: 100 },
|
|
173
|
-
...target.chokidar,
|
|
174
|
-
}),
|
|
175
|
-
}));
|
|
176
|
-
|
|
177
|
-
let readyCount = 0;
|
|
178
|
-
const readyGoal = watcherEntries.length;
|
|
179
|
-
const onReady = async () => {
|
|
180
|
-
readyCount += 1;
|
|
181
|
-
if (readyCount === readyGoal) {
|
|
182
|
-
await emit({ kind: 'watching' });
|
|
233
|
+
if (fatal) {
|
|
234
|
+
throw fatal.error;
|
|
183
235
|
}
|
|
236
|
+
})();
|
|
237
|
+
return {
|
|
238
|
+
done,
|
|
239
|
+
close: () => {
|
|
240
|
+
stop();
|
|
241
|
+
return done;
|
|
242
|
+
},
|
|
184
243
|
};
|
|
185
|
-
|
|
186
|
-
for (const { watcher, target } of watcherEntries) {
|
|
187
|
-
const emitIfIncluded = (filePath: string, kind: WatchEventKind) => {
|
|
188
|
-
if (!target.filter || target.filter(filePath)) {
|
|
189
|
-
onFileChange(filePath, kind);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
watcher.on('add', filePath => emitIfIncluded(filePath, 'add'));
|
|
193
|
-
watcher.on('change', filePath => emitIfIncluded(filePath, 'change'));
|
|
194
|
-
watcher.on('unlink', filePath => emitIfIncluded(filePath, 'unlink'));
|
|
195
|
-
watcher.on('ready', () => void onReady());
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return new Promise(() => {});
|
|
199
244
|
}
|
package/build/watch/index.ts
CHANGED
|
@@ -9,9 +9,16 @@ export async function runWatch(options: { httpPort?: number }): Promise<void> {
|
|
|
9
9
|
distDir: getDistDir(),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const compiler = new TadaWatchCompiler();
|
|
13
|
+
const handle = runWatchEngine({
|
|
14
|
+
targets: compiler.getWatchTargets(),
|
|
15
|
+
build: paths => compiler.build(paths),
|
|
14
16
|
onEvent: event => runtime.onEvent(event),
|
|
15
17
|
debounceMs: 300,
|
|
16
18
|
});
|
|
19
|
+
try {
|
|
20
|
+
await handle.done;
|
|
21
|
+
} finally {
|
|
22
|
+
runtime.close();
|
|
23
|
+
}
|
|
17
24
|
}
|