tauri-plugin-configurate-api 0.1.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/LICENSE +21 -0
- package/README.md +351 -0
- package/dist-js/index.cjs +497 -0
- package/dist-js/index.d.ts +354 -0
- package/dist-js/index.js +485 -0
- package/package.json +60 -0
package/dist-js/index.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { invoke } from '@tauri-apps/api/core';
|
|
2
|
+
export { BaseDirectory } from '@tauri-apps/api/path';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Keyring marker types
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Brand key used to tag keyring-protected fields at runtime.
|
|
9
|
+
*
|
|
10
|
+
* A plain string property is used instead of a `Symbol` because some bundlers
|
|
11
|
+
* (Vite / esbuild pre-bundling) incorrectly hoist computed `Symbol` property
|
|
12
|
+
* keys out of scope, causing `_keyringBrand is not defined` errors at runtime.
|
|
13
|
+
*
|
|
14
|
+
* The string is long and namespaced to avoid accidental collisions with
|
|
15
|
+
* user-defined keys.
|
|
16
|
+
*/
|
|
17
|
+
const KEYRING_BRAND_KEY = "__configurate_keyring__";
|
|
18
|
+
/**
|
|
19
|
+
* Marks a schema field as keyring-protected.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const schema = defineConfig({
|
|
24
|
+
* apiKey: keyring(String, { id: "api-key" }),
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function keyring(_type, opts) {
|
|
29
|
+
// Avoid computed property key syntax `{ [KEYRING_BRAND_KEY]: true }` in an
|
|
30
|
+
// object literal. Some bundlers (Vite / esbuild pre-bundling) incorrectly
|
|
31
|
+
// hoist the evaluated key expression out of the enclosing function scope,
|
|
32
|
+
// producing a "X is not defined" ReferenceError at runtime.
|
|
33
|
+
// Assigning via bracket notation after construction is semantically
|
|
34
|
+
// identical but is never subject to that hoisting behaviour.
|
|
35
|
+
const field = { _type: undefined, _id: opts.id };
|
|
36
|
+
field[KEYRING_BRAND_KEY] = true;
|
|
37
|
+
return field;
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Internal runtime helpers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/** Runtime check: is this value a KeyringField marker? */
|
|
43
|
+
function isKeyringField(val) {
|
|
44
|
+
return (typeof val === "object" &&
|
|
45
|
+
val !== null &&
|
|
46
|
+
val[KEYRING_BRAND_KEY] === true);
|
|
47
|
+
}
|
|
48
|
+
/** Runtime check: is this value a nested SchemaObject? */
|
|
49
|
+
function isSchemaObject(val) {
|
|
50
|
+
return typeof val === "object" && val !== null && !isKeyringField(val);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Recursively collects all keyring ids from a schema as a flat string array.
|
|
54
|
+
* Used for runtime duplicate validation.
|
|
55
|
+
*/
|
|
56
|
+
function collectKeyringIds(schema) {
|
|
57
|
+
const ids = [];
|
|
58
|
+
for (const val of Object.values(schema)) {
|
|
59
|
+
if (isKeyringField(val)) {
|
|
60
|
+
ids.push(val._id);
|
|
61
|
+
}
|
|
62
|
+
else if (isSchemaObject(val)) {
|
|
63
|
+
ids.push(...collectKeyringIds(val));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return ids;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Recursively collects all keyring entries as `{ id, dotpath }` pairs.
|
|
70
|
+
* `dotpath` is the dot-separated path to the field inside the config object.
|
|
71
|
+
*/
|
|
72
|
+
function collectKeyringPaths(schema, prefix = "") {
|
|
73
|
+
const result = [];
|
|
74
|
+
for (const [key, val] of Object.entries(schema)) {
|
|
75
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
76
|
+
if (isKeyringField(val)) {
|
|
77
|
+
result.push({ id: val._id, dotpath: path });
|
|
78
|
+
}
|
|
79
|
+
else if (isSchemaObject(val)) {
|
|
80
|
+
result.push(...collectKeyringPaths(val, path));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Separates a data object into non-secret plain fields and keyring entries.
|
|
87
|
+
* Secret values are extracted from their dotpath locations and serialized to
|
|
88
|
+
* strings so the Rust side can store them in the OS keyring.
|
|
89
|
+
* The corresponding dotpath in `plain` is set to `null` so secrets are never
|
|
90
|
+
* persisted to disk.
|
|
91
|
+
*/
|
|
92
|
+
function separateSecrets(data, keyringPaths) {
|
|
93
|
+
const plain = structuredClone(data);
|
|
94
|
+
const keyringEntries = [];
|
|
95
|
+
for (const { id, dotpath } of keyringPaths) {
|
|
96
|
+
const parts = dotpath.split(".");
|
|
97
|
+
let node = plain;
|
|
98
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
99
|
+
if (node === null || typeof node !== "object")
|
|
100
|
+
break;
|
|
101
|
+
node = node[parts[i] ?? ""];
|
|
102
|
+
}
|
|
103
|
+
const last = parts.at(-1) ?? "";
|
|
104
|
+
// Guard: after traversal node must still be a plain object.
|
|
105
|
+
if (node === null || typeof node !== "object")
|
|
106
|
+
continue;
|
|
107
|
+
const parent = node;
|
|
108
|
+
if (last in parent) {
|
|
109
|
+
const secret = parent[last];
|
|
110
|
+
const serialized = typeof secret === "string" ? secret : JSON.stringify(secret);
|
|
111
|
+
keyringEntries.push({ id, dotpath, value: serialized });
|
|
112
|
+
// Nullify the secret in the plain data so it is never written to disk.
|
|
113
|
+
parent[last] = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { plain, keyringEntries };
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// LockedConfig
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
/**
|
|
122
|
+
* A loaded configuration where keyring-protected fields are `null`.
|
|
123
|
+
* Call `.unlock(opts)` to fetch secrets and obtain an `UnlockedConfig<S>`.
|
|
124
|
+
*
|
|
125
|
+
* `unlock()` issues a single IPC call that only reads from the OS keyring —
|
|
126
|
+
* it does **not** re-read the file from disk.
|
|
127
|
+
*/
|
|
128
|
+
class LockedConfig {
|
|
129
|
+
_configurate;
|
|
130
|
+
data;
|
|
131
|
+
/** @internal */
|
|
132
|
+
constructor(data, _configurate) {
|
|
133
|
+
this._configurate = _configurate;
|
|
134
|
+
this.data = data;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Fetches all keyring secrets and returns an `UnlockedConfig`.
|
|
138
|
+
* Issues a single IPC call (keyring read only – file is not re-read).
|
|
139
|
+
*/
|
|
140
|
+
async unlock(opts) {
|
|
141
|
+
return this._configurate._unlockFromData(this.data, opts);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// UnlockedConfig
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
/**
|
|
148
|
+
* A configuration where all keyring-protected fields contain their real values.
|
|
149
|
+
* Call `.lock()` to discard in-memory secrets (no IPC required).
|
|
150
|
+
*/
|
|
151
|
+
class UnlockedConfig {
|
|
152
|
+
_data;
|
|
153
|
+
/** @internal */
|
|
154
|
+
constructor(data) {
|
|
155
|
+
this._data = data;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Returns the unlocked configuration data.
|
|
159
|
+
* Throws if `lock()` has already been called.
|
|
160
|
+
*/
|
|
161
|
+
get data() {
|
|
162
|
+
if (this._data === null) {
|
|
163
|
+
throw new Error("Cannot access data after lock() has been called. " +
|
|
164
|
+
"Load or unlock the config again to get a fresh instance.");
|
|
165
|
+
}
|
|
166
|
+
return this._data;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Discards in-memory secrets. Callers should drop all references to this
|
|
170
|
+
* object after calling `lock()`.
|
|
171
|
+
*
|
|
172
|
+
* > **Security note** — JavaScript does not provide a guaranteed way to
|
|
173
|
+
* > zero-out memory. Calling `lock()` nullifies the top-level reference
|
|
174
|
+
* > but the secret values remain in the JS heap until the GC collects them.
|
|
175
|
+
* > Avoid long-lived `UnlockedConfig` objects when handling sensitive data.
|
|
176
|
+
*/
|
|
177
|
+
lock() {
|
|
178
|
+
this._data = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// LazyConfigEntry
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
/**
|
|
185
|
+
* A lazy handle returned by `Configurate.load()`, `.create()` and `.save()`.
|
|
186
|
+
*
|
|
187
|
+
* - `await entry.run()` → `LockedConfig<S>` (one IPC, secrets are null)
|
|
188
|
+
* - `await entry.unlock(opts)` → `UnlockedConfig<S>` (one IPC, secrets inlined)
|
|
189
|
+
* - `.lock(opts)` (before awaiting) → write secrets to keyring in the same IPC
|
|
190
|
+
*
|
|
191
|
+
* Use `await entry.run()` instead of `await entry` directly to avoid the
|
|
192
|
+
* `no-thenable` lint rule and unintended Promise behaviour.
|
|
193
|
+
*/
|
|
194
|
+
class LazyConfigEntry {
|
|
195
|
+
_configurate;
|
|
196
|
+
_op;
|
|
197
|
+
_data;
|
|
198
|
+
_keyringOpts = null;
|
|
199
|
+
/** @internal */
|
|
200
|
+
constructor(_configurate, _op, _data) {
|
|
201
|
+
this._configurate = _configurate;
|
|
202
|
+
this._op = _op;
|
|
203
|
+
this._data = _data;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Attaches keyring options so secrets are written to / read from the OS
|
|
207
|
+
* keyring in the same IPC call as the main operation.
|
|
208
|
+
*
|
|
209
|
+
* Returns `this` to allow chaining: `entry.lock(opts).run()`.
|
|
210
|
+
*/
|
|
211
|
+
lock(opts) {
|
|
212
|
+
this._keyringOpts = opts;
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Executes the operation and returns a `LockedConfig` (secrets are null).
|
|
217
|
+
* Issues a single IPC call.
|
|
218
|
+
*/
|
|
219
|
+
run() {
|
|
220
|
+
return this._configurate._executeLocked(this._op, this._data, this._keyringOpts);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Executes the operation and returns an `UnlockedConfig` (secrets inlined).
|
|
224
|
+
* Issues a single IPC call – no extra round-trip compared to `run()`.
|
|
225
|
+
*/
|
|
226
|
+
unlock(opts) {
|
|
227
|
+
return this._configurate._executeUnlock(this._op, this._data, opts);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Main entry point for managing application configuration.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* import { Configurate, defineConfig, keyring, BaseDirectory } from "tauri-plugin-configurate-api";
|
|
236
|
+
*
|
|
237
|
+
* const schema = defineConfig({
|
|
238
|
+
* appName: String,
|
|
239
|
+
* port: Number,
|
|
240
|
+
* apiKey: keyring(String, { id: "api-key" }),
|
|
241
|
+
* });
|
|
242
|
+
*
|
|
243
|
+
* const config = new Configurate(schema, {
|
|
244
|
+
* id: "app-config",
|
|
245
|
+
* dir: BaseDirectory.AppConfig,
|
|
246
|
+
* format: "json",
|
|
247
|
+
* });
|
|
248
|
+
*
|
|
249
|
+
* // Create – IPC ×1
|
|
250
|
+
* await config
|
|
251
|
+
* .create({ appName: "MyApp", port: 3000, apiKey: "secret" })
|
|
252
|
+
* .lock({ service: "my-app", account: "default" })
|
|
253
|
+
* .run();
|
|
254
|
+
*
|
|
255
|
+
* // Load locked – IPC ×1
|
|
256
|
+
* const locked = await config.load().run();
|
|
257
|
+
* locked.data.apiKey; // null
|
|
258
|
+
*
|
|
259
|
+
* // Unlock from locked – IPC ×1 (keyring only, file is not re-read)
|
|
260
|
+
* const unlocked = await locked.unlock({ service: "my-app", account: "default" });
|
|
261
|
+
* unlocked.data.apiKey; // "secret"
|
|
262
|
+
*
|
|
263
|
+
* // Load and unlock in one shot – IPC ×1
|
|
264
|
+
* const unlocked2 = await config.load().unlock({ service: "my-app", account: "default" });
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
class Configurate {
|
|
268
|
+
_schema;
|
|
269
|
+
_opts;
|
|
270
|
+
_keyringPaths;
|
|
271
|
+
constructor(schema, opts) {
|
|
272
|
+
if (opts.encryptionKey !== undefined && opts.format !== "binary") {
|
|
273
|
+
throw new Error(`encryptionKey is only supported with format "binary", got "${opts.format}". ` +
|
|
274
|
+
`Remove encryptionKey or change format to "binary".`);
|
|
275
|
+
}
|
|
276
|
+
this._schema = schema;
|
|
277
|
+
this._opts = opts;
|
|
278
|
+
this._keyringPaths = collectKeyringPaths(this._schema);
|
|
279
|
+
}
|
|
280
|
+
// -------------------------------------------------------------------------
|
|
281
|
+
// Public API
|
|
282
|
+
// -------------------------------------------------------------------------
|
|
283
|
+
/** Returns a lazy entry that creates the config file on the Rust side. */
|
|
284
|
+
create(data) {
|
|
285
|
+
return new LazyConfigEntry(this, "create", data);
|
|
286
|
+
}
|
|
287
|
+
/** Returns a lazy entry that loads the config file from the Rust side. */
|
|
288
|
+
load() {
|
|
289
|
+
return new LazyConfigEntry(this, "load");
|
|
290
|
+
}
|
|
291
|
+
/** Returns a lazy entry that overwrites the config file on the Rust side. */
|
|
292
|
+
save(data) {
|
|
293
|
+
return new LazyConfigEntry(this, "save", data);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Deletes the configuration file from disk **and** removes all associated
|
|
297
|
+
* keyring entries from the OS keyring in a single IPC call.
|
|
298
|
+
*
|
|
299
|
+
* Pass `opts` when the schema contains `keyring()` fields so the plugin
|
|
300
|
+
* knows which keyring entries to wipe. Omit `opts` (or pass `null`) when
|
|
301
|
+
* the schema has no keyring fields.
|
|
302
|
+
*
|
|
303
|
+
* Returns `Promise<void>`. Resolves even if the file did not exist.
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```ts
|
|
307
|
+
* // Schema with keyring fields – pass keyring opts to wipe secrets too.
|
|
308
|
+
* await config.delete({ service: "my-app", account: "default" });
|
|
309
|
+
*
|
|
310
|
+
* // Schema with no keyring fields – opts may be omitted.
|
|
311
|
+
* await config.delete();
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
async delete(opts) {
|
|
315
|
+
const payload = this._buildPayload("load", undefined, opts ?? null, false);
|
|
316
|
+
await invoke("plugin:configurate|delete", { payload });
|
|
317
|
+
}
|
|
318
|
+
// -------------------------------------------------------------------------
|
|
319
|
+
// Internal helpers (called by LazyConfigEntry / LockedConfig)
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
/** @internal */
|
|
322
|
+
async _executeLocked(op, data, keyringOpts) {
|
|
323
|
+
const payload = this._buildPayload(op, data, keyringOpts, false);
|
|
324
|
+
const result = await invoke("plugin:configurate|" + op, {
|
|
325
|
+
payload,
|
|
326
|
+
});
|
|
327
|
+
return new LockedConfig(result, this);
|
|
328
|
+
}
|
|
329
|
+
/** @internal */
|
|
330
|
+
async _executeUnlock(op, data, keyringOpts) {
|
|
331
|
+
const payload = this._buildPayload(op, data, keyringOpts, true);
|
|
332
|
+
const result = await invoke("plugin:configurate|" + op, {
|
|
333
|
+
payload,
|
|
334
|
+
});
|
|
335
|
+
return new UnlockedConfig(result);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Fetches keyring secrets and merges them into already-loaded plain data
|
|
339
|
+
* without re-reading the file from disk.
|
|
340
|
+
* Issues a single IPC call to `plugin:configurate|unlock`.
|
|
341
|
+
*
|
|
342
|
+
* @internal
|
|
343
|
+
*/
|
|
344
|
+
async _unlockFromData(plainData, opts) {
|
|
345
|
+
if (this._keyringPaths.length === 0) {
|
|
346
|
+
return new UnlockedConfig(plainData);
|
|
347
|
+
}
|
|
348
|
+
const payload = {
|
|
349
|
+
data: plainData,
|
|
350
|
+
keyringEntries: this._keyringPaths.map(({ id, dotpath }) => ({
|
|
351
|
+
id,
|
|
352
|
+
dotpath,
|
|
353
|
+
value: "",
|
|
354
|
+
})),
|
|
355
|
+
keyringOptions: opts,
|
|
356
|
+
};
|
|
357
|
+
const result = await invoke("plugin:configurate|unlock", {
|
|
358
|
+
payload,
|
|
359
|
+
});
|
|
360
|
+
return new UnlockedConfig(result);
|
|
361
|
+
}
|
|
362
|
+
// -------------------------------------------------------------------------
|
|
363
|
+
// Payload builder
|
|
364
|
+
// -------------------------------------------------------------------------
|
|
365
|
+
/** @internal */
|
|
366
|
+
_buildPayload(op, data, keyringOpts, withUnlock) {
|
|
367
|
+
const base = {
|
|
368
|
+
id: this._opts.id,
|
|
369
|
+
dir: this._opts.dir,
|
|
370
|
+
format: this._opts.format,
|
|
371
|
+
withUnlock,
|
|
372
|
+
};
|
|
373
|
+
if (this._opts.subDir) {
|
|
374
|
+
base.subDir = this._opts.subDir;
|
|
375
|
+
}
|
|
376
|
+
if (this._opts.encryptionKey) {
|
|
377
|
+
base.encryptionKey = this._opts.encryptionKey;
|
|
378
|
+
}
|
|
379
|
+
if (op === "load") {
|
|
380
|
+
// For load we only need the keyring ids and dotpaths so the Rust side
|
|
381
|
+
// knows which dotpaths to populate when with_unlock is true.
|
|
382
|
+
if (keyringOpts && this._keyringPaths.length > 0) {
|
|
383
|
+
base.keyringEntries = this._keyringPaths.map(({ id, dotpath }) => ({
|
|
384
|
+
id,
|
|
385
|
+
dotpath,
|
|
386
|
+
value: "",
|
|
387
|
+
}));
|
|
388
|
+
base.keyringOptions = keyringOpts;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else if (data !== undefined) {
|
|
392
|
+
const { plain, keyringEntries } = separateSecrets(data, this._keyringPaths);
|
|
393
|
+
base.data = plain;
|
|
394
|
+
if (keyringOpts && keyringEntries.length > 0) {
|
|
395
|
+
base.keyringEntries = keyringEntries;
|
|
396
|
+
base.keyringOptions = keyringOpts;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return base;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* A factory that creates `Configurate` instances with pre-set shared options
|
|
404
|
+
* (`dir`, `format`, and optionally `encryptionKey`).
|
|
405
|
+
*
|
|
406
|
+
* Each call to `build()` creates a fresh `Configurate` instance — schema,
|
|
407
|
+
* `id`, and all other options can differ freely. This is the recommended
|
|
408
|
+
* way to manage multiple config files with different schemas in a single
|
|
409
|
+
* application.
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```ts
|
|
413
|
+
* import { ConfigurateFactory, defineConfig, BaseDirectory } from "tauri-plugin-configurate-api";
|
|
414
|
+
*
|
|
415
|
+
* const appSchema = defineConfig({ theme: String, language: String });
|
|
416
|
+
* const cacheSchema = defineConfig({ lastSync: Number });
|
|
417
|
+
* const secretSchema = defineConfig({ token: String });
|
|
418
|
+
*
|
|
419
|
+
* const factory = new ConfigurateFactory({
|
|
420
|
+
* dir: BaseDirectory.AppConfig,
|
|
421
|
+
* format: "json",
|
|
422
|
+
* });
|
|
423
|
+
*
|
|
424
|
+
* const appConfig = factory.build(appSchema, "app"); // → app.json
|
|
425
|
+
* const cacheConfig = factory.build(cacheSchema, "cache"); // → cache.json
|
|
426
|
+
* const secretConfig = factory.build(secretSchema, "secrets"); // → secrets.json
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
class ConfigurateFactory {
|
|
430
|
+
_baseOpts;
|
|
431
|
+
constructor(_baseOpts) {
|
|
432
|
+
this._baseOpts = _baseOpts;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Creates a `Configurate<S>` for the given schema and id, applying the
|
|
436
|
+
* shared base options.
|
|
437
|
+
*
|
|
438
|
+
* The optional `subDir` argument overrides the factory-level `subDir` for
|
|
439
|
+
* this specific instance.
|
|
440
|
+
*/
|
|
441
|
+
build(schema, id, subDir) {
|
|
442
|
+
const opts = { ...this._baseOpts, id };
|
|
443
|
+
if (subDir !== undefined) {
|
|
444
|
+
opts.subDir = subDir;
|
|
445
|
+
}
|
|
446
|
+
// Explicitly pass <S> to prevent TypeScript from re-inferring the type
|
|
447
|
+
// parameter from the argument and double-evaluating HasDuplicateKeyringIds
|
|
448
|
+
// on the already-constrained type. The duplicate-id guarantee was already
|
|
449
|
+
// enforced at the call-site when the schema was created.
|
|
450
|
+
return new Configurate(schema, opts);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// defineConfig helper
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
/**
|
|
457
|
+
* Defines a configuration schema. Provides a convenient declaration site and
|
|
458
|
+
* compile-time duplicate keyring id checks.
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```ts
|
|
462
|
+
* const schema = defineConfig({
|
|
463
|
+
* appName: String,
|
|
464
|
+
* port: Number,
|
|
465
|
+
* database: {
|
|
466
|
+
* host: String,
|
|
467
|
+
* password: keyring(String, { id: "db-password" }),
|
|
468
|
+
* },
|
|
469
|
+
* });
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
function defineConfig(schema) {
|
|
473
|
+
// Runtime duplicate id validation (belt-and-suspenders on top of type checks).
|
|
474
|
+
const ids = collectKeyringIds(schema);
|
|
475
|
+
const seen = new Set();
|
|
476
|
+
for (const id of ids) {
|
|
477
|
+
if (seen.has(id)) {
|
|
478
|
+
throw new Error(`Duplicate keyring id: '${id}'. Each keyring() call must use a unique id within the same schema.`);
|
|
479
|
+
}
|
|
480
|
+
seen.add(id);
|
|
481
|
+
}
|
|
482
|
+
return schema;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export { Configurate, ConfigurateFactory, LazyConfigEntry, LockedConfig, UnlockedConfig, defineConfig, keyring };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tauri-plugin-configurate-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Tauri v2 plugin for type-safe application configuration management.",
|
|
5
|
+
"author": "Crysta1221",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Crysta1221/tauri-plugin-configurate.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Crysta1221/tauri-plugin-configurate",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Crysta1221/tauri-plugin-configurate/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"tauri",
|
|
17
|
+
"tauri-plugin",
|
|
18
|
+
"config",
|
|
19
|
+
"keyring",
|
|
20
|
+
"configuration"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"dist-js",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist-js/index.cjs",
|
|
29
|
+
"module": "./dist-js/index.js",
|
|
30
|
+
"types": "./dist-js/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
"types": "./dist-js/index.d.ts",
|
|
33
|
+
"import": "./dist-js/index.js",
|
|
34
|
+
"require": "./dist-js/index.cjs"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "rollup -c",
|
|
38
|
+
"build:sync": "rollup -c && cd examples/tauri-app && pnpm install",
|
|
39
|
+
"prepublishOnly": "bun run build",
|
|
40
|
+
"pretest": "pnpm build",
|
|
41
|
+
"fmt": "oxfmt",
|
|
42
|
+
"fmt:check": "oxfmt --check",
|
|
43
|
+
"lint": "oxlint --type-aware",
|
|
44
|
+
"lint:fix": "oxlint --type-aware --fix"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@tauri-apps/api": "^2.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"oxlint-tsgolint": "^0.14.2"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@rollup/plugin-typescript": "^12.0.0",
|
|
54
|
+
"oxfmt": "^0.35.0",
|
|
55
|
+
"oxlint": "^1.50.0",
|
|
56
|
+
"rollup": "^4.9.6",
|
|
57
|
+
"tslib": "^2.6.2",
|
|
58
|
+
"typescript": "^5.3.3"
|
|
59
|
+
}
|
|
60
|
+
}
|