@djangocfg/nextjs 2.1.48 → 2.1.50

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.
@@ -1,477 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Smart Link Checker using linkinator
5
- *
6
- * Checks all links on a website with proper error handling,
7
- * timeout management, and filtering of problematic URLs.
8
- *
9
- * @example
10
- * ```bash
11
- * node check-links.ts https://example.com
12
- * ```
13
- */
14
-
15
- import { mkdir, writeFile } from 'fs/promises';
16
- import * as linkinator from 'linkinator';
17
- import { dirname } from 'path';
18
- import pc from 'picocolors';
19
- import prompts from 'prompts';
20
-
21
- import type { CheckOptions } from 'linkinator';
22
- export interface CheckLinksOptions {
23
- /** Base URL to check */
24
- url: string;
25
- /** Timeout in milliseconds (default: 60000) */
26
- timeout?: number;
27
- /** URLs to skip (regex pattern) */
28
- skipPattern?: string;
29
- /** Show only broken links (default: true) */
30
- showOnlyBroken?: boolean;
31
- /** Maximum number of concurrent requests (default: 50) */
32
- concurrency?: number;
33
- /** Output file path for report (optional) */
34
- outputFile?: string;
35
- /** Report format: 'json', 'markdown', 'text' (default: 'text') */
36
- reportFormat?: 'json' | 'markdown' | 'text';
37
- /** Verbose logging (default: true) */
38
- verbose?: boolean;
39
- }
40
-
41
- const DEFAULT_SKIP_PATTERN = [
42
- 'github.com',
43
- 'twitter.com',
44
- 'linkedin.com',
45
- 'x.com',
46
- '127.0.0.1',
47
- 'uneralon.com',
48
- 'localhost:[0-9]+',
49
- 'api\\.localhost',
50
- 'demo\\.localhost',
51
- 'cdn-cgi', // Cloudflare email protection
52
- 'mailto:', // Email links
53
- 'tel:', // Phone links
54
- 'javascript:', // JavaScript links
55
- ].join('|');
56
-
57
- export interface CheckLinksResult {
58
- success: boolean;
59
- broken: number;
60
- total: number;
61
- errors: Array<{ url: string; status: number | string; reason?: string }>;
62
- url: string;
63
- timestamp: string;
64
- duration?: number;
65
- }
66
-
67
- export async function checkLinks(options: CheckLinksOptions): Promise<CheckLinksResult> {
68
- const {
69
- url,
70
- timeout = 60000, // Increased to 60 seconds
71
- skipPattern = DEFAULT_SKIP_PATTERN,
72
- showOnlyBroken = true,
73
- concurrency = 50, // Increased to 50 concurrent requests
74
- outputFile,
75
- reportFormat = 'text',
76
- verbose = true,
77
- } = options;
78
-
79
- const startTime = Date.now();
80
-
81
- if (verbose) {
82
- console.log(pc.cyan(`\nšŸ” Starting link check for: ${pc.bold(url)}`));
83
- console.log(pc.dim(` Timeout: ${timeout}ms | Concurrency: ${concurrency}`));
84
- console.log('');
85
- }
86
-
87
- // Build linksToSkip array from regex pattern
88
- const skipRegex = new RegExp(skipPattern);
89
- const linksToSkip: string[] = [];
90
-
91
- const checkOptions: CheckOptions = {
92
- path: url,
93
- recurse: true,
94
- timeout,
95
- concurrency,
96
- linksToSkip: (link: string) => {
97
- return Promise.resolve(skipRegex.test(link));
98
- },
99
- };
100
- const broken: Array<{ url: string; status: number | string; reason?: string }> = [];
101
- let total = 0;
102
-
103
- try {
104
- const results = await linkinator.check(checkOptions);
105
-
106
- // linkinator.check() returns { links: LinkResult[], passed: boolean }
107
- for (const result of results.links) {
108
- total++;
109
- const status = result.status || 0;
110
-
111
- // Consider broken: non-2xx status codes, timeouts, errors
112
- if (status < 200 || status >= 400 || result.state === 'BROKEN') {
113
- broken.push({
114
- url: result.url,
115
- status: status || 'TIMEOUT',
116
- reason: result.state === 'BROKEN' ? 'BROKEN' : undefined,
117
- });
118
- }
119
- }
120
-
121
- const success = broken.length === 0;
122
-
123
- if (!showOnlyBroken || broken.length > 0) {
124
- if (success) {
125
- console.log(`āœ… All links are valid!`);
126
- console.log(` Checked ${total} links.`);
127
- } else {
128
- console.log(`āŒ Found ${broken.length} broken links out of ${total} total:`);
129
- console.log('');
130
- for (const { url, status, reason } of broken) {
131
- console.log(` [${status}] ${url}${reason ? ` (${reason})` : ''}`);
132
- }
133
- }
134
- }
135
-
136
- const duration = Date.now() - startTime;
137
- const result: CheckLinksResult = {
138
- success,
139
- broken: broken.length,
140
- total,
141
- errors: broken,
142
- url,
143
- timestamp: new Date().toISOString(),
144
- duration,
145
- };
146
-
147
- // Save report if output file is specified
148
- if (outputFile) {
149
- await saveReport(result, outputFile, reportFormat);
150
- console.log(pc.green(`\nšŸ“„ Report saved to: ${pc.cyan(outputFile)}`));
151
- }
152
-
153
- return result;
154
- } catch (error) {
155
- // Handle timeout and other errors gracefully
156
- const errorMessage = error instanceof Error ? error.message : String(error);
157
- const errorName = error instanceof Error ? error.name : 'UnknownError';
158
-
159
- if (
160
- errorMessage.includes('timeout') ||
161
- errorMessage.includes('TimeoutError') ||
162
- errorName === 'TimeoutError' ||
163
- errorMessage.includes('aborted')
164
- ) {
165
- console.warn(pc.yellow(`āš ļø Some links timed out after ${timeout}ms`));
166
- console.warn(pc.dim(` This is normal for slow or protected URLs.`));
167
- if (total > 0) {
168
- console.warn(pc.dim(` Checked ${total} links before timeout.`));
169
- }
170
-
171
- // If we have some broken links, show them
172
- if (broken.length > 0) {
173
- console.log(pc.red(`\nāŒ Found ${broken.length} broken links:`));
174
- for (const { url, status, reason } of broken) {
175
- const statusColor = typeof status === 'number' && status >= 500 ? pc.red : pc.yellow;
176
- console.log(` ${statusColor(`[${status}]`)} ${pc.cyan(url)}${reason ? pc.dim(` (${reason})`) : ''}`);
177
- }
178
- }
179
- } else {
180
- console.error(pc.red(`āŒ Error checking links: ${errorMessage}`));
181
- if (error instanceof Error && error.stack) {
182
- console.error(pc.dim(error.stack));
183
- }
184
- }
185
-
186
- const duration = Date.now() - startTime;
187
- // Return partial results even on error
188
- const result: CheckLinksResult = {
189
- success: broken.length === 0 && total > 0, // Success only if no broken links and we checked something
190
- broken: broken.length,
191
- total,
192
- errors: broken,
193
- url,
194
- timestamp: new Date().toISOString(),
195
- duration,
196
- };
197
-
198
- // Save report if output file is specified
199
- if (outputFile) {
200
- try {
201
- await saveReport(result, outputFile, reportFormat);
202
- console.log(pc.green(`\nšŸ“„ Report saved to: ${pc.cyan(outputFile)}`));
203
- } catch (saveError) {
204
- console.warn(pc.yellow(`\nāš ļø Failed to save report: ${saveError instanceof Error ? saveError.message : String(saveError)}`));
205
- }
206
- }
207
-
208
- return result;
209
- }
210
- }
211
-
212
- /**
213
- * Save report to file in specified format
214
- */
215
- async function saveReport(
216
- result: CheckLinksResult,
217
- filePath: string,
218
- format: 'json' | 'markdown' | 'text'
219
- ): Promise<void> {
220
- // Ensure directory exists
221
- const dir = dirname(filePath);
222
- if (dir !== '.') {
223
- await mkdir(dir, { recursive: true });
224
- }
225
-
226
- let content: string;
227
-
228
- switch (format) {
229
- case 'json':
230
- content = JSON.stringify(result, null, 2);
231
- break;
232
-
233
- case 'markdown':
234
- content = generateMarkdownReport(result);
235
- break;
236
-
237
- case 'text':
238
- default:
239
- content = generateTextReport(result);
240
- break;
241
- }
242
-
243
- await writeFile(filePath, content, 'utf-8');
244
- }
245
-
246
- /**
247
- * Generate markdown report
248
- */
249
- function generateMarkdownReport(result: CheckLinksResult): string {
250
- const lines: string[] = [];
251
-
252
- lines.push('# Link Check Report');
253
- lines.push('');
254
- lines.push(`**URL:** ${result.url}`);
255
- lines.push(`**Timestamp:** ${result.timestamp}`);
256
- if (result.duration) {
257
- lines.push(`**Duration:** ${(result.duration / 1000).toFixed(2)}s`);
258
- }
259
- lines.push('');
260
- lines.push(`**Status:** ${result.success ? 'āœ… All links valid' : 'āŒ Broken links found'}`);
261
- lines.push(`**Total links:** ${result.total}`);
262
- lines.push(`**Broken links:** ${result.broken}`);
263
- lines.push('');
264
-
265
- if (result.errors.length > 0) {
266
- lines.push('## Broken Links');
267
- lines.push('');
268
- lines.push('| Status | URL | Reason |');
269
- lines.push('|--------|-----|--------|');
270
- for (const { url, status, reason } of result.errors) {
271
- lines.push(`| ${status} | ${url} | ${reason || '-'} |`);
272
- }
273
- lines.push('');
274
- }
275
-
276
- return lines.join('\n');
277
- }
278
-
279
- /**
280
- * Generate text report
281
- */
282
- function generateTextReport(result: CheckLinksResult): string {
283
- const lines: string[] = [];
284
-
285
- lines.push('Link Check Report');
286
- lines.push('='.repeat(50));
287
- lines.push(`URL: ${result.url}`);
288
- lines.push(`Timestamp: ${result.timestamp}`);
289
- if (result.duration) {
290
- lines.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
291
- }
292
- lines.push('');
293
- lines.push(`Status: ${result.success ? 'āœ… All links valid' : 'āŒ Broken links found'}`);
294
- lines.push(`Total links: ${result.total}`);
295
- lines.push(`Broken links: ${result.broken}`);
296
- lines.push('');
297
-
298
- if (result.errors.length > 0) {
299
- lines.push('Broken Links:');
300
- lines.push('-'.repeat(50));
301
- for (const { url, status, reason } of result.errors) {
302
- lines.push(`[${status}] ${url}${reason ? ` (${reason})` : ''}`);
303
- }
304
- lines.push('');
305
- }
306
-
307
- return lines.join('\n');
308
- }
309
-
310
- // CLI interface
311
- if (import.meta.url === `file://${process.argv[1]}`) {
312
- // Handle unhandled promise rejections and errors
313
- process.on('unhandledRejection', (error) => {
314
- if (error instanceof Error) {
315
- if (error.message.includes('timeout') || error.message.includes('TimeoutError') || error.name === 'TimeoutError') {
316
- console.warn(pc.yellow(`\nāš ļø Operation timed out. This is normal for slow or protected URLs.`));
317
- process.exit(0); // Exit gracefully
318
- } else {
319
- console.error(pc.red(`\nāŒ Unhandled error: ${error.message}`));
320
- process.exit(1);
321
- }
322
- } else {
323
- console.error(pc.red(`\nāŒ Unhandled error:`), error);
324
- process.exit(1);
325
- }
326
- });
327
-
328
- process.on('uncaughtException', (error) => {
329
- if (error.message.includes('timeout') || error.message.includes('TimeoutError') || error.name === 'TimeoutError') {
330
- console.warn(pc.yellow(`\nāš ļø Operation timed out. This is normal for slow or protected URLs.`));
331
- process.exit(0); // Exit gracefully
332
- } else {
333
- console.error(pc.red(`\nāŒ Uncaught exception: ${error.message}`));
334
- process.exit(1);
335
- }
336
- });
337
-
338
- const args = process.argv.slice(2);
339
-
340
- // Interactive mode if no arguments
341
- if (args.length === 0) {
342
- (async () => {
343
- console.log(pc.bold(pc.cyan('\nšŸ”— Link Checker\n')));
344
-
345
- const response = await prompts([
346
- {
347
- type: 'text',
348
- name: 'url',
349
- message: 'Enter URL to check:',
350
- validate: (value) => value.length > 0 || 'URL is required',
351
- },
352
- {
353
- type: 'number',
354
- name: 'timeout',
355
- message: 'Timeout (ms):',
356
- initial: 60000,
357
- min: 1000,
358
- },
359
- {
360
- type: 'number',
361
- name: 'concurrency',
362
- message: 'Concurrency:',
363
- initial: 50,
364
- min: 1,
365
- max: 200,
366
- },
367
- {
368
- type: 'confirm',
369
- name: 'verbose',
370
- message: 'Verbose logging (show progress)?',
371
- initial: true,
372
- },
373
- {
374
- type: 'confirm',
375
- name: 'showAll',
376
- message: 'Show all links (not just broken)?',
377
- initial: false,
378
- },
379
- {
380
- type: 'text',
381
- name: 'outputFile',
382
- message: 'Save report to file (optional, leave empty to skip):',
383
- initial: '',
384
- },
385
- {
386
- type: (prev) => prev ? 'select' : null,
387
- name: 'reportFormat',
388
- message: 'Report format:',
389
- choices: [
390
- { title: 'Text', value: 'text' },
391
- { title: 'Markdown', value: 'markdown' },
392
- { title: 'JSON', value: 'json' },
393
- ],
394
- initial: 0,
395
- },
396
- ]);
397
-
398
- if (!response.url) {
399
- console.log(pc.yellow('Cancelled.'));
400
- process.exit(0);
401
- }
402
-
403
- const options: CheckLinksOptions = {
404
- url: response.url,
405
- timeout: response.timeout,
406
- concurrency: response.concurrency,
407
- showOnlyBroken: !response.showAll,
408
- outputFile: response.outputFile || undefined,
409
- reportFormat: response.reportFormat || 'text',
410
- verbose: response.verbose !== undefined ? response.verbose : true,
411
- };
412
-
413
- await checkLinks(options);
414
- process.exit(0);
415
- })().catch((error) => {
416
- console.error(pc.red('Fatal error:'), error);
417
- process.exit(1);
418
- });
419
- } else {
420
- // Non-interactive mode with arguments
421
- if (args[0] === '--help' || args[0] === '-h') {
422
- console.log(pc.bold('Usage: check-links.ts <url> [options]'));
423
- console.log('');
424
- console.log(pc.bold('Options:'));
425
- console.log(` ${pc.cyan('--timeout <ms>')} Timeout in milliseconds (default: 60000)`);
426
- console.log(` ${pc.cyan('--skip <pattern>')} Regex pattern for URLs to skip`);
427
- console.log(` ${pc.cyan('--show-all')} Show all links, not just broken ones`);
428
- console.log(` ${pc.cyan('--concurrency <num>')} Max concurrent requests (default: 50)`);
429
- console.log(` ${pc.cyan('--output <file>')} Save report to file`);
430
- console.log(` ${pc.cyan('--format <format>')} Report format: json, markdown, text (default: text)`);
431
- console.log(` ${pc.cyan('--quiet, -q')} Disable verbose logging`);
432
- console.log(` ${pc.cyan('--help, -h')} Show this help message`);
433
- console.log('');
434
- console.log(pc.dim('If no arguments provided, interactive mode will start.'));
435
- process.exit(0);
436
- }
437
-
438
- const url = args[0];
439
- const options: CheckLinksOptions = { url };
440
-
441
- // Parse options
442
- for (let i = 1; i < args.length; i++) {
443
- const arg = args[i];
444
- if (arg === '--timeout' && args[i + 1]) {
445
- options.timeout = parseInt(args[++i], 10);
446
- } else if (arg === '--skip' && args[i + 1]) {
447
- options.skipPattern = args[++i];
448
- } else if (arg === '--show-all') {
449
- options.showOnlyBroken = false;
450
- } else if (arg === '--concurrency' && args[i + 1]) {
451
- options.concurrency = parseInt(args[++i], 10);
452
- } else if ((arg === '--output' || arg === '-o') && args[i + 1]) {
453
- options.outputFile = args[++i];
454
- } else if (arg === '--format' && args[i + 1]) {
455
- const format = args[++i] as 'json' | 'markdown' | 'text';
456
- if (['json', 'markdown', 'text'].includes(format)) {
457
- options.reportFormat = format;
458
- } else {
459
- console.error(pc.red(`Invalid format: ${format}. Use: json, markdown, or text`));
460
- process.exit(1);
461
- }
462
- } else if (arg === '--quiet' || arg === '-q') {
463
- options.verbose = false;
464
- }
465
- }
466
-
467
- checkLinks(options)
468
- .then((result) => {
469
- process.exit(result.success ? 0 : 1);
470
- })
471
- .catch((error) => {
472
- console.error(pc.red('Fatal error:'), error);
473
- process.exit(1);
474
- });
475
- }
476
- }
477
-
@@ -1,6 +0,0 @@
1
- /**
2
- * Scripts and utilities for Next.js projects
3
- */
4
-
5
- export { checkLinks, type CheckLinksOptions } from './check-links';
6
-