@guanjinke/artifact-client 1.10.0 → 1.12.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 +217 -217
- package/dist/cli/setup.js +2 -2
- package/dist/cli/setup.js.map +1 -1
- package/dist/environment/builder.d.ts.map +1 -1
- package/dist/environment/builder.js +78 -2
- package/dist/environment/builder.js.map +1 -1
- package/dist/environment/catalog.d.ts +38 -4
- package/dist/environment/catalog.d.ts.map +1 -1
- package/dist/environment/catalog.js +36 -2
- package/dist/environment/catalog.js.map +1 -1
- package/dist/environment/deferred.d.ts +38 -0
- package/dist/environment/deferred.d.ts.map +1 -0
- package/dist/environment/deferred.js +133 -0
- package/dist/environment/deferred.js.map +1 -0
- package/dist/environment/plan.d.ts +17 -7
- package/dist/environment/plan.d.ts.map +1 -1
- package/dist/environment/plan.js +35 -11
- package/dist/environment/plan.js.map +1 -1
- package/dist/environment/posix-process.d.ts +12 -0
- package/dist/environment/posix-process.d.ts.map +1 -1
- package/dist/environment/posix-process.js +35 -5
- package/dist/environment/posix-process.js.map +1 -1
- package/dist/environment/types.d.ts +36 -8
- package/dist/environment/types.d.ts.map +1 -1
- package/dist/environment/types.js +5 -0
- package/dist/environment/types.js.map +1 -1
- package/dist/environment/update.d.ts +6 -4
- package/dist/environment/update.d.ts.map +1 -1
- package/dist/environment/update.js +238 -31
- package/dist/environment/update.js.map +1 -1
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ const fs = __importStar(require("fs/promises"));
|
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
42
42
|
const catalog_1 = require("./catalog");
|
|
43
|
+
const deferred_1 = require("./deferred");
|
|
43
44
|
const archive_1 = require("./archive");
|
|
44
45
|
const builder_1 = require("./builder");
|
|
45
46
|
const config_1 = require("./config");
|
|
@@ -57,12 +58,14 @@ const windowsPath = (value, platform) => (0, local_path_1.nativeSeparators)(valu
|
|
|
57
58
|
/** The separator the recorded entries use on the target platform. */
|
|
58
59
|
const separatorOf = (platform) => (platform === 'win32' ? '\\' : '/');
|
|
59
60
|
/**
|
|
60
|
-
* Every catalog module
|
|
61
|
-
* excluded:
|
|
62
|
-
* the first
|
|
63
|
-
*
|
|
61
|
+
* Every catalog module a user may refresh. Both the create-only modules and the
|
|
62
|
+
* internal ones are excluded: a create-only module seeds a tree the Server owns
|
|
63
|
+
* (the first-start sample data) which no update can reproduce, and an internal
|
|
64
|
+
* module (the built-in 3D scene and map resources) is maintained by the updater
|
|
65
|
+
* itself — so an update never refreshes, preserves or re-deploys either on
|
|
66
|
+
* request. The web update picker reads this same list.
|
|
64
67
|
*/
|
|
65
|
-
exports.updatableModules = Object.keys(catalog_1.catalog).filter(module => !catalog_1.catalog[module].createOnly).sort();
|
|
68
|
+
exports.updatableModules = Object.keys(catalog_1.catalog).filter(module => !catalog_1.catalog[module].createOnly && !catalog_1.catalog[module].internal).sort();
|
|
66
69
|
/**
|
|
67
70
|
* Server-root settings the environment owns.
|
|
68
71
|
*
|
|
@@ -91,7 +94,49 @@ const requiredPackageFiles = {
|
|
|
91
94
|
*/
|
|
92
95
|
const excludedCopyNames = (mapping) => mapping.role === 'server' ? [...serverOwnedFiles] : mapping.role === 'monitor' ? ['conf'] : [];
|
|
93
96
|
/**
|
|
94
|
-
*
|
|
97
|
+
* Download the payload of a deferred module.
|
|
98
|
+
*
|
|
99
|
+
* It is published as files, not as an archive: the provider keeps the relative
|
|
100
|
+
* sub-path and the file is deployed exactly as downloaded, so nothing is
|
|
101
|
+
* extracted. Exactly one object is downloaded for `patch`. The public label is
|
|
102
|
+
* the only name in progress output or in errors.
|
|
103
|
+
*/
|
|
104
|
+
async function stageReplacement(client, module, version, payloads, workspace, onProgress) {
|
|
105
|
+
const entry = catalog_1.catalog[module];
|
|
106
|
+
const label = (0, catalog_1.moduleLabel)(entry);
|
|
107
|
+
const download = path.join(workspace, 'download', label);
|
|
108
|
+
try {
|
|
109
|
+
await fs.mkdir(download, { recursive: true });
|
|
110
|
+
onProgress?.(`[${label}] ${label}/${version}: Downloading replacement`);
|
|
111
|
+
const files = [];
|
|
112
|
+
for (const payload of payloads) {
|
|
113
|
+
await client.downloadArtifact(entry.plugin, version, payload.name, download);
|
|
114
|
+
const source = path.join(download, ...payload.name.split('/'));
|
|
115
|
+
await (0, builder_1.regularFile)(source);
|
|
116
|
+
files.push({ name: payload.name, source, sha256: await (0, builder_1.hashFile)(source) });
|
|
117
|
+
}
|
|
118
|
+
onProgress?.(`[${label}] ${label}/${version}: Download complete; replacement ready`);
|
|
119
|
+
return { version, files };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error instanceof Error && error.message.startsWith(`Module ${label} could not be applied:`))
|
|
123
|
+
throw error;
|
|
124
|
+
throw new Error(`Module update failed for ${label}/${version} (download or layout).`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** One environment-relative Server path inside a staged Server package. */
|
|
128
|
+
function stagedServerPath(staged, relative) {
|
|
129
|
+
const server = catalog_1.catalog.server.mappings.find(mapping => mapping.role === 'server');
|
|
130
|
+
const prefix = server ? `${server.destination}/` : '';
|
|
131
|
+
const inner = prefix && relative.startsWith(prefix) ? relative.slice(prefix.length) : relative;
|
|
132
|
+
return path.join(staged.expanded, ...(server?.source ? server.source.split('/') : []), ...inner.split('/'));
|
|
133
|
+
}
|
|
134
|
+
/** The version file inside a staged Server package, before it is laid out. */
|
|
135
|
+
function stagedServerFile(staged, module) {
|
|
136
|
+
const descriptor = catalog_1.catalog[module].deferred;
|
|
137
|
+
return stagedServerPath(staged, descriptor.versionFile);
|
|
138
|
+
}
|
|
139
|
+
/** The modules a selection may name, from the same table `create-env` and the web
|
|
95
140
|
* interface read.
|
|
96
141
|
*
|
|
97
142
|
* Reading it through the call keeps this module loadable on its own: the policy
|
|
@@ -109,9 +154,14 @@ function validateUpdatableModule(module) {
|
|
|
109
154
|
// `hasOwnProperty` keeps prototype names (`constructor`) out of the catalog.
|
|
110
155
|
if (!Object.prototype.hasOwnProperty.call(catalog_1.catalog, module))
|
|
111
156
|
throw new Error(`Unsupported module "${module}": supported modules are ${exports.updatableModules.join(', ')}.`);
|
|
112
|
-
//
|
|
113
|
-
// registry, the manifest or the configuration is read — because
|
|
114
|
-
//
|
|
157
|
+
// An internal module is refused before anything else happens — before the
|
|
158
|
+
// registry, the manifest or the configuration is read — because the user never
|
|
159
|
+
// selects it: the updater deploys it with the environment and re-deploys it
|
|
160
|
+
// together with the package whose directory holds its files.
|
|
161
|
+
if (catalog_1.catalog[module].internal)
|
|
162
|
+
throw new Error(`Module ${module} is internal: it is deployed with the environment and refreshed automatically with the Server package, so it cannot be selected or updated directly.`);
|
|
163
|
+
// A create-only module is refused for the same reason: no update can produce
|
|
164
|
+
// it, because it is deployed only while the environment is created.
|
|
115
165
|
if (catalog_1.catalog[module].createOnly)
|
|
116
166
|
throw new Error(`Module ${module} is create-only: it is deployed only when the environment is created and is never refreshed, preserved or re-deployed by update-env. Recreate the environment with create-env to get it again.`);
|
|
117
167
|
}
|
|
@@ -178,6 +228,11 @@ function pushSpec(specs, seen, spec) {
|
|
|
178
228
|
}
|
|
179
229
|
else {
|
|
180
230
|
validateUpdatableModule(spec.module);
|
|
231
|
+
// A deferred module's version is always the one recorded by the deployed (or
|
|
232
|
+
// newly staged) Server package. Pinning a different one would either be
|
|
233
|
+
// ignored or produce a mismatched environment, so it is refused.
|
|
234
|
+
if (catalog_1.catalog[spec.module].deferred && spec.version !== undefined)
|
|
235
|
+
throw new Error(`Module ${spec.module} takes its version from the deployed Server package: a pinned version is not supported.`);
|
|
181
236
|
if (seen.has(allModules))
|
|
182
237
|
throw new Error('Module spec all cannot be combined with individual modules.');
|
|
183
238
|
const previous = seen.get(spec.module);
|
|
@@ -553,14 +608,17 @@ function environmentSchema(manifest, configuration) {
|
|
|
553
608
|
* that policy. Returns the warning text, or undefined when the package is
|
|
554
609
|
* complete.
|
|
555
610
|
*/
|
|
556
|
-
async function verifyStagedPackage(module, staged, schema) {
|
|
611
|
+
async function verifyStagedPackage(module, staged, schema, required = requiredPackageFiles[module] ?? []) {
|
|
557
612
|
const label = `${staged.plugin}/${staged.version}/${staged.name}`;
|
|
558
|
-
for (const name of
|
|
613
|
+
for (const name of required) {
|
|
559
614
|
try {
|
|
560
615
|
await (0, builder_1.regularFile)(path.join(staged.expanded, name));
|
|
561
616
|
}
|
|
617
|
+
// The Server's required file is the one the selectable patch replaces, so the
|
|
618
|
+
// message stays generic: its name must not be user-visible. Every other
|
|
619
|
+
// module's required file name is public and useful.
|
|
562
620
|
catch {
|
|
563
|
-
throw new Error(`Module update failed for ${label}: the package is missing ${name}.`);
|
|
621
|
+
throw new Error(`Module update failed for ${label}: the package is missing ${module === 'server' ? 'a required runtime file' : name}.`);
|
|
564
622
|
}
|
|
565
623
|
}
|
|
566
624
|
const relative = `xsd/${schema}/Wyn.conf.xsd`;
|
|
@@ -636,10 +694,17 @@ function mappingForAssembly(module, assembly) {
|
|
|
636
694
|
* explicitly, for a build whose file name matches no assembly at all.
|
|
637
695
|
*/
|
|
638
696
|
async function resolveLocalSource(entry, module, assembly, input) {
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
// refused on its own terms before any path is
|
|
642
|
-
//
|
|
697
|
+
// A deferred module is a published replacement for one file the Server package
|
|
698
|
+
// owns; a developer build has no mapping the configuration could point at, so
|
|
699
|
+
// a local source is refused on its own terms before any path is looked at.
|
|
700
|
+
// The message names the module only.
|
|
701
|
+
if (catalog_1.catalog[module].deferred)
|
|
702
|
+
throw new Error(`Module ${module} is refreshed from its published artifact only and has no local source.`);
|
|
703
|
+
// Any built-in trailing module is never a developer-build target either: it
|
|
704
|
+
// ships a static asset tree no local DLL could stand in for. Both are refused
|
|
705
|
+
// as explicit specs before this point (one internal, one create-only); this
|
|
706
|
+
// guard is the safety net for a trailing module a user could select, whose
|
|
707
|
+
// remote specs would keep working.
|
|
643
708
|
if (plan_1.trailingModules.includes(module))
|
|
644
709
|
throw new Error(`Module ${module} is built in and has no local source: it ships the 3D scene and map assets and is always deployed from its published artifact. Refresh it from the cloud (\`${module}\`, \`${module}:<version>\` or \`all\`).`);
|
|
645
710
|
const mappings = localMappings(module);
|
|
@@ -789,12 +854,15 @@ class EnvironmentUpdater {
|
|
|
789
854
|
localOverrides.set(spec.module, targets);
|
|
790
855
|
}
|
|
791
856
|
const remote = parsed.filter(spec => spec.localPath === undefined);
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
//
|
|
857
|
+
// Internal modules are never named by a spec (they are refused while the
|
|
858
|
+
// module specs are parsed) and `all` never selects them, but the updater
|
|
859
|
+
// still deploys them on its own. `dashboard-resource` ships the 3D scene and
|
|
860
|
+
// map trees *inside* the Server directory, and a Server refresh replaces that
|
|
861
|
+
// whole directory: an environment that has it therefore re-deploys it from
|
|
862
|
+
// its own artifact whenever the Server is refreshed in the same call, because
|
|
863
|
+
// there is no other copy to restore the trees from. A create-only trailing
|
|
864
|
+
// module is different: it seeds data the product imported once, so a Server
|
|
865
|
+
// refresh simply drops what it had seeded.
|
|
798
866
|
const autoRefreshed = [];
|
|
799
867
|
if (remote.some(spec => spec.module === 'server')) {
|
|
800
868
|
for (const module of plan_1.trailingModules) {
|
|
@@ -806,6 +874,21 @@ class EnvironmentUpdater {
|
|
|
806
874
|
onProgress?.(`${module} is refreshed with the Server package: its trees deploy inside the Server directory`);
|
|
807
875
|
}
|
|
808
876
|
}
|
|
877
|
+
// A Server refresh restores the unpatched file the Server package ships, so
|
|
878
|
+
// an installed deferred module is re-applied in the same call. Its version is
|
|
879
|
+
// resolved from the *new* package below, never from the old tree. Without a
|
|
880
|
+
// Server refresh it is only applied when the caller names it explicitly.
|
|
881
|
+
const autoReplaced = [];
|
|
882
|
+
if (remote.some(spec => spec.module === 'server')) {
|
|
883
|
+
for (const module of Object.keys(catalog_1.catalog)) {
|
|
884
|
+
if (!catalog_1.catalog[module].deferred || !installed.has(module) || parsed.some(spec => spec.module === module))
|
|
885
|
+
continue;
|
|
886
|
+
parsed.push({ module });
|
|
887
|
+
remote.push({ module });
|
|
888
|
+
autoReplaced.push(module);
|
|
889
|
+
onProgress?.(`${module} is re-applied with the Server package: the new package replaces the file it patches`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
809
892
|
// Smart documents cannot run without the collaboration service, which
|
|
810
893
|
// create-env deploys and declares in the Server configuration alongside
|
|
811
894
|
// documents. Installing documents into an environment with no collaboration
|
|
@@ -836,15 +919,57 @@ class EnvironmentUpdater {
|
|
|
836
919
|
try {
|
|
837
920
|
const resolved = new Map();
|
|
838
921
|
const staged = new Map();
|
|
922
|
+
const replacements = new Map();
|
|
839
923
|
if (remote.length) {
|
|
840
924
|
const client = clientFactory();
|
|
841
|
-
|
|
925
|
+
const direct = remote.filter(spec => !catalog_1.catalog[spec.module].deferred);
|
|
926
|
+
for (const spec of direct) {
|
|
842
927
|
onProgress?.(`Resolving ${catalog_1.catalog[spec.module].plugin}`);
|
|
843
928
|
resolved.set(spec.module, await resolveRemoteArtifact(client, entry, spec));
|
|
844
929
|
}
|
|
845
930
|
if (!dryRun)
|
|
846
|
-
for (const spec of
|
|
931
|
+
for (const spec of direct)
|
|
847
932
|
staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), editionOf(entry), workspace, onProgress));
|
|
933
|
+
// A deferred module resolves after the direct artifacts are staged, so a
|
|
934
|
+
// Server refresh contributes the *new* package's version. Without a
|
|
935
|
+
// Server refresh the already deployed tree is the authority. A dry-run
|
|
936
|
+
// that refreshes the Server cannot know that version without downloading
|
|
937
|
+
// the package, so it reports the deferred resolution and stops there.
|
|
938
|
+
for (const spec of remote.filter(candidate => catalog_1.catalog[candidate.module].deferred)) {
|
|
939
|
+
const module = spec.module;
|
|
940
|
+
const source = catalog_1.catalog[module];
|
|
941
|
+
const label = (0, catalog_1.moduleLabel)(source);
|
|
942
|
+
const server = staged.get('server');
|
|
943
|
+
let version;
|
|
944
|
+
if (server) {
|
|
945
|
+
version = await (0, deferred_1.deferredVersionAt)(source, stagedServerFile(server, module));
|
|
946
|
+
// The replacement must overwrite an existing regular file in the new
|
|
947
|
+
// package; checking the staged tree catches a package that no longer
|
|
948
|
+
// ships it before anything has been written.
|
|
949
|
+
for (const mapping of (0, deferred_1.replacementMappings)(source)) {
|
|
950
|
+
const file = stagedServerPath(server, mapping.destination);
|
|
951
|
+
const stat = await lstatOrUndefined(file);
|
|
952
|
+
if (!stat)
|
|
953
|
+
throw new Error(`Module ${label} could not be applied: the Server package holds no file to replace.`);
|
|
954
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
955
|
+
throw new Error(`Module ${label} could not be applied: the Server package holds no regular file to replace.`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
else if (remote.some(candidate => candidate.module === 'server')) {
|
|
959
|
+
onProgress?.(`${label}: version resolves from the refreshed Server package at execution time`);
|
|
960
|
+
replacements.set(module, { version: '' });
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
else
|
|
964
|
+
version = await (0, deferred_1.deferredVersion)(source, entry.root);
|
|
965
|
+
const payloads = await (0, deferred_1.deferredPayload)(source, version, client);
|
|
966
|
+
if (dryRun) {
|
|
967
|
+
onProgress?.(`${label}/${version}: resolved from the deployed Server package; nothing downloaded`);
|
|
968
|
+
replacements.set(module, { version });
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
replacements.set(module, await stageReplacement(client, module, version, payloads, workspace, onProgress));
|
|
972
|
+
}
|
|
848
973
|
}
|
|
849
974
|
const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
|
|
850
975
|
// A Server or Monitor package is only usable when it still serves the
|
|
@@ -856,11 +981,16 @@ class EnvironmentUpdater {
|
|
|
856
981
|
if (remote.some(spec => spec.module === 'server' || spec.module === 'monitoring') && !schema) {
|
|
857
982
|
throw new Error(`Module update for ${remote.filter(spec => spec.module === 'server' || spec.module === 'monitoring').map(spec => spec.module).join(' and ')} needs the configuration schema of ${entry.name}, which neither its manifest nor its conf/Wyn.conf records. Recreate the environment with create-env.`);
|
|
858
983
|
}
|
|
984
|
+
// A planned deferred module validates the exact Server file it replaces,
|
|
985
|
+
// with a message that names only the module; the Server's own required-file
|
|
986
|
+
// check would otherwise report the same missing file by its internal name.
|
|
987
|
+
const deferredPlanned = remote.some(spec => catalog_1.catalog[spec.module].deferred);
|
|
859
988
|
for (const spec of remote) {
|
|
860
989
|
const stagedArtifact = staged.get(spec.module);
|
|
861
990
|
if (!stagedArtifact || !schema || !(spec.module in requiredPackageFiles))
|
|
862
991
|
continue;
|
|
863
|
-
const
|
|
992
|
+
const required = spec.module === 'server' && deferredPlanned ? [] : requiredPackageFiles[spec.module];
|
|
993
|
+
const warning = await verifyStagedPackage(spec.module, stagedArtifact, schema, required);
|
|
864
994
|
if (warning)
|
|
865
995
|
warnings.push(warning);
|
|
866
996
|
}
|
|
@@ -868,9 +998,11 @@ class EnvironmentUpdater {
|
|
|
868
998
|
// A module may carry several local specs: plan it once, for all its targets.
|
|
869
999
|
// The built-in trailing modules are planned last, in the same order
|
|
870
1000
|
// `create-env` assembles: their trees live inside other modules'
|
|
871
|
-
// directories, so a Server replacement must not wipe them again.
|
|
872
|
-
|
|
873
|
-
|
|
1001
|
+
// directories, so a Server replacement must not wipe them again. A deferred
|
|
1002
|
+
// module runs after `server` even when the caller wrote it first: the Server
|
|
1003
|
+
// directory must hold the new package before it is patched.
|
|
1004
|
+
const rank = (module) => plan_1.trailingModules.includes(module) ? 2 : catalog_1.catalog[module].deferred ? 1 : 0;
|
|
1005
|
+
const planOrder = [...new Set(parsed.map(spec => spec.module))].sort((left, right) => rank(left) - rank(right));
|
|
874
1006
|
for (const module of planOrder) {
|
|
875
1007
|
const artifact = artifacts.find(candidate => candidate.module === module);
|
|
876
1008
|
const previous = moduleSources[module] ? { local: moduleSources[module].path } : { version: artifact?.version };
|
|
@@ -900,6 +1032,37 @@ class EnvironmentUpdater {
|
|
|
900
1032
|
});
|
|
901
1033
|
continue;
|
|
902
1034
|
}
|
|
1035
|
+
// A deferred module replaces one existing regular file and contributes
|
|
1036
|
+
// no directory, plugin, worker, service or runtime requirement. Its
|
|
1037
|
+
// version and payload were resolved above; nothing about the provider is
|
|
1038
|
+
// recorded, and the target is checked before any write.
|
|
1039
|
+
if (catalog_1.catalog[module].deferred) {
|
|
1040
|
+
const replacement = replacements.get(module);
|
|
1041
|
+
const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[module]);
|
|
1042
|
+
const version = replacement?.version || undefined;
|
|
1043
|
+
const files = [];
|
|
1044
|
+
if (replacement?.files?.length) {
|
|
1045
|
+
const mappings = (0, deferred_1.replacementMappings)(catalog_1.catalog[module]);
|
|
1046
|
+
for (const file of replacement.files) {
|
|
1047
|
+
const matches = mappings.filter(mapping => mapping.source === file.name);
|
|
1048
|
+
if (matches.length !== 1)
|
|
1049
|
+
throw new Error(`Module ${label} could not be applied: the replacement target is not configured.`);
|
|
1050
|
+
const destination = installationDirectory(entry.root, matches[0].destination);
|
|
1051
|
+
const stat = await lstatOrUndefined(destination);
|
|
1052
|
+
if (!stat)
|
|
1053
|
+
throw new Error(`Module ${label} could not be applied: the Server tree holds no file to replace.`);
|
|
1054
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
1055
|
+
throw new Error(`Module ${label} could not be applied: the Server tree holds no regular file to replace.`);
|
|
1056
|
+
files.push({ source: file.source, destination, sha256: file.sha256 });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
plans.push({
|
|
1060
|
+
module, previous, next: version ? { version } : {},
|
|
1061
|
+
...(files.length ? { replacement: { version: version, files } } : {}),
|
|
1062
|
+
entries: [], copies: [], removals: [], preserved: [], copiedDirectories: [], removedDirectories: [],
|
|
1063
|
+
});
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
903
1066
|
const remoteArtifact = resolved.get(module);
|
|
904
1067
|
const stagedArtifact = staged.get(module);
|
|
905
1068
|
const copies = [];
|
|
@@ -1018,6 +1181,28 @@ class EnvironmentUpdater {
|
|
|
1018
1181
|
}
|
|
1019
1182
|
delete moduleSources[plan.module];
|
|
1020
1183
|
}
|
|
1184
|
+
if (plan.replacement) {
|
|
1185
|
+
// The record keeps the public label, the resolved version and the
|
|
1186
|
+
// payload's SHA256. No remote path and no mapping are written: both
|
|
1187
|
+
// would expose a name the module hides.
|
|
1188
|
+
const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[plan.module]);
|
|
1189
|
+
const replacement = { module: plan.module, plugin: label, version: plan.replacement.version, name: label };
|
|
1190
|
+
const index = nextArtifacts.findIndex(candidate => candidate.module === plan.module);
|
|
1191
|
+
if (index < 0)
|
|
1192
|
+
nextArtifacts.push(replacement);
|
|
1193
|
+
else
|
|
1194
|
+
nextArtifacts[index] = { ...nextArtifacts[index], ...replacement };
|
|
1195
|
+
for (const file of plan.replacement.files) {
|
|
1196
|
+
const download = { plugin: label, version: plan.replacement.version, name: label, sha256: file.sha256 };
|
|
1197
|
+
const downloadIndex = nextDownloads.findIndex(candidate => candidate.plugin === label);
|
|
1198
|
+
if (downloadIndex < 0)
|
|
1199
|
+
nextDownloads.push(download);
|
|
1200
|
+
else
|
|
1201
|
+
nextDownloads[downloadIndex] = download;
|
|
1202
|
+
}
|
|
1203
|
+
requirements = requirements.filter(line => !line.endsWith(` (${plan.module})`));
|
|
1204
|
+
delete moduleSources[plan.module];
|
|
1205
|
+
}
|
|
1021
1206
|
if (plan.local)
|
|
1022
1207
|
moduleSources[plan.module] = manifestSource(plan.module, plan.local, moduleSources[plan.module]);
|
|
1023
1208
|
// First installing a module records it the way `create-env` would: a
|
|
@@ -1097,6 +1282,8 @@ class EnvironmentUpdater {
|
|
|
1097
1282
|
}
|
|
1098
1283
|
if (autoRefreshed.length)
|
|
1099
1284
|
warnings.push(`${autoRefreshed.join(', ')} was refreshed with the Server package: its trees live inside the Server directory and would otherwise be lost.`);
|
|
1285
|
+
if (autoReplaced.length)
|
|
1286
|
+
warnings.push(`${autoReplaced.join(', ')} was re-applied with the Server package: the new package ships the file it replaces, so it was applied again.`);
|
|
1100
1287
|
if (removedCreateOnly.length)
|
|
1101
1288
|
warnings.push(`${removedCreateOnly.join(', ')} was removed with the Server package: it is deployed only when an environment is created.`);
|
|
1102
1289
|
if (!dryRun) {
|
|
@@ -1121,6 +1308,25 @@ class EnvironmentUpdater {
|
|
|
1121
1308
|
await (0, builder_1.copyTree)(copy.source, copy.destination, copy.exclude);
|
|
1122
1309
|
for (const file of plan.preserved)
|
|
1123
1310
|
await fs.copyFile(file.source, path.join(file.directory, file.name));
|
|
1311
|
+
// The replacement is applied after the directory it lives in has been
|
|
1312
|
+
// copied, so a Server refresh cannot overwrite it again. The target is
|
|
1313
|
+
// re-checked here and every error names the module only.
|
|
1314
|
+
if (plan.replacement) {
|
|
1315
|
+
const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[plan.module]);
|
|
1316
|
+
try {
|
|
1317
|
+
for (const file of plan.replacement.files) {
|
|
1318
|
+
const stat = await fs.lstat(file.destination).catch(() => undefined);
|
|
1319
|
+
if (!stat)
|
|
1320
|
+
throw new Error('missing');
|
|
1321
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
1322
|
+
throw new Error('not a regular file');
|
|
1323
|
+
await fs.copyFile(file.source, file.destination);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
catch {
|
|
1327
|
+
throw new Error(`Module ${label} could not be applied: the Server tree holds no regular file to replace.`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1124
1330
|
if (plan.configurationAssets) {
|
|
1125
1331
|
onProgress?.(`Refreshing Monitor configuration assets in ${path.relative(entry.root, plan.configurationAssets.destination)}`);
|
|
1126
1332
|
await mergeConfigurationAssets(plan.configurationAssets.source, plan.configurationAssets.destination);
|
|
@@ -1129,9 +1335,10 @@ class EnvironmentUpdater {
|
|
|
1129
1335
|
await fs.symlink(path.join(entry.root, 'conf'), plan.configurationLink, 'junction');
|
|
1130
1336
|
}
|
|
1131
1337
|
// Only a plan that really contributes something to the configuration
|
|
1132
|
-
// rewrites it:
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1338
|
+
// rewrites it: a module that adds no plugin, worker or service entry
|
|
1339
|
+
// leaves `conf/Wyn.conf` exactly as it is instead of round-tripping it
|
|
1340
|
+
// through the XML builder. (The built-in modules are create-only, so an
|
|
1341
|
+
// update never plans one.)
|
|
1135
1342
|
const configurationChanged = plans.some(plan => plan.entries.length > 0 || plan.configurationAssets !== undefined || plan.configurationLink !== undefined);
|
|
1136
1343
|
onProgress?.(configurationChanged ? 'Writing configuration, manifest and registry record' : 'Writing manifest and registry record');
|
|
1137
1344
|
if (configurationChanged)
|