@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 +57 -0
- package/dist/index.d.mts +102 -0
- package/dist/index.mjs +153 -0
- package/package.json +43 -0
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
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|