@specbook/vite 0.3.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,10 +18,21 @@ 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
- var WHOLE = /^\s*\{\{\s*([\w.$-]+)\s*\}\}\s*$/;
24
- var EMBED = /\{\{\s*([\w.$-]+)\s*\}\}/g;
33
+ var NAME = String.raw`[\w.$\[\]-]+`;
34
+ var WHOLE = new RegExp(String.raw`^\s*\{\{\s*(${NAME})\s*\}\}\s*$`);
35
+ var EMBED = new RegExp(String.raw`\{\{\s*(${NAME})\s*\}\}`, "g");
25
36
  var DEFERRED_NAMESPACES = ["secrets", "session"];
26
37
  var TemplateError = class extends Error {
27
38
  constructor(variable) {
@@ -36,21 +47,32 @@ function isDeferred(name) {
36
47
  return DEFERRED_NAMESPACES.includes(ns) && name.includes(".");
37
48
  }
38
49
  var isFakerName = (name) => name.startsWith("faker.");
39
- function fromFaker(path) {
40
- let parent = faker;
41
- let cur = faker;
42
- for (const part of path.split(".").slice(1)) {
43
- parent = cur;
44
- cur = cur?.[part];
45
- 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";
46
55
  }
47
- 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) };
48
69
  }
70
+ var steps = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
49
71
  function lookup(scope, path) {
50
72
  if (isFakerName(path)) return fromFaker(path);
51
73
  let cur = scope;
52
- for (const part of path.split(".")) {
53
- if (cur !== null && typeof cur === "object" && part in cur) {
74
+ for (const part of steps(path)) {
75
+ if (cur !== null && typeof cur === "object" && Object.hasOwn(cur, part)) {
54
76
  cur = cur[part];
55
77
  } else {
56
78
  return { found: false };
@@ -58,7 +80,19 @@ function lookup(scope, path) {
58
80
  }
59
81
  return { found: true, value: cur };
60
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
+ }
61
94
  function renderString(s, scope) {
95
+ rejectFakerCall(s);
62
96
  const whole = WHOLE.exec(s);
63
97
  if (whole) {
64
98
  const name = whole[1];
@@ -727,7 +761,7 @@ async function writeEnvKey(file, key, value) {
727
761
  import { relative as relative2, sep as sep2 } from "path";
728
762
  import fg2 from "fast-glob";
729
763
  var IGNORE = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
730
- var NAME = /\.example\.json$/;
764
+ var NAME2 = /\.example\.json$/;
731
765
  async function scanExamples(root, outDir, operations) {
732
766
  const examples = {};
733
767
  const assets = {};
@@ -745,7 +779,7 @@ async function scanExamples(root, outDir, operations) {
745
779
  const [svc, , id, file] = relative2(gen, abs).split(sep2);
746
780
  if (!operations.has(`${svc}/${id}`)) continue;
747
781
  add(`${svc}/${id}`, {
748
- name: file.replace(NAME, ""),
782
+ name: file.replace(NAME2, ""),
749
783
  abs,
750
784
  source: "generated",
751
785
  dir: `${svc}/${id}@generated`
@@ -759,7 +793,7 @@ async function scanExamples(root, outDir, operations) {
759
793
  const key = `${svc}/${id}`;
760
794
  if (!svc || !id || !operations.has(key)) continue;
761
795
  add(key, {
762
- name: file.replace(NAME, ""),
796
+ name: file.replace(NAME2, ""),
763
797
  abs,
764
798
  source: "project",
765
799
  dir: `${svc}/${id}@project`
@@ -774,7 +808,7 @@ async function scanExamples(root, outDir, operations) {
774
808
  list.sort((a, b) => order(a) - order(b) || a.name.localeCompare(b.name));
775
809
  return { examples, assets };
776
810
  }
777
- 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;
778
812
 
779
813
  // src/files.ts
780
814
  import { existsSync as existsSync2 } from "fs";
@@ -1079,7 +1113,7 @@ async function readBody2(req) {
1079
1113
  req.on("error", fail);
1080
1114
  });
1081
1115
  }
1082
- function fakerLocales(entry) {
1116
+ function fakerLocales(entry, cfg) {
1083
1117
  const names = /* @__PURE__ */ new Set(["en", "base"]);
1084
1118
  const dir = join5(dirname(entry), "locale");
1085
1119
  if (existsSync3(dir))
@@ -1087,8 +1121,11 @@ function fakerLocales(entry) {
1087
1121
  const m = /^([A-Za-z0-9_]+)\.js$/.exec(f);
1088
1122
  if (m) names.add(m[1]);
1089
1123
  }
1090
- 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();
1091
1127
  }
1128
+ var USES_FAKER = /@specbook\/core\/faker|\{\{\s*faker\./;
1092
1129
  function slim(manifests) {
1093
1130
  return Object.fromEntries(
1094
1131
  Object.entries(manifests).map(([k, m]) => [
@@ -1187,6 +1224,14 @@ function specbook(options) {
1187
1224
  fakerEntry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
1188
1225
  } catch {
1189
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
+ }
1190
1235
  const operationEntries = Object.values(manifests).flatMap((m) => {
1191
1236
  const mm = m;
1192
1237
  return mm.operations.map(
@@ -1558,7 +1603,10 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
1558
1603
  const r = await this.resolve("@faker-js/faker", join5(root, "package.json"), {
1559
1604
  skipSelf: true
1560
1605
  });
1561
- 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
+ );
1562
1610
  const pkg = JSON.stringify(r.id);
1563
1611
  if (id.startsWith(RESOLVED_FAKER_LOCALE))
1564
1612
  return `export { ${id.slice(RESOLVED_FAKER_LOCALE.length)} as default } from ${pkg}`;
@@ -1567,7 +1615,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
1567
1615
  entry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
1568
1616
  } catch {
1569
1617
  }
1570
- const names = fakerLocales(entry);
1618
+ const names = fakerLocales(entry, options.faker);
1571
1619
  return `export { Faker, base } from ${pkg}
1572
1620
  export const locales = {
1573
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.3.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.3.0",
38
- "@specbook/core": "0.3.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')}