| | #!/usr/bin/env node |
| |
|
| | const packages = ['nodes-base', '@n8n/nodes-langchain']; |
| | const concurrency = 20; |
| | let exitCode = 0; |
| |
|
| | const debug = require('debug')('n8n'); |
| | const path = require('path'); |
| | const https = require('https'); |
| | const glob = require('glob'); |
| | const pLimit = require('p-limit'); |
| | const picocolors = require('picocolors'); |
| | const Lookup = require('cacheable-lookup').default; |
| |
|
| | const agent = new https.Agent({ keepAlive: true, keepAliveMsecs: 5000 }); |
| | new Lookup().install(agent); |
| | const limiter = pLimit(concurrency); |
| |
|
| | const validateUrl = async (packageName, kind, type) => |
| | new Promise((resolve, reject) => { |
| | const name = type.displayName; |
| | const documentationUrl = |
| | kind === 'credentials' |
| | ? type.documentationUrl |
| | : type.codex?.resources?.primaryDocumentation?.[0]?.url; |
| | if (!documentationUrl) resolve([name, null]); |
| |
|
| | const url = new URL( |
| | /^https?:\/\//.test(documentationUrl) |
| | ? documentationUrl |
| | : `https://docs.n8n.io/integrations/builtin/${kind}/${documentationUrl.toLowerCase()}/`, |
| | ); |
| | https |
| | .request( |
| | { |
| | hostname: url.hostname, |
| | port: 443, |
| | path: url.pathname, |
| | method: 'HEAD', |
| | agent, |
| | }, |
| | (res) => { |
| | debug(picocolors.green('✓'), packageName, kind, name); |
| | resolve([name, res.statusCode]); |
| | }, |
| | ) |
| | .on('error', (e) => { |
| | debug(picocolors.red('✘'), packageName, kind, name); |
| | reject(e); |
| | }) |
| | .end(); |
| | }); |
| |
|
| | const checkLinks = async (packageName, kind) => { |
| | const baseDir = path.resolve(__dirname, '../../packages', packageName); |
| | let types = require(path.join(baseDir, `dist/types/${kind}.json`)); |
| | if (kind === 'nodes') |
| | types = types.filter( |
| | ({ codex, hidden }) => !!codex?.resources?.primaryDocumentation && !hidden, |
| | ); |
| | debug(packageName, kind, types.length); |
| |
|
| | const statuses = await Promise.all( |
| | types.map((type) => |
| | limiter(() => { |
| | return validateUrl(packageName, kind, type); |
| | }), |
| | ), |
| | ); |
| |
|
| | const missingDocs = []; |
| | const invalidUrls = []; |
| | for (const [name, statusCode] of statuses) { |
| | if (statusCode === null) missingDocs.push(name); |
| | if (statusCode !== 200) invalidUrls.push(name); |
| | } |
| |
|
| | if (missingDocs.length) |
| | console.log('Documentation URL missing in %s for %s', packageName, kind, missingDocs); |
| | if (invalidUrls.length) |
| | console.log('Documentation URL invalid in %s for %s', packageName, kind, invalidUrls); |
| | if (missingDocs.length || invalidUrls.length) exitCode = 1; |
| | }; |
| |
|
| | (async () => { |
| | for (const packageName of packages) { |
| | await Promise.all([checkLinks(packageName, 'credentials'), checkLinks(packageName, 'nodes')]); |
| | if (exitCode !== 0) process.exit(exitCode); |
| | } |
| | })(); |
| |
|