@guanjinke/artifact-client 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -363
- package/dist/cli/environment.d.ts +2 -1
- package/dist/cli/environment.d.ts.map +1 -1
- package/dist/cli/environment.js +71 -5
- package/dist/cli/environment.js.map +1 -1
- package/dist/cli/setup.js +1 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/environment/update.d.ts +26 -5
- package/dist/environment/update.d.ts.map +1 -1
- package/dist/environment/update.js +252 -86
- package/dist/environment/update.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -67,19 +67,26 @@ const allModules = 'all';
|
|
|
67
67
|
function parseModuleSpec(value) {
|
|
68
68
|
const equals = value.indexOf('=');
|
|
69
69
|
if (equals >= 0) {
|
|
70
|
-
const
|
|
71
|
-
const
|
|
70
|
+
const token = value.slice(0, equals).trim();
|
|
71
|
+
const hash = token.indexOf('#');
|
|
72
|
+
const module = (hash >= 0 ? token.slice(0, hash) : token).trim().toLowerCase();
|
|
73
|
+
const assembly = hash >= 0 ? token.slice(hash + 1).trim() : undefined;
|
|
72
74
|
if (!module)
|
|
73
75
|
throw new Error(`Invalid module spec "${value}": missing module name.`);
|
|
76
|
+
if (assembly !== undefined && !assembly)
|
|
77
|
+
throw new Error(`Invalid module spec "${value}": missing assembly name after #.`);
|
|
78
|
+
if (assembly !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(assembly))
|
|
79
|
+
throw new Error(`Invalid module spec "${value}": invalid assembly file name.`);
|
|
74
80
|
if (module === allModules)
|
|
75
81
|
throw new Error(`Invalid module spec "${value}": all selects every configured module from remote and cannot point at a local source.`);
|
|
76
82
|
// The module token itself may not carry a pinned version: `=` and `:` select
|
|
77
83
|
// different sources. A Windows drive letter inside the path is not that.
|
|
78
84
|
if (module.includes(':'))
|
|
79
85
|
throw new Error(`Invalid module spec "${value}": a local source (=) cannot also pin a version (:).`);
|
|
86
|
+
const localPath = value.slice(equals + 1).trim();
|
|
80
87
|
if (!localPath)
|
|
81
88
|
throw new Error(`Invalid module spec "${value}": missing local DLL path.`);
|
|
82
|
-
return { module, localPath };
|
|
89
|
+
return { module, localPath, ...(assembly !== undefined ? { assembly } : {}) };
|
|
83
90
|
}
|
|
84
91
|
const colon = value.indexOf(':');
|
|
85
92
|
if (colon >= 0) {
|
|
@@ -100,8 +107,16 @@ function parseModuleSpec(value) {
|
|
|
100
107
|
return { module: allModules };
|
|
101
108
|
return { module };
|
|
102
109
|
}
|
|
103
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Record one parsed spec, enforcing "all alone" and duplicate rules.
|
|
112
|
+
*
|
|
113
|
+
* A module may be named several times only when every one of its specs is a
|
|
114
|
+
* local source: each local spec resolves to one or more targets, and the update
|
|
115
|
+
* refuses a target that is selected twice. A remote spec replaces the whole
|
|
116
|
+
* module, so it may never share a module with another spec.
|
|
117
|
+
*/
|
|
104
118
|
function pushSpec(specs, seen, spec) {
|
|
119
|
+
const kind = spec.localPath === undefined ? 'remote' : 'local';
|
|
105
120
|
if (spec.module === allModules) {
|
|
106
121
|
if (seen.has(allModules))
|
|
107
122
|
throw new Error('Duplicate module in one update: all.');
|
|
@@ -112,10 +127,11 @@ function pushSpec(specs, seen, spec) {
|
|
|
112
127
|
validateUpdatableModule(spec.module);
|
|
113
128
|
if (seen.has(allModules))
|
|
114
129
|
throw new Error('Module spec all cannot be combined with individual modules.');
|
|
115
|
-
|
|
130
|
+
const previous = seen.get(spec.module);
|
|
131
|
+
if (previous !== undefined && (previous === 'remote' || kind === 'remote'))
|
|
116
132
|
throw new Error(`Duplicate module in one update: ${spec.module}.`);
|
|
117
133
|
}
|
|
118
|
-
seen.
|
|
134
|
+
seen.set(spec.module, kind);
|
|
119
135
|
specs.push(spec);
|
|
120
136
|
}
|
|
121
137
|
/**
|
|
@@ -130,7 +146,7 @@ function parseModuleSpecs(values) {
|
|
|
130
146
|
if (!Array.isArray(values))
|
|
131
147
|
throw new Error('Module specs must be an array of strings.');
|
|
132
148
|
const specs = [];
|
|
133
|
-
const seen = new
|
|
149
|
+
const seen = new Map();
|
|
134
150
|
for (const value of values) {
|
|
135
151
|
const text = String(value ?? '');
|
|
136
152
|
for (const fragment of text.includes('=') ? text.split(',') : text.split(/[\s,]+/)) {
|
|
@@ -146,7 +162,7 @@ function normalizeModuleSpecs(values) {
|
|
|
146
162
|
if (!Array.isArray(values))
|
|
147
163
|
throw new Error('Module specs must be an array.');
|
|
148
164
|
const specs = [];
|
|
149
|
-
const seen = new
|
|
165
|
+
const seen = new Map();
|
|
150
166
|
const push = (spec) => pushSpec(specs, seen, spec);
|
|
151
167
|
for (const value of values) {
|
|
152
168
|
if (typeof value === 'string') {
|
|
@@ -159,10 +175,11 @@ function normalizeModuleSpecs(values) {
|
|
|
159
175
|
const module = typeof value.module === 'string' ? value.module.trim().toLowerCase() : '';
|
|
160
176
|
const version = value.version === undefined ? undefined : String(value.version);
|
|
161
177
|
const localPath = value.localPath === undefined ? undefined : String(value.localPath);
|
|
178
|
+
const assembly = value.assembly === undefined ? undefined : String(value.assembly);
|
|
162
179
|
if (!module)
|
|
163
180
|
throw new Error('Invalid module spec: missing module name.');
|
|
164
181
|
if (module === allModules) {
|
|
165
|
-
if (version !== undefined || localPath !== undefined)
|
|
182
|
+
if (version !== undefined || localPath !== undefined || assembly !== undefined)
|
|
166
183
|
throw new Error('Module spec all cannot pin a version or point at a local source.');
|
|
167
184
|
push({ module: allModules });
|
|
168
185
|
continue;
|
|
@@ -171,37 +188,28 @@ function normalizeModuleSpecs(values) {
|
|
|
171
188
|
throw new Error(`Invalid version for module ${module}.`);
|
|
172
189
|
if (version !== undefined && localPath !== undefined)
|
|
173
190
|
throw new Error(`Module ${module} cannot combine a local path (=) with a pinned version (:).`);
|
|
191
|
+
if (assembly !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(assembly))
|
|
192
|
+
throw new Error(`Invalid assembly file name for module ${module}.`);
|
|
193
|
+
if (assembly !== undefined && localPath === undefined)
|
|
194
|
+
throw new Error(`Module ${module} names an assembly (#) without a local path (=).`);
|
|
174
195
|
if (version === undefined && localPath === undefined) {
|
|
175
196
|
push({ module });
|
|
176
197
|
continue;
|
|
177
198
|
}
|
|
178
|
-
push({ module, ...(version !== undefined ? { version } : {}), ...(localPath !== undefined ? { localPath } : {}) });
|
|
199
|
+
push({ module, ...(version !== undefined ? { version } : {}), ...(localPath !== undefined ? { localPath } : {}), ...(assembly !== undefined ? { assembly } : {}) });
|
|
179
200
|
}
|
|
180
201
|
return specs;
|
|
181
202
|
}
|
|
182
|
-
/**
|
|
183
|
-
|
|
184
|
-
*
|
|
185
|
-
* The document is parsed with the creation path's parser options, only the
|
|
186
|
-
* `sys:string` plugin list is mutated, and the result is rebuilt with the same
|
|
187
|
-
* builder options and re-validated before it is returned. Dashboard stays the
|
|
188
|
-
* first entry; unknown existing entries keep their order.
|
|
189
|
-
*/
|
|
190
|
-
function patchEnvironmentConfiguration(xml, entries) {
|
|
191
|
-
if (typeof xml !== 'string' || /<!DOCTYPE|<!ENTITY/i.test(xml) || fast_xml_parser_1.XMLValidator.validate(xml) !== true)
|
|
192
|
-
throw new Error('Invalid environment configuration.');
|
|
193
|
-
const document = new fast_xml_parser_1.XMLParser(parserOptions).parse(xml);
|
|
194
|
-
const server = document?.SystemConfig?.Services?.Server;
|
|
195
|
-
if (!server || typeof server !== 'object' || Array.isArray(server))
|
|
196
|
-
throw new Error('Environment configuration has no Server service.');
|
|
197
|
-
const plugins = server.Plugins ?? {};
|
|
198
|
-
if (typeof plugins !== 'object' || Array.isArray(plugins))
|
|
199
|
-
throw new Error('Invalid environment configuration.');
|
|
200
|
-
const raw = plugins['sys:string'];
|
|
203
|
+
/** Read one `<sys:string>` list, accepting a single string instead of an array. */
|
|
204
|
+
function stringList(raw) {
|
|
201
205
|
const list = raw === undefined ? [] : Array.isArray(raw) ? [...raw] : [raw];
|
|
202
206
|
if (list.some(value => typeof value !== 'string'))
|
|
203
207
|
throw new Error('Invalid environment configuration.');
|
|
204
|
-
|
|
208
|
+
return list;
|
|
209
|
+
}
|
|
210
|
+
/** Replace or append one `<sys:string>` path list, matching by destination or assembly name. */
|
|
211
|
+
function rewritePathList(values, entries) {
|
|
212
|
+
const strings = values.map(value => value.trim());
|
|
205
213
|
const used = new Set();
|
|
206
214
|
const appended = [];
|
|
207
215
|
for (const entry of entries) {
|
|
@@ -220,16 +228,64 @@ function patchEnvironmentConfiguration(xml, entries) {
|
|
|
220
228
|
strings[index] = windowsPath(entry.path);
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
return [...strings, ...appended];
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Rewrite the `<Server><Plugins>`, `<Server><PluginWorkers>` and worker service
|
|
235
|
+
* entries of a Wyn configuration document.
|
|
236
|
+
*
|
|
237
|
+
* The document is parsed with the creation path's parser options, only the
|
|
238
|
+
* `sys:string` path lists and the matching worker service nodes are mutated, and
|
|
239
|
+
* the result is rebuilt with the same builder options and re-validated before it
|
|
240
|
+
* is returned. Dashboard stays the first `Plugins` entry; every other entry keeps
|
|
241
|
+
* its order. A worker service keeps all of its other fields, including
|
|
242
|
+
* `Disabled`; a configuration without that node is updated through
|
|
243
|
+
* `<PluginWorkers>` alone.
|
|
244
|
+
*/
|
|
245
|
+
function patchEnvironmentConfiguration(xml, entries) {
|
|
246
|
+
if (typeof xml !== 'string' || /<!DOCTYPE|<!ENTITY/i.test(xml) || fast_xml_parser_1.XMLValidator.validate(xml) !== true)
|
|
247
|
+
throw new Error('Invalid environment configuration.');
|
|
248
|
+
const document = new fast_xml_parser_1.XMLParser(parserOptions).parse(xml);
|
|
249
|
+
const server = document?.SystemConfig?.Services?.Server;
|
|
250
|
+
if (!server || typeof server !== 'object' || Array.isArray(server))
|
|
251
|
+
throw new Error('Environment configuration has no Server service.');
|
|
252
|
+
const pluginEntries = entries.filter(entry => entry.role !== 'worker');
|
|
253
|
+
if (server.Plugins !== undefined || pluginEntries.length) {
|
|
254
|
+
const plugins = server.Plugins === undefined || server.Plugins === '' ? {} : server.Plugins;
|
|
255
|
+
if (typeof plugins !== 'object' || Array.isArray(plugins))
|
|
256
|
+
throw new Error('Invalid environment configuration.');
|
|
257
|
+
const combined = rewritePathList(stringList(plugins['sys:string']), pluginEntries);
|
|
258
|
+
// Keep Dashboard first even when it is a local build path rather than a deployed one.
|
|
259
|
+
const dashboard = combined.findIndex(value => {
|
|
260
|
+
const folded = windowsPath(value).toLowerCase();
|
|
261
|
+
return folded.startsWith('plugins\\dashboard\\') || path.win32.basename(folded) === 'wyn.dashboards.plugin.dll';
|
|
262
|
+
});
|
|
263
|
+
if (dashboard > 0)
|
|
264
|
+
combined.unshift(combined.splice(dashboard, 1)[0]);
|
|
265
|
+
plugins['sys:string'] = combined;
|
|
266
|
+
server.Plugins = plugins;
|
|
267
|
+
}
|
|
268
|
+
const workers = entries.filter(entry => entry.role === 'worker');
|
|
269
|
+
if (workers.length) {
|
|
270
|
+
const pluginWorkers = server.PluginWorkers === undefined || server.PluginWorkers === '' ? {} : server.PluginWorkers;
|
|
271
|
+
if (typeof pluginWorkers !== 'object' || Array.isArray(pluginWorkers))
|
|
272
|
+
throw new Error('Invalid environment configuration.');
|
|
273
|
+
pluginWorkers['sys:string'] = rewritePathList(stringList(pluginWorkers['sys:string']), workers);
|
|
274
|
+
server.PluginWorkers = pluginWorkers;
|
|
275
|
+
const services = document?.SystemConfig?.Services;
|
|
276
|
+
for (const entry of workers) {
|
|
277
|
+
if (!entry.service || !services || typeof services !== 'object' || Array.isArray(services))
|
|
278
|
+
continue;
|
|
279
|
+
const service = services[entry.service];
|
|
280
|
+
if (!service || typeof service !== 'object' || Array.isArray(service))
|
|
281
|
+
continue;
|
|
282
|
+
const node = service;
|
|
283
|
+
// The worker is loaded from the path list above and uses its own directory
|
|
284
|
+
// as its base directory: keep the service in step without touching Disabled.
|
|
285
|
+
node.WorkingDirectory = windowsPath(path.win32.dirname(entry.path));
|
|
286
|
+
node.Assembly = path.win32.basename(entry.path);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
233
289
|
const output = new fast_xml_parser_1.XMLBuilder(builderOptions).build(document);
|
|
234
290
|
if (fast_xml_parser_1.XMLValidator.validate(output) !== true)
|
|
235
291
|
throw new Error('Invalid environment configuration.');
|
|
@@ -270,8 +326,37 @@ async function readManifest(root) {
|
|
|
270
326
|
throw new Error('Environment manifest is unreadable.');
|
|
271
327
|
if (manifest.moduleSources !== undefined && (!manifest.moduleSources || typeof manifest.moduleSources !== 'object' || Array.isArray(manifest.moduleSources)))
|
|
272
328
|
throw new Error('Environment manifest is unreadable.');
|
|
329
|
+
for (const source of Object.values(manifest.moduleSources ?? {})) {
|
|
330
|
+
if (!source || typeof source !== 'object' || Array.isArray(source) || source.type !== 'local' || typeof source.path !== 'string' || !Array.isArray(source.pluginDirectories) || source.pluginDirectories.some(directory => typeof directory !== 'string'))
|
|
331
|
+
throw new Error('Environment manifest is unreadable.');
|
|
332
|
+
if (source.targets !== undefined && (!Array.isArray(source.targets) || source.targets.some(target => !target || typeof target !== 'object' || target.kind !== 'plugin' && target.kind !== 'worker' || typeof target.destination !== 'string' || typeof target.assembly !== 'string' || typeof target.path !== 'string')))
|
|
333
|
+
throw new Error('Environment manifest is unreadable.');
|
|
334
|
+
}
|
|
273
335
|
return manifest;
|
|
274
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Normalize a recorded module source into targets.
|
|
339
|
+
*
|
|
340
|
+
* The 1.4.0 shape (`path` plus `pluginDirectories`) is still written whenever it
|
|
341
|
+
* describes the state exactly; it always meant one plugin target per recorded
|
|
342
|
+
* directory.
|
|
343
|
+
*/
|
|
344
|
+
function localTargetsOf(module, source) {
|
|
345
|
+
if (!source)
|
|
346
|
+
return [];
|
|
347
|
+
if (Array.isArray(source.targets) && source.targets.length)
|
|
348
|
+
return source.targets.map(target => ({ module, kind: target.kind, destination: target.destination, assembly: target.assembly, path: target.path }));
|
|
349
|
+
return (source.pluginDirectories ?? []).map(destination => ({ module, kind: 'plugin', destination, assembly: path.win32.basename(source.path), path: source.path }));
|
|
350
|
+
}
|
|
351
|
+
/** Record the local targets of one module, keeping earlier targets of that module. */
|
|
352
|
+
function manifestSource(module, targets, previous) {
|
|
353
|
+
const overridden = new Set(targets.map(target => target.destination.toLowerCase()));
|
|
354
|
+
const merged = [...localTargetsOf(module, previous).filter(target => !overridden.has(target.destination.toLowerCase())), ...targets];
|
|
355
|
+
const source = { type: 'local', path: merged[0].path, pluginDirectories: merged.filter(target => target.kind === 'plugin').map(target => target.destination) };
|
|
356
|
+
if (merged.length > 1 || merged[0].kind !== 'plugin')
|
|
357
|
+
source.targets = merged.map(({ kind, destination, assembly, path: file }) => ({ kind, destination, assembly, path: file }));
|
|
358
|
+
return source;
|
|
359
|
+
}
|
|
275
360
|
/** Write JSON (or any text) through `<file>.afm-tmp` and an atomic rename. */
|
|
276
361
|
async function writeAtomic(file, content) {
|
|
277
362
|
const temporary = `${file}.afm-tmp`;
|
|
@@ -348,45 +433,98 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
348
433
|
throw new Error(`Module update failed for ${artifact.plugin}/${artifact.version}/${artifact.name} (download, archive or layout): ${error.message}`);
|
|
349
434
|
}
|
|
350
435
|
}
|
|
351
|
-
/**
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
436
|
+
/** Mappings of one module that a local source may override. */
|
|
437
|
+
const localMappings = (module) => catalog_1.catalog[module].mappings.filter(mapping => mapping.role === 'plugin' || mapping.role === 'worker');
|
|
438
|
+
const expectedAssemblies = (module) => [...new Set(localMappings(module).flatMap(mapping => mapping.assemblies))];
|
|
439
|
+
const lstatOrUndefined = (target) => fs.lstat(target).catch((error) => { if (error.code === 'ENOENT')
|
|
440
|
+
return undefined; throw error; });
|
|
441
|
+
/** Locate the single mapping of `module` whose known assemblies include `assembly`. */
|
|
442
|
+
function mappingForAssembly(module, assembly) {
|
|
443
|
+
const wanted = assembly.toLowerCase();
|
|
444
|
+
const matches = localMappings(module).filter(mapping => mapping.assemblies.some(name => name.toLowerCase() === wanted));
|
|
445
|
+
if (matches.length !== 1)
|
|
446
|
+
throw new Error(`Local target ${assembly} is not one assembly of module ${module}. Expected one of ${expectedAssemblies(module).join(', ')}.`);
|
|
447
|
+
return { mapping: matches[0], assembly: matches[0].assemblies.find(name => name.toLowerCase() === wanted) };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Resolve one local source into the module targets it overrides.
|
|
451
|
+
*
|
|
452
|
+
* A DLL selects the mapping whose assembly file name it matches. A directory
|
|
453
|
+
* selects every mapping that holds exactly one of its assemblies, so a partial
|
|
454
|
+
* developer build only overrides what it really contains; the other mappings are
|
|
455
|
+
* reported as inactive and stay deployed. `module#<assembly>=` names the target
|
|
456
|
+
* explicitly, for a build whose file name matches no assembly at all.
|
|
457
|
+
*/
|
|
458
|
+
async function resolveLocalSource(entry, module, assembly, input) {
|
|
459
|
+
const mappings = localMappings(module);
|
|
460
|
+
const source = path.resolve(input);
|
|
461
|
+
const stat = await lstatOrUndefined(source);
|
|
360
462
|
if (!stat)
|
|
361
|
-
throw new Error(`Local source not found: ${
|
|
362
|
-
let dll;
|
|
463
|
+
throw new Error(`Local source not found: ${source}.`);
|
|
363
464
|
if (stat.isSymbolicLink())
|
|
364
|
-
throw new Error(`Local source must be a regular file or directory: ${
|
|
465
|
+
throw new Error(`Local source must be a regular file or directory: ${source}.`);
|
|
466
|
+
const selected = [];
|
|
467
|
+
const inactive = [];
|
|
365
468
|
if (stat.isFile()) {
|
|
366
|
-
if (path.extname(
|
|
367
|
-
throw new Error(`Local source must be a .dll file: ${
|
|
368
|
-
|
|
469
|
+
if (path.extname(source).toLowerCase() !== '.dll')
|
|
470
|
+
throw new Error(`Local source must be a .dll file: ${source}.`);
|
|
471
|
+
if (assembly === undefined) {
|
|
472
|
+
const name = path.win32.basename(source).toLowerCase();
|
|
473
|
+
const matches = mappings.filter(mapping => mapping.assemblies.some(known => known.toLowerCase() === name));
|
|
474
|
+
if (matches.length !== 1)
|
|
475
|
+
throw new Error(`Local source ${source} matches ${matches.length} targets of module ${module}. Expected one of ${expectedAssemblies(module).join(', ')}; select one explicitly with ${module}#<assembly>=<path>.`);
|
|
476
|
+
selected.push({ mapping: matches[0], assembly: matches[0].assemblies.find(known => known.toLowerCase() === name), path: source });
|
|
477
|
+
inactive.push(...mappings.filter(mapping => mapping !== matches[0]).map(mapping => mapping.destination));
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const named = mappingForAssembly(module, assembly);
|
|
481
|
+
// The explicit form exists for a build whose file name matches no assembly;
|
|
482
|
+
// a file that is another target's assembly is still a mistake.
|
|
483
|
+
const basename = path.win32.basename(source);
|
|
484
|
+
const conflicts = mappings.filter(mapping => mapping !== named.mapping && mapping.assemblies.some(known => known.toLowerCase() === basename.toLowerCase()));
|
|
485
|
+
if (conflicts.length)
|
|
486
|
+
throw new Error(`Local source ${source} is an assembly of ${conflicts.map(mapping => mapping.destination).join(', ')}, not of ${named.mapping.destination}: ${module}#${assembly}= selects ${named.mapping.destination}.`);
|
|
487
|
+
selected.push({ mapping: named.mapping, assembly: named.assembly, path: source });
|
|
488
|
+
inactive.push(...mappings.filter(mapping => mapping !== named.mapping).map(mapping => mapping.destination));
|
|
489
|
+
}
|
|
369
490
|
}
|
|
370
491
|
else if (stat.isDirectory()) {
|
|
371
|
-
const
|
|
372
|
-
for (const
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
492
|
+
const named = assembly === undefined ? undefined : mappingForAssembly(module, assembly);
|
|
493
|
+
for (const mapping of named ? [named.mapping] : mappings) {
|
|
494
|
+
const present = [];
|
|
495
|
+
for (const name of named ? [named.assembly] : mapping.assemblies) {
|
|
496
|
+
const candidate = path.join(source, name);
|
|
497
|
+
const candidateStat = await lstatOrUndefined(candidate);
|
|
498
|
+
if (candidateStat?.isFile() && !candidateStat.isSymbolicLink())
|
|
499
|
+
present.push({ assembly: name, path: candidate });
|
|
500
|
+
}
|
|
501
|
+
if (present.length === 1)
|
|
502
|
+
selected.push({ mapping, ...present[0] });
|
|
503
|
+
else
|
|
504
|
+
inactive.push(mapping.destination);
|
|
505
|
+
}
|
|
506
|
+
// An explicit selection leaves every other mapping of the module deployed.
|
|
507
|
+
if (named)
|
|
508
|
+
inactive.push(...mappings.filter(mapping => mapping !== named.mapping).map(mapping => mapping.destination));
|
|
509
|
+
if (!selected.length) {
|
|
510
|
+
if (named)
|
|
511
|
+
throw new Error(`Local source directory ${source} does not contain ${named.assembly}.`);
|
|
512
|
+
throw new Error(`Local source directory ${source} must contain exactly one of ${expectedAssemblies(module).join(', ')} for module ${module}; select one explicitly with ${module}#<assembly>=<path>.`);
|
|
378
513
|
}
|
|
379
|
-
if (found.length !== 1)
|
|
380
|
-
throw new Error(`Local source directory ${target} must contain exactly one of ${mapping.assemblies.join(', ')}; found ${found.length}.`);
|
|
381
|
-
dll = found[0];
|
|
382
514
|
}
|
|
383
515
|
else
|
|
384
|
-
throw new Error(`Local source must be a regular .dll file or a directory: ${
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
516
|
+
throw new Error(`Local source must be a regular .dll file or a directory: ${source}.`);
|
|
517
|
+
const targets = selected.map(item => {
|
|
518
|
+
const deployed = installationDirectory(entry.root, item.mapping.destination);
|
|
519
|
+
const relative = path.relative(deployed.toLowerCase(), item.path.toLowerCase());
|
|
520
|
+
if (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative))
|
|
521
|
+
throw new Error(`Local source ${item.path} is inside the deployed ${item.mapping.role === 'worker' ? 'worker' : 'plugin'} directory ${deployed}; point at the developer build output outside the environment.`);
|
|
522
|
+
const target = { module, kind: item.mapping.role, destination: item.mapping.destination, assembly: item.assembly, path: item.path };
|
|
523
|
+
if (item.mapping.service)
|
|
524
|
+
target.service = item.mapping.service;
|
|
525
|
+
return target;
|
|
526
|
+
});
|
|
527
|
+
return { targets, inactive, source };
|
|
390
528
|
}
|
|
391
529
|
/**
|
|
392
530
|
* Update selected modules of a registered environment.
|
|
@@ -442,12 +580,23 @@ class EnvironmentUpdater {
|
|
|
442
580
|
onProgress?.(`Updating all configured modules from remote: ${selected.join(', ')}${skipped.length ? ` (skipping local: ${skipped.map(item => item.module).join(', ')})` : ''}`);
|
|
443
581
|
}
|
|
444
582
|
const localOverrides = new Map();
|
|
583
|
+
const selectedTargets = new Set();
|
|
445
584
|
for (const spec of parsed) {
|
|
446
585
|
if (spec.module === 'sheets' && entry.edition !== 'en')
|
|
447
586
|
throw new Error(`Module sheets requires an EN edition environment; ${entry.name} is ${entry.edition}.`);
|
|
448
587
|
if (spec.localPath === undefined)
|
|
449
588
|
continue;
|
|
450
|
-
|
|
589
|
+
const resolution = await resolveLocalSource(entry, spec.module, spec.assembly, spec.localPath);
|
|
590
|
+
for (const destination of resolution.inactive)
|
|
591
|
+
onProgress?.(`${spec.module}: ${destination} stays deployed (not selected by ${resolution.source})`);
|
|
592
|
+
const targets = localOverrides.get(spec.module) ?? [];
|
|
593
|
+
for (const target of resolution.targets) {
|
|
594
|
+
if (selectedTargets.has(target.destination.toLowerCase()))
|
|
595
|
+
throw new Error(`Local target ${target.assembly} of module ${target.module} is selected twice in one update; give one spec per assembly.`);
|
|
596
|
+
selectedTargets.add(target.destination.toLowerCase());
|
|
597
|
+
targets.push(target);
|
|
598
|
+
}
|
|
599
|
+
localOverrides.set(spec.module, targets);
|
|
451
600
|
}
|
|
452
601
|
const remote = parsed.filter(spec => spec.localPath === undefined);
|
|
453
602
|
const state = await (0, validate_1.observeRuntimeState)(entry, this.processes);
|
|
@@ -476,26 +625,33 @@ class EnvironmentUpdater {
|
|
|
476
625
|
}
|
|
477
626
|
const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
|
|
478
627
|
const plans = [];
|
|
479
|
-
|
|
480
|
-
|
|
628
|
+
// A module may carry several local specs: plan it once, for all its targets.
|
|
629
|
+
for (const module of [...new Set(parsed.map(spec => spec.module))]) {
|
|
481
630
|
const artifact = artifacts.find(candidate => candidate.module === module);
|
|
482
631
|
const previous = moduleSources[module] ? { local: moduleSources[module].path } : { version: artifact?.version };
|
|
483
|
-
const pluginMappings = catalog_1.catalog[module].mappings.filter(mapping => mapping.role === 'plugin');
|
|
484
632
|
const override = localOverrides.get(module);
|
|
485
633
|
if (override) {
|
|
486
634
|
const removals = [];
|
|
487
635
|
const removedDirectories = [];
|
|
488
|
-
for (const
|
|
489
|
-
const destination = installationDirectory(entry.root,
|
|
636
|
+
for (const target of override) {
|
|
637
|
+
const destination = installationDirectory(entry.root, target.destination);
|
|
490
638
|
if (!await exists(destination))
|
|
491
639
|
continue;
|
|
492
640
|
removals.push(destination);
|
|
493
|
-
removedDirectories.push(
|
|
641
|
+
removedDirectories.push(target.destination);
|
|
494
642
|
}
|
|
643
|
+
const entries = override.map(target => {
|
|
644
|
+
const entry = { destination: target.destination, assemblies: [target.assembly], path: target.path };
|
|
645
|
+
if (target.kind === 'worker') {
|
|
646
|
+
entry.role = 'worker';
|
|
647
|
+
if (target.service)
|
|
648
|
+
entry.service = target.service;
|
|
649
|
+
}
|
|
650
|
+
return entry;
|
|
651
|
+
});
|
|
495
652
|
plans.push({
|
|
496
|
-
module, previous, next: { local: override.
|
|
497
|
-
entries
|
|
498
|
-
copies: [], removals, copiedDirectories: [], removedDirectories,
|
|
653
|
+
module, previous, next: { local: override.map(target => target.path).join(', ') }, local: override,
|
|
654
|
+
entries, copies: [], removals, copiedDirectories: [], removedDirectories,
|
|
499
655
|
});
|
|
500
656
|
continue;
|
|
501
657
|
}
|
|
@@ -516,8 +672,17 @@ class EnvironmentUpdater {
|
|
|
516
672
|
copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination });
|
|
517
673
|
copiedDirectories.push(mapping.destination);
|
|
518
674
|
const assembly = stagedArtifact?.assemblies.get(mapping.destination) ?? knownAssembly(configured, mapping);
|
|
519
|
-
|
|
520
|
-
|
|
675
|
+
// Plugins stay relative to the environment root; a worker entry does too,
|
|
676
|
+
// and its service keeps using the deployed destination as working directory.
|
|
677
|
+
if (mapping.role === 'plugin' || mapping.role === 'worker') {
|
|
678
|
+
const entry = { destination: mapping.destination, assemblies: mapping.assemblies, path: path.posix.join(mapping.destination, assembly) };
|
|
679
|
+
if (mapping.role === 'worker') {
|
|
680
|
+
entry.role = 'worker';
|
|
681
|
+
if (mapping.service)
|
|
682
|
+
entry.service = mapping.service;
|
|
683
|
+
}
|
|
684
|
+
entries.push(entry);
|
|
685
|
+
}
|
|
521
686
|
}
|
|
522
687
|
if (module === 'dashboard' && stagedArtifact) {
|
|
523
688
|
const contents = path.join(stagedArtifact.expanded, 'Web Contents');
|
|
@@ -562,7 +727,7 @@ class EnvironmentUpdater {
|
|
|
562
727
|
delete moduleSources[plan.module];
|
|
563
728
|
}
|
|
564
729
|
if (plan.local)
|
|
565
|
-
moduleSources[plan.module] =
|
|
730
|
+
moduleSources[plan.module] = manifestSource(plan.module, plan.local, moduleSources[plan.module]);
|
|
566
731
|
if (!installed.has(plan.module) && plan.module !== 'common' && selectedModules)
|
|
567
732
|
selectedModules.push(plan.module);
|
|
568
733
|
}
|
|
@@ -610,7 +775,7 @@ class EnvironmentUpdater {
|
|
|
610
775
|
const restartRequired = !dryRun && changes.length > 0;
|
|
611
776
|
if (restartRequired)
|
|
612
777
|
onProgress?.(`${entry.name}: modules updated; restart the environment to load the change (no service was started).`);
|
|
613
|
-
return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings };
|
|
778
|
+
return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []) };
|
|
614
779
|
}
|
|
615
780
|
finally {
|
|
616
781
|
if (workspace)
|
|
@@ -623,7 +788,8 @@ exports.EnvironmentUpdater = EnvironmentUpdater;
|
|
|
623
788
|
function knownAssembly(xml, mapping) {
|
|
624
789
|
try {
|
|
625
790
|
const document = new fast_xml_parser_1.XMLParser(parserOptions).parse(xml);
|
|
626
|
-
const
|
|
791
|
+
const server = document?.SystemConfig?.Services?.Server;
|
|
792
|
+
const raw = mapping.role === 'worker' ? server?.PluginWorkers?.['sys:string'] : server?.Plugins?.['sys:string'];
|
|
627
793
|
const list = raw === undefined ? [] : Array.isArray(raw) ? raw : [raw];
|
|
628
794
|
const prefix = windowsPath(mapping.destination).toLowerCase() + '\\';
|
|
629
795
|
for (const value of list) {
|