@specbook/core 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specbook/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The runner, mock engine, template engine and shared proxy behind Specbook: Storybook for APIs.",
5
5
  "keywords": [
6
6
  "specbook",
@@ -49,6 +49,17 @@
49
49
  "dependencies": {
50
50
  "uvu": "0.5.6"
51
51
  },
52
+ "peerDependencies": {
53
+ "@faker-js/faker": "^9.3.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@faker-js/faker": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@faker-js/faker": "9.3.0"
62
+ },
52
63
  "files": [
53
64
  "src",
54
65
  "!**/*.test.*"
package/src/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FakerConfig } from './faker'
1
+ import type { FakerConfig } from './faker-runtime'
2
2
  import type { AuthProfile } from './runner/run'
3
3
  import type { SecretProvider, SecretRef } from './secrets'
4
4
 
@@ -0,0 +1,88 @@
1
+ // Locale-aware faker proxy. `@faker-js/faker` is a peer dependency of the user's project;
2
+ // the runtime (CLI / UI) installs the concrete instance for the selected locale before a run.
3
+
4
+ type FakerLike = Record<string, any>
5
+
6
+ // Held on globalThis so the CLI's bundled copy of core and the project's own import share one instance.
7
+ const KEY = '__specbookFaker'
8
+ const store = globalThis as unknown as Record<string, FakerLike | undefined>
9
+
10
+ export const faker: FakerLike = new Proxy({} as FakerLike, {
11
+ get(_t, key) {
12
+ const current = store[KEY]
13
+ if (!current) {
14
+ throw new Error(
15
+ 'faker is not initialised. Install "@faker-js/faker" in your project; specbook sets it up before each run.',
16
+ )
17
+ }
18
+ return current[key as string]
19
+ },
20
+ })
21
+
22
+ export function setFaker(instance: FakerLike | undefined): void {
23
+ store[KEY] = instance
24
+ }
25
+
26
+ export interface FakerConfig {
27
+ /** locale name -> fallback chain of faker locale export names, e.g. { de: ['de', 'en', 'base'] } */
28
+ locales?: Record<string, string[]>
29
+ default?: string
30
+ }
31
+
32
+ interface FakerModule {
33
+ Faker: new (opts: { locale: unknown[] }) => FakerLike
34
+ [locale: string]: unknown
35
+ }
36
+
37
+ /** The locale exports a run needs, in fallback order: the chain for `locale`, then `base`. */
38
+ export function fakerLocaleNames(config: FakerConfig = {}, locale?: string): string[] {
39
+ const name = locale ?? config.default ?? 'en'
40
+ return [...new Set([...(config.locales?.[name] ?? [name, 'en']), 'base'])]
41
+ }
42
+
43
+ /** Every locale export a project can reach: the default's chain and each configured locale's, plus `en` and `base`. */
44
+ export function fakerLocaleExports(config: FakerConfig = {}): string[] {
45
+ const names = new Set<string>(['en', ...fakerLocaleNames(config)])
46
+ for (const name of Object.keys(config.locales ?? {}))
47
+ for (const n of fakerLocaleNames(config, name)) names.add(n)
48
+ return [...names].sort()
49
+ }
50
+
51
+ /**
52
+ * The module `installFaker` needs. A bundled build offers each locale as `locales[name]()` so only
53
+ * the ones a run falls back through are fetched; a plain faker module already has them all.
54
+ */
55
+ export async function prepareFaker(
56
+ mod: Record<string, any>,
57
+ config: FakerConfig = {},
58
+ locale?: string,
59
+ ): Promise<FakerModule> {
60
+ const locales = mod.locales as Record<string, () => Promise<unknown>> | undefined
61
+ if (!locales) return mod as FakerModule
62
+ const ready: Record<string, unknown> = { Faker: mod.Faker }
63
+ await Promise.all(
64
+ fakerLocaleNames(config, locale).map(async (n) => {
65
+ const load = locales[n]
66
+ if (load) ready[n] = await load()
67
+ }),
68
+ )
69
+ return ready as FakerModule
70
+ }
71
+
72
+ /** Build a Faker for `locale` from the (dynamically imported) module, install it, and optionally seed it. */
73
+ export function installFaker(
74
+ mod: FakerModule,
75
+ config: FakerConfig = {},
76
+ opts: { locale?: string; seed?: number } = {},
77
+ ): FakerLike {
78
+ const name = opts.locale ?? config.default ?? 'en'
79
+ const locales = fakerLocaleNames(config, name).map((n) => {
80
+ const l = mod[n]
81
+ if (!l) throw new Error(`Unknown faker locale "${n}" (from "${name}")`)
82
+ return l
83
+ })
84
+ const instance = new mod.Faker({ locale: locales })
85
+ if (opts.seed !== undefined) instance.seed(opts.seed)
86
+ setFaker(instance)
87
+ return instance
88
+ }
package/src/faker.ts CHANGED
@@ -1,80 +1,8 @@
1
- // Locale-aware faker proxy. `@faker-js/faker` is a peer dependency of the user's project;
2
- // the runtime (CLI / UI) installs the concrete instance for the selected locale before a run.
1
+ // The `faker` request and flow files import (`@specbook/core/faker`), typed as faker's own `Faker` so a typo or a
2
+ // wrong argument fails `tsc` and the editor completes. `@faker-js/faker` is the project's dependency (`specbook init` adds it);
3
+ // the runtime in ./faker-runtime installs the real instance before each run.
3
4
 
4
- type FakerLike = Record<string, any>
5
+ import type { Faker } from '@faker-js/faker'
6
+ import { faker as runtime } from './faker-runtime'
5
7
 
6
- // Held on globalThis so the CLI's bundled copy of core and the project's own import share one instance.
7
- const KEY = '__specbookFaker'
8
- const store = globalThis as unknown as Record<string, FakerLike | undefined>
9
-
10
- export const faker: FakerLike = new Proxy({} as FakerLike, {
11
- get(_t, key) {
12
- const current = store[KEY]
13
- if (!current) {
14
- throw new Error(
15
- 'faker is not initialised. Install "@faker-js/faker" in your project; specbook sets it up before each run.',
16
- )
17
- }
18
- return current[key as string]
19
- },
20
- })
21
-
22
- export function setFaker(instance: FakerLike | undefined): void {
23
- store[KEY] = instance
24
- }
25
-
26
- export interface FakerConfig {
27
- /** locale name -> fallback chain of faker locale export names, e.g. { de: ['de', 'en', 'base'] } */
28
- locales?: Record<string, string[]>
29
- default?: string
30
- }
31
-
32
- interface FakerModule {
33
- Faker: new (opts: { locale: unknown[] }) => FakerLike
34
- [locale: string]: unknown
35
- }
36
-
37
- /** The locale exports a run needs, in fallback order: the chain for `locale`, then `base`. */
38
- export function fakerLocaleNames(config: FakerConfig = {}, locale?: string): string[] {
39
- const name = locale ?? config.default ?? 'en'
40
- return [...new Set([...(config.locales?.[name] ?? [name, 'en']), 'base'])]
41
- }
42
-
43
- /**
44
- * The module `installFaker` needs. A bundled build offers each locale as `locales[name]()` so only
45
- * the ones a run falls back through are fetched; a plain faker module already has them all.
46
- */
47
- export async function prepareFaker(
48
- mod: Record<string, any>,
49
- config: FakerConfig = {},
50
- locale?: string,
51
- ): Promise<FakerModule> {
52
- const locales = mod.locales as Record<string, () => Promise<unknown>> | undefined
53
- if (!locales) return mod as FakerModule
54
- const ready: Record<string, unknown> = { Faker: mod.Faker }
55
- await Promise.all(
56
- fakerLocaleNames(config, locale).map(async (n) => {
57
- const load = locales[n]
58
- if (load) ready[n] = await load()
59
- }),
60
- )
61
- return ready as FakerModule
62
- }
63
-
64
- /** Build a Faker for `locale` from the (dynamically imported) module, install it, and optionally seed it. */
65
- export function installFaker(
66
- mod: FakerModule,
67
- config: FakerConfig = {},
68
- opts: { locale?: string; seed?: number } = {},
69
- ): FakerLike {
70
- const name = opts.locale ?? config.default ?? 'en'
71
- const locales = fakerLocaleNames(config, name).map((n) => {
72
- const l = mod[n]
73
- if (!l) throw new Error(`Unknown faker locale "${n}" (from "${name}")`)
74
- return l
75
- })
76
- const instance = new mod.Faker({ locale: locales })
77
- if (opts.seed !== undefined) instance.seed(opts.seed)
78
- setFaker(instance)
79
- return instance
80
- }
8
+ export const faker = runtime as unknown as Faker
package/src/index.ts CHANGED
@@ -5,8 +5,15 @@ export * from './diff'
5
5
  export * from './environment'
6
6
  export type { Example, ExampleLoader, FileReader, Row } from './example'
7
7
  export { fromFormRows, fromRows, toFormRows, toRows } from './example'
8
- export type { FakerConfig } from './faker'
9
- export { faker, fakerLocaleNames, installFaker, prepareFaker, setFaker } from './faker'
8
+ export type { FakerConfig } from './faker-runtime'
9
+ export {
10
+ faker,
11
+ fakerLocaleExports,
12
+ fakerLocaleNames,
13
+ installFaker,
14
+ prepareFaker,
15
+ setFaker,
16
+ } from './faker-runtime'
10
17
  export * from './mock'
11
18
  export * from './oauth'
12
19
  export * from './proxy/index'
@@ -31,6 +38,7 @@ export {
31
38
  export {
32
39
  collectPlaceholders,
33
40
  deepMerge,
41
+ FakerPlaceholderError,
34
42
  render,
35
43
  renderString,
36
44
  rootName,
package/src/template.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Template engine: `{{ name }}` placeholders, variable lookup only (no expressions).
2
2
 
3
- import { faker } from './faker'
3
+ import { faker } from './faker-runtime'
4
4
 
5
5
  // a name is words joined by dots, and a position in an array can be a dot (`tags.0.name`) or brackets (`tags[0].name`)
6
6
  const NAME = String.raw`[\w.$\[\]-]+`
@@ -27,16 +27,30 @@ export function isDeferred(name: string): boolean {
27
27
 
28
28
  export const isFakerName = (name: string) => name.startsWith('faker.')
29
29
 
30
+ /** A `{{faker.…}}` that names no generator, or is not written as a bare `faker.<module>.<method>`. */
31
+ export class FakerPlaceholderError extends TemplateError {
32
+ constructor(
33
+ variable: string,
34
+ override readonly message: string,
35
+ ) {
36
+ super(variable)
37
+ this.name = 'FakerPlaceholderError'
38
+ }
39
+ }
40
+
30
41
  /** `faker.person.firstName` calls the generator on the active faker instance, fresh on every render. */
31
42
  function fromFaker(path: string): { found: boolean; value?: unknown } {
32
- let parent: any = faker
33
- let cur: any = faker
34
- for (const part of path.split('.').slice(1)) {
35
- parent = cur
36
- cur = cur?.[part]
37
- if (cur === undefined) return { found: false }
38
- }
39
- return { found: true, value: typeof cur === 'function' ? cur.call(parent) : cur }
43
+ const [, group, method, ...rest] = path.split('.')
44
+ const unknown = () =>
45
+ new FakerPlaceholderError(
46
+ path,
47
+ `Unknown faker generator "{{${path}}}": use faker.<module>.<method>`,
48
+ )
49
+ if (!group || !method || rest.length > 0) throw unknown()
50
+ const mod = (faker as Record<string, any>)[group]
51
+ const fn = typeof mod === 'object' && mod !== null ? mod[method] : undefined
52
+ if (typeof fn !== 'function' || method === 'constructor') throw unknown()
53
+ return { found: true, value: fn.call(mod) }
40
54
  }
41
55
 
42
56
  /** True when any placeholder in `value` needs the faker instance. */
@@ -61,7 +75,21 @@ function lookup(scope: Record<string, unknown>, path: string): { found: boolean;
61
75
  return { found: true, value: cur }
62
76
  }
63
77
 
78
+ /** `{{faker.number.int(5)}}` does not read as a placeholder: refuse it rather than send it as it is. */
79
+ function rejectFakerCall(s: string): void {
80
+ if (!s.includes('faker')) return
81
+ const left = /\{\{\s*(faker\.[\s\S]*?)(?:\}\}|$)/.exec(s.replace(EMBED, ''))
82
+ if (left) {
83
+ const name = (left[1] as string).trim()
84
+ throw new FakerPlaceholderError(
85
+ name,
86
+ `"{{${name}}}" is not a faker placeholder: placeholders take no arguments. Use faker in a request file for those.`,
87
+ )
88
+ }
89
+ }
90
+
64
91
  export function renderString(s: string, scope: Record<string, unknown>): unknown {
92
+ rejectFakerCall(s)
65
93
  const whole = WHOLE.exec(s)
66
94
  if (whole) {
67
95
  const name = whole[1] as string