@hydrafetch/node-sdk 0.1.1 → 0.3.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.
package/README.md CHANGED
@@ -82,6 +82,35 @@ out.results.forEach((item) => console.log(item.url, item.data?.name));
82
82
 
83
83
  Pass `prompt` instead of, or alongside, `schema` to describe the fields in plain language.
84
84
 
85
+ ### Zod schemas
86
+
87
+ If you already use Zod, pass the schema directly. Zod stays optional and is imported only when you
88
+ pass one, so this package still installs with no dependencies of its own.
89
+
90
+ Zod 4 converts its own schemas and needs nothing else. On Zod 3, install `zod-to-json-schema`
91
+ alongside it and that is used instead. Which one is used is decided by the schema you pass, not by
92
+ what is installed, so a workspace holding both majors still works.
93
+
94
+ Nested schemas are flattened before sending, including one reused in two places. A schema that
95
+ refers to itself cannot be flattened and is refused rather than sent.
96
+
97
+ ```ts
98
+ import { z } from 'zod';
99
+
100
+ const Product = z.object({
101
+ name: z.string().describe("the name of the product this page is selling, not a recommended one"),
102
+ priceUsd: z.number().describe('its current price in US dollars'),
103
+ });
104
+
105
+ const out = await hf.extract<z.infer<typeof Product>>(['https://example.com/product/1'], {
106
+ schema: Product,
107
+ });
108
+ ```
109
+
110
+ `.describe()` is the reason to bother. The description travels with the field to the model doing the
111
+ extraction, and on a page carrying several products, several dates or several addresses it is what
112
+ tells the model which one you meant. The same works in a plain JSON Schema with `description`.
113
+
85
114
  ## Discovery and bulk work
86
115
 
87
116
  `map` lists a site's URLs for one credit without fetching any page.
@@ -133,6 +162,7 @@ results.forEach((r) => console.log(r.title, r.url, r.data?.markdown));
133
162
  await hf.brand('stripe.com'); // logos, colours, fonts, socials
134
163
  await hf.logo('stripe.com', { theme: 'dark', type: 'icon' }); // one asset
135
164
  await hf.styleguide('stripe.com'); // computed design system
165
+ await hf.styleguide('https://stripe.com/pricing', { maxAge: 0 }); // one page, read fresh
136
166
  ```
137
167
 
138
168
  For logos in a browser use [`@hydrafetch/client-sdk`](https://github.com/Hydrafetch/client-sdk) or [`@hydrafetch/react`](https://github.com/Hydrafetch/react-sdk) with a publishable key. Those bill against logo pulls rather than credits.
@@ -196,7 +226,7 @@ const hf = new Hydrafetch({
196
226
  | `extract(urls, options?)` | `ExtractResult<T>` | 5 per URL |
197
227
  | `brand(domain)` | `BrandResult` | 5 |
198
228
  | `logo(domain, options?)` | `LogoResult` | 1 |
199
- | `styleguide(domain)` | `StyleguideResult` | 10 |
229
+ | `styleguide(domainOrUrl, options?)` | `StyleguideResult` | 10 |
200
230
  | `screenshot(url, options?)` | `ScreenshotResult` | 5 |
201
231
  | `images(url)` | `ImagesResult` | 1 |
202
232
  | `links(url, options?)` | `ScrapeResult<'links'>` | 1 |
package/dist/index.cjs CHANGED
@@ -67,6 +67,62 @@ var DEFAULT_MAX_RETRIES = 2;
67
67
  function sleep(ms) {
68
68
  return new Promise((resolve) => setTimeout(resolve, ms));
69
69
  }
70
+ async function toJsonSchema(schema) {
71
+ const zodVersion = zodMajorOf(schema);
72
+ if (zodVersion === null) return schema;
73
+ const converter = await zodConverter(zodVersion);
74
+ let converted;
75
+ try {
76
+ converted = converter(schema);
77
+ } catch (cause) {
78
+ throw new HydrafetchError({
79
+ code: "SCHEMA_CONVERSION_FAILED",
80
+ message: `The Zod schema could not be converted: ${cause?.message ?? cause}`,
81
+ status: 0
82
+ });
83
+ }
84
+ if (JSON.stringify(converted).includes('"$ref":"#')) {
85
+ throw new HydrafetchError({
86
+ code: "SCHEMA_CONVERSION_FAILED",
87
+ message: "The Zod schema refers to itself, so it cannot be expressed as a flat JSON Schema. Flatten the schema or pass JSON Schema directly.",
88
+ status: 0
89
+ });
90
+ }
91
+ return converted;
92
+ }
93
+ function zodMajorOf(schema) {
94
+ if (typeof schema !== "object" || schema === null) return null;
95
+ if (typeof schema.parse !== "function") return null;
96
+ if ("_zod" in schema) return 4;
97
+ return "_def" in schema ? 3 : null;
98
+ }
99
+ async function zodConverter(version) {
100
+ if (version === 4) {
101
+ const zod = await optionalImport(
102
+ "zod"
103
+ );
104
+ if (zod && typeof zod.toJSONSchema === "function") return zod.toJSONSchema;
105
+ }
106
+ const legacy = await optionalImport("zod-to-json-schema");
107
+ if (legacy && typeof legacy.zodToJsonSchema === "function") {
108
+ return (schema) => legacy.zodToJsonSchema(schema, { $refStrategy: "none" });
109
+ }
110
+ throw new HydrafetchError({
111
+ code: "SCHEMA_CONVERSION_FAILED",
112
+ message: `A Zod ${version} schema was passed but nothing here can convert it. Use zod 4, which converts its own schemas, or install zod-to-json-schema alongside zod 3, or pass a JSON Schema.`,
113
+ status: 0
114
+ });
115
+ }
116
+ async function optionalImport(name) {
117
+ try {
118
+ return await import(
119
+ /* @vite-ignore */
120
+ name
121
+ );
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
70
126
  var Hydrafetch = class {
71
127
  apiKey;
72
128
  baseUrl;
@@ -171,10 +227,12 @@ var Hydrafetch = class {
171
227
  search(query, options = {}) {
172
228
  return this.request("POST", "/v1/web/search", { query, ...options });
173
229
  }
174
- extract(urls, options = {}) {
230
+ async extract(urls, options = {}) {
231
+ const { schema, ...rest } = options;
175
232
  return this.request("POST", "/v1/web/extract", {
176
233
  urls: Array.isArray(urls) ? urls : [urls],
177
- ...options
234
+ ...rest,
235
+ ...schema === void 0 ? {} : { schema: await toJsonSchema(schema) }
178
236
  });
179
237
  }
180
238
  brand(domain) {
@@ -183,8 +241,12 @@ var Hydrafetch = class {
183
241
  logo(domain, options = {}) {
184
242
  return this.request("GET", "/v1/web/brand/logo", void 0, { domain, ...options });
185
243
  }
186
- styleguide(domain) {
187
- return this.request("GET", "/v1/web/styleguide", void 0, { domain });
244
+ styleguide(domainOrUrl, options = {}) {
245
+ const target = /^https?:\/\//i.test(domainOrUrl) ? { url: domainOrUrl } : { domain: domainOrUrl };
246
+ return this.request("GET", "/v1/web/styleguide", void 0, {
247
+ ...target,
248
+ ...options
249
+ });
188
250
  }
189
251
  screenshot(url, options = {}) {
190
252
  return this.request("POST", "/v1/web/screenshot", { url, ...options });
package/dist/index.d.cts CHANGED
@@ -90,8 +90,23 @@ interface SearchResult {
90
90
  status?: string;
91
91
  results: SearchResultItem[];
92
92
  }
93
+ /**
94
+ * A standards-compliant JSON Schema, or a Zod schema you already have.
95
+ *
96
+ * Zod is not a dependency of this package and never will be: it is loaded only if you actually
97
+ * pass one. The reason to pass one is `.describe()`. A field description reaches the model that
98
+ * does the extraction, and on a page carrying several products or several dates it is what tells
99
+ * the model which one you meant.
100
+ */
101
+ type ExtractSchema = Record<string, unknown> | ZodLike;
102
+ /** Structurally a Zod schema, without importing Zod to say so. */
103
+ interface ZodLike {
104
+ _zod?: unknown;
105
+ _def?: unknown;
106
+ parse?: (value: unknown) => unknown;
107
+ }
93
108
  interface ExtractOptions {
94
- schema?: Record<string, unknown>;
109
+ schema?: ExtractSchema;
95
110
  prompt?: string;
96
111
  preferStructure?: boolean;
97
112
  enableWebSearch?: boolean;
@@ -170,8 +185,12 @@ interface LogoResult {
170
185
  type?: string;
171
186
  [key: string]: unknown;
172
187
  }
188
+ interface StyleguideOptions {
189
+ maxAge?: number;
190
+ }
173
191
  interface StyleguideResult {
174
192
  domain?: string;
193
+ page?: string;
175
194
  colors?: Record<string, unknown>;
176
195
  typography?: Record<string, unknown>;
177
196
  [key: string]: unknown;
@@ -235,7 +254,7 @@ declare class Hydrafetch {
235
254
  extract<T = unknown>(urls: string | string[], options?: ExtractOptions): Promise<ExtractResult<T>>;
236
255
  brand(domain: string): Promise<BrandResult>;
237
256
  logo(domain: string, options?: BrandOptions): Promise<LogoResult>;
238
- styleguide(domain: string): Promise<StyleguideResult>;
257
+ styleguide(domainOrUrl: string, options?: StyleguideOptions): Promise<StyleguideResult>;
239
258
  screenshot(url: string, options?: ScreenshotOptions): Promise<ScreenshotResult>;
240
259
  images(url: string, options?: {
241
260
  maxAge?: number;
@@ -271,4 +290,4 @@ declare class HydrafetchTimeoutError extends HydrafetchError {
271
290
  constructor(message: string);
272
291
  }
273
292
 
274
- export { type BatchOptions, type BrandOptions, type BrandResult, type CrawlOptions, type ExtractItem, type ExtractOptions, type ExtractResult, type Format, Hydrafetch, HydrafetchError, type HydrafetchOptions, HydrafetchTimeoutError, type ImagesResult, type JobPage, type JobResult, type JobStatus, type LogoResult, type MapOptions, type MapResult, type PageImage, type PageMetadata, type ScrapeBase, type ScrapeContent, type ScrapeOptions, type ScrapeResult, type ScreenshotOptions, type ScreenshotResult, type SearchOptions, type SearchResult, type SearchResultItem, type StyleguideResult, type Usage, type WaitOptions };
293
+ export { type BatchOptions, type BrandOptions, type BrandResult, type CrawlOptions, type ExtractItem, type ExtractOptions, type ExtractResult, type ExtractSchema, type Format, Hydrafetch, HydrafetchError, type HydrafetchOptions, HydrafetchTimeoutError, type ImagesResult, type JobPage, type JobResult, type JobStatus, type LogoResult, type MapOptions, type MapResult, type PageImage, type PageMetadata, type ScrapeBase, type ScrapeContent, type ScrapeOptions, type ScrapeResult, type ScreenshotOptions, type ScreenshotResult, type SearchOptions, type SearchResult, type SearchResultItem, type StyleguideOptions, type StyleguideResult, type Usage, type WaitOptions, type ZodLike };
package/dist/index.d.ts CHANGED
@@ -90,8 +90,23 @@ interface SearchResult {
90
90
  status?: string;
91
91
  results: SearchResultItem[];
92
92
  }
93
+ /**
94
+ * A standards-compliant JSON Schema, or a Zod schema you already have.
95
+ *
96
+ * Zod is not a dependency of this package and never will be: it is loaded only if you actually
97
+ * pass one. The reason to pass one is `.describe()`. A field description reaches the model that
98
+ * does the extraction, and on a page carrying several products or several dates it is what tells
99
+ * the model which one you meant.
100
+ */
101
+ type ExtractSchema = Record<string, unknown> | ZodLike;
102
+ /** Structurally a Zod schema, without importing Zod to say so. */
103
+ interface ZodLike {
104
+ _zod?: unknown;
105
+ _def?: unknown;
106
+ parse?: (value: unknown) => unknown;
107
+ }
93
108
  interface ExtractOptions {
94
- schema?: Record<string, unknown>;
109
+ schema?: ExtractSchema;
95
110
  prompt?: string;
96
111
  preferStructure?: boolean;
97
112
  enableWebSearch?: boolean;
@@ -170,8 +185,12 @@ interface LogoResult {
170
185
  type?: string;
171
186
  [key: string]: unknown;
172
187
  }
188
+ interface StyleguideOptions {
189
+ maxAge?: number;
190
+ }
173
191
  interface StyleguideResult {
174
192
  domain?: string;
193
+ page?: string;
175
194
  colors?: Record<string, unknown>;
176
195
  typography?: Record<string, unknown>;
177
196
  [key: string]: unknown;
@@ -235,7 +254,7 @@ declare class Hydrafetch {
235
254
  extract<T = unknown>(urls: string | string[], options?: ExtractOptions): Promise<ExtractResult<T>>;
236
255
  brand(domain: string): Promise<BrandResult>;
237
256
  logo(domain: string, options?: BrandOptions): Promise<LogoResult>;
238
- styleguide(domain: string): Promise<StyleguideResult>;
257
+ styleguide(domainOrUrl: string, options?: StyleguideOptions): Promise<StyleguideResult>;
239
258
  screenshot(url: string, options?: ScreenshotOptions): Promise<ScreenshotResult>;
240
259
  images(url: string, options?: {
241
260
  maxAge?: number;
@@ -271,4 +290,4 @@ declare class HydrafetchTimeoutError extends HydrafetchError {
271
290
  constructor(message: string);
272
291
  }
273
292
 
274
- export { type BatchOptions, type BrandOptions, type BrandResult, type CrawlOptions, type ExtractItem, type ExtractOptions, type ExtractResult, type Format, Hydrafetch, HydrafetchError, type HydrafetchOptions, HydrafetchTimeoutError, type ImagesResult, type JobPage, type JobResult, type JobStatus, type LogoResult, type MapOptions, type MapResult, type PageImage, type PageMetadata, type ScrapeBase, type ScrapeContent, type ScrapeOptions, type ScrapeResult, type ScreenshotOptions, type ScreenshotResult, type SearchOptions, type SearchResult, type SearchResultItem, type StyleguideResult, type Usage, type WaitOptions };
293
+ export { type BatchOptions, type BrandOptions, type BrandResult, type CrawlOptions, type ExtractItem, type ExtractOptions, type ExtractResult, type ExtractSchema, type Format, Hydrafetch, HydrafetchError, type HydrafetchOptions, HydrafetchTimeoutError, type ImagesResult, type JobPage, type JobResult, type JobStatus, type LogoResult, type MapOptions, type MapResult, type PageImage, type PageMetadata, type ScrapeBase, type ScrapeContent, type ScrapeOptions, type ScrapeResult, type ScreenshotOptions, type ScreenshotResult, type SearchOptions, type SearchResult, type SearchResultItem, type StyleguideOptions, type StyleguideResult, type Usage, type WaitOptions, type ZodLike };
package/dist/index.js CHANGED
@@ -39,6 +39,62 @@ var DEFAULT_MAX_RETRIES = 2;
39
39
  function sleep(ms) {
40
40
  return new Promise((resolve) => setTimeout(resolve, ms));
41
41
  }
42
+ async function toJsonSchema(schema) {
43
+ const zodVersion = zodMajorOf(schema);
44
+ if (zodVersion === null) return schema;
45
+ const converter = await zodConverter(zodVersion);
46
+ let converted;
47
+ try {
48
+ converted = converter(schema);
49
+ } catch (cause) {
50
+ throw new HydrafetchError({
51
+ code: "SCHEMA_CONVERSION_FAILED",
52
+ message: `The Zod schema could not be converted: ${cause?.message ?? cause}`,
53
+ status: 0
54
+ });
55
+ }
56
+ if (JSON.stringify(converted).includes('"$ref":"#')) {
57
+ throw new HydrafetchError({
58
+ code: "SCHEMA_CONVERSION_FAILED",
59
+ message: "The Zod schema refers to itself, so it cannot be expressed as a flat JSON Schema. Flatten the schema or pass JSON Schema directly.",
60
+ status: 0
61
+ });
62
+ }
63
+ return converted;
64
+ }
65
+ function zodMajorOf(schema) {
66
+ if (typeof schema !== "object" || schema === null) return null;
67
+ if (typeof schema.parse !== "function") return null;
68
+ if ("_zod" in schema) return 4;
69
+ return "_def" in schema ? 3 : null;
70
+ }
71
+ async function zodConverter(version) {
72
+ if (version === 4) {
73
+ const zod = await optionalImport(
74
+ "zod"
75
+ );
76
+ if (zod && typeof zod.toJSONSchema === "function") return zod.toJSONSchema;
77
+ }
78
+ const legacy = await optionalImport("zod-to-json-schema");
79
+ if (legacy && typeof legacy.zodToJsonSchema === "function") {
80
+ return (schema) => legacy.zodToJsonSchema(schema, { $refStrategy: "none" });
81
+ }
82
+ throw new HydrafetchError({
83
+ code: "SCHEMA_CONVERSION_FAILED",
84
+ message: `A Zod ${version} schema was passed but nothing here can convert it. Use zod 4, which converts its own schemas, or install zod-to-json-schema alongside zod 3, or pass a JSON Schema.`,
85
+ status: 0
86
+ });
87
+ }
88
+ async function optionalImport(name) {
89
+ try {
90
+ return await import(
91
+ /* @vite-ignore */
92
+ name
93
+ );
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
42
98
  var Hydrafetch = class {
43
99
  apiKey;
44
100
  baseUrl;
@@ -143,10 +199,12 @@ var Hydrafetch = class {
143
199
  search(query, options = {}) {
144
200
  return this.request("POST", "/v1/web/search", { query, ...options });
145
201
  }
146
- extract(urls, options = {}) {
202
+ async extract(urls, options = {}) {
203
+ const { schema, ...rest } = options;
147
204
  return this.request("POST", "/v1/web/extract", {
148
205
  urls: Array.isArray(urls) ? urls : [urls],
149
- ...options
206
+ ...rest,
207
+ ...schema === void 0 ? {} : { schema: await toJsonSchema(schema) }
150
208
  });
151
209
  }
152
210
  brand(domain) {
@@ -155,8 +213,12 @@ var Hydrafetch = class {
155
213
  logo(domain, options = {}) {
156
214
  return this.request("GET", "/v1/web/brand/logo", void 0, { domain, ...options });
157
215
  }
158
- styleguide(domain) {
159
- return this.request("GET", "/v1/web/styleguide", void 0, { domain });
216
+ styleguide(domainOrUrl, options = {}) {
217
+ const target = /^https?:\/\//i.test(domainOrUrl) ? { url: domainOrUrl } : { domain: domainOrUrl };
218
+ return this.request("GET", "/v1/web/styleguide", void 0, {
219
+ ...target,
220
+ ...options
221
+ });
160
222
  }
161
223
  screenshot(url, options = {}) {
162
224
  return this.request("POST", "/v1/web/screenshot", { url, ...options });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydrafetch/node-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Official Node/TypeScript client for the Hydrafetch web data API. Turn any URL into clean Markdown and structured data.",
5
5
  "keywords": [
6
6
  "hydrafetch",
@@ -47,9 +47,22 @@
47
47
  "devDependencies": {
48
48
  "tsup": "^8.5.0",
49
49
  "typescript": "^5.9.2",
50
- "vitest": "^3.2.4"
50
+ "vitest": "^3.2.4",
51
+ "zod": "^4.5.4"
51
52
  },
52
53
  "publishConfig": {
53
54
  "access": "public"
55
+ },
56
+ "peerDependencies": {
57
+ "zod": ">=3",
58
+ "zod-to-json-schema": ">=3"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "zod": {
62
+ "optional": true
63
+ },
64
+ "zod-to-json-schema": {
65
+ "optional": true
66
+ }
54
67
  }
55
68
  }