crawlforge-extractors 1.1.0 → 1.2.0

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/templates.js +85 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-extractors",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Extraction logic shared by the CrawlForge MCP server and REST API — scrape templates, charset-correct capped body reading, and structural fingerprinting. One implementation, so the two surfaces cannot drift apart.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/templates.js CHANGED
@@ -104,6 +104,33 @@ function tidy(value) {
104
104
  * "by Jonathan Haidt (Author) Format: Hardcover" on a book. Each states the
105
105
  * same fact wrapped in different chrome.
106
106
  */
107
+ function npmLicense(value) {
108
+ if (!value) return null;
109
+ if (typeof value === 'string') return value;
110
+ // Very old packages use { type, url } or an array of them.
111
+ if (Array.isArray(value)) return value.map(npmLicense).filter(Boolean).join(', ') || null;
112
+ return value.type || null;
113
+ }
114
+
115
+ function npmRepositoryUrl(repository) {
116
+ const raw = typeof repository === 'string' ? repository : repository?.url;
117
+ if (!raw) return null;
118
+ // Registry URLs come as git+ssh://git@host/o/r.git, git+https://…, git://…
119
+ // or a bare "owner/repo" shorthand. Normalise to a browsable https URL.
120
+ let url = raw.replace(/^git\+/, '').replace(/\.git$/, '');
121
+ if (/^[\w.-]+\/[\w.-]+$/.test(url)) return `https://github.com/${url}`;
122
+ url = url.replace(/^git@([^:]+):/, 'https://$1/');
123
+ url = url.replace(/^(?:git|ssh):\/\/(?:git@)?/, 'https://');
124
+ url = url.replace(/^https:\/\/git@/, 'https://');
125
+ return url;
126
+ }
127
+
128
+ function npmBugsUrl(bugs) {
129
+ if (!bugs) return null;
130
+ if (typeof bugs === 'string') return bugs;
131
+ return bugs.url || bugs.email || null;
132
+ }
133
+
107
134
  function amazonByline($) {
108
135
  const contributor = tidy($('#bylineInfo .contributorNameID').first().text());
109
136
  const raw = tidy($('#bylineInfo').first().text());
@@ -466,25 +493,66 @@ export const TEMPLATES = [
466
493
  {
467
494
  id: 'npm-package',
468
495
  name: 'npm Package',
469
- description: 'Scrape an npm package page for name, version, description, weekly downloads, license, and dependencies.',
470
- targetPattern: /npmjs\.com\/package\//i,
471
- extract($) {
472
- const scripts = [];
473
- $('script[type="application/ld+json"]').each((_, el) => {
474
- try { scripts.push(JSON.parse($(el).html())); } catch {}
475
- });
476
- const ld = scripts[0] || {};
496
+ description:
497
+ 'Read a package from the npm registry API rather than the rendered npmjs.com page: exact ' +
498
+ 'latest version, license, repository, homepage, maintainers, dependencies and any ' +
499
+ 'deprecation notice. npmjs.com blocks plain HTTP fetches, and its markup carries no stable ' +
500
+ 'hooks, so the page itself yields almost nothing. Weekly download counts are not included — ' +
501
+ 'they live on a separate api.npmjs.org endpoint.',
502
+ targetPattern: /npmjs\.com\/package\/|registry\.npmjs\.org\//i,
503
+
504
+ /** Point the fetch at the registry document for the same package. */
505
+ resolveUrl(url) {
506
+ const parsed = new URL(url);
507
+ if (parsed.hostname === 'registry.npmjs.org') return url;
508
+ // /package/<name>, /package/@scope/<name>, either optionally followed by
509
+ // /v/<version> or /access etc.
510
+ const match = parsed.pathname.match(/\/package\/((?:@[^/]+\/)?[^/]+)/i);
511
+ if (!match) return url;
512
+ return `https://registry.npmjs.org/${match[1]}`;
513
+ },
514
+
515
+ extractRaw(body, url) {
516
+ let doc;
517
+ try {
518
+ doc = JSON.parse(body);
519
+ } catch {
520
+ throw new Error(
521
+ `Not an npm registry document: ${url} did not return JSON. ` +
522
+ 'This template reads the npm registry API.'
523
+ );
524
+ }
525
+
526
+ // The registry answers an unknown package with {"error":"Not found"}.
527
+ if (!doc || typeof doc.name !== 'string') {
528
+ const reason = typeof doc?.error === 'string' ? doc.error : 'no package document';
529
+ throw new Error(`No npm package at ${url}: ${reason}.`);
530
+ }
531
+
532
+ const latest = doc['dist-tags']?.latest || null;
533
+ const release = (latest && doc.versions?.[latest]) || {};
534
+ const deps = release.dependencies || {};
477
535
 
478
536
  return {
479
- name: text($, 'h1') || ld.name,
480
- version: text($, 'h3[data-testid="package-version-number"]') || text($, '[class*="version"]'),
481
- description: attr($, 'meta[name="description"]', 'content') || text($, 'p[class*="description"]'),
482
- license: text($, 'span[class*="license"]') || text($, '[data-cy="license"]') || ld.license,
483
- weekly_downloads: text($, 'span[class*="weekly-downloads"]') || text($, '[data-cy="downloads"]'),
484
- install_command: `npm install ${ld.name || text($, 'h1') || ''}`.trim(),
485
- homepage: attr($, 'a[href][class*="homepage"]', 'href'),
486
- repository: attr($, 'a[href*="github.com"]', 'href'),
487
- maintainers: list($, 'a[href*="/~"]')
537
+ name: doc.name,
538
+ version: latest,
539
+ description: release.description || doc.description || null,
540
+ license: npmLicense(release.license ?? doc.license),
541
+ homepage: release.homepage || doc.homepage || null,
542
+ repository: npmRepositoryUrl(release.repository || doc.repository),
543
+ bugs: npmBugsUrl(release.bugs || doc.bugs),
544
+ keywords: release.keywords || doc.keywords || [],
545
+ maintainers: (doc.maintainers || [])
546
+ .map(m => (typeof m === 'string' ? m : m?.name))
547
+ .filter(Boolean),
548
+ dependencies: deps,
549
+ dependency_count: Object.keys(deps).length,
550
+ // A string when the publisher deprecated it, false otherwise — npm
551
+ // keeps serving deprecated packages, so this is the only signal.
552
+ deprecated: typeof release.deprecated === 'string' ? release.deprecated : false,
553
+ published: (latest && doc.time?.[latest]) || null,
554
+ last_modified: doc.time?.modified || null,
555
+ install_command: `npm install ${doc.name}`
488
556
  };
489
557
  }
490
558
  }