recipe-scrapers-js 1.0.0-rc.1 → 1.0.0-rc.3

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/CHANGELOG.md CHANGED
@@ -6,26 +6,45 @@ All notable changes to this project will be documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [1.0.0-rc.3] - 2025-12-21
10
+
11
+ ### Changed
12
+
13
+ - Enforce positive integer values for recipe time fields (`totalTime`, `cookTime`, `prepTime`)
14
+ - Rename schema helper `zPositiveNumber` to `zPositiveInteger`
15
+ - Stop defaulting nullable schema fields to `null`
16
+
17
+ ## [1.0.0-rc.2] - 2025-12-20
18
+
19
+ ### Added
20
+
21
+ - Add `schemaVersion` to recipe schema
22
+
23
+ ### Changed
24
+
25
+ - Make `links` optional; it stays `undefined` unless link parsing is enabled
26
+ - Extract schema transform/refinement validations into `applyRecipeValidations`
27
+
9
28
  ## [1.0.0-rc.1] - 2025-12-20
10
29
 
11
30
  ### Added
12
31
 
13
- - chore: tsdown configuration file
32
+ - Add `tsdown` configuration file
14
33
 
15
34
  ### Fixed
16
35
 
17
- - fix: main/module/type entriess in package.json; add exports field
36
+ - Fix `main`/`module`/`types` entries in `package.json`; add `exports`
18
37
 
19
38
  ## [1.0.0-rc.0] - 2025-12-20
20
39
 
21
40
  ### Added
22
41
 
23
42
  - Optional ingredient parsing via [parse-ingredient](https://github.com/jakeboone02/parse-ingredient)
24
- - `parse()` and `safeParse()` methods for Zod schema validated recipe extraction
43
+ - `parse()` and `safeParse()` methods for Zod-validated recipe extraction
25
44
 
26
45
  ### Changed
27
46
 
28
- - **BREAKING**: Renamed `toObject()` method to `toRecipeObject()` for clarity
47
+ - **BREAKING**: Rename `toObject()` method to `toRecipeObject()` for clarity
29
48
  - **BREAKING**: Ingredients and instructions now require grouped structures (each group has `name` and `items`) instead of flat arrays
30
49
 
31
50
  ---
package/dist/index.d.mts CHANGED
@@ -3,7 +3,45 @@ import { CheerioAPI } from "cheerio";
3
3
  import z$1, { z } from "zod";
4
4
  import { ParseIngredientOptions } from "parse-ingredient";
5
5
 
6
+ //#region src/logger.d.ts
7
+ declare enum LogLevel {
8
+ VERBOSE = 0,
9
+ DEBUG = 1,
10
+ INFO = 2,
11
+ WARN = 3,
12
+ ERROR = 4,
13
+ }
14
+ declare class Logger {
15
+ private context;
16
+ private logLevel;
17
+ constructor(context: string, logLevel?: LogLevel);
18
+ verbose(...args: unknown[]): void;
19
+ debug(...args: unknown[]): void;
20
+ log(...args: unknown[]): void;
21
+ info(...args: unknown[]): void;
22
+ warn(...args: unknown[]): void;
23
+ error(...args: unknown[]): void;
24
+ }
25
+ //#endregion
26
+ //#region src/abstract-plugin.d.ts
27
+ declare abstract class AbstractPlugin {
28
+ readonly $: CheerioAPI;
29
+ /** The name of the plugin */
30
+ abstract name: string;
31
+ /** The priority of the plugin */
32
+ abstract priority: number;
33
+ constructor($: CheerioAPI);
34
+ }
35
+ //#endregion
6
36
  //#region src/schemas/recipe.schema.d.ts
37
+ /**
38
+ * Current schema version for recipe objects.
39
+ * Increment this when making breaking changes to the schema.
40
+ *
41
+ * Version history:
42
+ * - 1.0.0: Initial schema version
43
+ */
44
+ declare const RECIPE_SCHEMA_VERSION: "1.0.0";
7
45
  /**
8
46
  * Schema for a parsed ingredient from the parse-ingredient library.
9
47
  * This represents the structured data extracted from an ingredient string.
@@ -35,7 +73,7 @@ declare const IngredientItemSchema: z.ZodObject<{
35
73
  * Schema for a group of ingredients
36
74
  */
37
75
  declare const IngredientGroupSchema: z.ZodObject<{
38
- name: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
76
+ name: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
39
77
  items: z.ZodArray<z.core.$ZodType<{
40
78
  value: string;
41
79
  parsed?: {
@@ -63,7 +101,7 @@ declare const IngredientGroupSchema: z.ZodObject<{
63
101
  * Must have at least one group with at least one ingredient
64
102
  */
65
103
  declare const IngredientsSchema: z.ZodArray<z.ZodObject<{
66
- name: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
104
+ name: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
67
105
  items: z.ZodArray<z.core.$ZodType<{
68
106
  value: string;
69
107
  parsed?: {
@@ -96,7 +134,7 @@ declare const InstructionItemSchema: z.ZodObject<{
96
134
  * Schema for a group of instruction steps
97
135
  */
98
136
  declare const InstructionGroupSchema: z.ZodObject<{
99
- name: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
137
+ name: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
100
138
  items: z.ZodArray<z.core.$ZodType<{
101
139
  value: string;
102
140
  }, unknown, z.core.$ZodTypeInternals<{
@@ -108,7 +146,7 @@ declare const InstructionGroupSchema: z.ZodObject<{
108
146
  * Must have at least one group with at least one step
109
147
  */
110
148
  declare const InstructionsSchema: z.ZodArray<z.ZodObject<{
111
- name: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
149
+ name: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
112
150
  items: z.ZodArray<z.core.$ZodType<{
113
151
  value: string;
114
152
  }, unknown, z.core.$ZodTypeInternals<{
@@ -123,15 +161,28 @@ declare const LinkSchema: z.ZodObject<{
123
161
  text: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
124
162
  }, z.core.$strip>;
125
163
  /**
126
- * Base RecipeObject schema without cross-field validations
127
- * Used as the foundation for both strict and lenient schemas
164
+ * Base RecipeObject schema without cross-field validations.
165
+ * Use this schema when you need to extend the recipe object with custom fields.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * import { RecipeObjectBaseSchema, applyRecipeValidations } from 'recipe-scrapers-js'
170
+ *
171
+ * const MyCustomRecipeSchema = RecipeObjectBaseSchema.extend({
172
+ * customField: z.string(),
173
+ * })
174
+ *
175
+ * // Apply the standard recipe validations
176
+ * const MyValidatedRecipeSchema = applyRecipeValidations(MyCustomRecipeSchema)
177
+ * ```
128
178
  */
129
179
  declare const RecipeObjectBaseSchema: z.ZodObject<{
180
+ schemaVersion: z.ZodDefault<z.ZodLiteral<"1.0.0">>;
130
181
  host: z.ZodCustomStringFormat<"hostname">;
131
182
  title: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
132
183
  author: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
133
184
  ingredients: z.ZodArray<z.ZodObject<{
134
- name: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
185
+ name: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
135
186
  items: z.ZodArray<z.core.$ZodType<{
136
187
  value: string;
137
188
  parsed?: {
@@ -155,7 +206,7 @@ declare const RecipeObjectBaseSchema: z.ZodObject<{
155
206
  }, unknown>>>;
156
207
  }, z.core.$strip>>;
157
208
  instructions: z.ZodArray<z.ZodObject<{
158
- name: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
209
+ name: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
159
210
  items: z.ZodArray<z.core.$ZodType<{
160
211
  value: string;
161
212
  }, unknown, z.core.$ZodTypeInternals<{
@@ -164,22 +215,22 @@ declare const RecipeObjectBaseSchema: z.ZodObject<{
164
215
  }, z.core.$strip>>;
165
216
  canonicalUrl: z.ZodURL;
166
217
  image: z.ZodURL;
167
- totalTime: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
168
- cookTime: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
169
- prepTime: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
218
+ totalTime: z.ZodNullable<z.ZodInt>;
219
+ cookTime: z.ZodNullable<z.ZodInt>;
220
+ prepTime: z.ZodNullable<z.ZodInt>;
170
221
  ratings: z.ZodDefault<z.ZodNumber>;
171
222
  ratingsCount: z.ZodDefault<z.ZodInt>;
172
223
  yields: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
173
224
  description: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
174
225
  language: z.ZodDefault<z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
175
- siteName: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
176
- cookingMethod: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
226
+ siteName: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
227
+ cookingMethod: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
177
228
  category: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
178
229
  cuisine: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
179
230
  keywords: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
180
231
  dietaryRestrictions: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
181
232
  equipment: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
182
- links: z.ZodDefault<z.ZodArray<z.ZodObject<{
233
+ links: z.ZodOptional<z.ZodArray<z.ZodObject<{
183
234
  href: z.ZodURL;
184
235
  text: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
185
236
  }, z.core.$strip>>>;
@@ -187,15 +238,38 @@ declare const RecipeObjectBaseSchema: z.ZodObject<{
187
238
  reviews: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
188
239
  }, z.core.$strip>;
189
240
  /**
190
- * Strict RecipeObject schema with all validations enforced
241
+ * Applies recipe-specific transformations and validations to a schema.
242
+ * Use this when extending RecipeObjectBaseSchema with custom fields.
243
+ *
244
+ * @param schema - A Zod object schema that includes
245
+ * all RecipeObjectBaseSchema fields
246
+ * @returns A schema with transforms and field validations applied
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * const CustomSchema = RecipeObjectBaseSchema.extend({
251
+ * tags: z.array(z.string()),
252
+ * })
253
+ *
254
+ * const ValidatedCustomSchema = applyRecipeValidations(CustomSchema)
255
+ * ```
191
256
  */
192
- declare const RecipeObjectSchema: z.ZodPipe<z.ZodObject<{
193
- host: z.ZodCustomStringFormat<"hostname">;
194
- title: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
195
- author: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
196
- ingredients: z.ZodArray<z.ZodObject<{
197
- name: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
198
- items: z.ZodArray<z.core.$ZodType<{
257
+ declare function applyRecipeValidations<T extends z.infer<typeof RecipeObjectBaseSchema>>(schema: z.ZodType<T>): z.ZodPipe<z.ZodType<T, unknown, z.core.$ZodTypeInternals<T, unknown>>, z.ZodTransform<Awaited<T>, T>>;
258
+ /**
259
+ * Strict RecipeObject schema with all validations enforced.
260
+ * This is the standard schema used by recipe scrapers.
261
+ *
262
+ * For custom extensions, use RecipeObjectBaseSchema.extend() and then
263
+ * apply validations with applyRecipeValidations().
264
+ */
265
+ declare const RecipeObjectSchema: z.ZodPipe<z.ZodType<{
266
+ schemaVersion: "1.0.0";
267
+ host: string;
268
+ title: string;
269
+ author: string;
270
+ ingredients: {
271
+ name: string | null;
272
+ items: {
199
273
  value: string;
200
274
  parsed?: {
201
275
  quantity: number | null;
@@ -205,7 +279,45 @@ declare const RecipeObjectSchema: z.ZodPipe<z.ZodObject<{
205
279
  description: string;
206
280
  isGroupHeader: boolean;
207
281
  } | undefined;
208
- }, unknown, z.core.$ZodTypeInternals<{
282
+ }[];
283
+ }[];
284
+ instructions: {
285
+ name: string | null;
286
+ items: {
287
+ value: string;
288
+ }[];
289
+ }[];
290
+ canonicalUrl: string;
291
+ image: string;
292
+ totalTime: number | null;
293
+ cookTime: number | null;
294
+ prepTime: number | null;
295
+ ratings: number;
296
+ ratingsCount: number;
297
+ yields: string;
298
+ description: string;
299
+ language: string;
300
+ siteName: string | null;
301
+ cookingMethod: string | null;
302
+ category: string[];
303
+ cuisine: string[];
304
+ keywords: string[];
305
+ dietaryRestrictions: string[];
306
+ equipment: string[];
307
+ nutrients: Record<string, string>;
308
+ reviews: Record<string, string>;
309
+ links?: {
310
+ href: string;
311
+ text: string;
312
+ }[] | undefined;
313
+ }, unknown, z.core.$ZodTypeInternals<{
314
+ schemaVersion: "1.0.0";
315
+ host: string;
316
+ title: string;
317
+ author: string;
318
+ ingredients: {
319
+ name: string | null;
320
+ items: {
209
321
  value: string;
210
322
  parsed?: {
211
323
  quantity: number | null;
@@ -215,40 +327,39 @@ declare const RecipeObjectSchema: z.ZodPipe<z.ZodObject<{
215
327
  description: string;
216
328
  isGroupHeader: boolean;
217
329
  } | undefined;
218
- }, unknown>>>;
219
- }, z.core.$strip>>;
220
- instructions: z.ZodArray<z.ZodObject<{
221
- name: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
222
- items: z.ZodArray<z.core.$ZodType<{
223
- value: string;
224
- }, unknown, z.core.$ZodTypeInternals<{
330
+ }[];
331
+ }[];
332
+ instructions: {
333
+ name: string | null;
334
+ items: {
225
335
  value: string;
226
- }, unknown>>>;
227
- }, z.core.$strip>>;
228
- canonicalUrl: z.ZodURL;
229
- image: z.ZodURL;
230
- totalTime: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
231
- cookTime: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
232
- prepTime: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
233
- ratings: z.ZodDefault<z.ZodNumber>;
234
- ratingsCount: z.ZodDefault<z.ZodInt>;
235
- yields: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
236
- description: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
237
- language: z.ZodDefault<z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
238
- siteName: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
239
- cookingMethod: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
240
- category: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
241
- cuisine: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
242
- keywords: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
243
- dietaryRestrictions: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
244
- equipment: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
245
- links: z.ZodDefault<z.ZodArray<z.ZodObject<{
246
- href: z.ZodURL;
247
- text: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
248
- }, z.core.$strip>>>;
249
- nutrients: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
250
- reviews: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
251
- }, z.core.$strip>, z.ZodTransform<{
336
+ }[];
337
+ }[];
338
+ canonicalUrl: string;
339
+ image: string;
340
+ totalTime: number | null;
341
+ cookTime: number | null;
342
+ prepTime: number | null;
343
+ ratings: number;
344
+ ratingsCount: number;
345
+ yields: string;
346
+ description: string;
347
+ language: string;
348
+ siteName: string | null;
349
+ cookingMethod: string | null;
350
+ category: string[];
351
+ cuisine: string[];
352
+ keywords: string[];
353
+ dietaryRestrictions: string[];
354
+ equipment: string[];
355
+ nutrients: Record<string, string>;
356
+ reviews: Record<string, string>;
357
+ links?: {
358
+ href: string;
359
+ text: string;
360
+ }[] | undefined;
361
+ }, unknown>>, z.ZodTransform<{
362
+ schemaVersion: "1.0.0";
252
363
  host: string;
253
364
  title: string;
254
365
  author: string;
@@ -289,13 +400,14 @@ declare const RecipeObjectSchema: z.ZodPipe<z.ZodObject<{
289
400
  keywords: string[];
290
401
  dietaryRestrictions: string[];
291
402
  equipment: string[];
292
- links: {
293
- href: string;
294
- text: string;
295
- }[];
296
403
  nutrients: Record<string, string>;
297
404
  reviews: Record<string, string>;
405
+ links?: {
406
+ href: string;
407
+ text: string;
408
+ }[] | undefined;
298
409
  }, {
410
+ schemaVersion: "1.0.0";
299
411
  host: string;
300
412
  title: string;
301
413
  author: string;
@@ -336,44 +448,13 @@ declare const RecipeObjectSchema: z.ZodPipe<z.ZodObject<{
336
448
  keywords: string[];
337
449
  dietaryRestrictions: string[];
338
450
  equipment: string[];
339
- links: {
340
- href: string;
341
- text: string;
342
- }[];
343
451
  nutrients: Record<string, string>;
344
452
  reviews: Record<string, string>;
453
+ links?: {
454
+ href: string;
455
+ text: string;
456
+ }[] | undefined;
345
457
  }>>;
346
- type RecipeObjectValidated = z.infer<typeof RecipeObjectSchema>;
347
- //#endregion
348
- //#region src/logger.d.ts
349
- declare enum LogLevel {
350
- VERBOSE = 0,
351
- DEBUG = 1,
352
- INFO = 2,
353
- WARN = 3,
354
- ERROR = 4,
355
- }
356
- declare class Logger {
357
- private context;
358
- private logLevel;
359
- constructor(context: string, logLevel?: LogLevel);
360
- verbose(...args: unknown[]): void;
361
- debug(...args: unknown[]): void;
362
- log(...args: unknown[]): void;
363
- info(...args: unknown[]): void;
364
- warn(...args: unknown[]): void;
365
- error(...args: unknown[]): void;
366
- }
367
- //#endregion
368
- //#region src/abstract-plugin.d.ts
369
- declare abstract class AbstractPlugin {
370
- readonly $: CheerioAPI;
371
- /** The name of the plugin */
372
- abstract name: string;
373
- /** The priority of the plugin */
374
- abstract priority: number;
375
- constructor($: CheerioAPI);
376
- }
377
458
  //#endregion
378
459
  //#region src/types/recipe.interface.d.ts
379
460
  type List = Set<string>;
@@ -405,6 +486,10 @@ type InstructionGroup = z.infer<typeof InstructionGroupSchema>;
405
486
  * All recipe instructions as an array of groups
406
487
  */
407
488
  type Instructions = InstructionGroup[];
489
+ /**
490
+ * The complete recipe object
491
+ */
492
+ type RecipeObject = z.infer<typeof RecipeObjectSchema>;
408
493
  /**
409
494
  * A link with href and display text
410
495
  */
@@ -461,8 +546,9 @@ interface RecipeData {
461
546
  /**
462
547
  * An list of all links found in the page HTML defined within an anchor
463
548
  * `<a>` element.
549
+ * Only present when `linksEnabled` option is set to `true`.
464
550
  */
465
- links: Link[];
551
+ links?: Link[];
466
552
  /**
467
553
  * A description of the recipe. This is normally a sentence or short
468
554
  * paragraph describing the recipe. Often the website defines the
@@ -613,20 +699,7 @@ type RecipeFields = Omit<RecipeData, 'host'>;
613
699
  /**
614
700
  * The fields that aren't required for a recipe to be valid.
615
701
  */
616
- type OptionalRecipeFields = Pick<RecipeFields, 'siteName' | 'category' | 'cookTime' | 'prepTime' | 'totalTime' | 'cuisine' | 'cookingMethod' | 'ratings' | 'ratingsCount' | 'equipment' | 'reviews' | 'nutrients' | 'dietaryRestrictions' | 'keywords'>;
617
- /**
618
- * The validated recipe object output from the schema.
619
- * This represents a recipe that has passed all validation rules.
620
- */
621
- interface RecipeObject extends Omit<RecipeData, 'category' | 'cuisine' | 'dietaryRestrictions' | 'equipment' | 'keywords' | 'nutrients' | 'reviews'> {
622
- category: string[];
623
- cuisine: string[];
624
- dietaryRestrictions: string[];
625
- equipment: string[];
626
- keywords: string[];
627
- nutrients: Record<string, string>;
628
- reviews: Record<string, string>;
629
- }
702
+ type OptionalRecipeFields = Pick<RecipeFields, 'siteName' | 'category' | 'cookTime' | 'prepTime' | 'totalTime' | 'cuisine' | 'cookingMethod' | 'ratings' | 'ratingsCount' | 'equipment' | 'reviews' | 'nutrients' | 'dietaryRestrictions' | 'keywords' | 'links'>;
630
703
  //#endregion
631
704
  //#region src/abstract-extractor-plugin.d.ts
632
705
  declare abstract class ExtractorPlugin extends AbstractPlugin {
@@ -764,19 +837,21 @@ declare abstract class AbstractScraper {
764
837
  scrape(): Promise<RecipeData>;
765
838
  /**
766
839
  * Converts the scraper's data into a JSON-serializable object.
840
+ * Note: schemaVersion is added during validation by parse() or safeParse().
767
841
  */
768
- toRecipeObject(): Promise<RecipeObject>;
842
+ toRecipeObject(): Promise<Omit<RecipeObject, 'schemaVersion'>>;
769
843
  /**
770
844
  * Get the Zod schema to use for validation.
771
845
  * Subclasses can override to provide custom schemas.
772
846
  */
773
- protected getSchema(): z$1.ZodPipe<z$1.ZodObject<{
774
- host: z$1.ZodCustomStringFormat<"hostname">;
775
- title: z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>;
776
- author: z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>;
777
- ingredients: z$1.ZodArray<z$1.ZodObject<{
778
- name: z$1.ZodDefault<z$1.ZodNullable<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
779
- items: z$1.ZodArray<z$1.core.$ZodType<{
847
+ protected getSchema(): z$1.ZodPipe<z$1.ZodType<{
848
+ schemaVersion: "1.0.0";
849
+ host: string;
850
+ title: string;
851
+ author: string;
852
+ ingredients: {
853
+ name: string | null;
854
+ items: {
780
855
  value: string;
781
856
  parsed?: {
782
857
  quantity: number | null;
@@ -786,7 +861,45 @@ declare abstract class AbstractScraper {
786
861
  description: string;
787
862
  isGroupHeader: boolean;
788
863
  } | undefined;
789
- }, unknown, z$1.core.$ZodTypeInternals<{
864
+ }[];
865
+ }[];
866
+ instructions: {
867
+ name: string | null;
868
+ items: {
869
+ value: string;
870
+ }[];
871
+ }[];
872
+ canonicalUrl: string;
873
+ image: string;
874
+ totalTime: number | null;
875
+ cookTime: number | null;
876
+ prepTime: number | null;
877
+ ratings: number;
878
+ ratingsCount: number;
879
+ yields: string;
880
+ description: string;
881
+ language: string;
882
+ siteName: string | null;
883
+ cookingMethod: string | null;
884
+ category: string[];
885
+ cuisine: string[];
886
+ keywords: string[];
887
+ dietaryRestrictions: string[];
888
+ equipment: string[];
889
+ nutrients: Record<string, string>;
890
+ reviews: Record<string, string>;
891
+ links?: {
892
+ href: string;
893
+ text: string;
894
+ }[] | undefined;
895
+ }, unknown, z$1.core.$ZodTypeInternals<{
896
+ schemaVersion: "1.0.0";
897
+ host: string;
898
+ title: string;
899
+ author: string;
900
+ ingredients: {
901
+ name: string | null;
902
+ items: {
790
903
  value: string;
791
904
  parsed?: {
792
905
  quantity: number | null;
@@ -796,40 +909,39 @@ declare abstract class AbstractScraper {
796
909
  description: string;
797
910
  isGroupHeader: boolean;
798
911
  } | undefined;
799
- }, unknown>>>;
800
- }, z$1.core.$strip>>;
801
- instructions: z$1.ZodArray<z$1.ZodObject<{
802
- name: z$1.ZodDefault<z$1.ZodNullable<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
803
- items: z$1.ZodArray<z$1.core.$ZodType<{
804
- value: string;
805
- }, unknown, z$1.core.$ZodTypeInternals<{
912
+ }[];
913
+ }[];
914
+ instructions: {
915
+ name: string | null;
916
+ items: {
806
917
  value: string;
807
- }, unknown>>>;
808
- }, z$1.core.$strip>>;
809
- canonicalUrl: z$1.ZodURL;
810
- image: z$1.ZodURL;
811
- totalTime: z$1.ZodDefault<z$1.ZodNullable<z$1.ZodNumber>>;
812
- cookTime: z$1.ZodDefault<z$1.ZodNullable<z$1.ZodNumber>>;
813
- prepTime: z$1.ZodDefault<z$1.ZodNullable<z$1.ZodNumber>>;
814
- ratings: z$1.ZodDefault<z$1.ZodNumber>;
815
- ratingsCount: z$1.ZodDefault<z$1.ZodInt>;
816
- yields: z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>;
817
- description: z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>;
818
- language: z$1.ZodDefault<z$1.ZodOptional<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
819
- siteName: z$1.ZodDefault<z$1.ZodNullable<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
820
- cookingMethod: z$1.ZodDefault<z$1.ZodNullable<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
821
- category: z$1.ZodDefault<z$1.ZodArray<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
822
- cuisine: z$1.ZodDefault<z$1.ZodArray<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
823
- keywords: z$1.ZodDefault<z$1.ZodArray<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
824
- dietaryRestrictions: z$1.ZodDefault<z$1.ZodArray<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
825
- equipment: z$1.ZodDefault<z$1.ZodArray<z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>>>;
826
- links: z$1.ZodDefault<z$1.ZodArray<z$1.ZodObject<{
827
- href: z$1.ZodURL;
828
- text: z$1.ZodPipe<z$1.ZodString, z$1.ZodTransform<string, string>>;
829
- }, z$1.core.$strip>>>;
830
- nutrients: z$1.ZodDefault<z$1.ZodRecord<z$1.ZodString, z$1.ZodString>>;
831
- reviews: z$1.ZodDefault<z$1.ZodRecord<z$1.ZodString, z$1.ZodString>>;
832
- }, z$1.core.$strip>, z$1.ZodTransform<{
918
+ }[];
919
+ }[];
920
+ canonicalUrl: string;
921
+ image: string;
922
+ totalTime: number | null;
923
+ cookTime: number | null;
924
+ prepTime: number | null;
925
+ ratings: number;
926
+ ratingsCount: number;
927
+ yields: string;
928
+ description: string;
929
+ language: string;
930
+ siteName: string | null;
931
+ cookingMethod: string | null;
932
+ category: string[];
933
+ cuisine: string[];
934
+ keywords: string[];
935
+ dietaryRestrictions: string[];
936
+ equipment: string[];
937
+ nutrients: Record<string, string>;
938
+ reviews: Record<string, string>;
939
+ links?: {
940
+ href: string;
941
+ text: string;
942
+ }[] | undefined;
943
+ }, unknown>>, z$1.ZodTransform<{
944
+ schemaVersion: "1.0.0";
833
945
  host: string;
834
946
  title: string;
835
947
  author: string;
@@ -870,13 +982,14 @@ declare abstract class AbstractScraper {
870
982
  keywords: string[];
871
983
  dietaryRestrictions: string[];
872
984
  equipment: string[];
873
- links: {
874
- href: string;
875
- text: string;
876
- }[];
877
985
  nutrients: Record<string, string>;
878
986
  reviews: Record<string, string>;
987
+ links?: {
988
+ href: string;
989
+ text: string;
990
+ }[] | undefined;
879
991
  }, {
992
+ schemaVersion: "1.0.0";
880
993
  host: string;
881
994
  title: string;
882
995
  author: string;
@@ -917,12 +1030,12 @@ declare abstract class AbstractScraper {
917
1030
  keywords: string[];
918
1031
  dietaryRestrictions: string[];
919
1032
  equipment: string[];
920
- links: {
921
- href: string;
922
- text: string;
923
- }[];
924
1033
  nutrients: Record<string, string>;
925
1034
  reviews: Record<string, string>;
1035
+ links?: {
1036
+ href: string;
1037
+ text: string;
1038
+ }[] | undefined;
926
1039
  }>>;
927
1040
  /**
928
1041
  * Extract and validate recipe data.
@@ -931,14 +1044,14 @@ declare abstract class AbstractScraper {
931
1044
  * @returns Validated recipe object
932
1045
  * @throws {ZodError} If validation fails
933
1046
  */
934
- parse(): Promise<RecipeObjectValidated>;
1047
+ parse(): Promise<RecipeObject>;
935
1048
  /**
936
1049
  * Extract and validate recipe data without throwing.
937
1050
  * Returns a result object indicating success or failure.
938
1051
  *
939
1052
  * @returns Result object with either data or error
940
1053
  */
941
- safeParse(): Promise<z$1.ZodSafeParseResult<RecipeObjectValidated>>;
1054
+ safeParse(): Promise<z$1.ZodSafeParseResult<RecipeObject>>;
942
1055
  }
943
1056
  //#endregion
944
1057
  //#region src/scrapers/allrecipes.d.ts
@@ -961,4 +1074,4 @@ declare const scrapers: {
961
1074
  */
962
1075
  declare function getScraper(url: string): typeof AllRecipes;
963
1076
  //#endregion
964
- export { ExtractorPlugin, IngredientGroup, IngredientGroupSchema, IngredientItem, IngredientItemSchema, Ingredients, IngredientsSchema, InstructionGroup, InstructionGroupSchema, InstructionItem, InstructionItemSchema, Instructions, InstructionsSchema, Link, LinkSchema, List, LogLevel, Logger, OptionalRecipeFields, ParsedIngredient, ParsedIngredientSchema, PostProcessorPlugin, RecipeData, RecipeFields, RecipeObject, RecipeObjectBaseSchema, RecipeObjectSchema, RecipeObjectValidated, ScraperOptions, getScraper, scrapers };
1077
+ export { ExtractorPlugin, IngredientGroup, IngredientGroupSchema, IngredientItem, IngredientItemSchema, Ingredients, IngredientsSchema, InstructionGroup, InstructionGroupSchema, InstructionItem, InstructionItemSchema, Instructions, InstructionsSchema, Link, LinkSchema, List, LogLevel, Logger, OptionalRecipeFields, ParsedIngredient, ParsedIngredientSchema, PostProcessorPlugin, RECIPE_SCHEMA_VERSION, RecipeData, RecipeFields, RecipeObject, RecipeObjectBaseSchema, RecipeObjectSchema, ScraperOptions, applyRecipeValidations, getScraper, scrapers };
package/dist/index.mjs CHANGED
@@ -48,14 +48,22 @@ const zString = (fieldName, { min = 1, max = 0 } = {}) => z.string(`${fieldName}
48
48
  */
49
49
  const zHttpUrl = (fieldName) => z.httpUrl(`${fieldName} must be a valid URL`);
50
50
  /**
51
- * Helper to create a positive number field
51
+ * Helper to create a positive integer field
52
52
  */
53
- const zPositiveNumber = (fieldName) => z.number(`${fieldName} must be a number`).positive(`${fieldName} must be positive`).nullable().default(null);
53
+ const zPositiveInteger = (fieldName) => z.int(`${fieldName} must be an integer`).positive(`${fieldName} must be positive`).nullable();
54
54
  const zNonEmptyArray = (schema, fieldName) => z.array(schema, `${fieldName} items must be an array`).min(1, `${fieldName} group must have at least one item`);
55
55
 
56
56
  //#endregion
57
57
  //#region src/schemas/recipe.schema.ts
58
58
  /**
59
+ * Current schema version for recipe objects.
60
+ * Increment this when making breaking changes to the schema.
61
+ *
62
+ * Version history:
63
+ * - 1.0.0: Initial schema version
64
+ */
65
+ const RECIPE_SCHEMA_VERSION = "1.0.0";
66
+ /**
59
67
  * Schema for a parsed ingredient from the parse-ingredient library.
60
68
  * This represents the structured data extracted from an ingredient string.
61
69
  * @see https://github.com/jakeboone02/parse-ingredient
@@ -79,7 +87,7 @@ const IngredientItemSchema = z.object({
79
87
  * Schema for a group of ingredients
80
88
  */
81
89
  const IngredientGroupSchema = z.object({
82
- name: zString("Ingredient group name").nullable().default(null),
90
+ name: zString("Ingredient group name").nullable(),
83
91
  items: zNonEmptyArray(IngredientItemSchema, "Ingredient")
84
92
  });
85
93
  /**
@@ -95,7 +103,7 @@ const InstructionItemSchema = z.object({ value: zString("Instruction value") });
95
103
  * Schema for a group of instruction steps
96
104
  */
97
105
  const InstructionGroupSchema = z.object({
98
- name: zString("Instruction group name").nullable().default(null),
106
+ name: zString("Instruction group name").nullable(),
99
107
  items: zNonEmptyArray(InstructionItemSchema, "Instruction")
100
108
  });
101
109
  /**
@@ -111,10 +119,23 @@ const LinkSchema = z.object({
111
119
  text: zString("Link text")
112
120
  });
113
121
  /**
114
- * Base RecipeObject schema without cross-field validations
115
- * Used as the foundation for both strict and lenient schemas
122
+ * Base RecipeObject schema without cross-field validations.
123
+ * Use this schema when you need to extend the recipe object with custom fields.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * import { RecipeObjectBaseSchema, applyRecipeValidations } from 'recipe-scrapers-js'
128
+ *
129
+ * const MyCustomRecipeSchema = RecipeObjectBaseSchema.extend({
130
+ * customField: z.string(),
131
+ * })
132
+ *
133
+ * // Apply the standard recipe validations
134
+ * const MyValidatedRecipeSchema = applyRecipeValidations(MyCustomRecipeSchema)
135
+ * ```
116
136
  */
117
137
  const RecipeObjectBaseSchema = z.object({
138
+ schemaVersion: z.literal(RECIPE_SCHEMA_VERSION).default(RECIPE_SCHEMA_VERSION).describe("Schema version for recipe data migrations"),
118
139
  host: z.hostname("Host must be a valid hostname"),
119
140
  title: zString("Title", { max: 500 }),
120
141
  author: zString("Author", { max: 255 }),
@@ -122,42 +143,67 @@ const RecipeObjectBaseSchema = z.object({
122
143
  instructions: InstructionsSchema,
123
144
  canonicalUrl: zHttpUrl("Canonical URL"),
124
145
  image: zHttpUrl("Image"),
125
- totalTime: zPositiveNumber("Total time"),
126
- cookTime: zPositiveNumber("Cook time"),
127
- prepTime: zPositiveNumber("Prep time"),
146
+ totalTime: zPositiveInteger("Total time"),
147
+ cookTime: zPositiveInteger("Cook time"),
148
+ prepTime: zPositiveInteger("Prep time"),
128
149
  ratings: z.number("Ratings must be a number").min(0, "Ratings must be at least 0").max(5, "Ratings must be at most 5").default(0),
129
150
  ratingsCount: z.int("Ratings count must be an integer").nonnegative("Ratings count must be non-negative").default(0),
130
151
  yields: zString("Yields"),
131
152
  description: zString("Description"),
132
153
  language: zString("Language", { min: 2 }).optional().default("en"),
133
- siteName: zString("Site name").nullable().default(null),
134
- cookingMethod: zString("Cooking method").nullable().default(null),
154
+ siteName: zString("Site name").nullable(),
155
+ cookingMethod: zString("Cooking method").nullable(),
135
156
  category: z.array(zString("Category item"), "Category must be an array").default([]),
136
157
  cuisine: z.array(zString("Cuisine item"), "Cuisine must be an array").default([]),
137
158
  keywords: z.array(zString("Keyword item"), "Keywords must be an array").default([]),
138
159
  dietaryRestrictions: z.array(zString("Dietary restriction item"), "Dietary restrictions must be an array").default([]),
139
160
  equipment: z.array(zString("Equipment item"), "Equipment must be an array").default([]),
140
- links: z.array(LinkSchema, "Links must be an array").default([]),
161
+ links: z.array(LinkSchema, "Links must be an array").optional(),
141
162
  nutrients: z.record(z.string(), z.string(), "Nutrients must be an object").default({}),
142
163
  reviews: z.record(z.string(), z.string(), "Reviews must be an object").default({})
143
164
  });
144
165
  /**
145
- * Strict RecipeObject schema with all validations enforced
166
+ * Applies recipe-specific transformations and validations to a schema.
167
+ * Use this when extending RecipeObjectBaseSchema with custom fields.
168
+ *
169
+ * @param schema - A Zod object schema that includes
170
+ * all RecipeObjectBaseSchema fields
171
+ * @returns A schema with transforms and field validations applied
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const CustomSchema = RecipeObjectBaseSchema.extend({
176
+ * tags: z.array(z.string()),
177
+ * })
178
+ *
179
+ * const ValidatedCustomSchema = applyRecipeValidations(CustomSchema)
180
+ * ```
146
181
  */
147
- const RecipeObjectSchema = RecipeObjectBaseSchema.transform((data) => {
148
- if (!data.totalTime && !isNull(data.cookTime) && !isNull(data.prepTime)) data.totalTime = data.cookTime + data.prepTime;
149
- return data;
150
- }).refine((data) => {
151
- const { totalTime, cookTime, prepTime } = data;
152
- if (!isNull(totalTime) && !isNull(cookTime) && !isNull(prepTime)) return totalTime >= cookTime + prepTime;
153
- return true;
154
- }, {
155
- message: "Total time should be at least the sum of cook time and prep time",
156
- path: ["totalTime"]
157
- }).refine((data) => data.ratings === 0 || data.ratingsCount > 0, {
158
- message: "Ratings count should be greater than 0 when ratings exist",
159
- path: ["ratingsCount"]
160
- });
182
+ function applyRecipeValidations(schema) {
183
+ return schema.transform((data) => {
184
+ if (!data.totalTime && !isNull(data.cookTime) && !isNull(data.prepTime)) data.totalTime = data.cookTime + data.prepTime;
185
+ return data;
186
+ }).refine(({ totalTime, cookTime, prepTime }) => {
187
+ if (!isNull(totalTime) && !isNull(cookTime) && !isNull(prepTime)) return totalTime >= cookTime + prepTime;
188
+ return true;
189
+ }, {
190
+ message: "Total time should be at least the sum of cook time and prep time",
191
+ path: ["totalTime"]
192
+ }).refine((data) => {
193
+ return data.ratings === 0 || data.ratingsCount > 0;
194
+ }, {
195
+ message: "Ratings count should be greater than 0 when ratings exist",
196
+ path: ["ratingsCount"]
197
+ });
198
+ }
199
+ /**
200
+ * Strict RecipeObject schema with all validations enforced.
201
+ * This is the standard schema used by recipe scrapers.
202
+ *
203
+ * For custom extensions, use RecipeObjectBaseSchema.extend() and then
204
+ * apply validations with applyRecipeValidations().
205
+ */
206
+ const RecipeObjectSchema = applyRecipeValidations(RecipeObjectBaseSchema);
161
207
 
162
208
  //#endregion
163
209
  //#region src/exceptions/index.ts
@@ -1235,7 +1281,7 @@ var AbstractScraper = class {
1235
1281
  return "en";
1236
1282
  }
1237
1283
  links() {
1238
- if (!this.options.linksEnabled) return [];
1284
+ if (!this.options.linksEnabled) return void 0;
1239
1285
  return this.$("a[href]").map((_, el) => {
1240
1286
  const href = this.$(el).attr("href");
1241
1287
  if (!href?.startsWith("http")) return null;
@@ -1282,6 +1328,7 @@ var AbstractScraper = class {
1282
1328
  }
1283
1329
  /**
1284
1330
  * Converts the scraper's data into a JSON-serializable object.
1331
+ * Note: schemaVersion is added during validation by parse() or safeParse().
1285
1332
  */
1286
1333
  async toRecipeObject() {
1287
1334
  const { category, cuisine, dietaryRestrictions, equipment, keywords, nutrients, reviews, ...rest } = await this.scrape();
@@ -1600,4 +1647,4 @@ function getScraper(url) {
1600
1647
  }
1601
1648
 
1602
1649
  //#endregion
1603
- export { ExtractorPlugin, IngredientGroupSchema, IngredientItemSchema, IngredientsSchema, InstructionGroupSchema, InstructionItemSchema, InstructionsSchema, LinkSchema, LogLevel, Logger, ParsedIngredientSchema, PostProcessorPlugin, RecipeObjectBaseSchema, RecipeObjectSchema, getScraper, scrapers };
1650
+ export { ExtractorPlugin, IngredientGroupSchema, IngredientItemSchema, IngredientsSchema, InstructionGroupSchema, InstructionItemSchema, InstructionsSchema, LinkSchema, LogLevel, Logger, ParsedIngredientSchema, PostProcessorPlugin, RECIPE_SCHEMA_VERSION, RecipeObjectBaseSchema, RecipeObjectSchema, applyRecipeValidations, getScraper, scrapers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recipe-scrapers-js",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.3",
4
4
  "license": "MIT",
5
5
  "description": "A recipe scrapers library",
6
6
  "author": {