eptaadmin-sdk 0.1.4 → 0.1.6

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 CHANGED
@@ -83,6 +83,105 @@ const avatars = await client.getValue("acme/clients/avatar");
83
83
  // ["https://your-eptaadmin-instance.example.com/api/v1/workspaces/acme/uploads/6f2dff985af5d290.png"]
84
84
  ```
85
85
 
86
+ ## Build-time prefetch (for static/SPA builds)
87
+
88
+ If you're shipping a static single-page app, you usually don't want the production bundle calling out to your EptaAdmin instance at runtime — that means exposing your API key client-side, an extra network round-trip, and a hard runtime dependency on EptaAdmin staying up. There are two ways to prefetch data at **build time** instead; pick based on your bundler.
89
+
90
+ ### Option A — Vite plugin (recommended for Vite): identical code in dev and prod
91
+
92
+ Your app code always calls `client.getDataSource(...)` / `client.getValue(...)` — never anything special-cased per environment, and **you don't list what to prefetch**: the plugin scans your source code for those calls (literal-string arguments only) and prefetches exactly what it finds. The plugin makes those calls resolve from build-time-fetched data with zero network request in the production bundle, while `vite` (the dev server) leaves them as real, live requests. Same code, same config, either way.
93
+
94
+ ```js
95
+ // vite.config.js
96
+ import { eptaadminPrefetch } from "eptaadmin-sdk/vite";
97
+
98
+ export default {
99
+ plugins: [
100
+ eptaadminPrefetch({
101
+ baseUrl: "https://admin.example.com",
102
+ apiKeyEnv: "EPTAADMIN_API_KEY", // env var read only at build time, never bundled
103
+ // scanDir: "src", // defaults to "src" — where to look for SDK calls
104
+ }),
105
+ ],
106
+ };
107
+ ```
108
+
109
+ ```js
110
+ // anywhere in your app — identical in dev and prod, nothing to register anywhere
111
+ import { EptaAdminClient } from "eptaadmin-sdk";
112
+
113
+ const client = new EptaAdminClient({
114
+ // In dev this needs a real key (e.g. from import.meta.env.VITE_EPTAADMIN_API_KEY)
115
+ // so the live fallback request can authenticate. In a prod build that only
116
+ // reads prefetched sources, this can be left undefined entirely — the
117
+ // apiKey is never read unless a call actually misses the prefetch cache.
118
+ apiKey: import.meta.env.VITE_EPTAADMIN_API_KEY,
119
+ baseUrl: "https://admin.example.com",
120
+ });
121
+
122
+ const home = await client.getDataSource("acme", "home");
123
+ const title = await client.getValue("acme/home/hero_title/0");
124
+ ```
125
+
126
+ Run `EPTAADMIN_API_KEY=eak_your_key npm run build` — the plugin finds both calls above by scanning `src/`, fetches `acme/home` once, and bakes the result into the bundle, throwing (failing the build) if the key is missing or a fetch fails. `vite` / `vite dev` are untouched, so nothing changes about your normal dev workflow.
127
+
128
+ Scanning only sees **literal string arguments** — `getDataSource("acme", "home")`, not `getDataSource(ws, ds)` with variables. This is an inherent limit of static analysis, not something a smarter regex can fix — a scanner can't know a variable's value without running the code. Rather than silently prefetching nothing for those calls, the plugin **flags every one it finds but can't resolve**, with the exact file and line, at build time:
129
+
130
+ ```
131
+ [eptaadmin-prefetch] src/pages/Home.jsx:42 — getDataSource(ws, ds) has non-literal arguments, can't be scanned; add it via "sources" if it should be prefetched.
132
+ ```
133
+
134
+ Add exactly the pairs it points out via `sources`:
135
+
136
+ ```js
137
+ eptaadminPrefetch({
138
+ baseUrl: "https://admin.example.com",
139
+ sources: [{ workspace: "acme", dataSource: "home" }], // merged with whatever scanning finds
140
+ });
141
+ ```
142
+
143
+ ### Option B — CLI (any bundler): static JSON file you import yourself
144
+
145
+ If you're not on Vite, or you'd rather have an explicit static JSON file, the `eptaadmin-prefetch` CLI (installed alongside the SDK) does the fetching and writes plain JSON files instead of hooking into a bundler.
146
+
147
+ Add a config file (`eptaadmin.config.json`, resolved from your current working directory):
148
+
149
+ ```json
150
+ {
151
+ "baseUrl": "https://admin.example.com",
152
+ "apiKeyEnv": "EPTAADMIN_API_KEY",
153
+ "sources": [
154
+ { "workspace": "acme", "dataSource": "home", "out": "src/data/home.json" }
155
+ ]
156
+ }
157
+ ```
158
+
159
+ Run it before your build, e.g. as a `prebuild` script in `package.json`:
160
+
161
+ ```json
162
+ {
163
+ "scripts": {
164
+ "prebuild": "eptaadmin-prefetch",
165
+ "build": "vite build"
166
+ }
167
+ }
168
+ ```
169
+
170
+ ```sh
171
+ EPTAADMIN_API_KEY=eak_your_key npm run build
172
+ ```
173
+
174
+ Each output file matches `getDataSource()`'s `columns` shape exactly:
175
+
176
+ ```js
177
+ import homeData from "./data/home.json";
178
+ // homeData.hero_title[0], homeData.testimonial_author, ...
179
+ ```
180
+
181
+ Unlike Option A, this means writing a plain `import` instead of an `EptaAdminClient` call, so dev and prod aren't using identical code — the tradeoff for working with any build tool, not just Vite.
182
+
183
+ Both options exit non-zero (Option A: the build fails outright; Option B: the CLI process fails) if a source can't be fetched or the API key is missing, so a broken EptaAdmin connection fails your build loudly instead of silently shipping stale or missing data.
184
+
86
185
  ## Error handling
87
186
 
88
187
  Failed requests reject with an `EptaAdminError` carrying the HTTP status and the server's error message:
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * eptaadmin-prefetch — fetches EptaAdmin data sources at BUILD time and
4
+ * writes them to static JSON files, so a production SPA build never calls
5
+ * out to your EptaAdmin instance at runtime. Import the generated file(s)
6
+ * like any other static asset instead of calling EptaAdminClient in the
7
+ * shipped bundle.
8
+ *
9
+ * Usage:
10
+ * npx eptaadmin-prefetch [--config eptaadmin.config.json]
11
+ *
12
+ * Typical setup: add a "prebuild" script to package.json —
13
+ * "prebuild": "eptaadmin-prefetch"
14
+ * — so it always runs right before "build".
15
+ *
16
+ * Config file (JSON), resolved relative to the current working directory:
17
+ * {
18
+ * "baseUrl": "https://admin.example.com",
19
+ * "apiKeyEnv": "EPTAADMIN_API_KEY", // env var holding the API key — never put the key itself in this file
20
+ * "sources": [
21
+ * { "workspace": "acme", "dataSource": "home", "out": "src/data/home.json" }
22
+ * ]
23
+ * }
24
+ *
25
+ * Each output file is the same shape as EptaAdminClient#getDataSource()'s
26
+ * "columns" object: { "columnKey": [value, ...] }.
27
+ */
28
+
29
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
30
+ import { dirname, resolve } from "node:path";
31
+ import { EptaAdminClient } from "../src/index.js";
32
+
33
+ function parseArgs(argv) {
34
+ const args = { config: "eptaadmin.config.json" };
35
+ for (let i = 0; i < argv.length; i++) {
36
+ if (argv[i] === "--config" && argv[i + 1]) {
37
+ args.config = argv[++i];
38
+ }
39
+ }
40
+ return args;
41
+ }
42
+
43
+ async function loadConfig(path) {
44
+ let raw;
45
+ try {
46
+ raw = await readFile(path, "utf8");
47
+ } catch (err) {
48
+ throw new Error(`could not read config file "${path}": ${err.message}`);
49
+ }
50
+ let config;
51
+ try {
52
+ config = JSON.parse(raw);
53
+ } catch (err) {
54
+ throw new Error(`config file "${path}" is not valid JSON: ${err.message}`);
55
+ }
56
+ if (!config.baseUrl) throw new Error(`config is missing "baseUrl"`);
57
+ if (!Array.isArray(config.sources) || config.sources.length === 0) {
58
+ throw new Error(`config must list at least one entry under "sources"`);
59
+ }
60
+ return config;
61
+ }
62
+
63
+ async function main() {
64
+ const { config: configPath } = parseArgs(process.argv.slice(2));
65
+ const config = await loadConfig(configPath);
66
+
67
+ const apiKeyEnv = config.apiKeyEnv || "EPTAADMIN_API_KEY";
68
+ const apiKey = process.env[apiKeyEnv];
69
+ if (!apiKey) {
70
+ throw new Error(`environment variable "${apiKeyEnv}" is not set (or empty) — it must hold a personal API key from your EptaAdmin profile page`);
71
+ }
72
+
73
+ const client = new EptaAdminClient({ apiKey, baseUrl: config.baseUrl });
74
+
75
+ let failures = 0;
76
+ for (const source of config.sources) {
77
+ const { workspace, dataSource, out } = source;
78
+ if (!workspace || !dataSource || !out) {
79
+ console.error(`✗ skipping invalid source entry (needs "workspace", "dataSource" and "out"): ${JSON.stringify(source)}`);
80
+ failures++;
81
+ continue;
82
+ }
83
+ try {
84
+ const data = await client.getDataSource(workspace, dataSource);
85
+ const outPath = resolve(process.cwd(), out);
86
+ await mkdir(dirname(outPath), { recursive: true });
87
+ await writeFile(outPath, JSON.stringify(data.columns, null, 2) + "\n", "utf8");
88
+ console.log(`✓ ${workspace}/${dataSource} → ${out}`);
89
+ } catch (err) {
90
+ console.error(`✗ ${workspace}/${dataSource}: ${err.message}`);
91
+ failures++;
92
+ }
93
+ }
94
+
95
+ if (failures > 0) {
96
+ console.error(`\neptaadmin-prefetch: ${failures} source(s) failed — failing the build rather than shipping stale or missing data.`);
97
+ process.exit(1);
98
+ }
99
+ }
100
+
101
+ main().catch((err) => {
102
+ console.error(`eptaadmin-prefetch: ${err.message}`);
103
+ process.exit(1);
104
+ });
package/index.d.ts CHANGED
@@ -22,8 +22,13 @@ export declare class EptaAdminError extends Error {
22
22
  }
23
23
 
24
24
  export interface EptaAdminClientOptions {
25
- /** A personal API key generated from the EptaAdmin profile page. */
26
- apiKey: string;
25
+ /**
26
+ * A personal API key generated from the EptaAdmin profile page. Only
27
+ * required for calls that actually reach the network — a call fully
28
+ * served from build-time-prefetched data (see eptaadmin-sdk/vite) never
29
+ * needs one.
30
+ */
31
+ apiKey?: string;
27
32
  /** The URL of your EptaAdmin instance. Defaults to http://localhost:8080. */
28
33
  baseUrl?: string;
29
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eptaadmin-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Client SDK for reading your EptaAdmin workspace data from your own project.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -9,13 +9,31 @@
9
9
  ".": {
10
10
  "types": "./index.d.ts",
11
11
  "import": "./src/index.js"
12
+ },
13
+ "./vite": {
14
+ "types": "./vite-plugin.d.ts",
15
+ "import": "./vite-plugin.js"
12
16
  }
13
17
  },
14
18
  "files": [
15
19
  "src",
20
+ "bin",
16
21
  "index.d.ts",
22
+ "vite-plugin.js",
23
+ "vite-plugin.d.ts",
17
24
  "README.md"
18
25
  ],
26
+ "bin": {
27
+ "eptaadmin-prefetch": "./bin/prefetch.js"
28
+ },
29
+ "peerDependencies": {
30
+ "vite": ">=4"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "vite": {
34
+ "optional": true
35
+ }
36
+ },
19
37
  "engines": {
20
38
  "node": ">=18"
21
39
  },
package/src/index.js CHANGED
@@ -31,21 +31,35 @@ function resolveImageURLs(value, baseUrl) {
31
31
  return Array.isArray(value) ? value.map((v) => resolveImageURL(v, baseUrl)) : resolveImageURL(value, baseUrl);
32
32
  }
33
33
 
34
+ // Populated by build tooling (see eptaadmin-sdk/vite) via a bundler `define`
35
+ // so the exact same `client.getDataSource()` / `client.getValue()` calls
36
+ // resolve from build-time-fetched data in a production bundle, with zero
37
+ // runtime request — while resolving live in dev, where that identifier is
38
+ // either undefined or defined as `{}`. `typeof` is deliberate: it's the one
39
+ // operator that never throws on an identifier no bundler has declared at
40
+ // all, so the SDK works unmodified outside of Vite too.
41
+ const PREFETCHED = typeof __EPTAADMIN_PREFETCH_DATA__ !== "undefined" ? __EPTAADMIN_PREFETCH_DATA__ : {};
42
+
34
43
  export class EptaAdminClient {
35
44
  /**
36
- * @param {{ apiKey: string, baseUrl?: string }} options
45
+ * @param {{ apiKey?: string, baseUrl?: string }} options
37
46
  * apiKey — a personal API key generated from the EptaAdmin profile page.
47
+ * Only required for calls that actually reach the network —
48
+ * a call fully served from build-time-prefetched data (see
49
+ * eptaadmin-sdk/vite) never needs one, so it's fine to leave
50
+ * unset in a production bundle that only reads prefetched
51
+ * sources.
38
52
  * baseUrl — the URL of your EptaAdmin instance (default: http://localhost:8080).
39
53
  */
40
54
  constructor({ apiKey, baseUrl = "http://localhost:8080" } = {}) {
41
- if (!apiKey) {
42
- throw new Error("EptaAdminClient requires an apiKey");
43
- }
44
55
  this.apiKey = apiKey;
45
56
  this.baseUrl = baseUrl.replace(/\/$/, "");
46
57
  }
47
58
 
48
59
  async _request(path) {
60
+ if (!this.apiKey) {
61
+ throw new Error("EptaAdminClient requires an apiKey for this call (it wasn't served from prefetched data)");
62
+ }
49
63
  const res = await fetch(this.baseUrl + path, {
50
64
  headers: { Authorization: `Bearer ${this.apiKey}` },
51
65
  });
@@ -74,9 +88,12 @@ export class EptaAdminClient {
74
88
  * object of `{ [columnKey]: value[] }`.
75
89
  */
76
90
  async getDataSource(workspaceSlug, dataSourceSlug) {
77
- const body = await this._request(
78
- `/api/v1/workspaces/${encodeURIComponent(workspaceSlug)}/datasources/${encodeURIComponent(dataSourceSlug)}`
79
- );
91
+ const cached = PREFETCHED[`${workspaceSlug}/${dataSourceSlug}`];
92
+ const body = cached
93
+ ? { name: cached.name, slug: cached.slug, columns: { ...cached.columns } }
94
+ : await this._request(
95
+ `/api/v1/workspaces/${encodeURIComponent(workspaceSlug)}/datasources/${encodeURIComponent(dataSourceSlug)}`
96
+ );
80
97
  for (const key of Object.keys(body.columns || {})) {
81
98
  body.columns[key] = resolveImageURLs(body.columns[key], this.baseUrl);
82
99
  }
@@ -99,6 +116,20 @@ export class EptaAdminClient {
99
116
  );
100
117
  }
101
118
  const [workspaceSlug, dataSourceSlug, column, index] = segments;
119
+
120
+ const cached = PREFETCHED[`${workspaceSlug}/${dataSourceSlug}`];
121
+ if (cached) {
122
+ const values = (cached.columns || {})[column];
123
+ if (values === undefined) {
124
+ throw new EptaAdminError(`column "${column}" not found`, 404);
125
+ }
126
+ const result = index !== undefined ? values[Number(index)] : values;
127
+ if (index !== undefined && result === undefined) {
128
+ throw new EptaAdminError(`index ${index} out of range for column "${column}"`, 404);
129
+ }
130
+ return resolveImageURLs(result, this.baseUrl);
131
+ }
132
+
102
133
  let url =
103
134
  `/api/v1/workspaces/${encodeURIComponent(workspaceSlug)}` +
104
135
  `/datasources/${encodeURIComponent(dataSourceSlug)}` +
@@ -0,0 +1,31 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ export interface EptaadminPrefetchSource {
4
+ workspace: string;
5
+ dataSource: string;
6
+ }
7
+
8
+ export interface EptaadminPrefetchOptions {
9
+ /** The URL of your EptaAdmin instance. */
10
+ baseUrl: string;
11
+ /** Env var holding the API key used at build time. Defaults to "EPTAADMIN_API_KEY". */
12
+ apiKeyEnv?: string;
13
+ /** Directory (relative to Vite's root) scanned for getDataSource()/getValue() calls. Defaults to "src". */
14
+ scanDir?: string;
15
+ /**
16
+ * Extra data sources to prefetch on top of whatever scanning finds —
17
+ * for calls whose arguments aren't literal strings (e.g. a variable),
18
+ * which static scanning can't see.
19
+ */
20
+ sources?: EptaadminPrefetchSource[];
21
+ }
22
+
23
+ /**
24
+ * A Vite plugin that scans your source code for `getDataSource()` /
25
+ * `getValue()` calls, fetches exactly those data sources once at
26
+ * `vite build` time, and injects them so those same calls resolve from
27
+ * that static data — with zero runtime request in the production bundle.
28
+ * Has no effect during `vite` (dev server): the same client code keeps
29
+ * making live requests there.
30
+ */
31
+ export declare function eptaadminPrefetch(options: EptaadminPrefetchOptions): Plugin;
package/vite-plugin.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Vite plugin: scans your project's source code for `client.getDataSource(...)`
3
+ * / `client.getValue(...)` calls, fetches exactly those data sources once at
4
+ * `vite build` time, and bakes the result into the bundle — so those same
5
+ * calls resolve from static data with zero runtime request in production,
6
+ * with no config listing what to prefetch and nothing to keep in sync by
7
+ * hand. `vite` (dev server) is left untouched, so the exact same client
8
+ * code keeps making live requests during development.
9
+ */
10
+ import { readFile, readdir } from "node:fs/promises";
11
+ import { join, extname } from "node:path";
12
+ import { EptaAdminClient } from "./src/index.js";
13
+
14
+ const SCANNABLE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".vue", ".svelte"]);
15
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".vite", ".next", "coverage"]);
16
+
17
+ // Matches getDataSource("ws", "ds") / getDataSource('ws', "ds") with any
18
+ // quote style, tolerant of whitespace — literal string arguments only,
19
+ // since this is static source scanning, not a real JS parser.
20
+ const GET_DATA_SOURCE_RE = /getDataSource\s*\(\s*(['"`])([^'"`]+)\1\s*,\s*(['"`])([^'"`]+)\3\s*\)/g;
21
+ // Matches getValue("ws/ds/column") / getValue("ws/ds/column/index") —
22
+ // only the workspace/dataSource prefix is needed to know what to prefetch.
23
+ const GET_VALUE_RE = /getValue\s*\(\s*(['"`])([^'"`]+)\1\s*\)/g;
24
+
25
+ // Broader "any call, any arguments" versions of the two above, used only to
26
+ // flag calls scanning *can't* resolve (e.g. variables) — doesn't handle
27
+ // arguments containing nested parens/commas, but that's fine for a warning
28
+ // whose job is just to point a human at the right line.
29
+ const ANY_GET_DATA_SOURCE_CALL_RE = /getDataSource\s*\(([^)]*)\)/g;
30
+ const ANY_GET_VALUE_CALL_RE = /getValue\s*\(([^)]*)\)/g;
31
+ const LITERAL_STRING_ARG_RE = /^\s*['"`][^'"`]*['"`]\s*$/;
32
+
33
+ function lineNumberAt(text, index) {
34
+ let line = 1;
35
+ for (let i = 0; i < index; i++) {
36
+ if (text.charCodeAt(i) === 10) line++;
37
+ }
38
+ return line;
39
+ }
40
+
41
+ async function collectFiles(dir, out) {
42
+ let entries;
43
+ try {
44
+ entries = await readdir(dir, { withFileTypes: true });
45
+ } catch {
46
+ return; // missing/unreadable dir — nothing to scan there
47
+ }
48
+ for (const entry of entries) {
49
+ if (entry.isDirectory()) {
50
+ if (SKIP_DIRS.has(entry.name)) continue;
51
+ await collectFiles(join(dir, entry.name), out);
52
+ } else if (SCANNABLE_EXTENSIONS.has(extname(entry.name))) {
53
+ out.push(join(dir, entry.name));
54
+ }
55
+ }
56
+ }
57
+
58
+ /** Scans every source file under `scanDir` for SDK calls with literal
59
+ * string arguments, returning the unique {workspace, dataSource} pairs
60
+ * actually referenced in the code, plus a warning for every call scanning
61
+ * found but couldn't resolve (non-literal arguments) — so a developer can
62
+ * fix each one by adding it to `sources` instead of it failing silently. */
63
+ async function scanForSources(scanDir) {
64
+ const files = [];
65
+ await collectFiles(scanDir, files);
66
+
67
+ const found = new Map(); // "workspace/dataSource" -> {workspace, dataSource}
68
+ const warnings = [];
69
+
70
+ for (const file of files) {
71
+ let text;
72
+ try {
73
+ text = await readFile(file, "utf8");
74
+ } catch {
75
+ continue;
76
+ }
77
+
78
+ const resolvedCallOffsets = new Set();
79
+ for (const m of text.matchAll(GET_DATA_SOURCE_RE)) {
80
+ const [, , workspace, , dataSource] = m;
81
+ found.set(`${workspace}/${dataSource}`, { workspace, dataSource });
82
+ resolvedCallOffsets.add(m.index);
83
+ }
84
+ for (const m of text.matchAll(GET_VALUE_RE)) {
85
+ const segments = m[2].split("/").filter(Boolean);
86
+ if (segments.length >= 3) {
87
+ const [workspace, dataSource] = segments;
88
+ found.set(`${workspace}/${dataSource}`, { workspace, dataSource });
89
+ }
90
+ resolvedCallOffsets.add(m.index);
91
+ }
92
+
93
+ for (const m of text.matchAll(ANY_GET_DATA_SOURCE_CALL_RE)) {
94
+ if (resolvedCallOffsets.has(m.index)) continue;
95
+ const args = m[1].split(",");
96
+ const isLiteral = args.length === 2 && args.every((a) => LITERAL_STRING_ARG_RE.test(a));
97
+ if (!isLiteral) {
98
+ warnings.push(`${file}:${lineNumberAt(text, m.index)} — getDataSource(${m[1].trim()}) has non-literal arguments, can't be scanned; add it via "sources" if it should be prefetched.`);
99
+ }
100
+ }
101
+ for (const m of text.matchAll(ANY_GET_VALUE_CALL_RE)) {
102
+ if (resolvedCallOffsets.has(m.index)) continue;
103
+ if (!LITERAL_STRING_ARG_RE.test(m[1])) {
104
+ warnings.push(`${file}:${lineNumberAt(text, m.index)} — getValue(${m[1].trim()}) has a non-literal argument, can't be scanned; add its {workspace, dataSource} via "sources" if it should be prefetched.`);
105
+ }
106
+ }
107
+ }
108
+
109
+ return { sources: [...found.values()], warnings };
110
+ }
111
+
112
+ /**
113
+ * @param {{
114
+ * baseUrl: string,
115
+ * apiKeyEnv?: string,
116
+ * scanDir?: string,
117
+ * sources?: { workspace: string, dataSource: string }[],
118
+ * }} options
119
+ * scanDir — directory to scan for SDK calls, relative to Vite's root.
120
+ * Defaults to "src".
121
+ * sources — extra {workspace, dataSource} pairs to prefetch on top of
122
+ * whatever scanning finds — for calls whose arguments aren't
123
+ * literal strings (e.g. a variable), which scanning can't see.
124
+ */
125
+ export function eptaadminPrefetch(options = {}) {
126
+ const { baseUrl, apiKeyEnv = "EPTAADMIN_API_KEY", scanDir = "src", sources: extraSources = [] } = options;
127
+ if (!baseUrl) throw new Error("eptaadminPrefetch: \"baseUrl\" is required");
128
+
129
+ return {
130
+ name: "eptaadmin-prefetch",
131
+ async config(config, { command }) {
132
+ // Dev server: leave the injected data empty so every client call in
133
+ // this mode falls through to a real, live fetch — same code path,
134
+ // just not pre-resolved.
135
+ let data = {};
136
+
137
+ if (command === "build") {
138
+ const apiKey = process.env[apiKeyEnv];
139
+ if (!apiKey) {
140
+ throw new Error(
141
+ `eptaadmin-prefetch: environment variable "${apiKeyEnv}" is not set — it must hold a personal API key from your EptaAdmin profile page to prefetch data at build time`
142
+ );
143
+ }
144
+
145
+ const root = config.root || process.cwd();
146
+ const { sources: scanned, warnings } = await scanForSources(join(root, scanDir));
147
+ for (const w of warnings) console.warn(`[eptaadmin-prefetch] ${w}`);
148
+
149
+ const byKey = new Map(scanned.map((s) => [`${s.workspace}/${s.dataSource}`, s]));
150
+ for (const s of extraSources) byKey.set(`${s.workspace}/${s.dataSource}`, s);
151
+ const sources = [...byKey.values()];
152
+
153
+ if (sources.length === 0) {
154
+ console.warn(
155
+ `[eptaadmin-prefetch] found no getDataSource()/getValue() calls with literal arguments under "${scanDir}" — nothing to prefetch. ` +
156
+ `If your calls use variables instead of string literals, list them explicitly via the "sources" option.`
157
+ );
158
+ }
159
+
160
+ const client = new EptaAdminClient({ apiKey, baseUrl });
161
+ for (const { workspace, dataSource } of sources) {
162
+ const result = await client.getDataSource(workspace, dataSource);
163
+ data[`${workspace}/${dataSource}`] = result;
164
+ console.log(`[eptaadmin-prefetch] ✓ ${workspace}/${dataSource}`);
165
+ }
166
+ }
167
+
168
+ return {
169
+ define: {
170
+ __EPTAADMIN_PREFETCH_DATA__: JSON.stringify(data),
171
+ },
172
+ };
173
+ },
174
+ };
175
+ }