@riebeckite/plugin-diagnostics 0.0.14 → 0.0.15

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 CHANGED
@@ -44,9 +44,9 @@ export default defineConfig({
44
44
  | `broken-link` | `error` | Markdown link points to a missing or excluded note/file |
45
45
  | `unused-asset` | `warning` | Image never referenced by any note (`reportUnusedAssets`) |
46
46
  | `orphan-note` | `info` | Published note has no incoming links (`reportOrphans`) |
47
- | `missing-frontmatter` | `warning` | No frontmatter, or required fields missing |
47
+ | `missing-frontmatter` | `warning` | A required frontmatter field is missing |
48
48
  | `publish-conflict` | `warning` | `publish: true` combined with `draft: true` / `private: true` |
49
- | `duplicate-title` | `warning` | Multiple published notes share a title |
49
+ | `duplicate-title` | `warning` | Multiple published notes share a title within the same language |
50
50
  | `slug-collision` | `error` | Slugs collide case-insensitively |
51
51
  | `duplicate-content-id` | `error` | Published notes share a stable content ID (`id`/`uid`) |
52
52
  | `invalid-content-id` | `error` | `id`/`uid` frontmatter violates the stable content ID contract |
@@ -80,7 +80,10 @@ the existing plugin output/asset contracts so integrity checks can treat them as
80
80
  valid public targets.
81
81
 
82
82
  `runDiagnostics()` and the standalone CLI still use the filesystem/content-source
83
- analyzer because no full manifest is available in that mode.
83
+ analyzer because no full manifest is available in that mode. As a result,
84
+ `orphan-note` and `unused-asset` are reported only by `runDiagnostics()` and the
85
+ standalone CLI; during a build they are omitted because link relationships come
86
+ from the manifest.
84
87
 
85
88
  ## Options
86
89
 
@@ -156,4 +159,5 @@ assertNoErrors(report);
156
159
 
157
160
  ## See also
158
161
 
159
- - [Plugin guide](../../../docs/en/reference/plugin-api.md)
162
+ - [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
163
+
package/README_ja.md CHANGED
@@ -28,22 +28,22 @@ export default defineConfig({
28
28
 
29
29
  | コード | 既定の重要度 | 内容 |
30
30
  | --- | --- | --- |
31
- | `broken-wikilink` | error | Wikiリンクの参照先またはフラグメントを解決できない |
31
+ | `broken-wikilink` | error | Wiki リンクの参照先またはフラグメントを解決できない |
32
32
  | `broken-image` | error | 埋め込み画像または画像リンクの参照先がない |
33
33
  | `broken-link` | error | Markdown リンクが存在しない、または除外されたノートを指す |
34
34
  | `unused-asset` | warning | どのノートからも参照されない画像 |
35
35
  | `orphan-note` | info | 公開ノートのうち、他のノートからリンクされていないもの |
36
- | `missing-frontmatter` | warning | frontmatter がない、または必須フィールドが欠けている |
36
+ | `missing-frontmatter` | warning | 必須フィールドが欠けている |
37
37
  | `publish-conflict` | warning | `publish: true` と `draft: true` または `private: true` が同居している |
38
- | `duplicate-title` | warning | 公開ノート同士でタイトルが重複している |
38
+ | `duplicate-title` | warning | 同じ言語の公開ノート同士でタイトルが重複している |
39
39
  | `slug-collision` | error | 大文字・小文字を区別しない slug が衝突している |
40
40
  | `duplicate-content-id` | error | 公開ノート同士で安定コンテンツ ID(`id`/`uid`)が重複している |
41
- | `invalid-content-id` | error | `id`/`uid` の frontmatter が安定コンテンツ ID の契約を満たしていない |
41
+ | `invalid-content-id` | error | `id`/`uid` の frontmatter が安定コンテンツ ID の要件を満たしていない |
42
42
  | `excluded-public` | warning | 除外されたノートに `publish: true` が指定されている |
43
43
  | `publish-boundary` | warning | 公開コンテンツから非公開コンテンツへリンク・埋め込みしている |
44
44
  | `analytics-untracked` | info | 安定 `id` のない公開ノートは analytics プラグインで計測されない(`reportAnalyticsCoverage` で有効化。設定で analytics プラグインが有効な場合は自動で有効化) |
45
45
  | `content-integrity:broken-link` | warning | 公開ページが、公開または登録されていないサイト内経路へリンクしている |
46
- | `content-integrity:unresolved-wikilink` | warning | 公開ページ内の Wikiリンクを、設定済みの解決規則で解決できない |
46
+ | `content-integrity:unresolved-wikilink` | warning | 公開ページ内の Wiki リンクを、設定済みの解決規則で解決できない |
47
47
  | `content-integrity:broken-asset` | warning | 公開ページが、解決済みアセット・プラグインアセット・生成出力・公開経路のいずれにもないローカルアセットを参照している |
48
48
  | `content-integrity:duplicate-public-location` | error | 複数の公開コンテンツまたは生成経路が、同じ最終公開パスを所有している |
49
49
  | `content-integrity:redirect-target-missing` | warning | 公開リダイレクトの転送先コンテンツが公開されない |
@@ -57,9 +57,9 @@ export default defineConfig({
57
57
 
58
58
  Riebeckite のプラグインとして使う場合、リンク関係の検査はコンテンツを再走査せず、解決済み manifest から行います。`publicEntries`、解決済み `ContentLink`、`ContentPublicLocation`、`publicRedirects`、プラグインページのパス、プラグインアセット、生成出力を参照します。これにより permalink、alias、rename によるリダイレクト、l10n、公開・除外設定、Page System の扱いがビルド処理と揃います。
59
59
 
60
- 外部 URL の疎通確認、SEO、スペルチェック、Lighthouse、自動修復は行いません。`http:`、`https:`、`mailto:`、`tel:`、`data:`、プロトコル相対 URL、ページ内フラグメントだけのリンクは対象外です。経路の存在確認では、クエリ文字列とフラグメントを除いて判定します。生成ページや生成アセットを提供するプラグインは、Page Type、生成出力、アセット登録の既存契約を使ってください。登録されていれば、整合性検査でも有効な公開先として扱われます。
60
+ 外部 URL の疎通確認、SEO、スペルチェック、Lighthouse、自動修復は行いません。`http:`、`https:`、`mailto:`、`tel:`、`data:`、プロトコル相対 URL、ページ内フラグメントだけのリンクは対象外です。経路の存在確認では、クエリ文字列とフラグメントを除いて判定します。生成ページや生成アセットを提供するプラグインは、Page Type、生成出力、アセット登録の既存の仕組みを使ってください。登録されていれば、整合性検査でも有効な公開先として扱われます。
61
61
 
62
- `runDiagnostics()` と単体 CLI は、完全な manifest を持たないため、従来の content source 解析を使います。
62
+ `runDiagnostics()` と単体 CLI は、完全な manifest を持たないため、従来の content source 解析を使います。そのため `orphan-note` と `unused-asset` は `runDiagnostics()` と単体 CLI でのみ報告されます。ビルド時はリンク関係を manifest から解決するため、これらは出力しません。
63
63
 
64
64
  ## 設定項目
65
65
 
@@ -115,4 +115,5 @@ assertNoErrors(report);
115
115
 
116
116
  ## 関連資料
117
117
 
118
- - [プラグインシステム](../../../docs/ja/reference/plugin-api.md)
118
+ - [プラグインシステム](../../../docs/ja/docs/reference/plugin-api.md)
119
+
package/dist/bin.js CHANGED
@@ -258,6 +258,7 @@ async function scanVault(config) {
258
258
  const note = {
259
259
  relativePath: relative,
260
260
  slug,
261
+ language: resolveNoteLanguage(relative, config.languages),
261
262
  markdown,
262
263
  fm,
263
264
  headings: extractHeadings(markdown),
@@ -416,6 +417,12 @@ function fragmentExists(note, fragment) {
416
417
  function normalizeMarkdown(markdown) {
417
418
  return markdown.replace(/\r\n/g, "\n");
418
419
  }
420
+ function resolveNoteLanguage(relativePath, languages) {
421
+ if (!languages || languages.length === 0) return void 0;
422
+ const segment = relativePath.split("/")[0]?.toLowerCase();
423
+ if (!segment) return void 0;
424
+ return languages.find((language) => language.toLowerCase() === segment);
425
+ }
419
426
  function getExtension(filePath) {
420
427
  const lastSegment = filePath.split("/").pop() ?? filePath;
421
428
  const dotIndex = lastSegment.lastIndexOf(".");
@@ -444,17 +451,7 @@ function normalizeRelativePath(parts) {
444
451
  function checkFrontmatter(note, source, state, options, diagnostics) {
445
452
  const location = { slug: note.slug, filePath: note.relativePath };
446
453
  const values = note.fm.values;
447
- if (!note.fm.hasFrontmatter) {
448
- pushDiagnostic(
449
- diagnostics,
450
- options,
451
- location,
452
- "missing-frontmatter",
453
- "note has no frontmatter",
454
- note.slug,
455
- "add frontmatter with the required fields"
456
- );
457
- } else {
454
+ if (note.fm.hasFrontmatter) {
458
455
  for (const field of options.requiredFrontmatter) {
459
456
  if (!hasField(values, field))
460
457
  pushDiagnostic(
@@ -467,6 +464,16 @@ function checkFrontmatter(note, source, state, options, diagnostics) {
467
464
  `add "${field}" to the frontmatter`
468
465
  );
469
466
  }
467
+ } else if (options.requiredFrontmatter.length > 0) {
468
+ pushDiagnostic(
469
+ diagnostics,
470
+ options,
471
+ location,
472
+ "missing-frontmatter",
473
+ "note has no frontmatter",
474
+ note.slug,
475
+ "add frontmatter with the required fields"
476
+ );
470
477
  }
471
478
  if (values.publish === true && values.draft === true)
472
479
  pushDiagnostic(
@@ -629,10 +636,11 @@ function checkSlugCollisions(source, options, diagnostics) {
629
636
  function checkDuplicateTitles(source, options, diagnostics) {
630
637
  const byTitle = groupNotes(
631
638
  source.includedNotes.filter((note) => note.published),
632
- (note) => getNoteTitle(note.fm.values, note.slug).toLowerCase()
639
+ (note) => `${note.language ?? ""}\0${getNoteTitle(note.fm.values, note.slug).toLowerCase()}`
633
640
  );
634
- for (const [title, notes] of byTitle) {
641
+ for (const [groupKey, notes] of byTitle) {
635
642
  if (notes.length < 2) continue;
643
+ const title = groupKey.slice(groupKey.indexOf("\0") + 1);
636
644
  const slugs = notes.map((note) => note.slug).join(", ");
637
645
  for (const note of notes)
638
646
  pushDiagnostic(
@@ -833,8 +841,10 @@ async function analyzeContent(config, options = {}) {
833
841
  checkSlugCollisions(source, normalizedOptions, diagnostics);
834
842
  checkDuplicateTitles(source, normalizedOptions, diagnostics);
835
843
  checkContentIdIntegrity(source, normalizedOptions, diagnostics);
836
- checkOrphans(source, state, normalizedOptions, diagnostics);
837
- checkUnusedAssets(source, state, normalizedOptions, diagnostics);
844
+ if (!options.skipReferenceIntegrity) {
845
+ checkOrphans(source, state, normalizedOptions, diagnostics);
846
+ checkUnusedAssets(source, state, normalizedOptions, diagnostics);
847
+ }
838
848
  checkExcludedPublic(source, normalizedOptions, diagnostics);
839
849
  if (!options.skipReferenceIntegrity)
840
850
  checkPublishBoundary(source, normalizedOptions, diagnostics);
@@ -978,6 +988,7 @@ async function runDiagnostics(target, options = {}) {
978
988
  }
979
989
  }
980
990
  var ANALYTICS_PLUGIN_NAME = "analytics";
991
+ var L10N_PLUGIN_NAME = "l10n";
981
992
  function hasEnabledAnalyticsPlugin(config) {
982
993
  return config?.plugins.some(
983
994
  (plugin) => plugin.name === ANALYTICS_PLUGIN_NAME && plugin.enabled !== false
@@ -1007,9 +1018,19 @@ function toAnalyzerConfig(target, options) {
1007
1018
  target.content.exclude
1008
1019
  ),
1009
1020
  exclude: [...target.content.exclude, ...options.exclude ?? []],
1010
- publishStrategy: target.content.filters.publishStrategy
1021
+ publishStrategy: target.content.filters.publishStrategy,
1022
+ languages: resolveLanguages(target)
1011
1023
  };
1012
1024
  }
1025
+ function resolveLanguages(config) {
1026
+ const plugin = config.plugins.find(
1027
+ (entry) => entry.name === L10N_PLUGIN_NAME && entry.enabled !== false
1028
+ );
1029
+ const languages = plugin?.options?.languages;
1030
+ return Array.isArray(languages) ? languages.filter(
1031
+ (language) => typeof language === "string"
1032
+ ) : [];
1033
+ }
1013
1034
 
1014
1035
  // bin.ts
1015
1036
  var HELP = `riebeckite-diagnostics
package/dist/index.js CHANGED
@@ -254,6 +254,7 @@ async function scanVault(config) {
254
254
  const note = {
255
255
  relativePath: relative,
256
256
  slug,
257
+ language: resolveNoteLanguage(relative, config.languages),
257
258
  markdown,
258
259
  fm,
259
260
  headings: extractHeadings(markdown),
@@ -412,6 +413,12 @@ function fragmentExists(note, fragment) {
412
413
  function normalizeMarkdown(markdown) {
413
414
  return markdown.replace(/\r\n/g, "\n");
414
415
  }
416
+ function resolveNoteLanguage(relativePath, languages) {
417
+ if (!languages || languages.length === 0) return void 0;
418
+ const segment = relativePath.split("/")[0]?.toLowerCase();
419
+ if (!segment) return void 0;
420
+ return languages.find((language) => language.toLowerCase() === segment);
421
+ }
415
422
  function getExtension(filePath) {
416
423
  const lastSegment = filePath.split("/").pop() ?? filePath;
417
424
  const dotIndex = lastSegment.lastIndexOf(".");
@@ -440,17 +447,7 @@ function normalizeRelativePath(parts) {
440
447
  function checkFrontmatter(note, source, state, options, diagnostics2) {
441
448
  const location = { slug: note.slug, filePath: note.relativePath };
442
449
  const values = note.fm.values;
443
- if (!note.fm.hasFrontmatter) {
444
- pushDiagnostic(
445
- diagnostics2,
446
- options,
447
- location,
448
- "missing-frontmatter",
449
- "note has no frontmatter",
450
- note.slug,
451
- "add frontmatter with the required fields"
452
- );
453
- } else {
450
+ if (note.fm.hasFrontmatter) {
454
451
  for (const field of options.requiredFrontmatter) {
455
452
  if (!hasField(values, field))
456
453
  pushDiagnostic(
@@ -463,6 +460,16 @@ function checkFrontmatter(note, source, state, options, diagnostics2) {
463
460
  `add "${field}" to the frontmatter`
464
461
  );
465
462
  }
463
+ } else if (options.requiredFrontmatter.length > 0) {
464
+ pushDiagnostic(
465
+ diagnostics2,
466
+ options,
467
+ location,
468
+ "missing-frontmatter",
469
+ "note has no frontmatter",
470
+ note.slug,
471
+ "add frontmatter with the required fields"
472
+ );
466
473
  }
467
474
  if (values.publish === true && values.draft === true)
468
475
  pushDiagnostic(
@@ -625,10 +632,11 @@ function checkSlugCollisions(source, options, diagnostics2) {
625
632
  function checkDuplicateTitles(source, options, diagnostics2) {
626
633
  const byTitle = groupNotes(
627
634
  source.includedNotes.filter((note) => note.published),
628
- (note) => getNoteTitle(note.fm.values, note.slug).toLowerCase()
635
+ (note) => `${note.language ?? ""}\0${getNoteTitle(note.fm.values, note.slug).toLowerCase()}`
629
636
  );
630
- for (const [title, notes] of byTitle) {
637
+ for (const [groupKey, notes] of byTitle) {
631
638
  if (notes.length < 2) continue;
639
+ const title = groupKey.slice(groupKey.indexOf("\0") + 1);
632
640
  const slugs = notes.map((note) => note.slug).join(", ");
633
641
  for (const note of notes)
634
642
  pushDiagnostic(
@@ -829,8 +837,10 @@ async function analyzeContent(config, options = {}) {
829
837
  checkSlugCollisions(source, normalizedOptions, diagnostics2);
830
838
  checkDuplicateTitles(source, normalizedOptions, diagnostics2);
831
839
  checkContentIdIntegrity(source, normalizedOptions, diagnostics2);
832
- checkOrphans(source, state, normalizedOptions, diagnostics2);
833
- checkUnusedAssets(source, state, normalizedOptions, diagnostics2);
840
+ if (!options.skipReferenceIntegrity) {
841
+ checkOrphans(source, state, normalizedOptions, diagnostics2);
842
+ checkUnusedAssets(source, state, normalizedOptions, diagnostics2);
843
+ }
834
844
  checkExcludedPublic(source, normalizedOptions, diagnostics2);
835
845
  if (!options.skipReferenceIntegrity)
836
846
  checkPublishBoundary(source, normalizedOptions, diagnostics2);
@@ -985,7 +995,8 @@ var DEFAULT_SEVERITY2 = {
985
995
  "content-integrity:duplicate-public-location": "error",
986
996
  "content-integrity:redirect-target-missing": "warning",
987
997
  "content-integrity:redirect-cycle": "error",
988
- "content-integrity:redirect-public-location-conflict": "error"
998
+ "content-integrity:redirect-public-location-conflict": "error",
999
+ "content-integrity:ambiguous-folder-page-owner": "warning"
989
1000
  };
990
1001
  function checkSiteIntegrity(manifest, options = {}) {
991
1002
  const diagnostics2 = [];
@@ -1001,6 +1012,7 @@ function checkSiteIntegrity(manifest, options = {}) {
1001
1012
  }
1002
1013
  checkRedirectTargets(manifest, publicRoutes, diagnostics2, options);
1003
1014
  checkRedirectCycles(manifest, diagnostics2, options);
1015
+ checkAmbiguousFolderPageOwners(manifest, diagnostics2, options);
1004
1016
  return diagnostics2;
1005
1017
  }
1006
1018
  function buildRouteOwners(manifest, diagnostics2, options) {
@@ -1019,14 +1031,24 @@ function buildRouteOwners(manifest, diagnostics2, options) {
1019
1031
  kind: "redirect"
1020
1032
  });
1021
1033
  }
1022
- const pagePaths = manifest.pagePaths ?? [];
1023
- for (const path of pagePaths) {
1034
+ const pageRoutes = manifest.pageRoutes ?? [];
1035
+ for (const route of pageRoutes) {
1024
1036
  reserveRoute(owners, diagnostics2, options, {
1025
- path: normalizeRoutePath(path),
1026
- slug: "(plugin-page)",
1027
- kind: "page"
1037
+ path: normalizeRoutePath(route.pathname),
1038
+ slug: `(plugin-page:${route.pluginName}:${route.pageType})`,
1039
+ kind: "page",
1040
+ producer: `${route.pluginName}:${route.pageType}`
1028
1041
  });
1029
1042
  }
1043
+ if (pageRoutes.length === 0) {
1044
+ for (const path of manifest.pagePaths ?? []) {
1045
+ reserveRoute(owners, diagnostics2, options, {
1046
+ path: normalizeRoutePath(path),
1047
+ slug: "(plugin-page)",
1048
+ kind: "page"
1049
+ });
1050
+ }
1051
+ }
1030
1052
  for (const output of manifest.generatedOutputs ?? []) {
1031
1053
  reserveRoute(owners, diagnostics2, options, {
1032
1054
  path: normalizeRoutePath(output.path),
@@ -1253,7 +1275,43 @@ function normalizeRoutePath(value) {
1253
1275
  return path === "/" ? path : path.replace(/\/$/, "");
1254
1276
  }
1255
1277
  function describeOwner(owner) {
1256
- return `${owner.kind} "${owner.slug}"`;
1278
+ return `${owner.kind} "${owner.producer ?? owner.slug}"`;
1279
+ }
1280
+ function checkAmbiguousFolderPageOwners(manifest, diagnostics2, options) {
1281
+ const candidates = /* @__PURE__ */ new Map();
1282
+ for (const entry of manifest.entries) {
1283
+ const basename = entry.slug.split("/").at(-1);
1284
+ if (basename !== "README" && basename !== "index") continue;
1285
+ const folder = entry.slug.split("/").slice(0, -1).join("/");
1286
+ if (!folder) continue;
1287
+ const folderCandidates = candidates.get(folder) ?? {};
1288
+ folderCandidates[basename] = entry;
1289
+ candidates.set(folder, folderCandidates);
1290
+ }
1291
+ for (const [folder, entries] of candidates) {
1292
+ const readme = entries.README;
1293
+ const index = entries.index;
1294
+ if (!readme || !index) continue;
1295
+ diagnostics2.push(
1296
+ diagnostic(options, {
1297
+ code: "content-integrity:ambiguous-folder-page-owner",
1298
+ target: folder,
1299
+ message: `Folder "${folder}" has both README and index Folder Page candidates.`,
1300
+ suggestion: "Keep either README or index so the Folder Page owner is unambiguous.",
1301
+ meta: {
1302
+ folder,
1303
+ readme: {
1304
+ slug: readme.slug,
1305
+ permalink: readme.publicLocation.permalink
1306
+ },
1307
+ index: {
1308
+ slug: index.slug,
1309
+ permalink: index.publicLocation.permalink
1310
+ }
1311
+ }
1312
+ })
1313
+ );
1314
+ }
1257
1315
  }
1258
1316
  function canonicalCycleKey(cycle) {
1259
1317
  const closed = cycle.at(0) === cycle.at(-1) ? cycle.slice(0, -1) : [...cycle];
@@ -1314,6 +1372,7 @@ function diagnostics(options = {}) {
1314
1372
  }
1315
1373
  var diagnosticsPlugin = diagnostics;
1316
1374
  var ANALYTICS_PLUGIN_NAME = "analytics";
1375
+ var L10N_PLUGIN_NAME = "l10n";
1317
1376
  function hasEnabledAnalyticsPlugin(config) {
1318
1377
  return config?.plugins.some(
1319
1378
  (plugin) => plugin.name === ANALYTICS_PLUGIN_NAME && plugin.enabled !== false
@@ -1349,9 +1408,19 @@ function toAnalyzerConfig(target, options) {
1349
1408
  target.content.exclude
1350
1409
  ),
1351
1410
  exclude: [...target.content.exclude, ...options.exclude ?? []],
1352
- publishStrategy: target.content.filters.publishStrategy
1411
+ publishStrategy: target.content.filters.publishStrategy,
1412
+ languages: resolveLanguages(target)
1353
1413
  };
1354
1414
  }
1415
+ function resolveLanguages(config) {
1416
+ const plugin = config.plugins.find(
1417
+ (entry) => entry.name === L10N_PLUGIN_NAME && entry.enabled !== false
1418
+ );
1419
+ const languages = plugin?.options?.languages;
1420
+ return Array.isArray(languages) ? languages.filter(
1421
+ (language) => typeof language === "string"
1422
+ ) : [];
1423
+ }
1355
1424
  export {
1356
1425
  DiagnosticsFailure,
1357
1426
  analyzeContent,
@@ -31,4 +31,5 @@ export type AnalyzerContentConfig = {
31
31
  source?: ContentSource;
32
32
  exclude: string[];
33
33
  publishStrategy: "explicit" | "selective";
34
+ languages?: readonly string[];
34
35
  };
@@ -3,6 +3,7 @@ import type { AnalyzerContentConfig } from "./types.js";
3
3
  export type ScannedNote = {
4
4
  relativePath: string;
5
5
  slug: string;
6
+ language?: string;
6
7
  markdown: string;
7
8
  fm: ParsedFrontmatter;
8
9
  headings: Set<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-diagnostics",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "remark-parse": "^11.0.0",
21
21
  "unified": "^11.0.5",
22
22
  "unist-util-visit": "^5.1.0",
23
- "@riebeckite/core": "0.0.14"
23
+ "@riebeckite/core": "0.0.15"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/mdast": "^4.0.4",