@abreen/tada 1.13.7 → 1.14.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.
Files changed (49) hide show
  1. package/README.md +46 -0
  2. package/build/generate-content-assets.ts +39 -185
  3. package/build/pipeline.ts +1 -1
  4. package/build/source-model.ts +2 -14
  5. package/build/types.d.ts +18 -18
  6. package/build/utils/html-comments.ts +3 -0
  7. package/build/utils/jdi-runner/TraceRunner.java +227 -38
  8. package/build/utils/markdown-partials.ts +235 -0
  9. package/build/utils/markdown.ts +12 -1
  10. package/build/utils/python-runner/trace_runner.py +58 -13
  11. package/build/utils/render.ts +14 -12
  12. package/build/utils/trace-core.ts +136 -14
  13. package/build/utils/trace-heap.ts +34 -0
  14. package/build/utils/trace-java.ts +60 -7
  15. package/build/utils/trace-layout.ts +58 -13
  16. package/build/utils/trace-python.ts +6 -2
  17. package/build/utils/trace-svg.ts +54 -60
  18. package/build/utils/trace.ts +193 -78
  19. package/build/watch/build-full.ts +26 -52
  20. package/build/watch/build-helpers.ts +66 -0
  21. package/build/watch/build-incremental.ts +32 -60
  22. package/build/watch/build-result.ts +19 -0
  23. package/build/watch/compiler-types.d.ts +2 -2
  24. package/build/watch/compiler.ts +30 -62
  25. package/build/watch/config-paths.ts +56 -0
  26. package/build/watch/engine.ts +199 -0
  27. package/{watch → build/watch}/fs-commit.ts +1 -3
  28. package/build/watch/index.ts +1 -1
  29. package/build/watch/mutations.ts +23 -25
  30. package/build/watch/planner.ts +14 -78
  31. package/build/watch/runtime.ts +26 -38
  32. package/build/watch/snapshot.ts +26 -47
  33. package/build/watch/types.d.ts +84 -0
  34. package/build/watch/validation.ts +1 -1
  35. package/init/content/lectures/02/_pr1.md +1 -1
  36. package/init/content/lectures/02/index.md +2 -2
  37. package/init/content/lectures/02/subdir/_foobar.md +3 -0
  38. package/init/content/lectures/02/subdir/_pr2.md +3 -3
  39. package/package.json +1 -2
  40. package/src/slides/index.ts +6 -2
  41. package/src/trace/index.ts +49 -12
  42. package/src/trace/style.scss +10 -0
  43. package/src/trace/types.d.ts +16 -4
  44. package/build/utils/include.ts +0 -57
  45. package/build/watch-reachability-state.ts +0 -305
  46. package/init/content/lectures/02/subdir/_foobar.html +0 -3
  47. package/watch/engine.ts +0 -296
  48. package/watch/events.ts +0 -10
  49. package/watch/types.d.ts +0 -103
@@ -1,32 +1,23 @@
1
1
  import { compileTemplates, config } from '../templates';
2
2
  import { getDistDir } from '../util';
3
- import { createContentRecord, createPublicRecord } from '../source-records';
4
- import type { CompilerBuildResult } from '../../watch/types';
5
- import type {
6
- TadaBuildMeta,
7
- TraceCache,
8
- WatchTraceOptions,
9
- } from './compiler-types';
10
- import type { TadaWatchPlan } from './planner';
11
- import {
12
- collectHtmlAnalysisByPath,
13
- collectHtmlAssetsByPath,
14
- createSnapshot,
15
- type TadaSnapshot,
16
- } from './snapshot';
3
+ import type { CompilerBuildResult } from './types';
4
+ import type { TraceCache, WatchTraceOptions } from './compiler-types';
5
+ import type { TadaIncrementalWatchPlan } from './planner';
6
+ import { createSnapshot, type TadaSnapshot } from './snapshot';
17
7
  import {
18
8
  copyExistingBuildAssets,
19
9
  ensureHighlighter,
20
10
  makeTempBuildDir,
21
11
  removeDirIfExists,
22
- writeAssets,
23
12
  } from './assets';
24
13
  import { computeMutations } from './mutations';
14
+ import { validateConfig, validateProjectConfigLinks } from './validation';
15
+ import { buildFailedFromError, buildSucceeded } from './build-result';
25
16
  import {
26
- diagnosticsFromMessages,
27
- validateConfig,
28
- validateProjectConfigLinks,
29
- } from './validation';
17
+ buildFailedWithDiagnostics,
18
+ renderContentRecord,
19
+ renderPublicRecord,
20
+ } from './build-helpers';
30
21
 
31
22
  export async function buildIncremental({
32
23
  plan,
@@ -34,23 +25,23 @@ export async function buildIncremental({
34
25
  traceCache,
35
26
  traceOptions,
36
27
  }: {
37
- plan: TadaWatchPlan;
28
+ plan: TadaIncrementalWatchPlan;
38
29
  snapshot: TadaSnapshot;
39
30
  traceCache: TraceCache;
40
31
  traceOptions: WatchTraceOptions;
41
- }): Promise<CompilerBuildResult<TadaSnapshot, TadaBuildMeta>> {
42
- const outputDir = makeTempBuildDir(plan.scan.distDir);
32
+ }): Promise<CompilerBuildResult> {
33
+ const distDir = getDistDir();
34
+ const outputDir = makeTempBuildDir(distDir);
43
35
  try {
44
36
  const siteVariables = snapshot.siteVariables;
45
37
  const configDiagnostics = validateConfig(plan.scan);
46
38
  if (configDiagnostics.length > 0) {
47
- removeDirIfExists(outputDir);
48
- return { ok: false, diagnostics: configDiagnostics };
39
+ return buildFailedWithDiagnostics(outputDir, configDiagnostics);
49
40
  }
50
41
 
51
42
  compileTemplates(siteVariables);
52
43
  await ensureHighlighter(siteVariables);
53
- copyExistingBuildAssets(getDistDir(), outputDir, snapshot.assetFiles);
44
+ copyExistingBuildAssets(distDir, outputDir, snapshot.assetFiles);
54
45
 
55
46
  const nextContentRecords = new Map(snapshot.contentRecords);
56
47
  const nextPublicRecords = new Map(snapshot.publicRecords);
@@ -64,25 +55,27 @@ export async function buildIncremental({
64
55
 
65
56
  for (const sourcePath of plan.contentToRender) {
66
57
  nextContentRecords.delete(sourcePath);
67
- const record = createContentRecord({
58
+ const record = renderContentRecord({
68
59
  filePath: sourcePath,
69
60
  siteVariables,
70
61
  scan: plan.scan,
71
62
  assetFiles: snapshot.assetFiles,
72
63
  outputDir,
73
64
  traceCache,
74
- traceToolAvailability: traceOptions.toolAvailability,
75
- cachedTraceSourceDir: getDistDir(),
65
+ traceOptions,
66
+ cachedTraceSourceDir: distDir,
76
67
  });
77
- if (record.outputs.size > 0) {
78
- writeAssets(outputDir, record.outputs);
68
+ if (record) {
79
69
  nextContentRecords.set(sourcePath, record);
80
70
  }
81
71
  }
82
72
 
83
73
  for (const sourcePath of plan.publicToRender) {
84
74
  nextPublicRecords.delete(sourcePath);
85
- const record = createPublicRecord(sourcePath, plan.scan.publicDir);
75
+ const record = renderPublicRecord({
76
+ filePath: sourcePath,
77
+ publicDir: plan.scan.publicDir,
78
+ });
86
79
  nextPublicRecords.set(sourcePath, record);
87
80
  }
88
81
 
@@ -97,11 +90,10 @@ export async function buildIncremental({
97
90
  });
98
91
 
99
92
  const linkDiagnostics = validateProjectConfigLinks(
100
- nextSnapshot.validTargets,
93
+ nextSnapshot.scan.validTargets,
101
94
  );
102
95
  if (linkDiagnostics.length > 0) {
103
- removeDirIfExists(outputDir);
104
- return { ok: false, diagnostics: linkDiagnostics };
96
+ return buildFailedWithDiagnostics(outputDir, linkDiagnostics);
105
97
  }
106
98
 
107
99
  const forceSourcePaths = new Set([
@@ -112,35 +104,15 @@ export async function buildIncremental({
112
104
  snapshot,
113
105
  nextSnapshot,
114
106
  forceSourcePaths,
115
- ).map(mutation =>
116
- mutation.kind === 'write'
117
- ? {
118
- kind: 'write' as const,
119
- path: mutation.path,
120
- content: mutation.content!,
121
- }
122
- : { kind: 'delete' as const, path: mutation.path },
123
107
  );
124
108
  removeDirIfExists(outputDir);
125
- return {
126
- ok: true,
127
- snapshot: nextSnapshot,
128
- commit: { kind: 'apply-mutations', rootDir: getDistDir(), mutations },
129
- meta: {
130
- htmlAssetsByPath: collectHtmlAssetsByPath(nextSnapshot.contentRecords),
131
- htmlAnalysisByPath: collectHtmlAnalysisByPath(
132
- nextSnapshot.contentRecords,
133
- ),
134
- siteVariables,
135
- },
136
- };
109
+ return buildSucceeded(nextSnapshot, {
110
+ kind: 'apply-mutations',
111
+ rootDir: distDir,
112
+ mutations,
113
+ });
137
114
  } catch (error) {
138
115
  removeDirIfExists(outputDir);
139
- return {
140
- ok: false,
141
- diagnostics: diagnosticsFromMessages([
142
- error instanceof Error ? error.message : String(error),
143
- ]),
144
- };
116
+ return buildFailedFromError(error);
145
117
  }
146
118
  }
@@ -0,0 +1,19 @@
1
+ import type { CommitPlan, CompilerBuildResult } from './types';
2
+ import { createBuildMeta, type TadaSnapshot } from './snapshot';
3
+ import { diagnosticsFromMessages } from './validation';
4
+
5
+ export function buildSucceeded(
6
+ snapshot: TadaSnapshot,
7
+ commit: CommitPlan,
8
+ ): CompilerBuildResult {
9
+ return { ok: true, snapshot, commit, meta: createBuildMeta(snapshot) };
10
+ }
11
+
12
+ export function buildFailedFromError(error: unknown): CompilerBuildResult {
13
+ return {
14
+ ok: false,
15
+ diagnostics: diagnosticsFromMessages([
16
+ error instanceof Error ? error.message : String(error),
17
+ ]),
18
+ };
19
+ }
@@ -7,9 +7,9 @@ import type {
7
7
  export interface TraceCacheEntry {
8
8
  manifestUrl: string;
9
9
  artifactId: string;
10
- highlightedSource: string;
10
+ highlightedSources: { file: string; highlightedSource: string }[];
11
11
  totalSteps: number;
12
- mtime: number;
12
+ sourceMtims: Record<string, number>;
13
13
  }
14
14
 
15
15
  export type TraceCache = Map<string, TraceCacheEntry>;
@@ -1,30 +1,20 @@
1
1
  import path from 'path';
2
- import {
3
- getProjectConfigBaseName,
4
- getSiteConfigBaseName,
5
- getSupportedConfigFilePaths,
6
- } from '../config-files';
7
- import { getDevSiteVariables } from '../site-variables';
8
2
  import { getProjectConfigDir } from '../templates';
9
3
  import { getContentDir, getPublicDir } from '../util';
10
4
  import type {
11
5
  ChangeBatch,
12
6
  CompilerBuildResult,
13
- CompilerPlanResult,
14
7
  WatchCompiler,
15
8
  WatchTarget,
16
- } from '../../watch/types';
9
+ } from './types';
17
10
  import { buildFull } from './build-full';
18
11
  import { buildIncremental } from './build-incremental';
19
- import type {
20
- TadaBuildMeta,
21
- TraceCache,
22
- WatchTraceOptions,
23
- } from './compiler-types';
12
+ import type { TraceCache, WatchTraceOptions } from './compiler-types';
24
13
  import { checkTraceToolAvailability, isTraceSourceFile } from '../utils/trace';
25
- import { createTadaWatchPlan, type TadaWatchPlan } from './planner';
26
- import { scanProject, updateProjectScan } from '../source-model';
14
+ import { createTadaWatchPlan } from './planner';
15
+ import { updateProjectScan } from '../source-model';
27
16
  import type { TadaSnapshot } from './snapshot';
17
+ import { getWatchConfigFilePaths } from './config-paths';
28
18
 
29
19
  export function invalidateTraceCacheForBatch(
30
20
  traceCache: TraceCache,
@@ -33,16 +23,16 @@ export function invalidateTraceCacheForBatch(
33
23
  for (const change of batch.changes) {
34
24
  const sourcePath = path.resolve(change.path);
35
25
  if (isTraceSourceFile(sourcePath)) {
36
- traceCache.delete(sourcePath);
26
+ for (const [cacheKey, entry] of traceCache) {
27
+ if (sourcePath in entry.sourceMtims) {
28
+ traceCache.delete(cacheKey);
29
+ }
30
+ }
37
31
  }
38
32
  }
39
33
  }
40
34
 
41
- export class TadaWatchCompiler implements WatchCompiler<
42
- TadaSnapshot,
43
- TadaWatchPlan,
44
- TadaBuildMeta
45
- > {
35
+ export class TadaWatchCompiler implements WatchCompiler {
46
36
  private traceCache: TraceCache;
47
37
  private traceOptions: WatchTraceOptions;
48
38
 
@@ -55,23 +45,11 @@ export class TadaWatchCompiler implements WatchCompiler<
55
45
  const contentDir = getContentDir();
56
46
  const publicDir = getPublicDir();
57
47
  const projectConfigDir = path.resolve(getProjectConfigDir());
58
- const configFilePaths = new Set([
59
- ...getSupportedConfigFilePaths('.', getSiteConfigBaseName('dev')).map(
60
- filePath => path.resolve(filePath),
61
- ),
62
- ...getSupportedConfigFilePaths(
63
- projectConfigDir,
64
- getProjectConfigBaseName('nav'),
65
- ).map(filePath => path.resolve(filePath)),
66
- ...getSupportedConfigFilePaths(
67
- projectConfigDir,
68
- getProjectConfigBaseName('authors'),
69
- ).map(filePath => path.resolve(filePath)),
70
- ]);
48
+ const configFilePaths = getWatchConfigFilePaths();
71
49
 
72
50
  return [
73
- { path: contentDir },
74
- { path: publicDir },
51
+ { path: contentDir, chokidar: { usePolling: true } },
52
+ { path: publicDir, chokidar: { usePolling: true } },
75
53
  {
76
54
  path: projectConfigDir,
77
55
  chokidar: { depth: 0 },
@@ -80,34 +58,24 @@ export class TadaWatchCompiler implements WatchCompiler<
80
58
  ];
81
59
  }
82
60
 
83
- async buildInitial(): Promise<
84
- CompilerBuildResult<TadaSnapshot, TadaBuildMeta>
85
- > {
86
- return buildFull({
87
- traceCache: this.traceCache,
88
- traceOptions: this.traceOptions,
89
- });
90
- }
91
-
92
- async plan(
61
+ async build(
93
62
  snapshot: TadaSnapshot | undefined,
94
- batch: ChangeBatch,
95
- ): Promise<CompilerPlanResult<TadaWatchPlan>> {
96
- invalidateTraceCacheForBatch(this.traceCache, batch);
97
- const scan = snapshot
98
- ? updateProjectScan(snapshot, batch)
99
- : scanProject(getDevSiteVariables());
100
- return {
101
- kind: 'build',
102
- plan: createTadaWatchPlan({ snapshot, batch, scan }),
103
- };
104
- }
63
+ batch?: ChangeBatch,
64
+ ): Promise<CompilerBuildResult> {
65
+ if (batch) {
66
+ invalidateTraceCacheForBatch(this.traceCache, batch);
67
+ }
105
68
 
106
- async run(
107
- plan: TadaWatchPlan,
108
- snapshot: TadaSnapshot | undefined,
109
- ): Promise<CompilerBuildResult<TadaSnapshot, TadaBuildMeta>> {
110
- if (plan.kind === 'full' || !snapshot) {
69
+ if (!snapshot || !batch) {
70
+ return buildFull({
71
+ traceCache: this.traceCache,
72
+ traceOptions: this.traceOptions,
73
+ });
74
+ }
75
+
76
+ const scan = updateProjectScan(snapshot.scan, batch);
77
+ const plan = createTadaWatchPlan({ snapshot, batch, scan });
78
+ if (plan.kind === 'full') {
111
79
  return buildFull({
112
80
  traceCache: this.traceCache,
113
81
  traceOptions: this.traceOptions,
@@ -0,0 +1,56 @@
1
+ import path from 'path';
2
+ import {
3
+ getProjectConfigBaseName,
4
+ getSiteConfigBaseName,
5
+ getSupportedConfigFilePaths,
6
+ } from '../config-files';
7
+ import { getProjectConfigDir } from '../templates';
8
+
9
+ export type WatchConfigPathKind = 'site' | 'nav' | 'authors';
10
+
11
+ function getConfigPathEntries(): Array<{
12
+ kind: WatchConfigPathKind;
13
+ paths: string[];
14
+ }> {
15
+ const projectConfigDir = getProjectConfigDir();
16
+ return [
17
+ {
18
+ kind: 'site',
19
+ paths: getSupportedConfigFilePaths('.', getSiteConfigBaseName('dev')),
20
+ },
21
+ {
22
+ kind: 'nav',
23
+ paths: getSupportedConfigFilePaths(
24
+ projectConfigDir,
25
+ getProjectConfigBaseName('nav'),
26
+ ),
27
+ },
28
+ {
29
+ kind: 'authors',
30
+ paths: getSupportedConfigFilePaths(
31
+ projectConfigDir,
32
+ getProjectConfigBaseName('authors'),
33
+ ),
34
+ },
35
+ ];
36
+ }
37
+
38
+ function getWatchConfigPathKinds(): Map<string, WatchConfigPathKind> {
39
+ return new Map(
40
+ getConfigPathEntries().flatMap(entry =>
41
+ entry.paths.map(
42
+ filePath => [path.resolve(filePath), entry.kind] as const,
43
+ ),
44
+ ),
45
+ );
46
+ }
47
+
48
+ export function getWatchConfigFilePaths(): Set<string> {
49
+ return new Set(getWatchConfigPathKinds().keys());
50
+ }
51
+
52
+ export function classifyWatchConfigPath(
53
+ filePath: string,
54
+ ): WatchConfigPathKind | undefined {
55
+ return getWatchConfigPathKinds().get(path.resolve(filePath));
56
+ }
@@ -0,0 +1,199 @@
1
+ import chokidar from 'chokidar';
2
+ import { applyCommitPlan } from './fs-commit';
3
+ import type {
4
+ ChangeBatch,
5
+ CompilerBuildResult,
6
+ WatchEngineOptions,
7
+ WatchEventKind,
8
+ WatchLifecycleEvent,
9
+ } from './types';
10
+ import type { TadaSnapshot } from './snapshot';
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 mapBatch(batch: ChangeBatch): Map<string, WatchEventKind> {
21
+ return new Map(batch.changes.map(change => [change.path, change.kind]));
22
+ }
23
+
24
+ function mergeChangeKind(
25
+ previous: WatchEventKind | undefined,
26
+ next: WatchEventKind,
27
+ ): WatchEventKind {
28
+ if (!previous) {
29
+ return next;
30
+ }
31
+ if (previous === 'add' && next === 'unlink') {
32
+ return 'change';
33
+ }
34
+ if (previous === 'unlink' && next === 'add') {
35
+ return 'change';
36
+ }
37
+ if (previous === 'add' || next === 'add') {
38
+ return 'add';
39
+ }
40
+ if (previous === 'unlink' || next === 'unlink') {
41
+ return 'unlink';
42
+ }
43
+ return 'change';
44
+ }
45
+
46
+ function toDiagnostics(error: unknown): { message: string }[] {
47
+ if (error instanceof Error) {
48
+ return [{ message: error.message }];
49
+ }
50
+ return [{ message: String(error) }];
51
+ }
52
+
53
+ export async function runWatchEngine(
54
+ options: WatchEngineOptions,
55
+ ): Promise<void> {
56
+ const debounceMs = options.debounceMs ?? 300;
57
+ let snapshot: TadaSnapshot | undefined;
58
+ let uncommitted = new Map<string, WatchEventKind>();
59
+ let pending = new Map<string, WatchEventKind>();
60
+ let timer: ReturnType<typeof setTimeout> | null = null;
61
+ let running = false;
62
+
63
+ async function emit(event: WatchLifecycleEvent): Promise<void> {
64
+ await options.onEvent?.(event);
65
+ }
66
+
67
+ async function waitForQuietPeriod(): Promise<void> {
68
+ await new Promise(resolve => setTimeout(resolve, debounceMs));
69
+ }
70
+
71
+ async function runBuild(batch?: ChangeBatch): Promise<CompilerBuildResult> {
72
+ await emit({ kind: 'build-started', batch });
73
+ try {
74
+ return await options.compiler.build(snapshot, batch);
75
+ } catch (error) {
76
+ return { ok: false, diagnostics: toDiagnostics(error) };
77
+ }
78
+ }
79
+
80
+ async function flush(): Promise<void> {
81
+ if (running) {
82
+ return;
83
+ }
84
+ running = true;
85
+ try {
86
+ let finalSuccess:
87
+ | {
88
+ batch?: ChangeBatch;
89
+ result: Extract<CompilerBuildResult, { ok: true }>;
90
+ }
91
+ | undefined;
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));
97
+ }
98
+
99
+ const batch = normalizeBatch(changes);
100
+ pending = new Map();
101
+ const outcome = await runBuild(batch);
102
+
103
+ if (!outcome.ok) {
104
+ uncommitted = mapBatch(batch);
105
+ await emit({
106
+ kind: 'build-failed',
107
+ batch,
108
+ diagnostics: outcome.diagnostics,
109
+ });
110
+ return;
111
+ }
112
+
113
+ applyCommitPlan(outcome.commit);
114
+ snapshot = outcome.snapshot;
115
+ uncommitted = new Map();
116
+ finalSuccess = { batch, result: outcome };
117
+
118
+ if (pending.size === 0) {
119
+ await waitForQuietPeriod();
120
+ }
121
+ }
122
+
123
+ if (finalSuccess) {
124
+ await emit({
125
+ kind: 'build-succeeded',
126
+ batch: finalSuccess.batch,
127
+ meta: finalSuccess.result.meta,
128
+ });
129
+ }
130
+ } finally {
131
+ running = false;
132
+ if (pending.size > 0) {
133
+ schedule();
134
+ }
135
+ }
136
+ }
137
+
138
+ function schedule(): void {
139
+ if (timer) {
140
+ clearTimeout(timer);
141
+ }
142
+ timer = setTimeout(() => {
143
+ timer = null;
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' });
183
+ }
184
+ };
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
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { globals, type Globals } from '../build/globals';
3
+ import { globals, type Globals } from '../globals';
4
4
  import type {
5
5
  ApplyMutationsCommitPlan,
6
6
  CommitPlan,
@@ -87,8 +87,6 @@ function applyFileMutation(
87
87
  const stagedPath = path.join(stagedRoot, mutation.path);
88
88
 
89
89
  if (mutation.kind === 'write') {
90
- ensureParentDir(stagedPath);
91
- fs.writeFileSync(stagedPath, mutation.content);
92
90
  ensureParentDir(targetPath);
93
91
  renameWithRetry(stagedPath, targetPath, globals);
94
92
  return;
@@ -1,5 +1,5 @@
1
1
  import { getDistDir } from '../util';
2
- import { runWatchEngine } from '../../watch/engine';
2
+ import { runWatchEngine } from './engine';
3
3
  import { TadaWatchCompiler } from './compiler';
4
4
  import { TadaWatchRuntime } from './runtime';
5
5
 
@@ -1,4 +1,5 @@
1
- import type { TadaSnapshot } from './snapshot';
1
+ import type { FileMutation } from './types';
2
+ import type { TadaOutputOwner, TadaSnapshot } from './snapshot';
2
3
 
3
4
  function outputsEqual(
4
5
  left: string | Buffer | undefined,
@@ -15,16 +16,25 @@ function outputsEqual(
15
16
  return Buffer.compare(leftBuffer, rightBuffer) === 0;
16
17
  }
17
18
 
19
+ function getOutputContent(
20
+ snapshot: TadaSnapshot,
21
+ owner: TadaOutputOwner | undefined,
22
+ outputPath: string,
23
+ ): string | Buffer | undefined {
24
+ if (!owner) {
25
+ return undefined;
26
+ }
27
+ const records =
28
+ owner.kind === 'content' ? snapshot.contentRecords : snapshot.publicRecords;
29
+ return records.get(owner.sourcePath)?.outputs.get(outputPath);
30
+ }
31
+
18
32
  export function computeMutations(
19
33
  previous: TadaSnapshot,
20
34
  next: TadaSnapshot,
21
35
  forceSourcePaths: Set<string> = new Set(),
22
- ): { path: string; kind: 'write' | 'delete'; content?: string | Buffer }[] {
23
- const mutations: {
24
- path: string;
25
- kind: 'write' | 'delete';
26
- content?: string | Buffer;
27
- }[] = [];
36
+ ): FileMutation[] {
37
+ const mutations: FileMutation[] = [];
28
38
  const allPaths = new Set([
29
39
  ...previous.outputOwners.keys(),
30
40
  ...next.outputOwners.keys(),
@@ -34,24 +44,12 @@ export function computeMutations(
34
44
  const previousOwner = previous.outputOwners.get(outputPath);
35
45
  const nextOwner = next.outputOwners.get(outputPath);
36
46
 
37
- const previousContent =
38
- previousOwner?.kind === 'content'
39
- ? previous.contentRecords
40
- .get(previousOwner.sourcePath)
41
- ?.outputs.get(outputPath)
42
- : previousOwner
43
- ? previous.publicRecords
44
- .get(previousOwner.sourcePath)
45
- ?.outputs.get(outputPath)
46
- : undefined;
47
- const nextContent =
48
- nextOwner?.kind === 'content'
49
- ? next.contentRecords.get(nextOwner.sourcePath)?.outputs.get(outputPath)
50
- : nextOwner
51
- ? next.publicRecords
52
- .get(nextOwner.sourcePath)
53
- ?.outputs.get(outputPath)
54
- : undefined;
47
+ const previousContent = getOutputContent(
48
+ previous,
49
+ previousOwner,
50
+ outputPath,
51
+ );
52
+ const nextContent = getOutputContent(next, nextOwner, outputPath);
55
53
 
56
54
  if (!nextOwner) {
57
55
  mutations.push({ path: outputPath, kind: 'delete' });