@stealthscale/tool-fixtures 0.1.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 ADDED
@@ -0,0 +1,57 @@
1
+ # @stealthscale/tool-fixtures
2
+
3
+ A **kit**: another repository takes it at development time.
4
+
5
+ A fixture builds the sample values a specification or a story shows. It answers the same
6
+ value on every run, so a snapshot, a visual diff and a failing assertion each mean what they
7
+ say.
8
+
9
+ ```ts
10
+ import { fixture, many } from '@stealthscale/tool-fixtures'
11
+
12
+ const person = fixture<Person>((source) => ({
13
+ email: source.internet.email(),
14
+ name: source.person.fullName(),
15
+ role: 'Reviewer',
16
+ }))
17
+
18
+ person() // the same person, every run
19
+ person({ name: 'Noor Haddad' }) // the case states what it is about; the rest is filled in
20
+ person(undefined, { locale: 'nl-BE' }) // a Flemish name, address and telephone number
21
+ many(person, 3) // three different people, the same three every run
22
+ ```
23
+
24
+ ## Determinism
25
+
26
+ A fixture is seeded by its position in a series, not by the order it was called in. `person()`
27
+ is position 0 and answers the same values whatever else ran first, so two specifications
28
+ cannot disturb each other and a case can be read on its own.
29
+
30
+ Each position gets its own source rather than one shared source that is re-seeded. A fixture
31
+ that builds another fixture would otherwise re-seed the source its caller is part-way
32
+ through, and the outer value would change according to what it happened to contain.
33
+
34
+ ## Locales
35
+
36
+ A locale is named as a BCP-47 tag, the same vocabulary the rest of the workspace uses, and
37
+ the nearest locale the sample data ships answers it. `nl-NL` reaches `nl`, `zh-Hans` reaches
38
+ `zh-CN`, and a tag nothing answers falls back to `en` rather than throwing, because a fixture
39
+ that refuses to build fails a specification about something else.
40
+
41
+ `LOCALES` lists every locale the sample data ships, as canonical tags, so a story that renders
42
+ in each of them reads the list rather than restating it. A layout that survives `Noor Haddad`
43
+ may still break on a German compound or an Arabic right-to-left label, and this is how a story
44
+ shows that before a customer does.
45
+
46
+ ## What belongs here, and what does not
47
+
48
+ This package holds the machinery. The values themselves belong to whichever repository owns
49
+ the domain, because a fixture is only worth having if it reads as something a person could
50
+ plausibly see in the product. `Lorem ipsum` and `Button` tell a reviewer nothing about
51
+ whether a layout survives a real label.
52
+
53
+ ## Install
54
+
55
+ ```sh
56
+ bun add -d @stealthscale/tool-fixtures
57
+ ```
@@ -0,0 +1,102 @@
1
+ import { Faker, LocaleDefinition } from "@faker-js/faker";
2
+ //#region src/fixtures.d.ts
3
+ /**
4
+ * Seeds every fixture.
5
+ *
6
+ * The number is arbitrary and fixed. What matters is that it never changes, because changing
7
+ * it rewrites every value every fixture in every repository has ever produced.
8
+ */
9
+ declare const SEED = 20260908;
10
+ /**
11
+ * Says which value of a series to build, and which locale to draw it from.
12
+ */
13
+ interface At {
14
+ /**
15
+ * The position in the series, counting from zero. Two positions differ, and the same
16
+ * position answers the same values on every run. Default: 0.
17
+ */
18
+ index?: number;
19
+ /**
20
+ * The locale to draw from, as a BCP-47 tag. The nearest locale the sample data ships
21
+ * answers it. Default: `en`.
22
+ */
23
+ locale?: string;
24
+ }
25
+ /**
26
+ * Builds one value of a series.
27
+ *
28
+ * @template Value - What the fixture produces.
29
+ * @param {Faker} source - Sample data, seeded for this position and drawn from the locale
30
+ * that was asked for.
31
+ * @param {number} index - The position in the series, counting from zero.
32
+ * @returns {Value} The value, before a caller's overrides go on top of it.
33
+ */
34
+ type Build<Value> = (source: Faker, index: number) => Value;
35
+ /**
36
+ * Makes one value of a fixture.
37
+ *
38
+ * @template Value - What the fixture produces.
39
+ * @param {Partial<Value>} [overrides] - The fields this caller states itself, which win over
40
+ * what the fixture built. Default: none.
41
+ * @param {At} [at] - Which value of the series to build, and which locale to draw it from.
42
+ * Default: the first, in `en`.
43
+ * @returns {Value} The value.
44
+ */
45
+ type Maker<Value> = (overrides?: Partial<Value>, at?: At) => Value;
46
+ /**
47
+ * Declares a fixture from the function that builds one value out of a seeded source and its
48
+ * position.
49
+ *
50
+ * @template Value - What the fixture produces.
51
+ * @param {Build<Value>} build - Builds one value.
52
+ * @returns {Maker<Value>} The maker, which takes overrides, a position and a locale.
53
+ */
54
+ declare function fixture<Value extends object>(build: Build<Value>): Maker<Value>;
55
+ /**
56
+ * Makes a series of values, each from its own position, so they differ from one another and
57
+ * every run produces the same series.
58
+ *
59
+ * @template Value - What the fixture produces.
60
+ * @param {Maker<Value>} make - The fixture to draw from.
61
+ * @param {number} count - How many to make.
62
+ * @param {Partial<Value>} [overrides] - The fields every value in the series carries.
63
+ * Default: none.
64
+ * @param {string} [locale] - The locale to draw them from, as a BCP-47 tag. Default: `en`.
65
+ * @returns {Value[]} The values, in order.
66
+ */
67
+ declare function many<Value extends object>(make: Maker<Value>, count: number, overrides?: Partial<Value>, locale?: string): Value[];
68
+ //#endregion
69
+ //#region src/locales.d.ts
70
+ /**
71
+ * Names the locale a caller gets without asking for one.
72
+ */
73
+ declare const DEFAULT_LOCALE = "en";
74
+ /**
75
+ * Lists every locale the sample data ships, as BCP-47 tags, sorted.
76
+ *
77
+ * A story that renders in each of them reads this rather than restating the list.
78
+ */
79
+ declare const LOCALES: readonly string[];
80
+ /**
81
+ * Finds the locale the sample data ships that is nearest the one asked for.
82
+ *
83
+ * The match is ECMA-402's lookup, so `nl-NL` reaches `nl` and `zh-Hans` reaches `zh-CN`. A
84
+ * tag nothing answers falls back to {@link DEFAULT_LOCALE} rather than throwing, because a
85
+ * fixture that refuses to build fails a specification about something else.
86
+ *
87
+ * @param {string} tag - The locale asked for, as a BCP-47 tag.
88
+ * @returns {string} The nearest locale the sample data ships.
89
+ */
90
+ declare function nearestLocale(tag: string): string;
91
+ /**
92
+ * Collects the definitions a locale is drawn from, nearest first.
93
+ *
94
+ * English and the language-independent base sit under every locale, so a field the locale
95
+ * does not define is filled in rather than throwing.
96
+ *
97
+ * @param {string} tag - The locale asked for, as a BCP-47 tag.
98
+ * @returns {LocaleDefinition[]} The definitions, the nearest match first.
99
+ */
100
+ declare function definitionsFor(tag: string): LocaleDefinition[];
101
+ //#endregion
102
+ export { type At, type Build, DEFAULT_LOCALE, LOCALES, type Maker, SEED, definitionsFor, fixture, many, nearestLocale };
package/dist/index.mjs ADDED
@@ -0,0 +1,153 @@
1
+ import { Faker, allLocales, base, en } from "@faker-js/faker";
2
+ import { canonical, negotiate } from "@stealthscale/core-locale";
3
+ //#region src/locales.ts
4
+ /**
5
+ * @fileoverview Picks the sample data a locale is drawn from. A caller names a BCP-47 tag,
6
+ * the same vocabulary the rest of the workspace uses, and gets the nearest locale the sample
7
+ * data actually ships.
8
+ */
9
+ /**
10
+ * Names the locale a caller gets without asking for one.
11
+ */
12
+ const DEFAULT_LOCALE = "en";
13
+ /**
14
+ * Maps the keys faker spells in its own way to the tag each stands for. Faker writes a script
15
+ * as a trailing word, `sr_RS_latin`, and a Kurdish variety as a subtag of `ku`; the tag is
16
+ * the ISO code with the script where BCP-47 puts it, so a request for the script reaches it.
17
+ * Kurmanji is `ku` here rather than `kmr`, because CLDR aliases the one to the other and
18
+ * Node's ICU applies the alias where Bun's does not.
19
+ */
20
+ const RESPELLED = {
21
+ ku_ckb: "ckb",
22
+ ku_kmr_latin: "ku-Latn",
23
+ mn_MN_cyrl: "mn-Cyrl-MN",
24
+ sr_RS_latin: "sr-Latn-RS",
25
+ uz_UZ_latin: "uz-Latn-UZ"
26
+ };
27
+ /**
28
+ * Lists the keys nothing should ask for: the language-independent base every locale sits on,
29
+ * and faker's two joke locales.
30
+ */
31
+ const DROPPED = /* @__PURE__ */ new Set([
32
+ "base",
33
+ "en_AU_ocker",
34
+ "en_BORK"
35
+ ]);
36
+ /**
37
+ * Reads the tag a faker key stands for.
38
+ *
39
+ * @param {string} key - The key as faker names it: `nl_BE`.
40
+ * @returns {string | undefined} The canonical tag, or `undefined` for a key nothing should
41
+ * ask for.
42
+ */
43
+ function tagOf(key) {
44
+ if (DROPPED.has(key)) return void 0;
45
+ return canonical(RESPELLED[key] ?? key.replaceAll("_", "-"));
46
+ }
47
+ /**
48
+ * Maps each locale the sample data ships to its definitions, keyed by canonical tag.
49
+ */
50
+ const SHIPPED = new Map(Object.entries(allLocales).map(([key, definition]) => [tagOf(key), definition]).filter((entry) => entry[0] !== void 0));
51
+ /**
52
+ * Lists every locale the sample data ships, as BCP-47 tags, sorted.
53
+ *
54
+ * A story that renders in each of them reads this rather than restating the list.
55
+ */
56
+ const LOCALES = [...SHIPPED.keys()].toSorted();
57
+ /**
58
+ * Finds the locale the sample data ships that is nearest the one asked for.
59
+ *
60
+ * The match is ECMA-402's lookup, so `nl-NL` reaches `nl` and `zh-Hans` reaches `zh-CN`. A
61
+ * tag nothing answers falls back to {@link DEFAULT_LOCALE} rather than throwing, because a
62
+ * fixture that refuses to build fails a specification about something else.
63
+ *
64
+ * @param {string} tag - The locale asked for, as a BCP-47 tag.
65
+ * @returns {string} The nearest locale the sample data ships.
66
+ */
67
+ function nearestLocale(tag) {
68
+ return negotiate([tag], LOCALES, "en");
69
+ }
70
+ /**
71
+ * Collects the definitions a locale is drawn from, nearest first.
72
+ *
73
+ * English and the language-independent base sit under every locale, so a field the locale
74
+ * does not define is filled in rather than throwing.
75
+ *
76
+ * @param {string} tag - The locale asked for, as a BCP-47 tag.
77
+ * @returns {LocaleDefinition[]} The definitions, the nearest match first.
78
+ */
79
+ function definitionsFor(tag) {
80
+ const nearest = SHIPPED.get(nearestLocale(tag));
81
+ return nearest === void 0 || nearest === en ? [en, base] : [
82
+ nearest,
83
+ en,
84
+ base
85
+ ];
86
+ }
87
+ //#endregion
88
+ //#region src/fixtures.ts
89
+ /**
90
+ * @fileoverview Builds the sample values a specification or a story shows. A fixture answers
91
+ * the same value on every run, so a snapshot, a visual diff and a failing assertion each
92
+ * mean what they say rather than reporting the weather.
93
+ */
94
+ /**
95
+ * Seeds every fixture.
96
+ *
97
+ * The number is arbitrary and fixed. What matters is that it never changes, because changing
98
+ * it rewrites every value every fixture in every repository has ever produced.
99
+ */
100
+ const SEED = 20260908;
101
+ /**
102
+ * Seeds a source for one position, drawn from one locale.
103
+ *
104
+ * The instance is fresh rather than shared and re-seeded. A fixture that builds another
105
+ * fixture would otherwise re-seed the source its caller is part-way through, and the outer
106
+ * value would change according to what it happened to contain.
107
+ *
108
+ * @param {number} index - The position in the series.
109
+ * @param {string} locale - The locale to draw from, as a BCP-47 tag.
110
+ * @returns {Faker} The source, seeded.
111
+ */
112
+ function sourceFor(index, locale) {
113
+ const source = new Faker({ locale: definitionsFor(locale) });
114
+ source.seed(SEED + index);
115
+ return source;
116
+ }
117
+ /**
118
+ * Declares a fixture from the function that builds one value out of a seeded source and its
119
+ * position.
120
+ *
121
+ * @template Value - What the fixture produces.
122
+ * @param {Build<Value>} build - Builds one value.
123
+ * @returns {Maker<Value>} The maker, which takes overrides, a position and a locale.
124
+ */
125
+ function fixture(build) {
126
+ return (overrides = {}, at = {}) => {
127
+ const index = at.index ?? 0;
128
+ return {
129
+ ...build(sourceFor(index, at.locale ?? "en"), index),
130
+ ...overrides
131
+ };
132
+ };
133
+ }
134
+ /**
135
+ * Makes a series of values, each from its own position, so they differ from one another and
136
+ * every run produces the same series.
137
+ *
138
+ * @template Value - What the fixture produces.
139
+ * @param {Maker<Value>} make - The fixture to draw from.
140
+ * @param {number} count - How many to make.
141
+ * @param {Partial<Value>} [overrides] - The fields every value in the series carries.
142
+ * Default: none.
143
+ * @param {string} [locale] - The locale to draw them from, as a BCP-47 tag. Default: `en`.
144
+ * @returns {Value[]} The values, in order.
145
+ */
146
+ function many(make, count, overrides = {}, locale = "en") {
147
+ return Array.from({ length: count }, (_, index) => make(overrides, {
148
+ index,
149
+ locale
150
+ }));
151
+ }
152
+ //#endregion
153
+ export { DEFAULT_LOCALE, LOCALES, SEED, definitionsFor, fixture, many, nearestLocale };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@stealthscale/tool-fixtures",
3
+ "version": "0.1.0",
4
+ "description": "Builds the sample values a specification or a story shows, the same on every run, drawn from any locale.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/stealth-scale/tooling.git",
9
+ "directory": "tools/fixtures"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "imports": {
17
+ "#*": "./src/*"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "tooling-source": "./src/index.ts",
22
+ "default": "./dist/index.mjs"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "publishConfig": {
27
+ "exports": {
28
+ ".": "./dist/index.mjs",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "vp pack src/index.ts"
35
+ },
36
+ "dependencies": {
37
+ "@faker-js/faker": "^10.6.0",
38
+ "@stealthscale/core-locale": "^0.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@stealthscale/tool-config": "^0.1.0"
42
+ }
43
+ }