@abreen/tada 1.8.1 → 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.
Files changed (79) hide show
  1. package/README.md +24 -34
  2. package/bin/tada.ts +1 -1
  3. package/bin/validators.ts +1 -1
  4. package/build/apply-base-path-plugin.ts +79 -17
  5. package/build/bundle.ts +9 -1
  6. package/build/content-watch.ts +3 -3
  7. package/build/copy.ts +30 -0
  8. package/build/generate-content-assets.ts +45 -59
  9. package/build/pagefind.ts +0 -7
  10. package/build/serve.ts +4 -0
  11. package/build/templates.ts +0 -2
  12. package/build/types.d.ts +19 -1
  13. package/build/util.ts +3 -0
  14. package/build/utils/content-files.ts +98 -0
  15. package/build/utils/include.ts +4 -0
  16. package/build/utils/python-runner/trace_runner.py +272 -0
  17. package/build/utils/render.ts +47 -15
  18. package/build/utils/source-template.ts +27 -0
  19. package/build/utils/trace-core.ts +203 -0
  20. package/build/utils/trace-java.ts +132 -0
  21. package/build/utils/trace-layout.ts +31 -3
  22. package/build/utils/trace-python.ts +26 -0
  23. package/build/utils/trace.ts +174 -294
  24. package/build/validate-internal-links-plugin.ts +16 -4
  25. package/build/watch/assets.ts +111 -0
  26. package/build/watch/build-full.ts +124 -0
  27. package/build/watch/build-incremental.ts +139 -0
  28. package/build/watch/compiler-types.d.ts +19 -0
  29. package/build/watch/compiler.ts +112 -0
  30. package/build/watch/index.ts +22 -0
  31. package/build/watch/mutations.ts +75 -0
  32. package/build/watch/planner.ts +230 -0
  33. package/build/watch/records.ts +190 -0
  34. package/build/watch/runtime.ts +143 -0
  35. package/build/watch/snapshot.ts +602 -0
  36. package/build/watch/validation.ts +36 -0
  37. package/build/watch-reload-client.ts +10 -5
  38. package/init/content/labs/01/index.md +6 -0
  39. package/init/content/labs/01/trace_demo.py +16 -0
  40. package/init/content/lectures/01/Rectangle.java +2 -0
  41. package/init/content/lectures/01/index.md +6 -0
  42. package/init/content/lectures/01/rectangle.py +5 -0
  43. package/package.json +42 -23
  44. package/python/command.ts +95 -0
  45. package/python/module.ts +18 -0
  46. package/src/_alerts.scss +11 -8
  47. package/src/_base.scss +25 -29
  48. package/src/_layout.scss +17 -14
  49. package/src/_mixins.scss +10 -9
  50. package/src/anchor/style.scss +4 -2
  51. package/src/code/index.ts +9 -2
  52. package/src/code/style.scss +14 -13
  53. package/src/code.scss +14 -13
  54. package/src/header/_base.scss +21 -24
  55. package/src/header/style.scss +16 -12
  56. package/src/index.ts +3 -0
  57. package/src/literate/style.scss +4 -4
  58. package/src/navigate/index.ts +43 -279
  59. package/src/navigate/lifecycle.ts +1 -1
  60. package/src/navigate/runtime.ts +341 -0
  61. package/src/page-update/README.md +4 -0
  62. package/src/page-update/index.ts +280 -0
  63. package/src/page-update/style.scss +53 -0
  64. package/src/print/style.scss +4 -4
  65. package/src/question/style.scss +10 -6
  66. package/src/search/index.ts +9 -12
  67. package/src/search/style.scss +44 -42
  68. package/src/style.scss +63 -58
  69. package/src/timezone/style.scss +5 -5
  70. package/src/toc/style.scss +17 -16
  71. package/src/trace/style.scss +27 -27
  72. package/src/validators.ts +45 -0
  73. package/templates/_footer.html +1 -1
  74. package/templates/_theme.scss +8 -19
  75. package/watch/engine.ts +296 -0
  76. package/watch/events.ts +10 -0
  77. package/watch/fs-commit.ts +102 -0
  78. package/watch/types.d.ts +103 -0
  79. package/build/watch.ts +0 -493
@@ -0,0 +1,111 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { bundle } from '../bundle';
4
+ import { copyFonts } from '../generate-fonts';
5
+ import { copyKatexAssets } from '../generate-katex-assets';
6
+ import { generateFavicons } from '../generate-favicon';
7
+ import { generateWebAppManifest } from '../generate-web-app-manifest';
8
+ import { getPackageDir, toPosix } from '../util';
9
+ import { initHighlighter } from '../utils/shiki-highlighter';
10
+ import type { SiteVariables } from '../types';
11
+
12
+ const RELOAD_CLIENT_PATH = path.resolve(
13
+ getPackageDir(),
14
+ 'build/watch-reload-client.ts',
15
+ );
16
+
17
+ export function makeTempBuildDir(distDir: string): string {
18
+ const parentDir = path.dirname(distDir);
19
+ const prefix = `${path.basename(distDir)}-watch-`;
20
+ return fs.mkdtempSync(path.join(parentDir, prefix));
21
+ }
22
+
23
+ export function removeDirIfExists(dir: string): void {
24
+ fs.rmSync(dir, { recursive: true, force: true });
25
+ }
26
+
27
+ export async function ensureHighlighter(
28
+ siteVariables: SiteVariables,
29
+ ): Promise<void> {
30
+ const langs = [
31
+ ...new Set([
32
+ 'plaintext',
33
+ 'text',
34
+ ...Object.values(siteVariables.codeLanguages || {}),
35
+ ]),
36
+ ];
37
+ await initHighlighter(langs);
38
+ }
39
+
40
+ export async function bundleWatchAssets(
41
+ outputDir: string,
42
+ siteVariables: SiteVariables,
43
+ wsPort: number,
44
+ ): Promise<string[]> {
45
+ const appAssets = await bundle(siteVariables, {
46
+ mode: 'development',
47
+ distDir: outputDir,
48
+ });
49
+ const reloadAssets = await Bun.build({
50
+ entrypoints: [RELOAD_CLIENT_PATH],
51
+ outdir: outputDir,
52
+ naming: '[name].bundle.[ext]',
53
+ sourcemap: 'inline',
54
+ define: { __WEBSOCKET_PORT__: String(wsPort) },
55
+ });
56
+ return [
57
+ ...appAssets,
58
+ ...reloadAssets.outputs.map(output =>
59
+ toPosix(path.relative(outputDir, output.path)),
60
+ ),
61
+ ];
62
+ }
63
+
64
+ export async function populateStaticAssets(
65
+ outputDir: string,
66
+ siteVariables: SiteVariables,
67
+ ): Promise<void> {
68
+ copyFonts(outputDir);
69
+ copyKatexAssets(outputDir);
70
+ if (siteVariables.features.favicon !== false) {
71
+ await generateFavicons(siteVariables, outputDir);
72
+ generateWebAppManifest(siteVariables, outputDir);
73
+ }
74
+ }
75
+
76
+ export function copyExistingBuildAssets(
77
+ distDir: string,
78
+ outputDir: string,
79
+ assetFiles: string[],
80
+ ): void {
81
+ const relPaths = [
82
+ ...assetFiles,
83
+ 'inter/InterVariable.woff2',
84
+ 'google-sans-code/GoogleSansCodeVariable.woff2',
85
+ ];
86
+
87
+ for (const relPath of relPaths) {
88
+ const sourcePath = path.join(distDir, relPath);
89
+ if (!fs.existsSync(sourcePath)) {
90
+ continue;
91
+ }
92
+ const destinationPath = path.join(outputDir, relPath);
93
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
94
+ try {
95
+ fs.linkSync(sourcePath, destinationPath);
96
+ } catch {
97
+ fs.copyFileSync(sourcePath, destinationPath);
98
+ }
99
+ }
100
+ }
101
+
102
+ export function writeAssets(
103
+ outputDir: string,
104
+ assets: Map<string, string | Buffer>,
105
+ ): void {
106
+ for (const [assetPath, content] of assets) {
107
+ const outputPath = path.join(outputDir, assetPath);
108
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
109
+ fs.writeFileSync(outputPath, content);
110
+ }
111
+ }
@@ -0,0 +1,124 @@
1
+ import { compileTemplates, json } from '../templates';
2
+ import { getDevSiteVariables } from '../site-variables';
3
+ import { getDistDir } from '../util';
4
+ import type { CompilerBuildResult } from '../../watch/types';
5
+ import {
6
+ collectHtmlAssetsByPath,
7
+ createSnapshot,
8
+ scanProject,
9
+ type TadaSourceRecord,
10
+ type TadaSnapshot,
11
+ } from './snapshot';
12
+ import type {
13
+ TadaBuildMeta,
14
+ TraceCache,
15
+ WatchTraceOptions,
16
+ } from './compiler-types';
17
+ import {
18
+ bundleWatchAssets,
19
+ ensureHighlighter,
20
+ makeTempBuildDir,
21
+ populateStaticAssets,
22
+ removeDirIfExists,
23
+ writeAssets,
24
+ } from './assets';
25
+ import { createPublicRecord, renderContentRecord } from './records';
26
+ import {
27
+ diagnosticsFromMessages,
28
+ validateConfig,
29
+ validateJsonLinks,
30
+ } from './validation';
31
+
32
+ export async function buildFull({
33
+ wsPort,
34
+ traceCache,
35
+ traceOptions,
36
+ }: {
37
+ wsPort: number;
38
+ traceCache: TraceCache;
39
+ traceOptions: WatchTraceOptions;
40
+ }): Promise<CompilerBuildResult<TadaSnapshot, TadaBuildMeta>> {
41
+ const distDir = getDistDir();
42
+ const outputDir = makeTempBuildDir(distDir);
43
+ try {
44
+ const siteVariables = getDevSiteVariables();
45
+ const scan = scanProject(siteVariables);
46
+ const configDiagnostics = validateConfig(scan);
47
+ if (configDiagnostics.length > 0) {
48
+ removeDirIfExists(outputDir);
49
+ return { ok: false, diagnostics: configDiagnostics };
50
+ }
51
+
52
+ compileTemplates(siteVariables);
53
+ await ensureHighlighter(siteVariables);
54
+
55
+ const assetFiles = await bundleWatchAssets(
56
+ outputDir,
57
+ siteVariables,
58
+ wsPort,
59
+ );
60
+ await populateStaticAssets(outputDir, siteVariables);
61
+
62
+ const contentRecords = new Map<string, TadaSourceRecord>();
63
+ for (const filePath of scan.contentFiles) {
64
+ const record = renderContentRecord({
65
+ filePath,
66
+ siteVariables,
67
+ scan,
68
+ assetFiles,
69
+ outputDir,
70
+ traceCache,
71
+ traceToolAvailability: traceOptions.toolAvailability,
72
+ cachedTraceSourceDir: distDir,
73
+ });
74
+ if (record.outputs.size > 0) {
75
+ contentRecords.set(filePath, record);
76
+ }
77
+ }
78
+
79
+ const publicRecords = new Map<string, TadaSourceRecord>();
80
+ for (const filePath of scan.publicFiles) {
81
+ const record = createPublicRecord(filePath, scan.publicDir);
82
+ writeAssets(outputDir, record.outputs);
83
+ publicRecords.set(filePath, record);
84
+ }
85
+
86
+ const linkDiagnostics = validateJsonLinks(scan.validTargets);
87
+ if (linkDiagnostics.length > 0) {
88
+ removeDirIfExists(outputDir);
89
+ return { ok: false, diagnostics: linkDiagnostics };
90
+ }
91
+
92
+ const nextSnapshot = createSnapshot({
93
+ siteVariables,
94
+ assetFiles,
95
+ navData: json('nav.json'),
96
+ authorsData: json('authors.json'),
97
+ scan,
98
+ contentRecords,
99
+ publicRecords,
100
+ });
101
+
102
+ return {
103
+ ok: true,
104
+ snapshot: nextSnapshot,
105
+ commit: {
106
+ kind: 'replace-root',
107
+ stagedPath: outputDir,
108
+ targetPath: distDir,
109
+ },
110
+ meta: {
111
+ htmlAssetsByPath: collectHtmlAssetsByPath(nextSnapshot.contentRecords),
112
+ siteVariables,
113
+ },
114
+ };
115
+ } catch (error) {
116
+ removeDirIfExists(outputDir);
117
+ return {
118
+ ok: false,
119
+ diagnostics: diagnosticsFromMessages([
120
+ error instanceof Error ? error.message : String(error),
121
+ ]),
122
+ };
123
+ }
124
+ }
@@ -0,0 +1,139 @@
1
+ import { compileTemplates, json } from '../templates';
2
+ import { getDistDir } from '../util';
3
+ import type { CompilerBuildResult } from '../../watch/types';
4
+ import type {
5
+ TadaBuildMeta,
6
+ TraceCache,
7
+ WatchTraceOptions,
8
+ } from './compiler-types';
9
+ import type { TadaWatchPlan } from './planner';
10
+ import {
11
+ collectHtmlAssetsByPath,
12
+ createSnapshot,
13
+ type TadaSnapshot,
14
+ } from './snapshot';
15
+ import {
16
+ copyExistingBuildAssets,
17
+ ensureHighlighter,
18
+ makeTempBuildDir,
19
+ removeDirIfExists,
20
+ } from './assets';
21
+ import { computeMutations } from './mutations';
22
+ import { createPublicRecord, renderContentRecord } from './records';
23
+ import {
24
+ diagnosticsFromMessages,
25
+ validateConfig,
26
+ validateJsonLinks,
27
+ } from './validation';
28
+
29
+ export async function buildIncremental({
30
+ plan,
31
+ snapshot,
32
+ traceCache,
33
+ traceOptions,
34
+ }: {
35
+ plan: TadaWatchPlan;
36
+ snapshot: TadaSnapshot;
37
+ traceCache: TraceCache;
38
+ traceOptions: WatchTraceOptions;
39
+ }): Promise<CompilerBuildResult<TadaSnapshot, TadaBuildMeta>> {
40
+ const outputDir = makeTempBuildDir(plan.scan.distDir);
41
+ try {
42
+ const siteVariables = snapshot.siteVariables;
43
+ const configDiagnostics = validateConfig(plan.scan);
44
+ if (configDiagnostics.length > 0) {
45
+ removeDirIfExists(outputDir);
46
+ return { ok: false, diagnostics: configDiagnostics };
47
+ }
48
+
49
+ compileTemplates(siteVariables);
50
+ await ensureHighlighter(siteVariables);
51
+ copyExistingBuildAssets(getDistDir(), outputDir, snapshot.assetFiles);
52
+
53
+ const nextContentRecords = new Map(snapshot.contentRecords);
54
+ const nextPublicRecords = new Map(snapshot.publicRecords);
55
+
56
+ for (const sourcePath of plan.contentToRemove) {
57
+ nextContentRecords.delete(sourcePath);
58
+ }
59
+ for (const sourcePath of plan.publicToRemove) {
60
+ nextPublicRecords.delete(sourcePath);
61
+ }
62
+
63
+ for (const sourcePath of plan.contentToRender) {
64
+ nextContentRecords.delete(sourcePath);
65
+ const record = renderContentRecord({
66
+ filePath: sourcePath,
67
+ siteVariables,
68
+ scan: plan.scan,
69
+ assetFiles: snapshot.assetFiles,
70
+ outputDir,
71
+ traceCache,
72
+ traceToolAvailability: traceOptions.toolAvailability,
73
+ cachedTraceSourceDir: getDistDir(),
74
+ persistOutputs: false,
75
+ });
76
+ if (record.outputs.size > 0) {
77
+ nextContentRecords.set(sourcePath, record);
78
+ }
79
+ }
80
+
81
+ for (const sourcePath of plan.publicToRender) {
82
+ nextPublicRecords.delete(sourcePath);
83
+ const record = createPublicRecord(sourcePath, plan.scan.publicDir);
84
+ nextPublicRecords.set(sourcePath, record);
85
+ }
86
+
87
+ const nextSnapshot = createSnapshot({
88
+ siteVariables,
89
+ assetFiles: snapshot.assetFiles,
90
+ navData: json('nav.json'),
91
+ authorsData: json('authors.json'),
92
+ scan: plan.scan,
93
+ contentRecords: nextContentRecords,
94
+ publicRecords: nextPublicRecords,
95
+ });
96
+
97
+ const linkDiagnostics = validateJsonLinks(nextSnapshot.validTargets);
98
+ if (linkDiagnostics.length > 0) {
99
+ removeDirIfExists(outputDir);
100
+ return { ok: false, diagnostics: linkDiagnostics };
101
+ }
102
+
103
+ const forceSourcePaths = new Set([
104
+ ...plan.contentToRender,
105
+ ...plan.publicToRender,
106
+ ]);
107
+ const mutations = computeMutations(
108
+ snapshot,
109
+ nextSnapshot,
110
+ forceSourcePaths,
111
+ ).map(mutation =>
112
+ mutation.kind === 'write'
113
+ ? {
114
+ kind: 'write' as const,
115
+ path: mutation.path,
116
+ content: mutation.content!,
117
+ }
118
+ : { kind: 'delete' as const, path: mutation.path },
119
+ );
120
+ removeDirIfExists(outputDir);
121
+ return {
122
+ ok: true,
123
+ snapshot: nextSnapshot,
124
+ commit: { kind: 'apply-mutations', rootDir: getDistDir(), mutations },
125
+ meta: {
126
+ htmlAssetsByPath: collectHtmlAssetsByPath(nextSnapshot.contentRecords),
127
+ siteVariables,
128
+ },
129
+ };
130
+ } catch (error) {
131
+ removeDirIfExists(outputDir);
132
+ return {
133
+ ok: false,
134
+ diagnostics: diagnosticsFromMessages([
135
+ error instanceof Error ? error.message : String(error),
136
+ ]),
137
+ };
138
+ }
139
+ }
@@ -0,0 +1,19 @@
1
+ import type { SiteVariables, TraceToolAvailability } from '../types';
2
+
3
+ export interface TraceCacheEntry {
4
+ manifestUrl: string;
5
+ highlightedSource: string;
6
+ totalSteps: number;
7
+ mtime: number;
8
+ }
9
+
10
+ export type TraceCache = Map<string, TraceCacheEntry>;
11
+
12
+ export interface TadaBuildMeta {
13
+ htmlAssetsByPath: Map<string, string>;
14
+ siteVariables: SiteVariables;
15
+ }
16
+
17
+ export interface WatchTraceOptions {
18
+ toolAvailability: TraceToolAvailability;
19
+ }
@@ -0,0 +1,112 @@
1
+ import path from 'path';
2
+ import { getDevSiteVariables } from '../site-variables';
3
+ import { getJsonDataDir } from '../templates';
4
+ import { getContentDir, getPublicDir } from '../util';
5
+ import type {
6
+ ChangeBatch,
7
+ CompilerBuildResult,
8
+ CompilerPlanResult,
9
+ WatchCompiler,
10
+ WatchTarget,
11
+ } from '../../watch/types';
12
+ import { buildFull } from './build-full';
13
+ import { buildIncremental } from './build-incremental';
14
+ import type {
15
+ TadaBuildMeta,
16
+ TraceCache,
17
+ WatchTraceOptions,
18
+ } from './compiler-types';
19
+ import { checkTraceToolAvailability, isTraceSourceFile } from '../utils/trace';
20
+ import { createTadaWatchPlan, type TadaWatchPlan } from './planner';
21
+ import { scanProject, type TadaSnapshot, updateProjectScan } from './snapshot';
22
+
23
+ export function invalidateTraceCacheForBatch(
24
+ traceCache: TraceCache,
25
+ batch: ChangeBatch,
26
+ ): void {
27
+ for (const change of batch.changes) {
28
+ const sourcePath = path.resolve(change.path);
29
+ if (isTraceSourceFile(sourcePath)) {
30
+ traceCache.delete(sourcePath);
31
+ }
32
+ }
33
+ }
34
+
35
+ export class TadaWatchCompiler
36
+ implements WatchCompiler<TadaSnapshot, TadaWatchPlan, TadaBuildMeta>
37
+ {
38
+ private wsPort: number;
39
+ private traceCache: TraceCache;
40
+ private traceOptions: WatchTraceOptions;
41
+
42
+ constructor({ wsPort }: { wsPort: number }) {
43
+ this.wsPort = wsPort;
44
+ this.traceCache = new Map();
45
+ this.traceOptions = { toolAvailability: checkTraceToolAvailability() };
46
+ }
47
+
48
+ getWatchTargets(): WatchTarget[] {
49
+ const contentDir = getContentDir();
50
+ const publicDir = getPublicDir();
51
+ const jsonDataDir = path.resolve(getJsonDataDir());
52
+ const configFilePaths = new Set([
53
+ path.resolve('site.dev.json'),
54
+ path.resolve(jsonDataDir, 'nav.json'),
55
+ path.resolve(jsonDataDir, 'authors.json'),
56
+ ]);
57
+
58
+ return [
59
+ { path: contentDir },
60
+ { path: publicDir },
61
+ {
62
+ path: jsonDataDir,
63
+ chokidar: { depth: 0 },
64
+ filter: filePath => configFilePaths.has(path.resolve(filePath)),
65
+ },
66
+ ];
67
+ }
68
+
69
+ async buildInitial(): Promise<
70
+ CompilerBuildResult<TadaSnapshot, TadaBuildMeta>
71
+ > {
72
+ return buildFull({
73
+ wsPort: this.wsPort,
74
+ traceCache: this.traceCache,
75
+ traceOptions: this.traceOptions,
76
+ });
77
+ }
78
+
79
+ async plan(
80
+ snapshot: TadaSnapshot | undefined,
81
+ batch: ChangeBatch,
82
+ ): Promise<CompilerPlanResult<TadaWatchPlan>> {
83
+ invalidateTraceCacheForBatch(this.traceCache, batch);
84
+ const scan = snapshot
85
+ ? updateProjectScan(snapshot, batch)
86
+ : scanProject(getDevSiteVariables());
87
+ return {
88
+ kind: 'build',
89
+ plan: createTadaWatchPlan({ snapshot, batch, scan }),
90
+ };
91
+ }
92
+
93
+ async run(
94
+ plan: TadaWatchPlan,
95
+ snapshot: TadaSnapshot | undefined,
96
+ ): Promise<CompilerBuildResult<TadaSnapshot, TadaBuildMeta>> {
97
+ if (plan.kind === 'full' || !snapshot) {
98
+ return buildFull({
99
+ wsPort: this.wsPort,
100
+ traceCache: this.traceCache,
101
+ traceOptions: this.traceOptions,
102
+ });
103
+ }
104
+
105
+ return buildIncremental({
106
+ plan,
107
+ snapshot,
108
+ traceCache: this.traceCache,
109
+ traceOptions: this.traceOptions,
110
+ });
111
+ }
112
+ }
@@ -0,0 +1,22 @@
1
+ import { getDistDir } from '../util';
2
+ import { runWatchEngine } from '../../watch/engine';
3
+ import { TadaWatchCompiler } from './compiler';
4
+ import { TadaWatchRuntime } from './runtime';
5
+
6
+ export async function runWatch(options: {
7
+ httpPort?: number;
8
+ wsPort?: number;
9
+ }): Promise<void> {
10
+ const wsPort = options.wsPort ?? 35729;
11
+ const runtime = new TadaWatchRuntime({
12
+ wsPort,
13
+ httpPort: options.httpPort,
14
+ distDir: getDistDir(),
15
+ });
16
+
17
+ await runWatchEngine({
18
+ compiler: new TadaWatchCompiler({ wsPort }),
19
+ onEvent: event => runtime.onEvent(event),
20
+ debounceMs: 300,
21
+ });
22
+ }
@@ -0,0 +1,75 @@
1
+ import type { TadaSnapshot } from './snapshot';
2
+
3
+ function outputsEqual(
4
+ left: string | Buffer | undefined,
5
+ right: string | Buffer | undefined,
6
+ ): boolean {
7
+ if (left === undefined || right === undefined) {
8
+ return left === right;
9
+ }
10
+ if (typeof left === 'string' && typeof right === 'string') {
11
+ return left === right;
12
+ }
13
+ const leftBuffer = typeof left === 'string' ? Buffer.from(left) : left;
14
+ const rightBuffer = typeof right === 'string' ? Buffer.from(right) : right;
15
+ return Buffer.compare(leftBuffer, rightBuffer) === 0;
16
+ }
17
+
18
+ export function computeMutations(
19
+ previous: TadaSnapshot,
20
+ next: TadaSnapshot,
21
+ 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
+ }[] = [];
28
+ const allPaths = new Set([
29
+ ...previous.outputOwners.keys(),
30
+ ...next.outputOwners.keys(),
31
+ ]);
32
+
33
+ for (const outputPath of [...allPaths].sort()) {
34
+ const previousOwner = previous.outputOwners.get(outputPath);
35
+ const nextOwner = next.outputOwners.get(outputPath);
36
+
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;
55
+
56
+ if (!nextOwner) {
57
+ mutations.push({ path: outputPath, kind: 'delete' });
58
+ continue;
59
+ }
60
+
61
+ if (
62
+ !previousOwner ||
63
+ !outputsEqual(previousContent, nextContent) ||
64
+ forceSourcePaths.has(nextOwner.sourcePath)
65
+ ) {
66
+ mutations.push({
67
+ path: outputPath,
68
+ kind: 'write',
69
+ content: nextContent!,
70
+ });
71
+ }
72
+ }
73
+
74
+ return mutations;
75
+ }