@deepseek-ai/cordis-plugin-include 1.0.7 → 1.0.8
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/lib/index.js +43 -81
- package/lib/types/index.d.ts +4 -18
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +55 -89
package/lib/index.js
CHANGED
|
@@ -43,8 +43,7 @@ function retryableWriteError(error) {
|
|
|
43
43
|
* Apply patch lists to an entry list — THE patch semantics of this include,
|
|
44
44
|
* shared by mounting (`applyPatches`) and offline config tooling
|
|
45
45
|
* (`dsh --dump-config`) so a dump can never drift from what boots. The input
|
|
46
|
-
* is never mutated
|
|
47
|
-
* patches): patching or mounting shared entry objects would bake earlier
|
|
46
|
+
* is never mutated: patching shared entry objects would bake earlier patch
|
|
48
47
|
* values into the cached parse, so repeated application (config hot-reloads)
|
|
49
48
|
* could never revert a removed or changed patch. Inserted entries are indexed
|
|
50
49
|
* as they are added, so a later patch in the same list can target a row an
|
|
@@ -55,8 +54,8 @@ function retryableWriteError(error) {
|
|
|
55
54
|
* @returns a detached entry list with every applicable patch applied.
|
|
56
55
|
*/
|
|
57
56
|
function applyEntryPatches(data, patches, warn) {
|
|
57
|
+
if (!patches?.length) return [...data];
|
|
58
58
|
data = structuredClone(data);
|
|
59
|
-
if (!patches?.length) return data;
|
|
60
59
|
const entryMap = /* @__PURE__ */ new Map();
|
|
61
60
|
const buildMap = (entries) => {
|
|
62
61
|
for (const entry of entries) {
|
|
@@ -104,14 +103,6 @@ function applyEntryPatches(data, patches, warn) {
|
|
|
104
103
|
}
|
|
105
104
|
return data;
|
|
106
105
|
}
|
|
107
|
-
var ConfigFileError = class extends Error {
|
|
108
|
-
stage;
|
|
109
|
-
constructor(stage, path, cause) {
|
|
110
|
-
super(`failed to ${stage} config file ${path}`, { cause });
|
|
111
|
-
this.stage = stage;
|
|
112
|
-
this.name = "ConfigFileError";
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
106
|
/** Loader entry tree backed by a YAML or JSON file. */
|
|
116
107
|
var Include = class extends EntryTree {
|
|
117
108
|
config;
|
|
@@ -125,7 +116,6 @@ var Include = class extends EntryTree {
|
|
|
125
116
|
writeTask;
|
|
126
117
|
pendingWrite;
|
|
127
118
|
writeQueue = Promise.resolve();
|
|
128
|
-
applyQueue = Promise.resolve();
|
|
129
119
|
constructor(ctx, config) {
|
|
130
120
|
super(ctx);
|
|
131
121
|
this.config = config;
|
|
@@ -136,29 +126,15 @@ var Include = class extends EntryTree {
|
|
|
136
126
|
this.type = writable[ext];
|
|
137
127
|
this.readonly = !this.type;
|
|
138
128
|
this.ctx.baseUrl = new URL(".", pathToFileURL(this.filename)).href;
|
|
139
|
-
ctx.on("internal/update",
|
|
129
|
+
ctx.on("internal/update", (config, _, next) => {
|
|
140
130
|
if (config.path !== this.config.path) return next();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.
|
|
131
|
+
this.config = config;
|
|
132
|
+
this.root.update(this.applyPatches(this.data, config.patches)).catch((error) => {
|
|
133
|
+
this.ctx.logger.warn("config update at %C failed", this.filename);
|
|
134
|
+
this.ctx.logger.warn(error);
|
|
145
135
|
});
|
|
146
136
|
});
|
|
147
137
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Serialize one child-tree mutation behind every earlier one. The group's
|
|
150
|
-
* transactional `update` is not reentrant: two concurrent applies (the init
|
|
151
|
-
* apply racing an HMR-triggered refresh from the watcher's initial scan)
|
|
152
|
-
* interleave create and rollback on the same entries and strand the include
|
|
153
|
-
* fiber without settling, so every apply path funnels through this queue.
|
|
154
|
-
* A predecessor's failure is its own caller's outcome and never gates the
|
|
155
|
-
* next task.
|
|
156
|
-
*/
|
|
157
|
-
enqueue(task) {
|
|
158
|
-
const run = this.applyQueue.then(task, task);
|
|
159
|
-
this.applyQueue = run.then(() => {}, () => {});
|
|
160
|
-
return run;
|
|
161
|
-
}
|
|
162
138
|
async checkAccess() {
|
|
163
139
|
if (!this.type) return;
|
|
164
140
|
try {
|
|
@@ -168,77 +144,63 @@ var Include = class extends EntryTree {
|
|
|
168
144
|
}
|
|
169
145
|
}
|
|
170
146
|
async read(forced = false) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
content = await readFile(this.filename, "utf8");
|
|
174
|
-
} catch (error) {
|
|
175
|
-
throw new ConfigFileError("read", this.filename, error);
|
|
176
|
-
}
|
|
177
|
-
if (!forced && this.content === content) return;
|
|
147
|
+
const content = await readFile(this.filename, "utf8");
|
|
148
|
+
if (!forced && this.content === content) return false;
|
|
178
149
|
let data;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
data = module.default || module;
|
|
188
|
-
}
|
|
189
|
-
} catch (error) {
|
|
190
|
-
throw new ConfigFileError("parse", this.filename, error);
|
|
150
|
+
if (this.type === "application/yaml") data = yaml.load(content, { schema: entryListSchema });
|
|
151
|
+
else if (this.type === "application/json") data = JSON.parse(content);
|
|
152
|
+
else {
|
|
153
|
+
const module = await import(__rewriteRelativeImportExtension(
|
|
154
|
+
/* @vite-ignore */
|
|
155
|
+
this.filename
|
|
156
|
+
));
|
|
157
|
+
data = module.default || module;
|
|
191
158
|
}
|
|
192
|
-
if (!Array.isArray(data)) throw new
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
159
|
+
if (!Array.isArray(data)) throw new TypeError(`config file must be a top-level array of entries: ${this.filename}`);
|
|
160
|
+
this.content = content;
|
|
161
|
+
this.data = data;
|
|
162
|
+
await this.checkAccess();
|
|
163
|
+
return true;
|
|
197
164
|
}
|
|
198
|
-
applyPatches(data, patches) {
|
|
165
|
+
applyPatches(data, patches = this.config.patches) {
|
|
199
166
|
return applyEntryPatches(data, patches, (message, ...args) => {
|
|
200
167
|
this.ctx.root.logger?.("loader").warn(message, ...args);
|
|
201
168
|
});
|
|
202
169
|
}
|
|
203
170
|
async *[Service.init]() {
|
|
204
|
-
let candidate;
|
|
205
171
|
try {
|
|
206
|
-
|
|
172
|
+
await this.read();
|
|
207
173
|
} catch (error) {
|
|
208
|
-
if (
|
|
174
|
+
if (error?.code !== "ENOENT") throw error;
|
|
209
175
|
if (this.config.initial) {
|
|
210
176
|
await this._writeFile(this.config.initial);
|
|
211
|
-
|
|
177
|
+
await this.read(true);
|
|
212
178
|
} else throw new Error(`config file not found: ${this.filename}`);
|
|
213
179
|
}
|
|
214
180
|
yield () => this.stop();
|
|
215
|
-
await this.
|
|
181
|
+
await this.root.update(this.applyPatches(this.data));
|
|
216
182
|
}
|
|
217
183
|
async stop() {
|
|
218
|
-
|
|
219
|
-
|
|
184
|
+
try {
|
|
185
|
+
await this.flushWrite();
|
|
186
|
+
} finally {
|
|
187
|
+
this.root.stop();
|
|
188
|
+
await this.flushWrite();
|
|
189
|
+
}
|
|
220
190
|
}
|
|
221
191
|
/**
|
|
222
|
-
* Re-read the file and
|
|
223
|
-
*
|
|
224
|
-
*
|
|
192
|
+
* Re-read the file and refresh child entries when content changed. An
|
|
193
|
+
* unreadable or unparsable file logs a warning and keeps the last good
|
|
194
|
+
* tree: a hot-reload of a live app must never take the process down.
|
|
225
195
|
*/
|
|
226
196
|
async refresh() {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return this.enqueue(() => this._apply(candidate));
|
|
235
|
-
}
|
|
236
|
-
async _apply(candidate) {
|
|
237
|
-
const data = this.applyPatches(candidate.data, this.config.patches);
|
|
238
|
-
await this.root.update(data);
|
|
239
|
-
this.content = candidate.content;
|
|
240
|
-
this.data = candidate.data;
|
|
241
|
-
await this.checkAccess();
|
|
197
|
+
try {
|
|
198
|
+
if (!await this.read()) return;
|
|
199
|
+
await this.root.update(this.applyPatches(this.data));
|
|
200
|
+
} catch (error) {
|
|
201
|
+
this.ctx.logger.warn("config reload at %C failed; keeping the running tree", this.filename);
|
|
202
|
+
this.ctx.logger.warn(error);
|
|
203
|
+
}
|
|
242
204
|
}
|
|
243
205
|
async _writeFile(config) {
|
|
244
206
|
if (this.readonly) throw new Error(`cannot overwrite readonly config`);
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,8 +12,7 @@ export declare const entryListSchema: yaml.Schema;
|
|
|
12
12
|
* Apply patch lists to an entry list — THE patch semantics of this include,
|
|
13
13
|
* shared by mounting (`applyPatches`) and offline config tooling
|
|
14
14
|
* (`dsh --dump-config`) so a dump can never drift from what boots. The input
|
|
15
|
-
* is never mutated
|
|
16
|
-
* patches): patching or mounting shared entry objects would bake earlier
|
|
15
|
+
* is never mutated: patching shared entry objects would bake earlier patch
|
|
17
16
|
* values into the cached parse, so repeated application (config hot-reloads)
|
|
18
17
|
* could never revert a removed or changed patch. Inserted entries are indexed
|
|
19
18
|
* as they are added, so a later patch in the same list can target a row an
|
|
@@ -64,31 +63,18 @@ export declare class Include extends EntryTree {
|
|
|
64
63
|
private writeTask?;
|
|
65
64
|
private pendingWrite?;
|
|
66
65
|
private writeQueue;
|
|
67
|
-
private applyQueue;
|
|
68
66
|
constructor(ctx: Context, config: Include.Config);
|
|
69
|
-
/**
|
|
70
|
-
* Serialize one child-tree mutation behind every earlier one. The group's
|
|
71
|
-
* transactional `update` is not reentrant: two concurrent applies (the init
|
|
72
|
-
* apply racing an HMR-triggered refresh from the watcher's initial scan)
|
|
73
|
-
* interleave create and rollback on the same entries and strand the include
|
|
74
|
-
* fiber without settling, so every apply path funnels through this queue.
|
|
75
|
-
* A predecessor's failure is its own caller's outcome and never gates the
|
|
76
|
-
* next task.
|
|
77
|
-
*/
|
|
78
|
-
private enqueue;
|
|
79
67
|
private checkAccess;
|
|
80
68
|
private read;
|
|
81
69
|
private applyPatches;
|
|
82
70
|
[Service.init](): AsyncGenerator<() => Promise<void>, void, unknown>;
|
|
83
71
|
stop(): Promise<void>;
|
|
84
72
|
/**
|
|
85
|
-
* Re-read the file and
|
|
86
|
-
*
|
|
87
|
-
*
|
|
73
|
+
* Re-read the file and refresh child entries when content changed. An
|
|
74
|
+
* unreadable or unparsable file logs a warning and keeps the last good
|
|
75
|
+
* tree: a hot-reload of a live app must never take the process down.
|
|
88
76
|
*/
|
|
89
77
|
refresh(): Promise<void>;
|
|
90
|
-
private apply;
|
|
91
|
-
private _apply;
|
|
92
78
|
private _writeFile;
|
|
93
79
|
private writeFile;
|
|
94
80
|
private flushWrite;
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAY,KAAK,YAAY,EAAE,MAAM,mCAAmC,CAAA;AACtG,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKtD,OAAO,KAAK,IAAI,MAAM,SAAS,CAAA;AAU/B;;;;;GAKG;AACH,eAAO,MAAM,eAAe,aAAkC,CAAA;AAoB9D
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAY,KAAK,YAAY,EAAE,MAAM,mCAAmC,CAAA;AACtG,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKtD,OAAO,KAAK,IAAI,MAAM,SAAS,CAAA;AAU/B;;;;;GAKG;AACH,eAAO,MAAM,eAAe,aAAkC,CAAA;AAoB9D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,YAAY,EAAE,EACpB,OAAO,EAAE,YAAY,EAAE,GAAG,SAAS,EACnC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAC9C,YAAY,EAAE,CAkEhB;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,YAAY,EAAE,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,SAAS,CAAC,EAAE,GAAG,CAAA;IACf,OAAO,CAAC,EAAE,GAAG,CAAA;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,2DAA2D;AAC3D,yBAAiB,OAAO,CAAC;IACvB,+CAA+C;IAC/C,UAAiB,MAAM;QACrB,qDAAqD;QACrD,IAAI,EAAE,MAAM,CAAA;QACZ,+DAA+D;QAC/D,OAAO,CAAC,EAAE,GAAG,EAAE,CAAA;QACf,sDAAsD;QACtD,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;QACxB,gEAAgE;QAChE,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB;CACF;AAED,uDAAuD;AACvD,qBAAa,OAAQ,SAAQ,SAAS;IAmBH,MAAM,EAAE,OAAO,CAAC,MAAM;IAlBvD,MAAM,CAAC,MAAM,WAAa;IAO1B,MAAM,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,QAAO;IAEhC,QAAQ,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,IAAI,CAAC,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAC,CAAQ;IACxB,OAAO,CAAC,IAAI,CAAC,CAAgB;IAC7B,OAAO,CAAC,SAAS,CAAC,CAA4B;IAC9C,OAAO,CAAC,YAAY,CAAC,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAmC;gBAEzC,GAAG,EAAE,OAAO,EAAS,MAAM,EAAE,OAAO,CAAC,MAAM;YA0BzC,WAAW;YASX,IAAI;IA0BlB,OAAO,CAAC,YAAY;IAMb,CAAC,OAAO,CAAC,IAAI,CAAC;IAoBf,IAAI;IASV;;;;OAIG;IACG,OAAO;YAUC,UAAU;IAqBxB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,UAAU;IAkBlB,uDAAuD;IACvD,KAAK;CAIN;AAED,eAAe,OAAO,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/cordis-plugin-include",
|
|
3
3
|
"description": "Include files in cordis configurations",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.8",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"author": "Shigma <shigma10826@gmail.com>",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@deepseek-ai/cordis-plugin-loader": "^1.0.
|
|
34
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
33
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.4",
|
|
34
|
+
"@deepseek-ai/cordis": "^4.0.3"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
|
-
"@deepseek-ai/cosmokit": "^1.8.
|
|
38
|
+
"@deepseek-ai/cosmokit": "^1.8.4"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,8 +44,7 @@ function retryableWriteError(error: unknown): boolean {
|
|
|
44
44
|
* Apply patch lists to an entry list — THE patch semantics of this include,
|
|
45
45
|
* shared by mounting (`applyPatches`) and offline config tooling
|
|
46
46
|
* (`dsh --dump-config`) so a dump can never drift from what boots. The input
|
|
47
|
-
* is never mutated
|
|
48
|
-
* patches): patching or mounting shared entry objects would bake earlier
|
|
47
|
+
* is never mutated: patching shared entry objects would bake earlier patch
|
|
49
48
|
* values into the cached parse, so repeated application (config hot-reloads)
|
|
50
49
|
* could never revert a removed or changed patch. Inserted entries are indexed
|
|
51
50
|
* as they are added, so a later patch in the same list can target a row an
|
|
@@ -60,8 +59,8 @@ export function applyEntryPatches(
|
|
|
60
59
|
patches: PatchOptions[] | undefined,
|
|
61
60
|
warn: (message: string, ...args: any[]) => void,
|
|
62
61
|
): EntryOptions[] {
|
|
62
|
+
if (!patches?.length) return [...data]
|
|
63
63
|
data = structuredClone(data)
|
|
64
|
-
if (!patches?.length) return data
|
|
65
64
|
|
|
66
65
|
const entryMap = new Map<string, EntryOptions>()
|
|
67
66
|
const buildMap = (entries: EntryOptions[]) => {
|
|
@@ -127,20 +126,6 @@ export function applyEntryPatches(
|
|
|
127
126
|
return data
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
type ConfigUpdateStage = 'read' | 'parse' | 'validate'
|
|
131
|
-
|
|
132
|
-
interface ReadCandidate {
|
|
133
|
-
content: string
|
|
134
|
-
data: EntryOptions[]
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
class ConfigFileError extends Error {
|
|
138
|
-
constructor(public readonly stage: ConfigUpdateStage, path: string, cause: unknown) {
|
|
139
|
-
super(`failed to ${stage} config file ${path}`, { cause })
|
|
140
|
-
this.name = 'ConfigFileError'
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
129
|
/** Runtime patch applied to entries loaded from an included config file. */
|
|
145
130
|
export interface PatchOptions {
|
|
146
131
|
id?: string
|
|
@@ -189,7 +174,6 @@ export class Include extends EntryTree {
|
|
|
189
174
|
private writeTask?: NodeJS.Timeout | undefined
|
|
190
175
|
private pendingWrite?: EntryOptions[]
|
|
191
176
|
private writeQueue: Promise<void> = Promise.resolve()
|
|
192
|
-
private applyQueue: Promise<unknown> = Promise.resolve()
|
|
193
177
|
|
|
194
178
|
constructor(ctx: Context, public config: Include.Config) {
|
|
195
179
|
super(ctx)
|
|
@@ -203,31 +187,20 @@ export class Include extends EntryTree {
|
|
|
203
187
|
this.readonly = !this.type
|
|
204
188
|
this.ctx.baseUrl = new URL('.', pathToFileURL(this.filename)).href
|
|
205
189
|
|
|
206
|
-
ctx.on('internal/update',
|
|
190
|
+
ctx.on('internal/update', (config, _, next) => {
|
|
207
191
|
if (config.path !== this.config.path) return next()
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
192
|
+
// Veto the fiber restart (children update in place), but persist the new
|
|
193
|
+
// config ourselves — `Fiber.update` only assigns `this.config` behind
|
|
194
|
+
// `next()`, and a stale `this.config.patches` would make the next
|
|
195
|
+
// `refresh()` re-apply the old overlay.
|
|
196
|
+
this.config = config
|
|
197
|
+
this.root.update(this.applyPatches(this.data!, config.patches)).catch((error) => {
|
|
198
|
+
this.ctx.logger.warn('config update at %C failed', this.filename)
|
|
199
|
+
this.ctx.logger.warn(error)
|
|
212
200
|
})
|
|
213
201
|
})
|
|
214
202
|
}
|
|
215
203
|
|
|
216
|
-
/**
|
|
217
|
-
* Serialize one child-tree mutation behind every earlier one. The group's
|
|
218
|
-
* transactional `update` is not reentrant: two concurrent applies (the init
|
|
219
|
-
* apply racing an HMR-triggered refresh from the watcher's initial scan)
|
|
220
|
-
* interleave create and rollback on the same entries and strand the include
|
|
221
|
-
* fiber without settling, so every apply path funnels through this queue.
|
|
222
|
-
* A predecessor's failure is its own caller's outcome and never gates the
|
|
223
|
-
* next task.
|
|
224
|
-
*/
|
|
225
|
-
private enqueue<T>(task: () => Promise<T>): Promise<T> {
|
|
226
|
-
const run = this.applyQueue.then(task, task)
|
|
227
|
-
this.applyQueue = run.then(() => {}, () => {})
|
|
228
|
-
return run
|
|
229
|
-
}
|
|
230
|
-
|
|
231
204
|
private async checkAccess() {
|
|
232
205
|
if (!this.type) return
|
|
233
206
|
try {
|
|
@@ -237,87 +210,80 @@ export class Include extends EntryTree {
|
|
|
237
210
|
}
|
|
238
211
|
}
|
|
239
212
|
|
|
240
|
-
private async read(forced = false)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
content = await readFile(this.filename, 'utf8')
|
|
244
|
-
} catch (error) {
|
|
245
|
-
throw new ConfigFileError('read', this.filename, error)
|
|
246
|
-
}
|
|
247
|
-
if (!forced && this.content === content) return
|
|
213
|
+
private async read(forced = false) {
|
|
214
|
+
const content = await readFile(this.filename, 'utf8')
|
|
215
|
+
if (!forced && this.content === content) return false
|
|
248
216
|
let data: any
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
data = module.default || module
|
|
257
|
-
}
|
|
258
|
-
} catch (error) {
|
|
259
|
-
throw new ConfigFileError('parse', this.filename, error)
|
|
217
|
+
if (this.type === 'application/yaml') {
|
|
218
|
+
data = yaml.load(content, { schema: entryListSchema })
|
|
219
|
+
} else if (this.type === 'application/json') {
|
|
220
|
+
data = JSON.parse(content)
|
|
221
|
+
} else {
|
|
222
|
+
const module = await import(/* @vite-ignore */ this.filename)
|
|
223
|
+
data = module.default || module
|
|
260
224
|
}
|
|
225
|
+
// An empty or truncated file (common mid-edit: editors and `sed -i` write
|
|
226
|
+
// through temp states) parses to `undefined`, not an error; reject every
|
|
227
|
+
// non-array shape here so callers see one "invalid file" signal. Content
|
|
228
|
+
// and data commit only on success, so an edit that is later reverted to
|
|
229
|
+
// the exact last good content correctly reads as "unchanged".
|
|
261
230
|
if (!Array.isArray(data)) {
|
|
262
|
-
throw new
|
|
231
|
+
throw new TypeError(`config file must be a top-level array of entries: ${this.filename}`)
|
|
263
232
|
}
|
|
264
|
-
|
|
233
|
+
this.content = content
|
|
234
|
+
this.data = data
|
|
235
|
+
await this.checkAccess()
|
|
236
|
+
return true
|
|
265
237
|
}
|
|
266
238
|
|
|
267
|
-
private applyPatches(data: EntryOptions[], patches
|
|
239
|
+
private applyPatches(data: EntryOptions[], patches = this.config.patches): EntryOptions[] {
|
|
268
240
|
return applyEntryPatches(data, patches, (message, ...args) => {
|
|
269
241
|
this.ctx.root.logger?.('loader').warn(message, ...args)
|
|
270
242
|
})
|
|
271
243
|
}
|
|
272
244
|
|
|
273
245
|
async* [Service.init]() {
|
|
274
|
-
let candidate: ReadCandidate
|
|
275
246
|
try {
|
|
276
|
-
|
|
247
|
+
await this.read()
|
|
277
248
|
} catch (error) {
|
|
278
|
-
|
|
249
|
+
// Only a missing file falls back to `initial` (or the not-found error):
|
|
250
|
+
// an existing-but-invalid file must fail loud with its real parse error,
|
|
251
|
+
// never be mislabelled as absent or silently overwritten.
|
|
252
|
+
if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') throw error
|
|
279
253
|
if (this.config.initial) {
|
|
280
254
|
await this._writeFile(this.config.initial as any)
|
|
281
|
-
|
|
255
|
+
await this.read(true)
|
|
282
256
|
} else {
|
|
283
257
|
throw new Error(`config file not found: ${this.filename}`)
|
|
284
258
|
}
|
|
285
259
|
}
|
|
286
260
|
|
|
287
261
|
yield () => this.stop()
|
|
288
|
-
await this.
|
|
262
|
+
await this.root.update(this.applyPatches(this.data!))
|
|
289
263
|
}
|
|
290
264
|
|
|
291
265
|
async stop() {
|
|
292
|
-
|
|
293
|
-
|
|
266
|
+
try {
|
|
267
|
+
await this.flushWrite()
|
|
268
|
+
} finally {
|
|
269
|
+
this.root.stop()
|
|
270
|
+
await this.flushWrite()
|
|
271
|
+
}
|
|
294
272
|
}
|
|
295
273
|
|
|
296
274
|
/**
|
|
297
|
-
* Re-read the file and
|
|
298
|
-
*
|
|
299
|
-
*
|
|
275
|
+
* Re-read the file and refresh child entries when content changed. An
|
|
276
|
+
* unreadable or unparsable file logs a warning and keeps the last good
|
|
277
|
+
* tree: a hot-reload of a live app must never take the process down.
|
|
300
278
|
*/
|
|
301
279
|
async refresh() {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private apply(candidate: ReadCandidate) {
|
|
312
|
-
return this.enqueue(() => this._apply(candidate))
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private async _apply(candidate: ReadCandidate) {
|
|
316
|
-
const data = this.applyPatches(candidate.data, this.config.patches)
|
|
317
|
-
await this.root.update(data)
|
|
318
|
-
this.content = candidate.content
|
|
319
|
-
this.data = candidate.data
|
|
320
|
-
await this.checkAccess()
|
|
280
|
+
try {
|
|
281
|
+
if (!await this.read()) return
|
|
282
|
+
await this.root.update(this.applyPatches(this.data!))
|
|
283
|
+
} catch (error) {
|
|
284
|
+
this.ctx.logger.warn('config reload at %C failed; keeping the running tree', this.filename)
|
|
285
|
+
this.ctx.logger.warn(error)
|
|
286
|
+
}
|
|
321
287
|
}
|
|
322
288
|
|
|
323
289
|
private async _writeFile(config: EntryOptions[]) {
|