@specbook/vite 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/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from
4
4
  import { createRequire } from "module";
5
5
  import { basename, dirname, join as join5, relative as relative4, resolve as resolve2, sep as sep4 } from "path";
6
6
 
7
- // ../core/src/faker.ts
7
+ // ../core/src/faker-runtime.ts
8
8
  var KEY = "__specbookFaker";
9
9
  var store = globalThis;
10
10
  var faker = new Proxy({}, {
@@ -18,6 +18,16 @@ var faker = new Proxy({}, {
18
18
  return current[key];
19
19
  }
20
20
  });
21
+ function fakerLocaleNames(config = {}, locale) {
22
+ const name = locale ?? config.default ?? "en";
23
+ return [.../* @__PURE__ */ new Set([...config.locales?.[name] ?? [name, "en"], "base"])];
24
+ }
25
+ function fakerLocaleExports(config = {}) {
26
+ const names = /* @__PURE__ */ new Set(["en", ...fakerLocaleNames(config)]);
27
+ for (const name of Object.keys(config.locales ?? {}))
28
+ for (const n of fakerLocaleNames(config, name)) names.add(n);
29
+ return [...names].sort();
30
+ }
21
31
 
22
32
  // ../core/src/template.ts
23
33
  var NAME = String.raw`[\w.$\[\]-]+`;
@@ -37,15 +47,25 @@ function isDeferred(name) {
37
47
  return DEFERRED_NAMESPACES.includes(ns) && name.includes(".");
38
48
  }
39
49
  var isFakerName = (name) => name.startsWith("faker.");
40
- function fromFaker(path) {
41
- let parent = faker;
42
- let cur = faker;
43
- for (const part of path.split(".").slice(1)) {
44
- parent = cur;
45
- cur = cur?.[part];
46
- if (cur === void 0) return { found: false };
50
+ var FakerPlaceholderError = class extends TemplateError {
51
+ constructor(variable, message) {
52
+ super(variable);
53
+ this.message = message;
54
+ this.name = "FakerPlaceholderError";
47
55
  }
48
- return { found: true, value: typeof cur === "function" ? cur.call(parent) : cur };
56
+ message;
57
+ };
58
+ function fromFaker(path) {
59
+ const [, group, method, ...rest] = path.split(".");
60
+ const unknown = () => new FakerPlaceholderError(
61
+ path,
62
+ `Unknown faker generator "{{${path}}}": use faker.<module>.<method>`
63
+ );
64
+ if (!group || !method || rest.length > 0) throw unknown();
65
+ const mod = faker[group];
66
+ const fn = typeof mod === "object" && mod !== null ? mod[method] : void 0;
67
+ if (typeof fn !== "function" || method === "constructor") throw unknown();
68
+ return { found: true, value: fn.call(mod) };
49
69
  }
50
70
  var steps = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
51
71
  function lookup(scope, path) {
@@ -60,7 +80,19 @@ function lookup(scope, path) {
60
80
  }
61
81
  return { found: true, value: cur };
62
82
  }
83
+ function rejectFakerCall(s) {
84
+ if (!s.includes("faker")) return;
85
+ const left = /\{\{\s*(faker\.[\s\S]*?)(?:\}\}|$)/.exec(s.replace(EMBED, ""));
86
+ if (left) {
87
+ const name = left[1].trim();
88
+ throw new FakerPlaceholderError(
89
+ name,
90
+ `"{{${name}}}" is not a faker placeholder: placeholders take no arguments. Use faker in a request file for those.`
91
+ );
92
+ }
93
+ }
63
94
  function renderString(s, scope) {
95
+ rejectFakerCall(s);
64
96
  const whole = WHOLE.exec(s);
65
97
  if (whole) {
66
98
  const name = whole[1];
@@ -776,7 +808,7 @@ async function scanExamples(root, outDir, operations) {
776
808
  list.sort((a, b) => order(a) - order(b) || a.name.localeCompare(b.name));
777
809
  return { examples, assets };
778
810
  }
779
- var order = (e) => e.source === "project" ? 0 : e.name === "default" ? 2 : e.name === "dynamic" ? 3 : 1;
811
+ var order = (e) => e.source === "project" ? 0 : e.name === "default" ? 2 : 1;
780
812
 
781
813
  // src/files.ts
782
814
  import { existsSync as existsSync2 } from "fs";
@@ -1081,7 +1113,7 @@ async function readBody2(req) {
1081
1113
  req.on("error", fail);
1082
1114
  });
1083
1115
  }
1084
- function fakerLocales(entry) {
1116
+ function fakerLocales(entry, cfg) {
1085
1117
  const names = /* @__PURE__ */ new Set(["en", "base"]);
1086
1118
  const dir = join5(dirname(entry), "locale");
1087
1119
  if (existsSync3(dir))
@@ -1089,8 +1121,11 @@ function fakerLocales(entry) {
1089
1121
  const m = /^([A-Za-z0-9_]+)\.js$/.exec(f);
1090
1122
  if (m) names.add(m[1]);
1091
1123
  }
1092
- return [...names].sort();
1124
+ if (!cfg) return [...names].sort();
1125
+ const wanted = new Set(fakerLocaleExports(cfg));
1126
+ return [...names].filter((n) => wanted.has(n)).sort();
1093
1127
  }
1128
+ var USES_FAKER = /@specbook\/core\/faker|\{\{\s*faker\./;
1094
1129
  function slim(manifests) {
1095
1130
  return Object.fromEntries(
1096
1131
  Object.entries(manifests).map(([k, m]) => [
@@ -1189,6 +1224,14 @@ function specbook(options) {
1189
1224
  fakerEntry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
1190
1225
  } catch {
1191
1226
  }
1227
+ if (!fakerEntry) {
1228
+ for (const f of [...files.requests, ...files.sequences]) {
1229
+ if (USES_FAKER.test(await readFile6(join5(root, f), "utf8").catch(() => "")))
1230
+ throw new Error(
1231
+ `${f} uses faker, but @faker-js/faker is not installed in this project. Add it to devDependencies (npm install --save-dev @faker-js/faker).`
1232
+ );
1233
+ }
1234
+ }
1192
1235
  const operationEntries = Object.values(manifests).flatMap((m) => {
1193
1236
  const mm = m;
1194
1237
  return mm.operations.map(
@@ -1560,7 +1603,10 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
1560
1603
  const r = await this.resolve("@faker-js/faker", join5(root, "package.json"), {
1561
1604
  skipSelf: true
1562
1605
  });
1563
- if (!r) return "export {}";
1606
+ if (!r)
1607
+ throw new Error(
1608
+ "@faker-js/faker could not be resolved from this project. Add it to devDependencies."
1609
+ );
1564
1610
  const pkg = JSON.stringify(r.id);
1565
1611
  if (id.startsWith(RESOLVED_FAKER_LOCALE))
1566
1612
  return `export { ${id.slice(RESOLVED_FAKER_LOCALE.length)} as default } from ${pkg}`;
@@ -1569,7 +1615,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
1569
1615
  entry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
1570
1616
  } catch {
1571
1617
  }
1572
- const names = fakerLocales(entry);
1618
+ const names = fakerLocales(entry, options.faker);
1573
1619
  return `export { Faker, base } from ${pkg}
1574
1620
  export const locales = {
1575
1621
  ${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIRTUAL_FAKER_LOCALE}${n}`)}).then((m) => m.default)`).join(",\n")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specbook/vite",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The Specbook plugin for Vite: the registry, the docs UI hooks and the dev server credential proxy.",
5
5
  "keywords": [
6
6
  "specbook",
@@ -34,8 +34,8 @@
34
34
  }
35
35
  },
36
36
  "dependencies": {
37
- "@specbook/codegen": "0.4.0",
38
- "@specbook/core": "0.4.0",
37
+ "@specbook/codegen": "0.5.0",
38
+ "@specbook/core": "0.5.0",
39
39
  "fast-glob": "3.3.2",
40
40
  "marked": "^18.1.1"
41
41
  },
package/src/examples.ts CHANGED
@@ -74,6 +74,5 @@ export async function scanExamples(
74
74
  return { examples, assets }
75
75
  }
76
76
 
77
- /** Spec and project examples first, then the generated default and dynamic ones. */
78
- const order = (e: ExampleEntry) =>
79
- e.source === 'project' ? 0 : e.name === 'default' ? 2 : e.name === 'dynamic' ? 3 : 1
77
+ /** Project examples first, then the spec's, then the generated default. */
78
+ const order = (e: ExampleEntry) => (e.source === 'project' ? 0 : e.name === 'default' ? 2 : 1)
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
3
3
  import type { IncomingMessage, ServerResponse } from 'node:http'
4
4
  import { createRequire } from 'node:module'
5
5
  import { basename, dirname, join, relative, resolve, sep } from 'node:path'
6
- import { environmentNames } from '@specbook/core'
6
+ import { environmentNames, type FakerConfig, fakerLocaleExports } from '@specbook/core'
7
7
  import fg from 'fast-glob'
8
8
  import type { Plugin, ViteDevServer } from 'vite'
9
9
  import { type AddressConfig, addressesFor, NO_ADDRESSES } from './addresses'
@@ -44,6 +44,11 @@ export interface SpecbookPluginOptions {
44
44
  * reveals the rest of the API.
45
45
  */
46
46
  operations?: Set<string>
47
+ /**
48
+ * The project's `faker` config. A build ships only the locales it can reach (its chains, plus `en` and `base`);
49
+ * without it every locale faker has is available.
50
+ */
51
+ faker?: FakerConfig
47
52
  }
48
53
 
49
54
  const VIRTUAL = 'virtual:specbook/registry'
@@ -85,7 +90,7 @@ async function readBody(req: IncomingMessage): Promise<string> {
85
90
  }
86
91
 
87
92
  /** Locale names a faker install ships (`de`, `en_US`, ...), from its `locale/` folder next to the entry. */
88
- function fakerLocales(entry: string): string[] {
93
+ function fakerLocales(entry: string, cfg?: FakerConfig): string[] {
89
94
  const names = new Set<string>(['en', 'base'])
90
95
  const dir = join(dirname(entry), 'locale')
91
96
  if (existsSync(dir))
@@ -93,9 +98,14 @@ function fakerLocales(entry: string): string[] {
93
98
  const m = /^([A-Za-z0-9_]+)\.js$/.exec(f)
94
99
  if (m) names.add(m[1] as string)
95
100
  }
96
- return [...names].sort()
101
+ if (!cfg) return [...names].sort()
102
+ const wanted = new Set(fakerLocaleExports(cfg))
103
+ return [...names].filter((n) => wanted.has(n)).sort()
97
104
  }
98
105
 
106
+ /** Whether a request or flow file uses faker: it imports `@specbook/core/faker` or has a `{{faker.` placeholder. */
107
+ const USES_FAKER = /@specbook\/core\/faker|\{\{\s*faker\./
108
+
99
109
  /** What the sidebar and palette need; descriptions and example titles load per operation (`operationDocs`). */
100
110
  function slim(manifests: Record<string, any>) {
101
111
  return Object.fromEntries(
@@ -223,7 +233,15 @@ export function specbook(options: SpecbookPluginOptions): Plugin {
223
233
  try {
224
234
  fakerEntry = createRequire(join(root, 'package.json')).resolve('@faker-js/faker')
225
235
  } catch {
226
- /* optional peer dependency */
236
+ /* not installed: fine unless a file uses it, checked below */
237
+ }
238
+ if (!fakerEntry) {
239
+ for (const f of [...files.requests, ...files.sequences]) {
240
+ if (USES_FAKER.test(await readFile(join(root, f), 'utf8').catch(() => '')))
241
+ throw new Error(
242
+ `${f} uses faker, but @faker-js/faker is not installed in this project. Add it to devDependencies (npm install --save-dev @faker-js/faker).`,
243
+ )
244
+ }
227
245
  }
228
246
  const operationEntries = Object.values(manifests).flatMap((m) => {
229
247
  const mm = m as { service: string; operations: { id: string }[] }
@@ -604,7 +622,10 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
604
622
  const r = await this.resolve('@faker-js/faker', join(root, 'package.json'), {
605
623
  skipSelf: true,
606
624
  })
607
- if (!r) return 'export {}'
625
+ if (!r)
626
+ throw new Error(
627
+ '@faker-js/faker could not be resolved from this project. Add it to devDependencies.',
628
+ )
608
629
  const pkg = JSON.stringify(r.id)
609
630
  if (id.startsWith(RESOLVED_FAKER_LOCALE))
610
631
  return `export { ${id.slice(RESOLVED_FAKER_LOCALE.length)} as default } from ${pkg}`
@@ -617,7 +638,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
617
638
  } catch {
618
639
  /* fall back to the resolved id */
619
640
  }
620
- const names = fakerLocales(entry)
641
+ const names = fakerLocales(entry, options.faker)
621
642
  return `export { Faker, base } from ${pkg}
622
643
  export const locales = {
623
644
  ${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIRTUAL_FAKER_LOCALE}${n}`)}).then((m) => m.default)`).join(',\n')}