@ghostry/fabricator-extension-faker-v10 0.0.1 → 0.0.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/README.md CHANGED
@@ -1,6 +1,22 @@
1
+ <div align="center">
2
+
3
+ <img src="https://docs.ghostry.dev/fabricator/logo.png" alt="fabricator" width="96" height="96">
4
+
1
5
  # @ghostry/fabricator-extension-faker-v10
2
6
 
3
- [`@faker-js/faker`](https://fakerjs.dev) integration for [`@ghostry/fabricator`](https://www.npmjs.com/package/@ghostry/fabricator). Faker's generators draw from the same seed as every other builder in a schema, so `T.faker.person.fullName()` replays exactly like `T.string` does — no second seed to track, no drift between runs.
7
+ **Seeded faker generators for `@ghostry/fabricator`.**
8
+
9
+ [![npm](https://img.shields.io/badge/npm-ffffff.svg?style=for-the-badge&color=000000&logo=npm&logoColor=CB3837)](https://www.npmjs.com/package/@ghostry/fabricator-extension-faker-v10)
10
+ [![npmx](https://img.shields.io/badge/npmx-ffffff.svg?style=for-the-badge&color=000000&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNTMiIGhlaWdodD0iMTUzIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxNTMgMTUzIj4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNi43MDQgOS45ODI3KSI+CiAgICA8cGF0aCBkPSJtMC45MzQ3NiA5Ny4yMDVoMjQuMDgxdjIzLjY5M2gtMjQuMDgxeiIgZmlsbD0iI2ZmZiI+PC9wYXRoPgogICAgPHBhdGggZD0ibTEwMy4xMi05LjIzMDctMy42MjExIDEwLjI0Ni00Ni4zMDkgMTMxLTMuNjIxMSAxMC4yNDZoMTUuNTM3bDMuNjIxMS0xMC4yNDYgMTEuNzE3LTMzLjE0OCAzOC4yMTEtMTA4LjF6IiBmaWxsPSIjNTFjOGZjIj48L3BhdGg+CiAgPC9nPgo8L3N2Zz4K&logoColor=51C8FC)](https://npmx.dev/package/@ghostry/fabricator-extension-faker-v10)
11
+ [![jsr](https://img.shields.io/badge/jsr-ffffff?style=for-the-badge&color=000000&logo=jsr&logoColor=F7DF1E)](https://jsr.io/@ghostry/fabricator-extension-faker-v10)
12
+ [![github](https://img.shields.io/badge/github-ffffff?style=for-the-badge&color=000000&logo=github&logoColor=ffffff)](https://github.com/ghostry-dev/fabricator)
13
+ [![typescript](https://img.shields.io/badge/typescript-ffffff?style=for-the-badge&color=000000&logo=typescript&logoColor=3178C6)](#)
14
+ [![bun](https://img.shields.io/badge/bun-ffffff?style=for-the-badge&color=000000&logo=bun&logoColor=FBF0DF)](#)
15
+ [![node](https://img.shields.io/badge/node-ffffff?style=for-the-badge&color=000000&logo=nodedotjs&logoColor=5FA04E)](#)
16
+
17
+ </div>
18
+
19
+ [`@faker-js/faker`](https://fakerjs.dev) integration for [`@ghostry/fabricator`](https://www.npmjs.com/package/@ghostry/fabricator). Faker's generators draw from the same stream as every other builder in a schema, so `T.faker.person.fullName()` replays exactly like `T.string` does — nothing additional to track, no drift between runs.
4
20
 
5
21
  Targets `@faker-js/faker` 10.x specifically — hence the `-v10` suffix, matching the adapter packages' convention. A future faker major is a sibling package with its own peer range, not a version bump of this one.
6
22
 
@@ -43,21 +59,21 @@ Faker's relative-date methods resolve against the same instance `clock` that `T.
43
59
 
44
60
  A faker method could have been wrapped as an opaque producer, and that is the one thing this package refuses to do — an `opaque` schema converts to `Type.Unknown()` in any adapter and is invisible to `combinatorial`/`coverage`. Each builder instead returns the core kind matching the method's own return type:
45
61
 
46
- | faker returns | builder returns | via the TypeBox adapter |
47
- | ------------------- | ----------------------- | --------------------------------- |
48
- | `string` (207) | `T.string` | `Type.String()` |
49
- | `Date` (7) | `T.date` | `Type.Date()` |
50
- | a record shape (7) | `T.object({ ... })` | `Type.Object({ ... })` |
51
- | a literal union (6) | `T.enum.uniform([...])` | `Type.Union([Type.Literal(), …])` |
52
- | `number` (6) | `T.number` | `Type.Number()` |
53
- | `boolean` (1) | `T.boolean` | `Type.Boolean()` |
54
- | `bigint` (1) | `T.bigint` | `Type.BigInt()` |
55
- | `number[]` (1) | `T.array(T.number)` | `Type.Array(Type.Number())` |
56
- | `Date[]` (1) | `T.array(T.date)` | `Type.Array(Type.Date())` |
62
+ | faker returns | builder returns | via the TypeBox adapter |
63
+ | ---------------------- | ------------------------------- | -------------------------------------------- |
64
+ | `string` (207) | `T.string` | `Type.String()` |
65
+ | `Date` (7) | `T.date` | `Type.Date()` |
66
+ | a record shape (7) | `T.object({ ... })` | `Type.Object({ ... })` |
67
+ | a literal union (6) | `T.enum.uniform([...])` | `Type.Union([Type.Literal(), …])` |
68
+ | `number` (6) | `T.number` | `Type.Number()` |
69
+ | `boolean` (1) | `T.boolean` | `Type.Boolean()` |
70
+ | `bigint` (1) | `T.bigint` | `Type.BigInt()` |
71
+ | `[number, number]` (1) | `T.tuple([T.number, T.number])` | `Type.Tuple([Type.Number(), Type.Number()])` |
72
+ | `Date[]` (1) | `T.array(T.date)` | `Type.Array(Type.Date())` |
57
73
 
58
74
  Seventeen string methods whose output satisfies a JSON-Schema format regardless of the options passed additionally carry it — `internet.email()` converts to `Type.String({ format: "email" })`, `database.mongodbObjectId()` to a `pattern`.
59
75
 
60
- The kind table is generated by probing a real `Faker`, and a compile-time assertion checks every entry's value type against faker's own declared return type — so a faker release that changes one fails `tsc` at that entry rather than silently emitting a wrong schema.
76
+ The mirror is hand-written, not generated — one declaration per builder and one implementation, kept in agreement by the compiler. A compile-time assertion checks every entry's value type against faker's own declared return type, so a faker release that changes one fails `tsc` at that entry rather than silently emitting a wrong schema.
61
77
 
62
78
  ## Three deviations from faker's own API
63
79
 
@@ -75,7 +91,7 @@ T.faker.color.rgb.channels({ includeAlpha: true });
75
91
 
76
92
  The same applies to `cmyk`, `hsl`, `hwb`, `lab`, `lch`, and `colorByCSSColorSpace`. `color`'s other four methods are ordinary builders.
77
93
 
78
- **A method declared as a literal union becomes an `enum`, not a `string`.** `person.sexType()` gives `T.enum.uniform(["female", "generic", "male"])`, so it converts to a union of literals and is enumerable by `combinatorial`/`coverage` rather than an unconstrained string.
94
+ **Where faker's declared return type is narrower than its JS type, the kind narrows with it.** A literal union becomes an `enum`, not a `string`: `person.sexType()` gives `T.enum.uniform(["female", "generic", "male"])`, so it converts to a union of literals and is enumerable by `combinatorial`/`coverage` rather than an unconstrained string. A fixed-arity array becomes a `tuple`, not an `array`: `location.nearbyGPSCoordinate()` gives `T.tuple([T.number, T.number])`, since no option can change its `[latitude, longitude]` arity — unlike the color channels above, which is why those stay `T.array`.
79
95
 
80
96
  ## `use` — for what the mirror doesn't cover
81
97
 
@@ -94,7 +110,7 @@ T.faker.use.opaque((f) => f.helpers.arrayElement(["free", "pro"] as const));
94
110
 
95
111
  ## Notes
96
112
 
97
- `faker.seed(...)` is inert here, by design: fabricator's seed governs, and a second one competing for control of the same output is the bug this package exists to remove. Pass a different `seed` to `initialize()` instead.
113
+ `faker.seed(...)` is inert here, by design: fabricator's seeding governs, and a second one competing for control of the same output is the bug this package exists to remove. Change the instance's `clock` — or its `salt`, if it has one — instead.
98
114
 
99
115
  A builder called outside `fabricate()` throws `FakerExtensionError.NoActiveScopeError` — there is no active fabrication to draw from. `FakerExtensionError` extends core's `FabricatorError`, so one `catch` still covers both packages.
100
116
 
package/dist/esm/Error.js CHANGED
@@ -2,14 +2,14 @@ import { FabricatorError } from "@ghostry/fabricator";
2
2
  class Error_FakerExtensionError extends FabricatorError {
3
3
  constructor(){
4
4
  super();
5
- this.name = "FakerExtensionError";
5
+ this.name = "@ghostry/fabricator-extension-faker-v10:FakerExtensionError";
6
6
  }
7
7
  }
8
8
  (function(FakerExtensionError) {
9
9
  class NoActiveScopeError extends FakerExtensionError {
10
10
  constructor(){
11
11
  super();
12
- this.name = "NoActiveScopeError";
12
+ this.name = "@ghostry/fabricator-extension-faker-v10:NoActiveScopeError";
13
13
  this.message = "A faker builder was invoked outside of fabricate(). Faker builders only draw randomness while the schema they belong to is being fabricated.";
14
14
  }
15
15
  }
@@ -1,7 +1,9 @@
1
+ import { toWords } from "./Words.js";
1
2
  function build(T, faker, draw) {
2
- const [f, d] = [
3
+ const [f, d, w] = [
3
4
  faker,
4
- draw
5
+ draw,
6
+ toWords(faker)
5
7
  ];
6
8
  const string = T.string.as(()=>"");
7
9
  return {
@@ -171,7 +173,7 @@ function build(T, faker, draw) {
171
173
  collation: (...args)=>T.string.as(d(()=>f.database.collation(...args))),
172
174
  column: (...args)=>T.string.as(d(()=>f.database.column(...args))),
173
175
  engine: (...args)=>T.string.as(d(()=>f.database.engine(...args))),
174
- mongodbObjectId: (...args)=>T.string.as(d(()=>f.database.mongodbObjectId(...args)), {
176
+ mongodbObjectId: (...args)=>T.string.as(d(()=>f.database.mongodbObjectId(...args))).admits({
175
177
  pattern: "^[0-9a-fA-F]{24}$"
176
178
  }),
177
179
  type: (...args)=>T.string.as(d(()=>f.database.type(...args)))
@@ -196,11 +198,11 @@ function build(T, faker, draw) {
196
198
  accountName: (...args)=>T.string.as(d(()=>f.finance.accountName(...args))),
197
199
  accountNumber: (...args)=>T.string.as(d(()=>f.finance.accountNumber(...args))),
198
200
  amount: (...args)=>T.string.as(d(()=>f.finance.amount(...args))),
199
- bic: (...args)=>T.string.as(d(()=>f.finance.bic(...args)), {
201
+ bic: (...args)=>T.string.as(d(()=>f.finance.bic(...args))).admits({
200
202
  pattern: "^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$"
201
203
  }),
202
204
  bitcoinAddress: (...args)=>T.string.as(d(()=>f.finance.bitcoinAddress(...args))),
203
- creditCardCVV: (...args)=>T.string.as(d(()=>f.finance.creditCardCVV(...args)), {
205
+ creditCardCVV: (...args)=>T.string.as(d(()=>f.finance.creditCardCVV(...args))).admits({
204
206
  pattern: "^[0-9]{3,4}$"
205
207
  }),
206
208
  creditCardIssuer: (...args)=>T.string.as(d(()=>f.finance.creditCardIssuer(...args))),
@@ -250,38 +252,38 @@ function build(T, faker, draw) {
250
252
  verb: (...args)=>T.string.as(d(()=>f.hacker.verb(...args)))
251
253
  },
252
254
  image: {
253
- avatar: (...args)=>T.string.as(d(()=>f.image.avatar(...args)), {
255
+ avatar: (...args)=>T.string.as(d(()=>f.image.avatar(...args))).admits({
254
256
  format: "uri"
255
257
  }),
256
- avatarGitHub: (...args)=>T.string.as(d(()=>f.image.avatarGitHub(...args)), {
258
+ avatarGitHub: (...args)=>T.string.as(d(()=>f.image.avatarGitHub(...args))).admits({
257
259
  format: "uri"
258
260
  }),
259
- dataUri: (...args)=>T.string.as(d(()=>f.image.dataUri(...args)), {
261
+ dataUri: (...args)=>T.string.as(d(()=>f.image.dataUri(...args))).admits({
260
262
  format: "uri"
261
263
  }),
262
- personPortrait: (...args)=>T.string.as(d(()=>f.image.personPortrait(...args)), {
264
+ personPortrait: (...args)=>T.string.as(d(()=>f.image.personPortrait(...args))).admits({
263
265
  format: "uri"
264
266
  }),
265
- url: (...args)=>T.string.as(d(()=>f.image.url(...args)), {
267
+ url: (...args)=>T.string.as(d(()=>f.image.url(...args))).admits({
266
268
  format: "uri"
267
269
  }),
268
270
  urlLoremFlickr: (...args)=>T.string.as(d(()=>f.image.urlLoremFlickr(...args))),
269
- urlPicsumPhotos: (...args)=>T.string.as(d(()=>f.image.urlPicsumPhotos(...args)), {
271
+ urlPicsumPhotos: (...args)=>T.string.as(d(()=>f.image.urlPicsumPhotos(...args))).admits({
270
272
  format: "uri"
271
273
  })
272
274
  },
273
275
  internet: {
274
276
  displayName: (...args)=>T.string.as(d(()=>f.internet.displayName(...args))),
275
- domainName: (...args)=>T.string.as(d(()=>f.internet.domainName(...args)), {
277
+ domainName: (...args)=>T.string.as(d(()=>f.internet.domainName(...args))).admits({
276
278
  format: "hostname"
277
279
  }),
278
280
  domainSuffix: (...args)=>T.string.as(d(()=>f.internet.domainSuffix(...args))),
279
281
  domainWord: (...args)=>T.string.as(d(()=>f.internet.domainWord(...args))),
280
- email: (...args)=>T.string.as(d(()=>f.internet.email(...args)), {
282
+ email: (...args)=>T.string.as(d(()=>f.internet.email(...args))).admits({
281
283
  format: "email"
282
284
  }),
283
285
  emoji: (...args)=>T.string.as(d(()=>f.internet.emoji(...args))),
284
- exampleEmail: (...args)=>T.string.as(d(()=>f.internet.exampleEmail(...args)), {
286
+ exampleEmail: (...args)=>T.string.as(d(()=>f.internet.exampleEmail(...args))).admits({
285
287
  format: "email"
286
288
  }),
287
289
  httpMethod: (...args)=>T.enum.uniform([
@@ -293,10 +295,10 @@ function build(T, faker, draw) {
293
295
  ]).as(d(()=>f.internet.httpMethod(...args))),
294
296
  httpStatusCode: (...args)=>T.number.as(d(()=>f.internet.httpStatusCode(...args))),
295
297
  ip: (...args)=>T.string.as(d(()=>f.internet.ip(...args))),
296
- ipv4: (...args)=>T.string.as(d(()=>f.internet.ipv4(...args)), {
298
+ ipv4: (...args)=>T.string.as(d(()=>f.internet.ipv4(...args))).admits({
297
299
  format: "ipv4"
298
300
  }),
299
- ipv6: (...args)=>T.string.as(d(()=>f.internet.ipv6(...args)), {
301
+ ipv6: (...args)=>T.string.as(d(()=>f.internet.ipv6(...args))).admits({
300
302
  format: "ipv6"
301
303
  }),
302
304
  jwt: (...args)=>T.string.as(d(()=>f.internet.jwt(...args))),
@@ -308,7 +310,7 @@ function build(T, faker, draw) {
308
310
  "http",
309
311
  "https"
310
312
  ]).as(d(()=>f.internet.protocol(...args))),
311
- url: (...args)=>T.string.as(d(()=>f.internet.url(...args)), {
313
+ url: (...args)=>T.string.as(d(()=>f.internet.url(...args))).admits({
312
314
  format: "uri"
313
315
  }),
314
316
  userAgent: (...args)=>T.string.as(d(()=>f.internet.userAgent(...args))),
@@ -344,15 +346,15 @@ function build(T, faker, draw) {
344
346
  zipCode: (...args)=>T.string.as(d(()=>f.location.zipCode(...args)))
345
347
  },
346
348
  lorem: {
347
- lines: (...args)=>T.string.as(d(()=>f.lorem.lines(...args))),
348
- paragraph: (...args)=>T.string.as(d(()=>f.lorem.paragraph(...args))),
349
- paragraphs: (...args)=>T.string.as(d(()=>f.lorem.paragraphs(...args))),
350
- sentence: (...args)=>T.string.as(d(()=>f.lorem.sentence(...args))),
351
- sentences: (...args)=>T.string.as(d(()=>f.lorem.sentences(...args))),
352
- slug: (...args)=>T.string.as(d(()=>f.lorem.slug(...args))),
353
- text: (...args)=>T.string.as(d(()=>f.lorem.text(...args))),
354
- word: (...args)=>T.string.as(d(()=>f.lorem.word(...args))),
355
- words: (...args)=>T.string.as(d(()=>f.lorem.words(...args)))
349
+ lines: (...args)=>T.string.as(d(w.lorem.lines(...args))),
350
+ paragraph: (...args)=>T.string.as(d(w.lorem.paragraph(...args))),
351
+ paragraphs: (...args)=>T.string.as(d(w.lorem.paragraphs(...args))),
352
+ sentence: (...args)=>T.string.as(d(w.lorem.sentence(...args))),
353
+ sentences: (...args)=>T.string.as(d(w.lorem.sentences(...args))),
354
+ slug: (...args)=>T.string.as(d(w.lorem.slug(...args))),
355
+ text: (...args)=>T.string.as(d(w.lorem.text(...args))),
356
+ word: (...args)=>T.string.as(d(w.lorem.word(...args))),
357
+ words: (...args)=>T.string.as(d(w.lorem.words(...args)))
356
358
  },
357
359
  music: {
358
360
  album: (...args)=>T.string.as(d(()=>f.music.album(...args))),
@@ -417,7 +419,7 @@ function build(T, faker, draw) {
417
419
  sample: (...args)=>T.string.as(d(()=>f.string.sample(...args))),
418
420
  symbol: (...args)=>T.string.as(d(()=>f.string.symbol(...args))),
419
421
  ulid: (...args)=>T.string.as(d(()=>f.string.ulid(...args))),
420
- uuid: (...args)=>T.string.as(d(()=>f.string.uuid(...args)), {
422
+ uuid: (...args)=>T.string.as(d(()=>f.string.uuid(...args))).admits({
421
423
  format: "uuid"
422
424
  })
423
425
  },
@@ -429,7 +431,7 @@ function build(T, faker, draw) {
429
431
  directoryPath: (...args)=>T.string.as(d(()=>f.system.directoryPath(...args))),
430
432
  fileExt: (...args)=>T.string.as(d(()=>f.system.fileExt(...args))),
431
433
  fileName: (...args)=>T.string.as(d(()=>f.system.fileName(...args))),
432
- filePath: (...args)=>T.string.as(d(()=>f.system.filePath(...args)), {
434
+ filePath: (...args)=>T.string.as(d(()=>f.system.filePath(...args))).admits({
433
435
  format: "uri-reference"
434
436
  }),
435
437
  fileType: (...args)=>T.string.as(d(()=>f.system.fileType(...args))),
@@ -449,15 +451,15 @@ function build(T, faker, draw) {
449
451
  vrm: (...args)=>T.string.as(d(()=>f.vehicle.vrm(...args)))
450
452
  },
451
453
  word: {
452
- adjective: (...args)=>T.string.as(d(()=>f.word.adjective(...args))),
453
- adverb: (...args)=>T.string.as(d(()=>f.word.adverb(...args))),
454
- conjunction: (...args)=>T.string.as(d(()=>f.word.conjunction(...args))),
455
- interjection: (...args)=>T.string.as(d(()=>f.word.interjection(...args))),
456
- noun: (...args)=>T.string.as(d(()=>f.word.noun(...args))),
457
- preposition: (...args)=>T.string.as(d(()=>f.word.preposition(...args))),
458
- sample: (...args)=>T.string.as(d(()=>f.word.sample(...args))),
459
- verb: (...args)=>T.string.as(d(()=>f.word.verb(...args))),
460
- words: (...args)=>T.string.as(d(()=>f.word.words(...args)))
454
+ adjective: (...args)=>T.string.as(d(w.word.adjective(...args))),
455
+ adverb: (...args)=>T.string.as(d(w.word.adverb(...args))),
456
+ conjunction: (...args)=>T.string.as(d(w.word.conjunction(...args))),
457
+ interjection: (...args)=>T.string.as(d(w.word.interjection(...args))),
458
+ noun: (...args)=>T.string.as(d(w.word.noun(...args))),
459
+ preposition: (...args)=>T.string.as(d(w.word.preposition(...args))),
460
+ sample: (...args)=>T.string.as(d(w.word.sample(...args))),
461
+ verb: (...args)=>T.string.as(d(w.word.verb(...args))),
462
+ words: (...args)=>T.string.as(d(w.word.words(...args)))
461
463
  }
462
464
  };
463
465
  }
@@ -0,0 +1,151 @@
1
+ import { FakerError } from "@faker-js/faker";
2
+ const categories = [
3
+ "adjective",
4
+ "adverb",
5
+ "conjunction",
6
+ "interjection",
7
+ "noun",
8
+ "preposition",
9
+ "verb"
10
+ ];
11
+ function filterByLength(list, options = {}) {
12
+ const { length, strategy = "fail" } = "number" == typeof options ? {
13
+ length: options
14
+ } : options;
15
+ if (void 0 === length) {
16
+ if ("shortest" === strategy || "longest" === strategy) return extreme(list, strategy);
17
+ return list;
18
+ }
19
+ const range = "number" == typeof length ? {
20
+ min: length,
21
+ max: length
22
+ } : length;
23
+ const matching = list.filter((word)=>word.length >= range.min && word.length <= range.max);
24
+ if (matching.length > 0) return matching;
25
+ switch(strategy){
26
+ case "fail":
27
+ throw new FakerError("No words found that match the given length.");
28
+ case "closest":
29
+ return closest(list, range);
30
+ case "shortest":
31
+ case "longest":
32
+ return extreme(list, strategy);
33
+ case "any-length":
34
+ return list;
35
+ }
36
+ }
37
+ function extreme(list, strategy) {
38
+ const lengths = list.map((word)=>word.length);
39
+ const target = "shortest" === strategy ? Math.min(...lengths) : Math.max(...lengths);
40
+ return list.filter((word)=>word.length === target);
41
+ }
42
+ function closest(list, range) {
43
+ let below = -1 / 0;
44
+ let above = 1 / 0;
45
+ for (const { length } of list){
46
+ if (length < range.min && length > below) below = length;
47
+ if (length > range.max && length < above) above = length;
48
+ }
49
+ const offset = Math.min(range.min - below, above - range.max);
50
+ return list.filter(({ length })=>length === range.min - offset || length === range.max + offset);
51
+ }
52
+ function listOf(read, options) {
53
+ let resolved;
54
+ return ()=>{
55
+ if (void 0 === resolved) resolved = filterByLength(read(), options);
56
+ return resolved;
57
+ };
58
+ }
59
+ function pick(random, list) {
60
+ if (0 === list.length) throw new FakerError("Cannot get value from empty dataset.");
61
+ return list[list.length > 1 ? Math.floor(random.next() * list.length) : 0];
62
+ }
63
+ function toWords(faker) {
64
+ const repeat = (count, separator, next)=>faker.helpers.multiple(next, {
65
+ count
66
+ }).join(separator);
67
+ const words = (word, count = 3)=>repeat(count, " ", word);
68
+ const sentence = (word, count = {
69
+ min: 3,
70
+ max: 10
71
+ })=>{
72
+ const text = words(word, count);
73
+ return `${text.charAt(0).toUpperCase()}${text.substring(1)}.`;
74
+ };
75
+ const sentences = (word, count = {
76
+ min: 2,
77
+ max: 6
78
+ }, separator = " ")=>repeat(count, separator, ()=>sentence(word));
79
+ const paragraph = (word, count = 3)=>sentences(word, count);
80
+ const paragraphs = (word, count = 3, separator = "\n")=>repeat(count, separator, ()=>paragraph(word));
81
+ const lines = (word, count = {
82
+ min: 1,
83
+ max: 5
84
+ })=>sentences(word, count, "\n");
85
+ const texts = [
86
+ sentence,
87
+ sentences,
88
+ paragraph,
89
+ paragraphs,
90
+ lines
91
+ ];
92
+ const lorem = (compose)=>{
93
+ const list = listOf(()=>faker.definitions.lorem.word, void 0);
94
+ return ({ random })=>compose(()=>pick(random, list()), random);
95
+ };
96
+ const category = (name)=>(options)=>{
97
+ const list = listOf(()=>faker.definitions.word[name], options);
98
+ return ({ random })=>pick(random, list());
99
+ };
100
+ const sampler = (options)=>{
101
+ const lists = categories.map((name)=>listOf(()=>faker.definitions.word[name], options));
102
+ return (random)=>{
103
+ for (const list of faker.helpers.shuffle(lists))try {
104
+ return pick(random, list());
105
+ } catch {
106
+ continue;
107
+ }
108
+ throw new FakerError("No matching word data available for the current locale");
109
+ };
110
+ };
111
+ return {
112
+ lorem: {
113
+ lines: (count)=>lorem((word)=>lines(word, count)),
114
+ paragraph: (count)=>lorem((word)=>paragraph(word, count)),
115
+ paragraphs: (count, separator)=>lorem((word)=>paragraphs(word, count, separator)),
116
+ sentence: (count)=>lorem((word)=>sentence(word, count)),
117
+ sentences: (count, separator)=>lorem((word)=>sentences(word, count, separator)),
118
+ slug: (count)=>lorem((word)=>faker.helpers.slugify(words(word, count))),
119
+ text: ()=>lorem((word, random)=>pick(random, texts)(word)),
120
+ word: (options)=>{
121
+ const list = listOf(()=>faker.definitions.lorem.word, options);
122
+ return ({ random })=>pick(random, list());
123
+ },
124
+ words: (count)=>lorem((word)=>words(word, count))
125
+ },
126
+ word: {
127
+ adjective: category("adjective"),
128
+ adverb: category("adverb"),
129
+ conjunction: category("conjunction"),
130
+ interjection: category("interjection"),
131
+ noun: category("noun"),
132
+ preposition: category("preposition"),
133
+ sample: (options)=>{
134
+ const sample = sampler(options);
135
+ return ({ random })=>sample(random);
136
+ },
137
+ verb: category("verb"),
138
+ words: (options = {})=>{
139
+ const { count = {
140
+ min: 1,
141
+ max: 3
142
+ } } = "number" == typeof options ? {
143
+ count: options
144
+ } : options;
145
+ const sample = sampler(void 0);
146
+ return ({ random })=>repeat(count, " ", ()=>sample(random));
147
+ }
148
+ }
149
+ };
150
+ }
151
+ export { toWords };
package/dist/esm/index.js CHANGED
@@ -27,7 +27,7 @@ function draw(produce) {
27
27
  const previous = scope;
28
28
  scope = context;
29
29
  try {
30
- return produce();
30
+ return produce(context);
31
31
  } finally{
32
32
  scope = previous;
33
33
  }
@@ -22,4 +22,4 @@ import type { FakerExtension, Registry } from "./Types";
22
22
  * runtime floor on the builder count: the type-level guards read the surface
23
23
  * alone and cannot see a spread that drops a module.
24
24
  */
25
- export declare function construct(T: Registry, faker: Faker, draw: <$T>(produce: () => $T) => (context: ProduceContext) => $T): FakerExtension;
25
+ export declare function construct(T: Registry, faker: Faker, draw: <$T>(produce: (context: ProduceContext) => $T) => (context: ProduceContext) => $T): FakerExtension;
@@ -3,7 +3,8 @@
3
3
  * exports that name, and importing both into one scope would collide. Follows
4
4
  * core's `FabricatorError` shape (abstract base plus a merged namespace of
5
5
  * subclasses) so `e instanceof FabricatorError` still catches everything either
6
- * package throws.
6
+ * package throws. A `name` is prefixed with this package's name, as core's are
7
+ * with its own.
7
8
  */
8
9
  import { FabricatorError } from "@ghostry/fabricator";
9
10
  export declare abstract class FakerExtensionError extends FabricatorError {
@@ -9,7 +9,7 @@
9
9
  * the faker-bump workflow; an entry added or changed here needs the matching
10
10
  * edit there, and vice versa.
11
11
  *
12
- * Three shapes before editing:
12
+ * Shapes before editing:
13
13
  *
14
14
  * - Every builder spreads faker's `Parameters<...>` rather than taking a single
15
15
  * `options?`. Faker's zero-parameter methods (`animal.bear()`) and its
@@ -24,16 +24,22 @@
24
24
  * producer rather than a length bound.
25
25
  * - `color`'s 7 split methods are a `{ text, channels }` pair each rather than
26
26
  * one ambiguous builder, since their return type depends on their arguments;
27
- * see the deviation policy in `CLAUDE.md`'s "The faker extension". All 7 name
27
+ * see the deviation policy in `AGENTS.md`'s "The faker extension". All 7 name
28
28
  * `format: "decimal"` on `.channels()`, not only `rgb`: bare is right at
29
29
  * runtime for the other 6, but not provable through faker's declarations,
30
30
  * whose _first_ overload takes `format?: StringColorFormat` and returns
31
31
  * `string`. `'decimal'` is `NumberColorFormat`'s sole member, so naming it is
32
32
  * exact.
33
+ * - `lorem`'s and `word`'s builders never call faker's own method: faker's
34
+ * per-word path copies the word list and parses an options object for every
35
+ * word drawn, so they draw through `./Words.ts`, which reproduces faker's
36
+ * output exactly from the same stream.
33
37
  *
34
38
  * Every node is a plain object or a bare builder function, never a function
35
39
  * carrying properties — the invariant `deepMerge` needs to recurse through an
36
40
  * extended registry, guarded by `test/Mergeable.test.ts`.
41
+ *
42
+ * @module
37
43
  */
38
44
  import type { Faker } from "@faker-js/faker";
39
45
  import type { ProduceContext } from "@ghostry/fabricator";
@@ -41,9 +47,9 @@ import type { Registry } from "../Types";
41
47
  import type { FakerModules } from "./Types";
42
48
  /**
43
49
  * `src/index.ts`'s `draw`, by type — the save/restore wrapper that binds a
44
- * producer to the leaf's own `random`/`clock`. Named here rather than imported
45
- * so this file stays type-only in its dependencies.
50
+ * producer to the leaf's own `random`/`clock`. Named here rather than imported,
51
+ * since `src/index.ts` imports this file.
46
52
  */
47
- type Draw = <$T>(produce: () => $T) => (context: ProduceContext) => $T;
53
+ type Draw = <$T>(produce: (context: ProduceContext) => $T) => (context: ProduceContext) => $T;
48
54
  export declare function build(T: Registry, faker: Faker, draw: Draw): FakerModules;
49
55
  export {};
@@ -41,11 +41,14 @@
41
41
  * union rather than a bare `string`.
42
42
  * 4. A reordered overload set changes `Parameters<...>` here, which the matching
43
43
  * builder's `...args` forwarding then fails to accept.
44
+ * 5. A change to how `lorem` or `word` draws fails `test/Parity.test.ts`, at `bun
45
+ * test` rather than `check`: those builders reimplement faker's method
46
+ * bodies in `./Words.ts`, which must then be brought back in line.
44
47
  *
45
48
  * Fix the entry in **both** files; the annotation on `build` fails until they
46
49
  * agree.
47
50
  *
48
- * These assertions resolve through built `dist/`, not `src/` (`CLAUDE.md`'s
51
+ * These assertions resolve through built `dist/`, not `src/` (`AGENTS.md`'s
49
52
  * test-import convention), so a `src`-only edit proves nothing about faker
50
53
  * drift until you rebuild. A disagreement between this file and `./Build.ts` is
51
54
  * caught without one, being internal to `src`.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Faker 10's `lorem` and `word` method bodies, minus the word-list copy.
3
+ *
4
+ * Faker's per-word path has two costs. With no `length`, its shared
5
+ * `filterWordListByLength` returns `[...wordList]`, copying a whole locale list
6
+ * (up to 1,000 entries in `en`) to index one element. That copy is close to
7
+ * free where the engine shares a literal array's storage on spread (Node 22–26
8
+ * and Bun 1.4.4 with faker's bundled locales), but costs about a microsecond
9
+ * per word on Bun 1.2.14, and on any engine when a locale's lists are built at
10
+ * runtime. Each word then goes through `helpers.arrayElement` → `number.int`,
11
+ * which parses an options object per draw and is most of what remains where the
12
+ * copy is free. `lorem.sentence`/`paragraph`/`text` pay both once per word.
13
+ * Every builder in `./Build.ts`'s `lorem`/`word` modules draws through this
14
+ * file instead and never calls faker's own method.
15
+ *
16
+ * The copy is reported upstream as
17
+ * https://github.com/faker-js/faker/issues/4242. A faker release that drops it
18
+ * is the point to weigh calling faker's methods again, not an automatic
19
+ * removal: the per-draw options parsing and definitions lookup would remain.
20
+ *
21
+ * **The output is faker's exactly, not an approximation.** `pick` reads
22
+ * `random.next()` and indexes with `arrayElement`'s own arithmetic,
23
+ * `Math.floor(u * length)`, from the same stream faker's randomizer reads
24
+ * (`src/index.ts`'s `draw` installs one `context` for both). Every draw made
25
+ * once per group rather than once per word — a `count` resolved through
26
+ * `rangeToNumber`, `word.sample`'s shuffle, `slug`'s `slugify` — goes through
27
+ * faker's own helpers, which keeps their semantics (an inverted range's error,
28
+ * a fractional count's truncation) without restating them, at a cost that is
29
+ * off the hot path. So a seed's data is unchanged, the surface keeps faker's
30
+ * `Parameters<...>`, and nothing about this is a deviation in the sense of
31
+ * `AGENTS.md`'s deviation policy. `test/Parity.test.ts` is the guard: it
32
+ * compares every builder here against faker's own method on an identical
33
+ * stream, so a faker release that changes how these draw fails there.
34
+ *
35
+ * Each filtered list is resolved once per builder call, lazily on the first
36
+ * draw, and a throw is never cached: missing locale data (thrown by faker's
37
+ * definitions proxy) and `strategy: "fail"` still throw from `fabricate()` on
38
+ * every call, as faker's own methods do, rather than from the builder call.
39
+ *
40
+ * @module
41
+ */
42
+ import type { Faker } from "@faker-js/faker";
43
+ import type { ProduceContext } from "@ghostry/fabricator";
44
+ type Producer = (context: ProduceContext) => string;
45
+ type WordOptions = Parameters<Faker["word"]["noun"]>[0];
46
+ type Count = NonNullable<Parameters<Faker["lorem"]["words"]>[0]>;
47
+ export declare function toWords(faker: Faker): {
48
+ lorem: {
49
+ lines: (count?: Count) => Producer;
50
+ paragraph: (count?: Count) => Producer;
51
+ paragraphs: (count?: Count, separator?: string) => Producer;
52
+ sentence: (count?: Count) => Producer;
53
+ sentences: (count?: Count, separator?: string) => Producer;
54
+ slug: (count?: Count) => Producer;
55
+ text: () => Producer;
56
+ word: (options?: WordOptions) => Producer;
57
+ words: (count?: Count) => Producer;
58
+ };
59
+ word: {
60
+ adjective: (options?: WordOptions) => Producer;
61
+ adverb: (options?: WordOptions) => Producer;
62
+ conjunction: (options?: WordOptions) => Producer;
63
+ interjection: (options?: WordOptions) => Producer;
64
+ noun: (options?: WordOptions) => Producer;
65
+ preposition: (options?: WordOptions) => Producer;
66
+ sample: (options?: WordOptions) => Producer;
67
+ verb: (options?: WordOptions) => Producer;
68
+ words: (options?: Parameters<Faker["word"]["words"]>[0]) => Producer;
69
+ };
70
+ };
71
+ export {};
@@ -12,7 +12,7 @@ export type Registry = Pick<typeof registry, "array" | "bigint" | "boolean" | "d
12
12
  * Every module the mirror covers, intersected with `keyof Faker` so every
13
13
  * downstream `Faker[$M]` indexing is sound. `FakerModules/Types.ts` is the
14
14
  * source of truth — `helpers` is absent from it deliberately (deviation policy
15
- * in `CLAUDE.md`'s "The faker extension"), so this type excludes it without
15
+ * in `AGENTS.md`'s "The faker extension"), so this type excludes it without
16
16
  * saying so twice.
17
17
  *
18
18
  * `test/index.types.test.ts`'s `_ModulesExhaustive` holds this to faker's own
@@ -60,7 +60,7 @@ export type FakerExtension = FakerModules & {
60
60
  * (`src/index.ts`'s `draw`).
61
61
  *
62
62
  * How `helpers` is reached (omitted from the mirror — deviation policy in
63
- * `CLAUDE.md`'s "The faker extension"). Kind-tagged forms (`.string`,
63
+ * `AGENTS.md`'s "The faker extension"). Kind-tagged forms (`.string`,
64
64
  * `.number`, `.date`, `.boolean`, `.bigint`) keep adapter compatibility
65
65
  * available there too. `.opaque` is the **only** way to get an `opaque`
66
66
  * schema out of this package — honest, since it is the one place the caller
@@ -7,7 +7,7 @@ export type { FakerExtension as FakerTypes } from "./Types";
7
7
  * The mirror's type-level machinery — not needed to _use_ it, but exported so a
8
8
  * consumer (or this package's compile-time checks in
9
9
  * `test/index.types.test.ts`) can name a module or method without a relative
10
- * `./Types` path tests otherwise avoid (`CLAUDE.md`: tests resolve through the
10
+ * `./Types` path tests otherwise avoid (`AGENTS.md`: tests resolve through the
11
11
  * package specifier, exercising built `dist/`).
12
12
  *
13
13
  * `FakerModules` is the surface those checks are made _against_ — every
@@ -29,9 +29,9 @@ export type FakerConfig = ConstructorParameters<typeof Faker>[0]["config"];
29
29
  *
30
30
  * No `seed` option and no pre-built `Faker`: a pre-built instance's randomizer
31
31
  * is fixed at construction, and accepting one would either ignore it silently
32
- * or let it compete with fabricator's seed. `create` is the honest escape: the
33
- * caller builds the instance; it still receives this bridge's `randomizer`, so
34
- * reproducibility survives.
32
+ * or let it compete with fabricator's seeding. `create` is the honest escape:
33
+ * the caller builds the instance; it still receives this bridge's `randomizer`,
34
+ * so reproducibility survives.
35
35
  */
36
36
  export type Options = {
37
37
  locale: LocaleDefinition | LocaleDefinition[];
@@ -63,11 +63,11 @@ export type Options = {
63
63
  * `FakerExtension` (`./Types.ts`) is spelled out explicitly rather than
64
64
  * inferred. An inferred type would reach into each kind's `Schema` interface —
65
65
  * internal to `@ghostry/fabricator`, unreachable from its `.` export — and
66
- * declaration emission (`tsgo`, not `tsc --noEmit`; see `CLAUDE.md`'s
66
+ * declaration emission (`tsgo`, not `tsc --noEmit`; see `AGENTS.md`'s
67
67
  * declaration-emit trap) refuses to print an anonymous expansion of one. Even
68
68
  * where it printed it would be the wrong contract: inference would surface each
69
- * builder's incidental narrowness (`Schema<{ produce; hints }>`) where the
70
- * declared surface states `Primitive.string.Schema`.
69
+ * builder's incidental narrowness (`Schema<{ produce }>`) where the declared
70
+ * surface states `Primitive.string.Schema`.
71
71
  *
72
72
  * Every node is a plain object or a leaf builder function, never a function
73
73
  * carrying properties: `deepMerge` recurses only where `isPlainObject` holds on
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@ghostry/fabricator-extension-faker-v10",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "license": "MIT",
5
5
  "description": "@faker-js/faker integration for @ghostry/fabricator — faker's data generators, seeded and reproducible through the fabricator instance that calls them.",
6
6
  "keywords": [
7
7
  "factory",
8
+ "reproducible",
9
+ "fixtures",
10
+ "testing",
11
+ "mock",
12
+ "schema",
8
13
  "faker",
9
- "extension"
14
+ "typescript"
10
15
  ],
11
16
  "author": {
12
17
  "name": "Patrick Rebsch"
@@ -16,9 +21,13 @@
16
21
  "url": "git+https://github.com/ghostry-dev/fabricator",
17
22
  "directory": "pkg/fabricator-extension-faker-v10"
18
23
  },
19
- "homepage": "https://github.com/ghostry-dev/fabricator",
24
+ "homepage": "https://docs.ghostry.dev/fabricator",
20
25
  "bugs": "https://github.com/ghostry-dev/fabricator/issues",
21
26
  "type": "module",
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=20.0.0"
30
+ },
22
31
  "types": "./dist/types/index.d.ts",
23
32
  "module": "./dist/esm/index.js",
24
33
  "exports": {
@@ -44,20 +53,21 @@
44
53
  "check": "tsc --noEmit",
45
54
  "check:ts5": "bun --bun ../../node_modules/typescript-5/lib/tsc.js --noEmit",
46
55
  "check:jsr": "DENO_BIN_PATH=$PWD/../../node_modules/.bin/deno bunx --no-install jsr publish --dry-run --allow-dirty",
47
- "format": "bunx --no-install oxfmt .",
56
+ "format": "bun fmt",
48
57
  "test": "bun run check && bun run check:ts5 && bun run check:jsr && bun test"
49
58
  },
50
59
  "files": [
51
60
  "dist"
52
61
  ],
53
62
  "peerDependencies": {
54
- "@ghostry/fabricator": ">=0.0.1 <0.1.0",
63
+ "@ghostry/fabricator": ">=0.0.14 <0.1.0",
55
64
  "@faker-js/faker": "^10.5.0"
56
65
  },
57
66
  "devDependencies": {
58
- "@ghostry/fabricator": "0.0.1",
59
- "@ghostry/fabricator-adapter-typebox-v0": "0.0.1",
60
- "@faker-js/faker": "^10.5.0",
67
+ "@ghostry/fabricator": "0.0.14",
68
+ "@ghostry/fabricator-adapter-typebox-v0": "0.0.4",
69
+ "@faker-js/faker": "^10.6.0",
70
+ "@ghostry/harness": "^0.0.6",
61
71
  "@sinclair/typebox": "^0.34.52"
62
72
  }
63
73
  }