@abreen/tada 1.17.1 → 1.18.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 (111) hide show
  1. package/README.md +38 -7
  2. package/assets/material-symbols/chevron_right_24px.svg +1 -0
  3. package/bin/tada.ts +16 -7
  4. package/bin/validators.ts +33 -0
  5. package/build/branding.ts +41 -0
  6. package/build/build-manifest.ts +9 -4
  7. package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
  8. package/build/build-validation.ts +176 -0
  9. package/build/bundle.ts +75 -65
  10. package/build/config-loader.ts +3 -3
  11. package/build/custom-fonts.ts +8 -5
  12. package/build/generate-favicon.ts +7 -6
  13. package/build/generate-fonts.ts +8 -15
  14. package/build/generate-katex-assets.ts +19 -17
  15. package/build/generate-web-app-manifest.ts +3 -11
  16. package/build/lodash-template.ts +14 -0
  17. package/build/material-symbols.ts +7 -4
  18. package/build/output-publication.ts +238 -0
  19. package/build/pagefind.ts +49 -21
  20. package/build/pdf-text.ts +41 -1
  21. package/build/pipeline.ts +95 -101
  22. package/build/site-assets.ts +46 -0
  23. package/build/site-build.ts +267 -0
  24. package/build/source-model.ts +223 -244
  25. package/build/source-records.ts +69 -81
  26. package/build/template-globals.ts +3 -0
  27. package/build/templates.ts +5 -2
  28. package/build/toc-plugin.ts +21 -21
  29. package/build/types.d.ts +44 -38
  30. package/build/util.ts +0 -4
  31. package/build/utils/code.ts +38 -101
  32. package/build/utils/final-html.ts +171 -47
  33. package/build/utils/literate-java.ts +11 -7
  34. package/build/utils/markdown-partials.ts +4 -3
  35. package/build/utils/markdown.ts +6 -17
  36. package/build/utils/paths.ts +3 -0
  37. package/build/utils/plain-text.ts +18 -0
  38. package/build/utils/render.ts +227 -165
  39. package/build/utils/shiki-highlighter.ts +11 -0
  40. package/build/utils/source-template.ts +2 -2
  41. package/build/utils/trace-core.ts +8 -83
  42. package/build/utils/trace.ts +38 -95
  43. package/build/validate-config-links.ts +60 -20
  44. package/build/watch/build-incremental.ts +46 -103
  45. package/build/watch/compiler.ts +158 -37
  46. package/build/watch/engine.ts +207 -176
  47. package/build/watch/index.ts +9 -2
  48. package/build/watch/planner.ts +88 -144
  49. package/build/watch/runtime.ts +21 -34
  50. package/build/watch/types.d.ts +32 -64
  51. package/init/content/labs/00/VowelCounter.java.md +5 -2
  52. package/init/content/labs/00/index.md +3 -2
  53. package/init/content/labs/01/index.md +3 -2
  54. package/init/content/labs/02/index.md +3 -2
  55. package/init/content/lectures/01/index.md +3 -2
  56. package/init/content/lectures/02/index.md +3 -2
  57. package/package.json +4 -4
  58. package/schema/site.schema.json +18 -2
  59. package/src/_content.scss +463 -0
  60. package/src/_fonts.scss +90 -0
  61. package/src/_layout.scss +50 -35
  62. package/src/{material-symbols.scss → _material-symbols.scss} +7 -0
  63. package/src/appearance-picker/{style.scss → _index.scss} +1 -1
  64. package/src/code/{style.scss → _index.scss} +61 -0
  65. package/src/header/{_base.scss → _index.scss} +98 -6
  66. package/src/index.ts +1 -17
  67. package/src/navigate/{style.scss → _index.scss} +29 -3
  68. package/src/navigate/breadcrumbs.ts +129 -0
  69. package/src/navigate/eligible.ts +0 -40
  70. package/src/navigate/index.ts +38 -4
  71. package/src/navigate/lifecycle.ts +19 -2
  72. package/src/navigate/runtime.ts +57 -22
  73. package/src/page-update/index.ts +33 -20
  74. package/src/print/{style.scss → _index.scss} +3 -1
  75. package/src/search/index.ts +7 -0
  76. package/src/slides/{style.scss → _index.scss} +5 -0
  77. package/src/style.scss +24 -536
  78. package/src/toc/index.ts +2 -2
  79. package/templates/_author.html +2 -2
  80. package/templates/_download.html +1 -1
  81. package/templates/_footer.html +1 -1
  82. package/templates/_heading.html +17 -4
  83. package/templates/_nav.html +2 -2
  84. package/templates/_page-bottom.html +6 -0
  85. package/templates/_top.html +18 -14
  86. package/templates/code.html +22 -21
  87. package/templates/default.html +12 -11
  88. package/templates/literate.html +6 -5
  89. package/assets/material-symbols/south_east_20px.svg +0 -1
  90. package/build/copy.ts +0 -82
  91. package/build/generate-content-assets.ts +0 -121
  92. package/build/utils/content-files.ts +0 -170
  93. package/build/watch/assets.ts +0 -103
  94. package/build/watch/build-full.ts +0 -97
  95. package/build/watch/build-helpers.ts +0 -66
  96. package/build/watch/build-result.ts +0 -19
  97. package/build/watch/fs-commit.ts +0 -218
  98. package/build/watch/mutations.ts +0 -73
  99. package/build/watch/snapshot.ts +0 -201
  100. package/build/watch/validation.ts +0 -57
  101. package/src/code.scss +0 -60
  102. package/src/critical.scss +0 -5
  103. package/src/header/style.scss +0 -91
  104. /package/src/{literate.scss → _literate.scss} +0 -0
  105. /package/src/anchor/{style.scss → _index.scss} +0 -0
  106. /package/src/page-update/{style.scss → _index.scss} +0 -0
  107. /package/src/question/{style.scss → _index.scss} +0 -0
  108. /package/src/search/{style.scss → _index.scss} +0 -0
  109. /package/src/timezone/{style.scss → _index.scss} +0 -0
  110. /package/src/toc/{style.scss → _index.scss} +0 -0
  111. /package/src/trace/{style.scss → _index.scss} +0 -0
@@ -15,34 +15,227 @@ import {
15
15
  normalizeOutputPath,
16
16
  toPosix,
17
17
  } from './utils/paths';
18
- import type { ChangeBatch } from './watch/types';
19
18
  import type { SiteVariables } from './types';
20
19
 
20
+ export type TadaSourceRenderKind =
21
+ | 'skip'
22
+ | 'plain-text-page'
23
+ | 'literate-java'
24
+ | 'code-page'
25
+ | 'content-copy'
26
+ | 'public-copy';
27
+
28
+ export interface SourceEntry {
29
+ readonly kind: 'content' | 'public';
30
+ readonly renderKind: TadaSourceRenderKind;
31
+ readonly outputs: ReadonlySet<string>;
32
+ readonly targets: ReadonlySet<string>;
33
+ }
34
+
21
35
  export interface TadaProjectScan {
22
- contentDir: string;
23
- publicDir: string;
24
- distDir: string;
25
- contentFiles: Set<string>;
26
- buildContentFiles: Set<string>;
27
- publicFiles: Set<string>;
28
- validTargets: Set<string>;
29
- literateJavaOutputPaths: Set<string>;
30
- processedExts: Set<string>;
31
- contentOwners: Map<string, string>;
32
- publicOwners: Map<string, string>;
33
- sourceOutputPaths: Map<string, Set<string>>;
34
- sourceTargetPaths: Map<string, Set<string>>;
36
+ readonly contentDir: string;
37
+ readonly publicDir: string;
38
+ readonly distDir: string;
39
+ readonly processedExts: ReadonlySet<string>;
40
+ readonly sources: ReadonlyMap<string, SourceEntry>;
41
+ readonly outputProducers: ReadonlyMap<string, ReadonlySet<string>>;
42
+ readonly codePageSourceTargets: ReadonlySet<string>;
43
+ readonly generatedPageTargets: ReadonlySet<string>;
44
+ readonly validTargets: ReadonlySet<string>;
45
+ readonly literateJavaOutputPaths: ReadonlySet<string>;
46
+ }
47
+
48
+ export function* sourcePaths(
49
+ scan: TadaProjectScan,
50
+ kind?: SourceEntry['kind'],
51
+ pagesOnly = false,
52
+ ): Iterable<string> {
53
+ for (const [filePath, entry] of scan.sources) {
54
+ if (kind && entry.kind !== kind) {
55
+ continue;
56
+ }
57
+ if (
58
+ pagesOnly &&
59
+ ['skip', 'content-copy', 'public-copy'].includes(entry.renderKind)
60
+ ) {
61
+ continue;
62
+ }
63
+ yield filePath;
64
+ }
65
+ }
66
+
67
+ export function getSourceRenderKind(
68
+ filePath: string,
69
+ kind: SourceEntry['kind'],
70
+ processedExts: ReadonlySet<string>,
71
+ buildContent: boolean,
72
+ ): TadaSourceRenderKind {
73
+ const ext = path.extname(filePath).toLowerCase();
74
+ if (kind === 'public') {
75
+ return 'public-copy';
76
+ }
77
+ if (!processedExts.has(ext.slice(1))) {
78
+ return 'content-copy';
79
+ }
80
+ if (!buildContent) {
81
+ return 'skip';
82
+ }
83
+ if (isLiterateJava(filePath)) {
84
+ return 'literate-java';
85
+ }
86
+ return extensionIsMarkdown(ext) || ext === '.html'
87
+ ? 'plain-text-page'
88
+ : 'code-page';
89
+ }
90
+
91
+ function createSourceEntry(
92
+ scan: Pick<TadaProjectScan, 'contentDir' | 'publicDir' | 'processedExts'>,
93
+ filePath: string,
94
+ kind: SourceEntry['kind'],
95
+ ): SourceEntry {
96
+ const buildContent =
97
+ kind === 'content' && isBuildContentSource(filePath, scan.processedExts);
98
+ const renderKind = getSourceRenderKind(
99
+ filePath,
100
+ kind,
101
+ scan.processedExts,
102
+ buildContent,
103
+ );
104
+ const rootDir = kind === 'content' ? scan.contentDir : scan.publicDir;
105
+ return {
106
+ kind,
107
+ renderKind,
108
+ outputs:
109
+ kind === 'public'
110
+ ? new Set([toPosix(path.relative(rootDir, filePath))])
111
+ : getSourceOutputPaths({
112
+ contentDir: rootDir,
113
+ filePath,
114
+ processedExts: scan.processedExts,
115
+ buildContent,
116
+ }),
117
+ targets: getSourceTargetPaths({
118
+ kind,
119
+ rootDir,
120
+ filePath,
121
+ processedExts: scan.processedExts,
122
+ buildContent,
123
+ }),
124
+ };
125
+ }
126
+
127
+ export function indexSources(
128
+ roots: Pick<
129
+ TadaProjectScan,
130
+ 'contentDir' | 'publicDir' | 'distDir' | 'processedExts'
131
+ >,
132
+ sources: ReadonlyMap<string, SourceEntry>,
133
+ ): TadaProjectScan {
134
+ const outputProducers = new Map<string, Set<string>>();
135
+ const generatedPageTargets = new Set<string>();
136
+ const codePageSourceTargets = new Set<string>();
137
+ const validTargets = new Set<string>();
138
+ const literateJavaOutputPaths = new Set<string>();
139
+ for (const [filePath, entry] of sources) {
140
+ for (const output of entry.outputs) {
141
+ if (!outputProducers.has(output)) {
142
+ outputProducers.set(output, new Set());
143
+ }
144
+ outputProducers.get(output)!.add(filePath);
145
+ }
146
+ if (
147
+ ['plain-text-page', 'code-page', 'literate-java'].includes(
148
+ entry.renderKind,
149
+ )
150
+ ) {
151
+ for (const output of entry.outputs) {
152
+ if (output.endsWith('.html')) {
153
+ addGeneratedRouteAliases(generatedPageTargets, `/${output}`);
154
+ }
155
+ }
156
+ }
157
+ for (const target of entry.targets) {
158
+ validTargets.add(target);
159
+ }
160
+ if (entry.renderKind === 'code-page') {
161
+ codePageSourceTargets.add(
162
+ normalizeOutputPath(
163
+ `/${toPosix(path.relative(roots.contentDir, filePath))}`,
164
+ ),
165
+ );
166
+ }
167
+ if (entry.renderKind === 'literate-java') {
168
+ const parsed = path.parse(path.relative(roots.contentDir, filePath));
169
+ literateJavaOutputPaths.add(
170
+ `/${toPosix(path.join(parsed.dir, parsed.name))}`,
171
+ );
172
+ }
173
+ }
174
+ return {
175
+ ...roots,
176
+ sources,
177
+ outputProducers,
178
+ validTargets,
179
+ generatedPageTargets,
180
+ codePageSourceTargets,
181
+ literateJavaOutputPaths,
182
+ };
35
183
  }
36
184
 
185
+ export function scanProject(siteVariables: SiteVariables): TadaProjectScan {
186
+ const roots = {
187
+ contentDir: getContentDir(),
188
+ publicDir: getPublicDir(),
189
+ distDir: getDistDir(),
190
+ processedExts: getProcessedExts(
191
+ Object.keys(getExtensionToShikiLanguage(siteVariables)),
192
+ ),
193
+ };
194
+ const sources = new Map<string, SourceEntry>();
195
+ for (const kind of ['content', 'public'] as const) {
196
+ const dir = kind === 'content' ? roots.contentDir : roots.publicDir;
197
+ for (const filePath of walkFiles(dir).sort()) {
198
+ sources.set(filePath, createSourceEntry(roots, filePath, kind));
199
+ }
200
+ }
201
+ return indexSources(roots, sources);
202
+ }
203
+
204
+ export function assertNoOutputPathConflicts(scan: TadaProjectScan): string[] {
205
+ return [...scan.outputProducers]
206
+ .filter(([, producers]) => producers.size > 1)
207
+ .map(([output]) => output)
208
+ .sort();
209
+ }
210
+
211
+ // Front matter `skip` results, reused while a file's size and modification
212
+ // time are unchanged, so watch rescans read only edited pages.
213
+ const skipCache = new Map<
214
+ string,
215
+ { size: number; mtimeMs: number; skip: boolean }
216
+ >();
217
+
37
218
  export function shouldSkipContentFile(filePath: string): boolean {
38
219
  const ext = path.extname(filePath).toLowerCase();
39
220
  if (!(extensionIsMarkdown(ext) || ext === '.html')) {
40
221
  return false;
41
222
  }
42
223
 
224
+ const { size, mtimeMs } = fs.statSync(filePath);
225
+ const cached = skipCache.get(filePath);
226
+ if (cached && cached.size === size && cached.mtimeMs === mtimeMs) {
227
+ return cached.skip;
228
+ }
229
+
43
230
  const raw = fs.readFileSync(filePath, 'utf-8');
44
- const { pageVariables } = parseFrontMatterAndContent(raw, ext);
45
- return pageVariables?.skip === true;
231
+ let skip = false;
232
+ try {
233
+ skip = parseFrontMatterAndContent(raw, ext).pageVariables?.skip === true;
234
+ } catch {
235
+ // Not skipped: rendering the page reports the error for this file.
236
+ }
237
+ skipCache.set(filePath, { size, mtimeMs, skip });
238
+ return skip;
46
239
  }
47
240
 
48
241
  export function getProcessedExts(codeExtensions: string[]): Set<string> {
@@ -53,7 +246,7 @@ export function getProcessedExts(codeExtensions: string[]): Set<string> {
53
246
 
54
247
  export function isBuildContentSource(
55
248
  filePath: string,
56
- processedExts: Set<string>,
249
+ processedExts: ReadonlySet<string>,
57
250
  ): boolean {
58
251
  const ext = path.extname(filePath).slice(1).toLowerCase();
59
252
  if (!processedExts.has(ext) || isPartial(filePath)) {
@@ -98,114 +291,6 @@ function walkFiles(dir: string): string[] {
98
291
  });
99
292
  }
100
293
 
101
- function cloneSetMap(
102
- source: Map<string, Set<string>>,
103
- ): Map<string, Set<string>> {
104
- return new Map(
105
- [...source.entries()].map(([key, values]) => [key, new Set(values)]),
106
- );
107
- }
108
-
109
- function addContentSourceToScan({
110
- scan,
111
- filePath,
112
- }: {
113
- scan: TadaProjectScan;
114
- filePath: string;
115
- }): void {
116
- const buildContent = isBuildContentSource(filePath, scan.processedExts);
117
-
118
- scan.contentFiles.add(filePath);
119
- if (buildContent) {
120
- scan.buildContentFiles.add(filePath);
121
- }
122
- if (buildContent && isLiterateJava(filePath)) {
123
- const parsed = path.parse(path.relative(scan.contentDir, filePath));
124
- scan.literateJavaOutputPaths.add(
125
- `/${toPosix(path.join(parsed.dir, parsed.name))}`,
126
- );
127
- }
128
-
129
- const outputs = getSourceOutputPaths({
130
- contentDir: scan.contentDir,
131
- filePath,
132
- processedExts: scan.processedExts,
133
- buildContent,
134
- });
135
- scan.sourceOutputPaths.set(filePath, outputs);
136
- for (const outputPath of outputs) {
137
- scan.contentOwners.set(outputPath, filePath);
138
- }
139
-
140
- const targets = getSourceTargetPaths({
141
- kind: 'content',
142
- rootDir: scan.contentDir,
143
- filePath,
144
- processedExts: scan.processedExts,
145
- buildContent,
146
- });
147
- scan.sourceTargetPaths.set(filePath, targets);
148
- for (const target of targets) {
149
- scan.validTargets.add(target);
150
- }
151
- }
152
-
153
- function addPublicSourceToScan({
154
- scan,
155
- filePath,
156
- }: {
157
- scan: TadaProjectScan;
158
- filePath: string;
159
- }): void {
160
- const relPath = toPosix(path.relative(scan.publicDir, filePath));
161
-
162
- scan.publicFiles.add(filePath);
163
- scan.publicOwners.set(relPath, filePath);
164
- scan.sourceOutputPaths.set(filePath, new Set([relPath]));
165
-
166
- const targets = getSourceTargetPaths({
167
- kind: 'public',
168
- rootDir: scan.publicDir,
169
- filePath,
170
- processedExts: scan.processedExts,
171
- buildContent: false,
172
- });
173
- scan.sourceTargetPaths.set(filePath, targets);
174
- for (const target of targets) {
175
- scan.validTargets.add(target);
176
- }
177
- }
178
-
179
- function collectValidTargets(
180
- sourceTargetPaths: Map<string, Set<string>>,
181
- ): Set<string> {
182
- const validTargets = new Set<string>();
183
- for (const targets of sourceTargetPaths.values()) {
184
- for (const target of targets) {
185
- validTargets.add(target);
186
- }
187
- }
188
- return validTargets;
189
- }
190
-
191
- function createEmptyScan(processedExts: Set<string>): TadaProjectScan {
192
- return {
193
- contentDir: getContentDir(),
194
- publicDir: getPublicDir(),
195
- distDir: getDistDir(),
196
- contentFiles: new Set(),
197
- buildContentFiles: new Set(),
198
- publicFiles: new Set(),
199
- validTargets: new Set(),
200
- literateJavaOutputPaths: new Set(),
201
- processedExts,
202
- contentOwners: new Map(),
203
- publicOwners: new Map(),
204
- sourceOutputPaths: new Map(),
205
- sourceTargetPaths: new Map(),
206
- };
207
- }
208
-
209
294
  export function getSourceOutputPaths({
210
295
  contentDir,
211
296
  filePath,
@@ -214,7 +299,7 @@ export function getSourceOutputPaths({
214
299
  }: {
215
300
  contentDir: string;
216
301
  filePath: string;
217
- processedExts: Set<string>;
302
+ processedExts: ReadonlySet<string>;
218
303
  buildContent: boolean;
219
304
  }): Set<string> {
220
305
  const relPath = toPosix(path.relative(contentDir, filePath));
@@ -260,7 +345,7 @@ export function getSourceTargetPaths({
260
345
  kind: 'content' | 'public';
261
346
  rootDir: string;
262
347
  filePath: string;
263
- processedExts: Set<string>;
348
+ processedExts: ReadonlySet<string>;
264
349
  buildContent: boolean;
265
350
  }): Set<string> {
266
351
  const targets = new Set<string>();
@@ -281,123 +366,17 @@ export function getSourceTargetPaths({
281
366
  return targets;
282
367
  }
283
368
 
284
- const parsed = path.parse(relPath);
285
- const subPath = toPosix(path.join(parsed.dir, parsed.name));
286
-
287
- if (isLiterateJava(filePath)) {
288
- const javaSubPath = toPosix(path.join(parsed.dir, parsed.name));
289
- addGeneratedRouteAliases(targets, `/${javaSubPath}.html`);
290
- targets.add(normalizeOutputPath(`/${javaSubPath}`));
291
- return targets;
292
- }
293
-
294
- if (
295
- extensionIsMarkdown(parsed.ext.toLowerCase()) ||
296
- parsed.ext.toLowerCase() === '.html'
297
- ) {
298
- addGeneratedRouteAliases(targets, `/${subPath}.html`);
299
- return targets;
300
- }
301
-
302
- addGeneratedRouteAliases(targets, `/${relPath}.html`);
303
- targets.add(normalizeOutputPath(`/${relPath}`));
304
- return targets;
305
- }
306
-
307
- export function scanProject(siteVariables: SiteVariables): TadaProjectScan {
308
- const scan = createEmptyScan(
309
- getProcessedExts(Object.keys(getExtensionToShikiLanguage(siteVariables))),
310
- );
311
-
312
- for (const filePath of walkFiles(scan.contentDir).sort()) {
313
- addContentSourceToScan({ scan, filePath });
314
- }
315
-
316
- for (const filePath of walkFiles(scan.publicDir).sort()) {
317
- addPublicSourceToScan({ scan, filePath });
318
- }
319
-
320
- return scan;
321
- }
322
-
323
- export function updateProjectScan(
324
- snapshot: TadaProjectScan,
325
- batch: ChangeBatch,
326
- ): TadaProjectScan {
327
- const scan = createEmptyScan(snapshot.processedExts);
328
- scan.contentFiles = new Set(snapshot.contentFiles);
329
- scan.buildContentFiles = new Set(snapshot.buildContentFiles);
330
- scan.publicFiles = new Set(snapshot.publicFiles);
331
- scan.literateJavaOutputPaths = new Set(snapshot.literateJavaOutputPaths);
332
- scan.contentOwners = new Map(snapshot.contentOwners);
333
- scan.publicOwners = new Map(snapshot.publicOwners);
334
- scan.sourceOutputPaths = cloneSetMap(snapshot.sourceOutputPaths);
335
- scan.sourceTargetPaths = cloneSetMap(snapshot.sourceTargetPaths);
336
- scan.validTargets = collectValidTargets(scan.sourceTargetPaths);
337
-
338
- for (const change of batch.changes) {
339
- const sourcePath = path.resolve(change.path);
340
- const inContent = sourcePath.startsWith(`${scan.contentDir}${path.sep}`);
341
- const inPublic = sourcePath.startsWith(`${scan.publicDir}${path.sep}`);
342
- if (!inContent && !inPublic) {
343
- continue;
344
- }
345
-
346
- for (const outputPath of scan.sourceOutputPaths.get(sourcePath) || []) {
347
- if (inContent) {
348
- scan.contentOwners.delete(outputPath);
349
- } else {
350
- scan.publicOwners.delete(outputPath);
351
- }
352
- }
353
-
354
- scan.sourceOutputPaths.delete(sourcePath);
355
- scan.sourceTargetPaths.delete(sourcePath);
356
- scan.contentFiles.delete(sourcePath);
357
- scan.buildContentFiles.delete(sourcePath);
358
- scan.publicFiles.delete(sourcePath);
359
-
360
- if (inContent && isLiterateJava(sourcePath)) {
361
- const parsed = path.parse(path.relative(scan.contentDir, sourcePath));
362
- scan.literateJavaOutputPaths.delete(
363
- `/${toPosix(path.join(parsed.dir, parsed.name))}`,
364
- );
365
- }
366
-
367
- if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
368
- continue;
369
- }
370
-
371
- if (inContent) {
372
- addContentSourceToScan({ scan, filePath: sourcePath });
373
- continue;
374
- }
375
-
376
- addPublicSourceToScan({ scan, filePath: sourcePath });
377
- }
378
-
379
- // Rebuild the selected owners from the complete source inventory. Removing
380
- // either producer of a formerly conflicting output must retain its survivor.
381
- scan.contentOwners.clear();
382
- for (const sourcePath of scan.contentFiles) {
383
- for (const outputPath of scan.sourceOutputPaths.get(sourcePath) || []) {
384
- scan.contentOwners.set(outputPath, sourcePath);
385
- }
386
- }
387
- scan.validTargets = collectValidTargets(scan.sourceTargetPaths);
388
- return scan;
389
- }
390
-
391
- export function assertNoOutputPathConflicts(scan: TadaProjectScan): string[] {
392
- const owners = new Set<string>();
393
- const conflicts = new Set<string>();
394
- for (const outputs of scan.sourceOutputPaths.values()) {
395
- for (const outputPath of outputs) {
396
- if (owners.has(outputPath)) {
397
- conflicts.add(outputPath);
398
- }
399
- owners.add(outputPath);
369
+ for (const output of getSourceOutputPaths({
370
+ contentDir: rootDir,
371
+ filePath,
372
+ processedExts,
373
+ buildContent,
374
+ })) {
375
+ if (output.endsWith('.html')) {
376
+ addGeneratedRouteAliases(targets, `/${output}`);
377
+ } else {
378
+ targets.add(normalizeOutputPath(`/${output}`));
400
379
  }
401
380
  }
402
- return [...conflicts].sort();
381
+ return targets;
403
382
  }