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 +99 -0
- package/bin/prefetch.js +104 -0
- package/index.d.ts +7 -2
- package/package.json +19 -1
- package/src/index.js +38 -7
- package/vite-plugin.d.ts +31 -0
- package/vite-plugin.js +175 -0
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:
|
package/bin/prefetch.js
ADDED
|
@@ -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
|
-
/**
|
|
26
|
-
|
|
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.
|
|
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
|
|
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
|
|
78
|
-
|
|
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)}` +
|
package/vite-plugin.d.ts
ADDED
|
@@ -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
|
+
}
|