astro-smart-links 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -12,6 +12,11 @@ No routes file, no second build. The integration validates links against the rea
12
12
 
13
13
  > Migrating from `rehype-smart-links@0.x`? See [Migration](#migration-from-rehype-smart-links).
14
14
 
15
+ ## Requirements
16
+
17
+ - Node.js 18.17 or newer.
18
+ - Astro 4 or newer. Both Astro 7's default Sätteri Markdown processor and the unified processor from `@astrojs/markdown-remark` are supported.
19
+
15
20
  ## Installation
16
21
 
17
22
  ```bash
@@ -113,6 +118,7 @@ npx astro-smart-links check --all --extensions html pdf zip
113
118
  | `--json` | Print the report to stdout |
114
119
  | `-a, --all` | Treat every file type as a valid route |
115
120
  | `-e, --extensions <ext...>` | File extensions to include (default `html`) |
121
+ | `--skip-classes <class...>` | Skip links inside elements with these classes |
116
122
  | `--fail-on-broken` | Exit with code 1 when broken links are found |
117
123
  | `-q, --quiet` | Only print the summary |
118
124
 
@@ -128,6 +134,7 @@ npx astro-smart-links check --all --extensions html pdf zip
128
134
  | `target` | `string \| null` | `'_blank'` | `target` attribute for external links |
129
135
  | `rel` | `string \| null` | `'noopener noreferrer'` | `rel` attribute for external links |
130
136
  | `ignore` | `(string \| RegExp)[]` | `[]` | Hrefs that are never processed (prefix match or RegExp) |
137
+ | `skipClasses` | `string[]` | `[]` | Skip links inside elements with these classes (e.g. `['not-content']`) |
131
138
  | `routes` | `string[]` | — | Explicit route list for broken-link detection |
132
139
  | `routesFile` | `string` | — | JSON file with a route list |
133
140
  | `publicDir` | `string` | — | Directory to scan for routes |
package/README.zh-CN.md CHANGED
@@ -12,6 +12,11 @@
12
12
 
13
13
  > 从 `rehype-smart-links@0.x` 迁移?见 [迁移指南](#从-rehype-smart-links-迁移)。
14
14
 
15
+ ## 环境要求
16
+
17
+ - Node.js 18.17 或更高版本。
18
+ - Astro 4 或更高版本。同时支持 Astro 7 默认的 Sätteri Markdown 处理器和 `@astrojs/markdown-remark` 的 unified 处理器。
19
+
15
20
  ## 安装
16
21
 
17
22
  ```bash
@@ -113,6 +118,7 @@ npx astro-smart-links check --all --extensions html pdf zip
113
118
  | `--json` | 以 JSON 输出到 stdout |
114
119
  | `-a, --all` | 将所有文件类型视为有效路由 |
115
120
  | `-e, --extensions <ext...>` | 要包含的文件扩展名(默认 `html`) |
121
+ | `--skip-classes <class...>` | 跳过位于这些类名元素内的链接 |
116
122
  | `--fail-on-broken` | 发现断链时以退出码 1 结束 |
117
123
  | `-q, --quiet` | 只输出摘要 |
118
124
 
@@ -128,6 +134,7 @@ npx astro-smart-links check --all --extensions html pdf zip
128
134
  | `target` | `string \| null` | `'_blank'` | 外部链接的 `target` |
129
135
  | `rel` | `string \| null` | `'noopener noreferrer'` | 外部链接的 `rel` |
130
136
  | `ignore` | `(string \| RegExp)[]` | `[]` | 不处理的链接(前缀匹配或正则) |
137
+ | `skipClasses` | `string[]` | `[]` | 跳过位于这些类名元素内的链接(如 `['not-content']`) |
131
138
  | `routes` | `string[]` | — | 显式路由列表 |
132
139
  | `routesFile` | `string` | — | 包含路由列表的 JSON 文件 |
133
140
  | `publicDir` | `string` | — | 扫描路由的目录 |
@@ -188,7 +188,6 @@ import { join as join2, relative } from "path";
188
188
  import rehypeParse from "rehype-parse";
189
189
  import rehypeStringify from "rehype-stringify";
190
190
  import { unified } from "unified";
191
- import { visit } from "unist-util-visit";
192
191
  function collectHtmlFiles(directory) {
193
192
  const files = [];
194
193
  const walk = (currentDir) => {
@@ -209,8 +208,14 @@ function routeFromHtmlFile(distDir, htmlFile) {
209
208
  relativePath = relativePath.replace(/(?:^|\/)index$/i, "");
210
209
  return normalizeRoute(relativePath);
211
210
  }
211
+ function hasAnyClass(node, classes) {
212
+ const current = node.properties?.className;
213
+ const list = Array.isArray(current) ? current.map(String) : typeof current === "string" ? current.split(/\s+/) : [];
214
+ return classes.some((className) => list.includes(className));
215
+ }
212
216
  function checkDirectory(directory, options = {}) {
213
217
  const routes = options.routes ?? scanRoutes(directory, options);
218
+ const skipClasses = options.skipClasses ?? [];
214
219
  const broken = [];
215
220
  let checkedLinks = 0;
216
221
  for (const htmlFile of collectHtmlFiles(directory)) {
@@ -221,30 +226,37 @@ function checkDirectory(directory, options = {}) {
221
226
  const tree = processor.parse(source);
222
227
  const pagePath = routeFromHtmlFile(directory, htmlFile);
223
228
  let changed = false;
224
- visit(tree, "element", (node) => {
225
- if (node.tagName !== "a" || !node.properties)
226
- return;
227
- const href = node.properties.href;
228
- if (typeof href !== "string" || href.length === 0)
229
- return;
230
- const classified = classifyHref(href, {
231
- base: options.base,
232
- routes,
233
- ignore: options.ignore,
234
- pagePath
235
- });
236
- if (classified.type === "ignored")
237
- return;
238
- checkedLinks++;
239
- if (classified.type !== "broken")
240
- return;
241
- broken.push({ href, pathname: classified.pathname ?? href, sourceFile: htmlFile });
242
- if (options.rewrite) {
243
- removeClass(node, options.internalLinkClass);
244
- addClass(node, options.brokenLinkClass);
245
- changed = true;
229
+ const walk = (parent, insideSkipped) => {
230
+ for (const child of parent.children) {
231
+ if (child.type !== "element")
232
+ continue;
233
+ const skipped = insideSkipped || skipClasses.length > 0 && hasAnyClass(child, skipClasses);
234
+ if (child.tagName === "a" && !skipped && child.properties) {
235
+ const href = child.properties.href;
236
+ if (typeof href === "string" && href.length > 0) {
237
+ const classified = classifyHref(href, {
238
+ base: options.base,
239
+ routes,
240
+ ignore: options.ignore,
241
+ pagePath
242
+ });
243
+ if (classified.type !== "ignored") {
244
+ checkedLinks++;
245
+ if (classified.type === "broken") {
246
+ broken.push({ href, pathname: classified.pathname ?? href, sourceFile: htmlFile });
247
+ if (options.rewrite) {
248
+ removeClass(child, options.internalLinkClass);
249
+ addClass(child, options.brokenLinkClass);
250
+ changed = true;
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ walk(child, skipped);
246
257
  }
247
- });
258
+ };
259
+ walk(tree, false);
248
260
  if (changed)
249
261
  writeFileSync2(htmlFile, processor.stringify(tree));
250
262
  }
package/dist/cli.cjs ADDED
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_node_fs3 = require("fs");
28
+ var import_node_path3 = require("path");
29
+ var import_node_process = __toESM(require("process"), 1);
30
+ var import_commander = require("commander");
31
+
32
+ // src/core/check.ts
33
+ var import_node_fs2 = require("fs");
34
+ var import_node_path2 = require("path");
35
+ var import_rehype_parse = __toESM(require("rehype-parse"), 1);
36
+ var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
37
+ var import_unified = require("unified");
38
+
39
+ // src/core/normalize.ts
40
+ var SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
41
+ function getScheme(href) {
42
+ const match = SCHEME_RE.exec(href.trim());
43
+ return match ? match[0].slice(0, -1).toLowerCase() : void 0;
44
+ }
45
+ function isExternalHref(href) {
46
+ const trimmed = href.trim();
47
+ if (trimmed.startsWith("//"))
48
+ return true;
49
+ const scheme = getScheme(trimmed);
50
+ return scheme === "http" || scheme === "https";
51
+ }
52
+ function isIgnoredHref(href) {
53
+ const trimmed = href.trim();
54
+ if (!trimmed || trimmed.startsWith("#"))
55
+ return true;
56
+ const scheme = getScheme(trimmed);
57
+ return Boolean(scheme) && scheme !== "http" && scheme !== "https";
58
+ }
59
+ function normalizeBase(base) {
60
+ if (!base)
61
+ return "";
62
+ let normalized = base.trim();
63
+ if (!normalized.startsWith("/"))
64
+ normalized = `/${normalized}`;
65
+ normalized = normalized.replace(/\/+$/, "");
66
+ return normalized === "/" ? "" : normalized;
67
+ }
68
+ function normalizeRoute(input, base) {
69
+ let path = (input ?? "").trim();
70
+ path = path.split(/[?#]/, 1)[0] ?? "";
71
+ try {
72
+ path = decodeURI(path);
73
+ } catch {
74
+ }
75
+ const normalizedBase = normalizeBase(base);
76
+ if (normalizedBase) {
77
+ if (path === normalizedBase)
78
+ path = "/";
79
+ else if (path.startsWith(`${normalizedBase}/`))
80
+ path = path.slice(normalizedBase.length);
81
+ }
82
+ if (!path.startsWith("/"))
83
+ path = `/${path}`;
84
+ path = path.replace(/\/{2,}/g, "/");
85
+ path = path.replace(/\/index(?:\.html)?$/i, "/");
86
+ if (path.length > 1)
87
+ path = path.replace(/\/+$/, "");
88
+ return path || "/";
89
+ }
90
+ function resolveInternalHref(href, pagePath) {
91
+ const trimmed = href.trim();
92
+ if (trimmed.startsWith("/"))
93
+ return normalizeRoute(trimmed);
94
+ const page = pagePath && pagePath !== "/" ? pagePath : "/index";
95
+ try {
96
+ const url = new URL(trimmed, `https://smart-links.local${page}`);
97
+ return normalizeRoute(url.pathname);
98
+ } catch {
99
+ return normalizeRoute(trimmed);
100
+ }
101
+ }
102
+ function matchesIgnoreList(href, pathname, ignore) {
103
+ for (const pattern of ignore) {
104
+ if (typeof pattern === "string") {
105
+ if (href === pattern || href.startsWith(pattern) || pathname === pattern || pathname?.startsWith(pattern))
106
+ return true;
107
+ } else if (pattern.test(href) || (pathname ? pattern.test(pathname) : false)) {
108
+ return true;
109
+ }
110
+ }
111
+ return false;
112
+ }
113
+
114
+ // src/core/classify.ts
115
+ function classifyHref(href, context = {}) {
116
+ if (isIgnoredHref(href))
117
+ return { type: "ignored", href };
118
+ if (isExternalHref(href))
119
+ return { type: "external", href };
120
+ const pathname = normalizeRoute(resolveInternalHref(href, context.pagePath), context.base);
121
+ if (matchesIgnoreList(href, pathname, context.ignore ?? []))
122
+ return { type: "ignored", href, pathname };
123
+ if (pathname === "/")
124
+ return { type: "internal", href, pathname };
125
+ if (context.routes && !context.routes.has(pathname))
126
+ return { type: "broken", href, pathname };
127
+ return { type: "internal", href, pathname };
128
+ }
129
+
130
+ // src/core/hast.ts
131
+ function addClass(node, className) {
132
+ if (!className)
133
+ return;
134
+ const current = node.properties?.className;
135
+ const classes = Array.isArray(current) ? [...current] : typeof current === "string" ? current.split(/\s+/).filter(Boolean) : [];
136
+ if (!classes.includes(className))
137
+ classes.push(className);
138
+ node.properties = { ...node.properties, className: classes };
139
+ }
140
+ function removeClass(node, className) {
141
+ if (!className || !node.properties?.className)
142
+ return;
143
+ const current = node.properties.className;
144
+ const classes = Array.isArray(current) ? current.filter((cls) => cls !== className) : typeof current === "string" ? current.split(/\s+/).filter((cls) => cls && cls !== className) : [];
145
+ if (classes.length > 0)
146
+ node.properties = { ...node.properties, className: classes };
147
+ else
148
+ delete node.properties.className;
149
+ }
150
+
151
+ // src/core/routes.ts
152
+ var import_node_fs = require("fs");
153
+ var import_node_path = require("path");
154
+ function scanRoutes(directory, options = {}) {
155
+ const routes = /* @__PURE__ */ new Set(["/"]);
156
+ const {
157
+ includeAllFiles = false,
158
+ includeFileExtensions = ["html"],
159
+ base
160
+ } = options;
161
+ if (!(0, import_node_fs.existsSync)(directory))
162
+ return routes;
163
+ const walk = (currentDir, basePath) => {
164
+ for (const entry of (0, import_node_fs.readdirSync)(currentDir, { withFileTypes: true })) {
165
+ const fullPath = (0, import_node_path.join)(currentDir, entry.name);
166
+ if (entry.isDirectory()) {
167
+ walk(fullPath, basePath ? `${basePath}/${entry.name}` : entry.name);
168
+ continue;
169
+ }
170
+ if (!entry.isFile())
171
+ continue;
172
+ const extension = (0, import_node_path.extname)(entry.name).slice(1).toLowerCase();
173
+ const included = includeAllFiles || includeFileExtensions.includes(extension);
174
+ if (!included)
175
+ continue;
176
+ let route = basePath ? `${basePath}/${entry.name}` : entry.name;
177
+ if (extension === "html") {
178
+ route = route.replace(/\.html$/i, "");
179
+ route = route.replace(/(?:^|\/)index$/i, "");
180
+ }
181
+ routes.add(normalizeRoute(route, base));
182
+ }
183
+ };
184
+ walk(directory, "");
185
+ return routes;
186
+ }
187
+ function writeRoutesFile(routes, routesFilePath) {
188
+ const sorted = Array.from(new Set(routes)).sort();
189
+ (0, import_node_fs.writeFileSync)(routesFilePath, `${JSON.stringify(sorted, null, 2)}
190
+ `);
191
+ }
192
+
193
+ // src/core/check.ts
194
+ function collectHtmlFiles(directory) {
195
+ const files = [];
196
+ const walk = (currentDir) => {
197
+ for (const entry of (0, import_node_fs2.readdirSync)(currentDir, { withFileTypes: true })) {
198
+ const fullPath = (0, import_node_path2.join)(currentDir, entry.name);
199
+ if (entry.isDirectory())
200
+ walk(fullPath);
201
+ else if (entry.isFile() && entry.name.endsWith(".html"))
202
+ files.push(fullPath);
203
+ }
204
+ };
205
+ walk(directory);
206
+ return files;
207
+ }
208
+ function routeFromHtmlFile(distDir, htmlFile) {
209
+ let relativePath = (0, import_node_path2.relative)(distDir, htmlFile).replace(/\\/g, "/");
210
+ relativePath = relativePath.replace(/\.html$/i, "");
211
+ relativePath = relativePath.replace(/(?:^|\/)index$/i, "");
212
+ return normalizeRoute(relativePath);
213
+ }
214
+ function hasAnyClass(node, classes) {
215
+ const current = node.properties?.className;
216
+ const list = Array.isArray(current) ? current.map(String) : typeof current === "string" ? current.split(/\s+/) : [];
217
+ return classes.some((className) => list.includes(className));
218
+ }
219
+ function checkDirectory(directory, options = {}) {
220
+ const routes = options.routes ?? scanRoutes(directory, options);
221
+ const skipClasses = options.skipClasses ?? [];
222
+ const broken = [];
223
+ let checkedLinks = 0;
224
+ for (const htmlFile of collectHtmlFiles(directory)) {
225
+ const source = (0, import_node_fs2.readFileSync)(htmlFile, "utf-8");
226
+ if (!source.includes("<a"))
227
+ continue;
228
+ const processor = (0, import_unified.unified)().use(import_rehype_parse.default).use(import_rehype_stringify.default);
229
+ const tree = processor.parse(source);
230
+ const pagePath = routeFromHtmlFile(directory, htmlFile);
231
+ let changed = false;
232
+ const walk = (parent, insideSkipped) => {
233
+ for (const child of parent.children) {
234
+ if (child.type !== "element")
235
+ continue;
236
+ const skipped = insideSkipped || skipClasses.length > 0 && hasAnyClass(child, skipClasses);
237
+ if (child.tagName === "a" && !skipped && child.properties) {
238
+ const href = child.properties.href;
239
+ if (typeof href === "string" && href.length > 0) {
240
+ const classified = classifyHref(href, {
241
+ base: options.base,
242
+ routes,
243
+ ignore: options.ignore,
244
+ pagePath
245
+ });
246
+ if (classified.type !== "ignored") {
247
+ checkedLinks++;
248
+ if (classified.type === "broken") {
249
+ broken.push({ href, pathname: classified.pathname ?? href, sourceFile: htmlFile });
250
+ if (options.rewrite) {
251
+ removeClass(child, options.internalLinkClass);
252
+ addClass(child, options.brokenLinkClass);
253
+ changed = true;
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ walk(child, skipped);
260
+ }
261
+ };
262
+ walk(tree, false);
263
+ if (changed)
264
+ (0, import_node_fs2.writeFileSync)(htmlFile, processor.stringify(tree));
265
+ }
266
+ return { routes, broken, checkedLinks };
267
+ }
268
+
269
+ // src/report.ts
270
+ function buildReport(input) {
271
+ const sourceMap = /* @__PURE__ */ new Map();
272
+ for (const record of input.records) {
273
+ if (!record.sourceFile)
274
+ continue;
275
+ const key = record.pathname ?? record.href;
276
+ const sources = sourceMap.get(key) ?? /* @__PURE__ */ new Set();
277
+ sources.add(record.sourceFile);
278
+ sourceMap.set(key, sources);
279
+ }
280
+ const brokenMap = /* @__PURE__ */ new Map();
281
+ for (const link of input.broken) {
282
+ const key = link.pathname || link.href;
283
+ const entry = brokenMap.get(key) ?? { href: link.href, pathname: link.pathname, sources: [] };
284
+ const sources = new Set(entry.sources);
285
+ if (link.sourceFile)
286
+ sources.add(link.sourceFile);
287
+ for (const source of sourceMap.get(key) ?? [])
288
+ sources.add(source);
289
+ entry.sources = Array.from(sources);
290
+ brokenMap.set(key, entry);
291
+ }
292
+ return {
293
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
294
+ routes: input.routeCount,
295
+ links: {
296
+ internal: input.records.filter((record) => record.type === "internal").length,
297
+ external: input.records.filter((record) => record.type === "external").length,
298
+ broken: brokenMap.size
299
+ },
300
+ broken: Array.from(brokenMap.values()).sort((a, b) => a.pathname.localeCompare(b.pathname))
301
+ };
302
+ }
303
+ function formatReport(report) {
304
+ const lines = [];
305
+ lines.push(`Smart links report (${report.generatedAt})`);
306
+ lines.push(`Routes scanned: ${report.routes}`);
307
+ lines.push(`Links: ${report.links.internal} internal, ${report.links.external} external, ${report.links.broken} broken`);
308
+ if (report.broken.length > 0) {
309
+ lines.push("");
310
+ lines.push("Broken internal links:");
311
+ for (const entry of report.broken) {
312
+ const sources = entry.sources.length > 0 ? ` (found in ${entry.sources.join(", ")})` : "";
313
+ lines.push(` ${entry.pathname || entry.href}${sources}`);
314
+ }
315
+ }
316
+ return lines.join("\n");
317
+ }
318
+ function reportToJSON(report) {
319
+ return `${JSON.stringify(report, null, 2)}
320
+ `;
321
+ }
322
+ function reportToHTML(report) {
323
+ const rows = report.broken.map((entry) => `
324
+ <tr>
325
+ <td><code>${escapeHtml(entry.pathname || entry.href)}</code></td>
326
+ <td><code>${escapeHtml(entry.href)}</code></td>
327
+ <td>${entry.sources.length > 0 ? entry.sources.map((source) => `<code>${escapeHtml(source)}</code>`).join("<br>") : "\u2014"}</td>
328
+ </tr>`).join("");
329
+ return `<!DOCTYPE html>
330
+ <html lang="en">
331
+ <head>
332
+ <meta charset="utf-8">
333
+ <meta name="viewport" content="width=device-width, initial-scale=1">
334
+ <title>astro-smart-links report</title>
335
+ <style>
336
+ :root { color-scheme: light dark; }
337
+ body { font-family: ui-sans-serif, system-ui, sans-serif; max-width: 960px; margin: 0 auto; padding: 2rem 1rem; line-height: 1.6; }
338
+ h1 { font-size: 1.5rem; }
339
+ table { border-collapse: collapse; width: 100%; }
340
+ th, td { border: 1px solid color-mix(in oklab, currentColor 20%, transparent); padding: 0.5rem 0.75rem; text-align: left; vertical-align: top; }
341
+ th { background: color-mix(in oklab, currentColor 8%, transparent); }
342
+ code { font-size: 0.9em; }
343
+ .ok { color: #16a34a; font-weight: 600; }
344
+ </style>
345
+ </head>
346
+ <body>
347
+ <h1>astro-smart-links report</h1>
348
+ <p>Generated at ${escapeHtml(report.generatedAt)} \xB7 Scanned ${report.routes} routes</p>
349
+ <p>${report.links.internal} internal \xB7 ${report.links.external} external \xB7 ${report.links.broken} broken</p>
350
+ ${report.broken.length === 0 ? '<p class="ok">No broken internal links found.</p>' : `<table>
351
+ <thead><tr><th>Missing route</th><th>Href</th><th>Found in</th></tr></thead>
352
+ <tbody>${rows}
353
+ </tbody>
354
+ </table>`}
355
+ </body>
356
+ </html>
357
+ `;
358
+ }
359
+ function escapeHtml(value) {
360
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
361
+ }
362
+
363
+ // src/cli.ts
364
+ var program = new import_commander.Command();
365
+ program.name("astro-smart-links").description("CLI utility for astro-smart-links").version("1.1.1");
366
+ program.command("check", { isDefault: true }).description("Check a build directory for broken internal links").option("-d, --dir <path>", "Build directory path", "./dist").option("-o, --output <path>", "Write the report to a file").option("--format <format>", "Report format: json or html", "json").option("--json", "Print the report to stdout as JSON", false).option("-a, --all", "Treat every file type as a valid route", false).option("-e, --extensions <ext...>", "File extensions to treat as routes", ["html"]).option("--skip-classes <class...>", "Skip links inside elements with these classes").option("--fail-on-broken", "Exit with code 1 when broken links are found", false).option("-q, --quiet", "Only print the summary line", false).action((options) => {
367
+ const buildDir = (0, import_node_path3.resolve)(import_node_process.default.cwd(), options.dir);
368
+ const routesFile = options.output ? (0, import_node_path3.resolve)(import_node_process.default.cwd(), options.output) : void 0;
369
+ if (!(0, import_node_fs3.existsSync)(buildDir)) {
370
+ console.error(`Error: build directory '${buildDir}' does not exist.`);
371
+ import_node_process.default.exit(2);
372
+ }
373
+ const { routes, broken, checkedLinks } = checkDirectory(buildDir, {
374
+ includeAllFiles: options.all,
375
+ includeFileExtensions: options.extensions,
376
+ skipClasses: options.skipClasses,
377
+ rewrite: false,
378
+ routes: scanRoutes(buildDir, {
379
+ includeAllFiles: options.all,
380
+ includeFileExtensions: options.extensions
381
+ })
382
+ });
383
+ const report = buildReport({ records: [], broken, routeCount: routes.size });
384
+ report.links.broken = broken.length;
385
+ if (options.json)
386
+ import_node_process.default.stdout.write(reportToJSON(report));
387
+ else if (!options.quiet)
388
+ console.warn(formatReport(report));
389
+ console.warn(
390
+ `Checked ${checkedLinks} links across ${routes.size} routes: ${broken.length} broken.`
391
+ );
392
+ if (routesFile) {
393
+ (0, import_node_fs3.writeFileSync)(routesFile, options.format === "html" ? reportToHTML(report) : reportToJSON(report));
394
+ console.warn(`Report written to ${routesFile}`);
395
+ }
396
+ if (options.failOnBroken && broken.length > 0)
397
+ import_node_process.default.exit(1);
398
+ import_node_process.default.exit(0);
399
+ });
400
+ program.command("build").description("[deprecated] Generate a routes file from a build directory").option("-d, --dir <path>", "Build directory path", "./dist").option("-o, --output <path>", "Output path for the routes file", "./.smart-links-routes.json").option("-a, --all", "Include all files", false).option("-e, --extensions <ext...>", "File extensions to include", ["html"]).action((options) => {
401
+ console.warn(
402
+ "[astro-smart-links] The `build` command is deprecated: the smartLinks() integration no longer needs a routes file."
403
+ );
404
+ const buildDir = (0, import_node_path3.resolve)(import_node_process.default.cwd(), options.dir);
405
+ if (!(0, import_node_fs3.existsSync)(buildDir)) {
406
+ console.error(`Error: build directory '${buildDir}' does not exist.`);
407
+ import_node_process.default.exit(2);
408
+ }
409
+ const routes = scanRoutes(buildDir, {
410
+ includeAllFiles: options.all,
411
+ includeFileExtensions: options.extensions
412
+ });
413
+ const routesFile = (0, import_node_path3.resolve)(import_node_process.default.cwd(), options.output);
414
+ writeRoutesFile(routes, routesFile);
415
+ console.warn(`Wrote ${routes.size} routes to ${routesFile}`);
416
+ });
417
+ program.parse(import_node_process.default.argv);
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  reportToJSON,
8
8
  scanRoutes,
9
9
  writeRoutesFile
10
- } from "./chunk-67PNI3F6.js";
10
+ } from "./chunk-JNP2MFPB.js";
11
11
 
12
12
  // src/cli.ts
13
13
  import { existsSync, writeFileSync } from "fs";
@@ -15,8 +15,8 @@ import { resolve } from "path";
15
15
  import process from "process";
16
16
  import { Command } from "commander";
17
17
  var program = new Command();
18
- program.name("astro-smart-links").description("CLI utility for astro-smart-links").version("1.0.0");
19
- program.command("check", { isDefault: true }).description("Check a build directory for broken internal links").option("-d, --dir <path>", "Build directory path", "./dist").option("-o, --output <path>", "Write the report to a file").option("--format <format>", "Report format: json or html", "json").option("--json", "Print the report to stdout as JSON", false).option("-a, --all", "Treat every file type as a valid route", false).option("-e, --extensions <ext...>", "File extensions to treat as routes", ["html"]).option("--fail-on-broken", "Exit with code 1 when broken links are found", false).option("-q, --quiet", "Only print the summary line", false).action((options) => {
18
+ program.name("astro-smart-links").description("CLI utility for astro-smart-links").version("1.1.1");
19
+ program.command("check", { isDefault: true }).description("Check a build directory for broken internal links").option("-d, --dir <path>", "Build directory path", "./dist").option("-o, --output <path>", "Write the report to a file").option("--format <format>", "Report format: json or html", "json").option("--json", "Print the report to stdout as JSON", false).option("-a, --all", "Treat every file type as a valid route", false).option("-e, --extensions <ext...>", "File extensions to treat as routes", ["html"]).option("--skip-classes <class...>", "Skip links inside elements with these classes").option("--fail-on-broken", "Exit with code 1 when broken links are found", false).option("-q, --quiet", "Only print the summary line", false).action((options) => {
20
20
  const buildDir = resolve(process.cwd(), options.dir);
21
21
  const routesFile = options.output ? resolve(process.cwd(), options.output) : void 0;
22
22
  if (!existsSync(buildDir)) {
@@ -26,6 +26,7 @@ program.command("check", { isDefault: true }).description("Check a build directo
26
26
  const { routes, broken, checkedLinks } = checkDirectory(buildDir, {
27
27
  includeAllFiles: options.all,
28
28
  includeFileExtensions: options.extensions,
29
+ skipClasses: options.skipClasses,
29
30
  rewrite: false,
30
31
  routes: scanRoutes(buildDir, {
31
32
  includeAllFiles: options.all,