@aiot-nexus/plugin-sdk 3.0.2 → 3.1.1
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 +67 -15
- package/dev-host/main.js +8 -5
- package/dist/.generated/plugin-framework-version.d.ts +1 -1
- package/dist/.generated/plugin-framework-version.js +1 -1
- package/dist/.generated/plugin-manifest.schema.json +2 -1
- package/dist/src/cli/run.js +79 -4
- package/dist/src/cli/update-notice.js +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/scaffold.js +6 -5
- package/dist/src/sdk-info.d.ts +17 -0
- package/dist/src/sdk-info.js +23 -0
- package/dist/src/types.d.ts +1 -0
- package/dist/src/upgrade.d.ts +44 -0
- package/dist/src/upgrade.js +1198 -0
- package/dist/src/validation.js +9 -0
- package/package.json +2 -2
- package/scripts/sdk-update-check.js +1 -1
- package/templates/minimal/README.md.tmpl +3 -3
- package/templates/sqlite/README.md.tmpl +3 -3
|
@@ -0,0 +1,1198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.formatPluginSdkUpgradeResult = exports.runPluginSdkUpgrade = void 0;
|
|
27
|
+
const child_process_1 = require("child_process");
|
|
28
|
+
const fs = __importStar(require("fs"));
|
|
29
|
+
const path = __importStar(require("path"));
|
|
30
|
+
const url_1 = require("url");
|
|
31
|
+
const sdk_info_1 = require("./sdk-info");
|
|
32
|
+
const sdkPackageName = "@aiot-nexus/plugin-sdk";
|
|
33
|
+
const strictVersionPattern = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
|
|
34
|
+
const pluginIdPattern = /^[a-z0-9][a-z0-9._-]{1,63}$/;
|
|
35
|
+
const supportedLockfileVersions = new Set([1, 2]);
|
|
36
|
+
const commandOutputLimit = 64 * 1024 * 1024;
|
|
37
|
+
const recoveryDirectoryName = ".nexus-plugin-upgrade-recovery";
|
|
38
|
+
const migrationGuideUrl = "https://github.com/ke4nec/aiot-nexus/blob/master/docs/plugin-framework-v2-v3-migration.md";
|
|
39
|
+
function runPluginSdkUpgrade(options, runtime = defaultRuntime()) {
|
|
40
|
+
const projectDirectory = path.resolve(options.projectDirectory);
|
|
41
|
+
const result = createResult(projectDirectory, options.sdkSource || sdk_info_1.PLUGIN_SDK_VERSION);
|
|
42
|
+
let inspection;
|
|
43
|
+
try {
|
|
44
|
+
inspection = inspectUpgrade(options, runtime, result);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
addCheck(result, "upgrade", "failed", errorMessage(error));
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
if (hasFailedCheck(result)) {
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
if (!inspection.needsUpgrade) {
|
|
54
|
+
try {
|
|
55
|
+
verifyInstalledState(inspection, runtime);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
addCheck(result, "installed SDK", "failed", errorMessage(error));
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
result.status = "up-to-date";
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
result.status = "upgrade-available";
|
|
65
|
+
result.changedFiles = plannedChangedFiles(inspection);
|
|
66
|
+
if (options.checkOnly) {
|
|
67
|
+
addCheck(result, "project checks", "skipped", "Check mode does not install dependencies or run project scripts.");
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
return applyUpgrade(inspection, options, runtime);
|
|
71
|
+
}
|
|
72
|
+
exports.runPluginSdkUpgrade = runPluginSdkUpgrade;
|
|
73
|
+
function formatPluginSdkUpgradeResult(result) {
|
|
74
|
+
const lines = [
|
|
75
|
+
"Plugin SDK upgrade: " + result.status,
|
|
76
|
+
"Project: " + result.projectDirectory,
|
|
77
|
+
"Current SDK: " + (result.currentVersion || "unknown"),
|
|
78
|
+
"Target SDK: " + result.targetVersion,
|
|
79
|
+
"Source: " + result.source,
|
|
80
|
+
];
|
|
81
|
+
if (result.dependencySection) {
|
|
82
|
+
lines.push("Dependency section: " + result.dependencySection);
|
|
83
|
+
}
|
|
84
|
+
lines.push("Checks:");
|
|
85
|
+
for (const check of result.checks) {
|
|
86
|
+
lines.push(" [" + check.status + "] " + check.name + ": " + check.detail);
|
|
87
|
+
}
|
|
88
|
+
lines.push("Changed files:");
|
|
89
|
+
if (result.changedFiles.length === 0) {
|
|
90
|
+
lines.push(" (none)");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
result.changedFiles.forEach((filePath) => lines.push(" - " + filePath));
|
|
94
|
+
}
|
|
95
|
+
if (result.rollback) {
|
|
96
|
+
lines.push("Rollback: " +
|
|
97
|
+
(result.rollback.succeeded ? "succeeded" : "failed") +
|
|
98
|
+
": " +
|
|
99
|
+
result.rollback.detail);
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
exports.formatPluginSdkUpgradeResult = formatPluginSdkUpgradeResult;
|
|
104
|
+
function inspectUpgrade(options, runtime, result) {
|
|
105
|
+
const projectDirectory = result.projectDirectory;
|
|
106
|
+
const packageJsonPath = path.join(projectDirectory, "package.json");
|
|
107
|
+
assertNoPendingTransaction(projectDirectory);
|
|
108
|
+
assertRegularFile(packageJsonPath, "Plugin project package.json");
|
|
109
|
+
const packageJson = readJson(packageJsonPath, "Plugin project package.json");
|
|
110
|
+
addCheck(result, "project", "passed", "Found an npm plugin project package.json.");
|
|
111
|
+
assertNpmProject(projectDirectory, result);
|
|
112
|
+
const packageLockPath = path.join(projectDirectory, "package-lock.json");
|
|
113
|
+
const packageLockExisted = fs.existsSync(packageLockPath);
|
|
114
|
+
if (packageLockExisted) {
|
|
115
|
+
assertRegularFile(packageLockPath, "Plugin project package-lock.json");
|
|
116
|
+
}
|
|
117
|
+
const packageLock = packageLockExisted
|
|
118
|
+
? readJson(packageLockPath, "Plugin project package-lock.json")
|
|
119
|
+
: undefined;
|
|
120
|
+
validatePackageLock(packageLock, result);
|
|
121
|
+
const dependency = findSdkDependency(packageJson, result);
|
|
122
|
+
result.dependencySection = dependency.section;
|
|
123
|
+
const currentVersion = resolveCurrentVersion(projectDirectory, dependency.spec, packageLock, result);
|
|
124
|
+
result.currentVersion = currentVersion;
|
|
125
|
+
if (!currentVersion) {
|
|
126
|
+
addCheck(result, "current SDK", "failed", "The current SDK version could not be resolved. Run npm install before upgrading.");
|
|
127
|
+
}
|
|
128
|
+
const npmVersionResult = runtime.runNpm(["--version"], projectDirectory);
|
|
129
|
+
if (!commandSucceeded(npmVersionResult)) {
|
|
130
|
+
addCheck(result, "npm", "failed", commandFailure("npm --version", npmVersionResult));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const npmVersion = npmVersionResult.stdout.trim();
|
|
134
|
+
addCheck(result, "npm", isAtLeastVersion(npmVersion, "6.13.4") ? "passed" : "failed", "Detected npm " + npmVersion + "; required " + sdk_info_1.PLUGIN_SDK_NPM_ENGINE + ".");
|
|
135
|
+
}
|
|
136
|
+
const nodeVersion = runtime.nodeVersion.replace(/^v/, "");
|
|
137
|
+
addCheck(result, "Node.js", isSupportedNodeVersion(nodeVersion) ? "passed" : "failed", "Detected Node " +
|
|
138
|
+
nodeVersion +
|
|
139
|
+
"; required " +
|
|
140
|
+
sdk_info_1.PLUGIN_SDK_NODE_ENGINE +
|
|
141
|
+
".");
|
|
142
|
+
validateProjectBaseline(packageJson, result);
|
|
143
|
+
const normalizedSource = normalizeSdkSource(options.sdkSource || sdk_info_1.PLUGIN_SDK_VERSION, options.workingDirectory, projectDirectory, runtime, result);
|
|
144
|
+
result.source = normalizedSource.display;
|
|
145
|
+
if (currentVersion) {
|
|
146
|
+
const comparison = compareVersions(sdk_info_1.PLUGIN_SDK_VERSION, currentVersion);
|
|
147
|
+
if (comparison < 0) {
|
|
148
|
+
addCheck(result, "version direction", "failed", "SDK downgrade is not supported: " +
|
|
149
|
+
currentVersion +
|
|
150
|
+
" -> " +
|
|
151
|
+
sdk_info_1.PLUGIN_SDK_VERSION +
|
|
152
|
+
".");
|
|
153
|
+
}
|
|
154
|
+
else if (majorVersion(currentVersion) !== majorVersion(sdk_info_1.PLUGIN_SDK_VERSION) &&
|
|
155
|
+
!options.allowMajor) {
|
|
156
|
+
addCheck(result, "major version", "failed", "Major SDK upgrade requires --allow-major and the matching framework migration guide: " +
|
|
157
|
+
migrationGuideUrl +
|
|
158
|
+
" (" +
|
|
159
|
+
currentVersion +
|
|
160
|
+
" -> " +
|
|
161
|
+
sdk_info_1.PLUGIN_SDK_VERSION +
|
|
162
|
+
").");
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
addCheck(result, "version direction", "passed", currentVersion + " -> " + sdk_info_1.PLUGIN_SDK_VERSION + ".");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const currentManagedArchivePath = resolveManagedArchivePath(dependency.spec, projectDirectory);
|
|
169
|
+
validateManagedArchivePaths(normalizedSource.vendoredArchivePath, currentManagedArchivePath, result);
|
|
170
|
+
const builtPackagePath = resolveBuiltPackagePath(packageJson, projectDirectory, result);
|
|
171
|
+
const installedVersion = readInstalledSdkVersion(projectDirectory);
|
|
172
|
+
const lockedVersion = readLockedSdkVersion(packageLock);
|
|
173
|
+
const needsUpgrade = dependency.spec !== normalizedSource.dependencySpec ||
|
|
174
|
+
installedVersion !== sdk_info_1.PLUGIN_SDK_VERSION ||
|
|
175
|
+
!packageLockExisted ||
|
|
176
|
+
lockedVersion !== sdk_info_1.PLUGIN_SDK_VERSION;
|
|
177
|
+
addCheck(result, "SDK dependency", needsUpgrade ? "warning" : "passed", needsUpgrade
|
|
178
|
+
? "The SDK dependency, lockfile, or installed package must be synchronized."
|
|
179
|
+
: "package.json, package-lock.json, and node_modules already use SDK " +
|
|
180
|
+
sdk_info_1.PLUGIN_SDK_VERSION +
|
|
181
|
+
".");
|
|
182
|
+
return {
|
|
183
|
+
result,
|
|
184
|
+
packageJson,
|
|
185
|
+
packageJsonPath,
|
|
186
|
+
packageLockPath,
|
|
187
|
+
packageLockExisted,
|
|
188
|
+
dependencySection: dependency.section,
|
|
189
|
+
normalizedSource,
|
|
190
|
+
currentManagedArchivePath,
|
|
191
|
+
builtPackagePath,
|
|
192
|
+
needsUpgrade,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function applyUpgrade(inspection, options, runtime) {
|
|
196
|
+
const result = inspection.result;
|
|
197
|
+
const snapshotPaths = [
|
|
198
|
+
inspection.packageJsonPath,
|
|
199
|
+
inspection.packageLockPath,
|
|
200
|
+
];
|
|
201
|
+
if (inspection.normalizedSource.vendoredArchivePath) {
|
|
202
|
+
snapshotPaths.push(inspection.normalizedSource.vendoredArchivePath);
|
|
203
|
+
}
|
|
204
|
+
if (inspection.currentManagedArchivePath) {
|
|
205
|
+
snapshotPaths.push(inspection.currentManagedArchivePath);
|
|
206
|
+
}
|
|
207
|
+
if (inspection.builtPackagePath) {
|
|
208
|
+
snapshotPaths.push(inspection.builtPackagePath);
|
|
209
|
+
}
|
|
210
|
+
let transaction;
|
|
211
|
+
try {
|
|
212
|
+
transaction = createUpgradeTransaction(inspection, Array.from(new Set(snapshotPaths)));
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
addCheck(result, "upgrade snapshot", "failed", errorMessage(error));
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
writeTransactionJournal(transaction, "installing");
|
|
220
|
+
prepareLocalArchive(inspection.normalizedSource);
|
|
221
|
+
const saveFlag = inspection.dependencySection === "devDependencies"
|
|
222
|
+
? "--save-dev"
|
|
223
|
+
: "--save";
|
|
224
|
+
const installResult = runtime.runNpm([
|
|
225
|
+
"install",
|
|
226
|
+
saveFlag,
|
|
227
|
+
"--save-exact",
|
|
228
|
+
inspection.normalizedSource.installSpec,
|
|
229
|
+
], result.projectDirectory);
|
|
230
|
+
if (!commandSucceeded(installResult)) {
|
|
231
|
+
throw new Error(commandFailure("npm install", installResult));
|
|
232
|
+
}
|
|
233
|
+
addCheck(result, "npm install", "passed", "SDK dependency and npm installation state were synchronized.");
|
|
234
|
+
verifyInstalledState(inspection, runtime);
|
|
235
|
+
writeTransactionJournal(transaction, "verifying");
|
|
236
|
+
if (options.skipProjectChecks) {
|
|
237
|
+
addCheck(result, "project checks", "skipped", "Skipped typecheck and build by explicit request.");
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
verifyProject(inspection, runtime);
|
|
241
|
+
}
|
|
242
|
+
removeCurrentManagedArchive(inspection);
|
|
243
|
+
result.changedFiles = changedFiles(transaction.snapshots, result.projectDirectory);
|
|
244
|
+
if (!result.changedFiles.includes("node_modules/@aiot-nexus/plugin-sdk")) {
|
|
245
|
+
result.changedFiles.push("node_modules/@aiot-nexus/plugin-sdk");
|
|
246
|
+
}
|
|
247
|
+
writeTransactionJournal(transaction, "committed");
|
|
248
|
+
try {
|
|
249
|
+
cleanupUpgradeTransaction(transaction);
|
|
250
|
+
result.status = "upgraded";
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
addCheck(result, "transaction cleanup", "failed", errorMessage(error));
|
|
254
|
+
addChangedFile(result, recoveryDirectoryName);
|
|
255
|
+
result.status = "blocked";
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
addCheck(result, "upgrade", "failed", errorMessage(error));
|
|
261
|
+
tryWriteTransactionJournal(transaction, "rolling-back", errorMessage(error));
|
|
262
|
+
result.rollback = rollbackUpgrade(inspection, transaction.snapshots, runtime);
|
|
263
|
+
result.changedFiles = changedFiles(transaction.snapshots, result.projectDirectory);
|
|
264
|
+
if (result.rollback.succeeded) {
|
|
265
|
+
try {
|
|
266
|
+
writeTransactionJournal(transaction, "rolled-back");
|
|
267
|
+
cleanupUpgradeTransaction(transaction);
|
|
268
|
+
}
|
|
269
|
+
catch (cleanupError) {
|
|
270
|
+
result.rollback = {
|
|
271
|
+
attempted: true,
|
|
272
|
+
succeeded: false,
|
|
273
|
+
detail: result.rollback.detail +
|
|
274
|
+
" Recovery data could not be removed: " +
|
|
275
|
+
errorMessage(cleanupError),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
tryWriteTransactionJournal(transaction, "rollback-failed", result.rollback.detail);
|
|
281
|
+
}
|
|
282
|
+
if (!result.rollback.succeeded) {
|
|
283
|
+
addChangedFile(result, "node_modules/@aiot-nexus/plugin-sdk");
|
|
284
|
+
addChangedFile(result, recoveryDirectoryName);
|
|
285
|
+
}
|
|
286
|
+
result.status = result.rollback.succeeded ? "rolled-back" : "blocked";
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function verifyInstalledState(inspection, runtime) {
|
|
291
|
+
const projectDirectory = inspection.result.projectDirectory;
|
|
292
|
+
const packageJson = readJson(inspection.packageJsonPath, "Updated package.json");
|
|
293
|
+
const dependencies = asRecord(packageJson[inspection.dependencySection]);
|
|
294
|
+
if (!dependencies ||
|
|
295
|
+
dependencies[sdkPackageName] !== inspection.normalizedSource.dependencySpec) {
|
|
296
|
+
throw new Error("npm install did not write the expected exact SDK dependency spec.");
|
|
297
|
+
}
|
|
298
|
+
const installedPackagePath = path.join(projectDirectory, "node_modules", "@aiot-nexus", "plugin-sdk", "package.json");
|
|
299
|
+
const installedPackage = readJson(installedPackagePath, "Installed SDK package.json");
|
|
300
|
+
if (installedPackage.name !== sdkPackageName ||
|
|
301
|
+
installedPackage.version !== sdk_info_1.PLUGIN_SDK_VERSION) {
|
|
302
|
+
throw new Error("Installed SDK version does not match " + sdk_info_1.PLUGIN_SDK_VERSION + ".");
|
|
303
|
+
}
|
|
304
|
+
const lock = readJson(inspection.packageLockPath, "Updated package-lock.json");
|
|
305
|
+
if (readLockedSdkVersion(lock) !== sdk_info_1.PLUGIN_SDK_VERSION) {
|
|
306
|
+
throw new Error("package-lock.json does not resolve SDK " + sdk_info_1.PLUGIN_SDK_VERSION + ".");
|
|
307
|
+
}
|
|
308
|
+
const installedCliPath = path.join(projectDirectory, "node_modules", "@aiot-nexus", "plugin-sdk", "dist", "src", "cli.js");
|
|
309
|
+
assertRegularFile(installedCliPath, "Installed SDK CLI");
|
|
310
|
+
const infoResult = runtime.runNode([installedCliPath, "info", "--json"], projectDirectory);
|
|
311
|
+
if (!commandSucceeded(infoResult)) {
|
|
312
|
+
throw new Error(commandFailure("installed nexus-plugin info --json", infoResult));
|
|
313
|
+
}
|
|
314
|
+
const installedInfo = parseJsonText(infoResult.stdout, "Installed SDK info output");
|
|
315
|
+
if (installedInfo.sdkVersion !== sdk_info_1.PLUGIN_SDK_VERSION) {
|
|
316
|
+
throw new Error("Installed SDK CLI reports version " +
|
|
317
|
+
String(installedInfo.sdkVersion) +
|
|
318
|
+
".");
|
|
319
|
+
}
|
|
320
|
+
addCheck(inspection.result, "installed SDK", "passed", "Installed CLI reports SDK " + sdk_info_1.PLUGIN_SDK_VERSION + ".");
|
|
321
|
+
}
|
|
322
|
+
function verifyProject(inspection, runtime) {
|
|
323
|
+
const scripts = asRecord(inspection.packageJson.scripts) || {};
|
|
324
|
+
let ranProjectCheck = false;
|
|
325
|
+
if (typeof scripts.typecheck === "string") {
|
|
326
|
+
runRequiredNpmScript("typecheck", inspection, runtime);
|
|
327
|
+
ranProjectCheck = true;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
addCheck(inspection.result, "typecheck", "skipped", "Project does not define a typecheck script.");
|
|
331
|
+
}
|
|
332
|
+
if (typeof scripts.build === "string") {
|
|
333
|
+
runRequiredNpmScript("build", inspection, runtime);
|
|
334
|
+
ranProjectCheck = true;
|
|
335
|
+
validateBuiltPlugin(inspection, runtime);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
addCheck(inspection.result, "build", "skipped", "Project does not define a build script.");
|
|
339
|
+
}
|
|
340
|
+
if (!ranProjectCheck) {
|
|
341
|
+
throw new Error("Project defines neither typecheck nor build; use --skip-project-checks only after manual verification.");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function runRequiredNpmScript(scriptName, inspection, runtime) {
|
|
345
|
+
const commandResult = runtime.runNpm(["run", scriptName], inspection.result.projectDirectory);
|
|
346
|
+
if (!commandSucceeded(commandResult)) {
|
|
347
|
+
throw new Error(commandFailure("npm run " + scriptName, commandResult));
|
|
348
|
+
}
|
|
349
|
+
addCheck(inspection.result, scriptName, "passed", "npm run " + scriptName + " completed successfully.");
|
|
350
|
+
}
|
|
351
|
+
function validateBuiltPlugin(inspection, runtime) {
|
|
352
|
+
if (!inspection.builtPackagePath) {
|
|
353
|
+
addCheck(inspection.result, "package validation", "skipped", "src/manifest.json was not found.");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const packageDirectory = inspection.builtPackagePath;
|
|
357
|
+
assertRegularFile(path.join(packageDirectory, "manifest.json"), "Built plugin manifest");
|
|
358
|
+
const installedCliPath = path.join(inspection.result.projectDirectory, "node_modules", "@aiot-nexus", "plugin-sdk", "dist", "src", "cli.js");
|
|
359
|
+
const validation = runtime.runNode([installedCliPath, "validate", packageDirectory], inspection.result.projectDirectory);
|
|
360
|
+
if (!commandSucceeded(validation)) {
|
|
361
|
+
throw new Error(commandFailure("built plugin validation", validation));
|
|
362
|
+
}
|
|
363
|
+
addCheck(inspection.result, "package validation", "passed", "Validated " +
|
|
364
|
+
relativePath(inspection.result.projectDirectory, packageDirectory) +
|
|
365
|
+
".");
|
|
366
|
+
}
|
|
367
|
+
function rollbackUpgrade(inspection, snapshots, runtime) {
|
|
368
|
+
try {
|
|
369
|
+
snapshots.slice().reverse().forEach(restoreSnapshot);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
return {
|
|
373
|
+
attempted: true,
|
|
374
|
+
succeeded: false,
|
|
375
|
+
detail: "Could not restore project files: " + errorMessage(error),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const restoreCommand = inspection.packageLockExisted
|
|
379
|
+
? ["ci"]
|
|
380
|
+
: ["install", "--no-package-lock"];
|
|
381
|
+
const reinstall = runtime.runNpm(restoreCommand, inspection.result.projectDirectory);
|
|
382
|
+
if (!commandSucceeded(reinstall)) {
|
|
383
|
+
return {
|
|
384
|
+
attempted: true,
|
|
385
|
+
succeeded: false,
|
|
386
|
+
detail: commandFailure("npm " + restoreCommand[0] + " after file restoration", reinstall),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const restoredVersion = readInstalledSdkVersion(inspection.result.projectDirectory);
|
|
390
|
+
if (inspection.result.currentVersion &&
|
|
391
|
+
restoredVersion !== inspection.result.currentVersion) {
|
|
392
|
+
return {
|
|
393
|
+
attempted: true,
|
|
394
|
+
succeeded: false,
|
|
395
|
+
detail: "Project files were restored, but installed SDK is " +
|
|
396
|
+
String(restoredVersion) +
|
|
397
|
+
".",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (!inspection.packageLockExisted) {
|
|
401
|
+
return {
|
|
402
|
+
attempted: true,
|
|
403
|
+
succeeded: false,
|
|
404
|
+
detail: "Project files and SDK were restored, but the complete npm dependency tree cannot be proven without the original package-lock.json. Recovery data was retained; run npm install and the project checks before continuing.",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
attempted: true,
|
|
409
|
+
succeeded: true,
|
|
410
|
+
detail: "Restored project files and npm installation state.",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function validateProjectBaseline(packageJson, result) {
|
|
414
|
+
const engines = asRecord(packageJson.engines);
|
|
415
|
+
const projectNodeEngine = engines && engines.node;
|
|
416
|
+
addCheck(result, "project Node engine", projectNodeEngine === sdk_info_1.PLUGIN_SDK_NODE_ENGINE ? "passed" : "failed", projectNodeEngine === sdk_info_1.PLUGIN_SDK_NODE_ENGINE
|
|
417
|
+
? "Project declares " + sdk_info_1.PLUGIN_SDK_NODE_ENGINE + "."
|
|
418
|
+
: "Expected engines.node " +
|
|
419
|
+
sdk_info_1.PLUGIN_SDK_NODE_ENGINE +
|
|
420
|
+
"; found " +
|
|
421
|
+
String(projectNodeEngine || "missing") +
|
|
422
|
+
".");
|
|
423
|
+
validateExactDependency(packageJson, "electron", sdk_info_1.PLUGIN_SDK_ELECTRON_VERSION, result);
|
|
424
|
+
validateExactDependency(packageJson, "typescript", sdk_info_1.PLUGIN_SDK_TYPESCRIPT_VERSION, result);
|
|
425
|
+
validateExactDependency(packageJson, "@types/node", sdk_info_1.PLUGIN_SDK_TYPES_NODE_VERSION, result);
|
|
426
|
+
}
|
|
427
|
+
function validateExactDependency(packageJson, dependencyName, expectedVersion, result) {
|
|
428
|
+
const locations = ["dependencies", "devDependencies"]
|
|
429
|
+
.map((section) => asRecord(packageJson[section]))
|
|
430
|
+
.filter((section) => Boolean(section && dependencyName in section));
|
|
431
|
+
const values = locations.map((section) => section[dependencyName]);
|
|
432
|
+
const passed = values.length === 1 && values[0] === expectedVersion;
|
|
433
|
+
addCheck(result, dependencyName, passed ? "passed" : "failed", passed
|
|
434
|
+
? "Project declares the required exact version " + expectedVersion + "."
|
|
435
|
+
: "Expected exactly one declaration at " +
|
|
436
|
+
expectedVersion +
|
|
437
|
+
"; found " +
|
|
438
|
+
(values.map(String).join(", ") || "missing") +
|
|
439
|
+
".");
|
|
440
|
+
}
|
|
441
|
+
function normalizeSdkSource(value, workingDirectory, projectDirectory, runtime, result) {
|
|
442
|
+
const source = value.trim();
|
|
443
|
+
const packagePrefix = sdkPackageName + "@";
|
|
444
|
+
const version = source.startsWith(packagePrefix)
|
|
445
|
+
? source.slice(packagePrefix.length)
|
|
446
|
+
: source;
|
|
447
|
+
if (isStrictVersion(version)) {
|
|
448
|
+
if (version !== sdk_info_1.PLUGIN_SDK_VERSION) {
|
|
449
|
+
addCheck(result, "SDK source", "failed", "The source version " +
|
|
450
|
+
version +
|
|
451
|
+
" does not match the executing CLI version " +
|
|
452
|
+
sdk_info_1.PLUGIN_SDK_VERSION +
|
|
453
|
+
".");
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
addCheck(result, "SDK source", "passed", "Using exact registry version " + version + ".");
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
display: version,
|
|
460
|
+
dependencySpec: version,
|
|
461
|
+
installSpec: sdkPackageName + "@" + version,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
let localArchivePath;
|
|
465
|
+
if (source.startsWith("file:")) {
|
|
466
|
+
try {
|
|
467
|
+
localArchivePath = (0, url_1.fileURLToPath)(source);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
addCheck(result, "SDK source", "failed", "Invalid SDK file URL: " + errorMessage(error));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else if (isPathLike(source)) {
|
|
474
|
+
localArchivePath = path.resolve(workingDirectory, source);
|
|
475
|
+
}
|
|
476
|
+
if (!localArchivePath) {
|
|
477
|
+
addCheck(result, "SDK source", "failed", "SDK source must be an exact X.Y.Z version or a local .tgz path.");
|
|
478
|
+
return { display: source, dependencySpec: source, installSpec: source };
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
assertRegularFile(localArchivePath, "Local SDK archive", ".tgz");
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
addCheck(result, "SDK source", "failed", errorMessage(error));
|
|
485
|
+
return {
|
|
486
|
+
display: localArchivePath,
|
|
487
|
+
dependencySpec: source,
|
|
488
|
+
installSpec: source,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const archiveInfo = runtime.runNpm([
|
|
492
|
+
"pack",
|
|
493
|
+
localArchivePath,
|
|
494
|
+
"--dry-run",
|
|
495
|
+
"--ignore-scripts",
|
|
496
|
+
"--json",
|
|
497
|
+
"--silent",
|
|
498
|
+
], workingDirectory);
|
|
499
|
+
if (!commandSucceeded(archiveInfo)) {
|
|
500
|
+
addCheck(result, "SDK source", "failed", commandFailure("npm pack --dry-run", archiveInfo));
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
try {
|
|
504
|
+
const report = JSON.parse(archiveInfo.stdout.trim());
|
|
505
|
+
const entry = Array.isArray(report) && report.length === 1
|
|
506
|
+
? asRecord(report[0])
|
|
507
|
+
: undefined;
|
|
508
|
+
if (!entry ||
|
|
509
|
+
entry.name !== sdkPackageName ||
|
|
510
|
+
entry.version !== sdk_info_1.PLUGIN_SDK_VERSION) {
|
|
511
|
+
throw new Error("Archive must contain " +
|
|
512
|
+
sdkPackageName +
|
|
513
|
+
"@" +
|
|
514
|
+
sdk_info_1.PLUGIN_SDK_VERSION +
|
|
515
|
+
"; found " +
|
|
516
|
+
String(entry && entry.name) +
|
|
517
|
+
"@" +
|
|
518
|
+
String(entry && entry.version) +
|
|
519
|
+
".");
|
|
520
|
+
}
|
|
521
|
+
addCheck(result, "SDK source", "passed", "Validated local SDK archive " + localArchivePath + ".");
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
addCheck(result, "SDK source", "failed", errorMessage(error));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const vendoredRelativePath = "vendor/aiot-nexus-plugin-sdk-" + sdk_info_1.PLUGIN_SDK_VERSION + ".tgz";
|
|
528
|
+
return {
|
|
529
|
+
display: localArchivePath,
|
|
530
|
+
dependencySpec: "file:" + vendoredRelativePath,
|
|
531
|
+
installSpec: "file:" + vendoredRelativePath,
|
|
532
|
+
localArchivePath,
|
|
533
|
+
vendoredArchivePath: path.join(projectDirectory, ...vendoredRelativePath.split("/")),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function assertNpmProject(projectDirectory, result) {
|
|
537
|
+
const conflictingLocks = ["yarn.lock", "pnpm-lock.yaml"].filter((name) => fs.existsSync(path.join(projectDirectory, name)));
|
|
538
|
+
if (conflictingLocks.length > 0) {
|
|
539
|
+
addCheck(result, "package manager", "failed", "Only npm projects are supported; found " +
|
|
540
|
+
conflictingLocks.join(", ") +
|
|
541
|
+
".");
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
addCheck(result, "package manager", "passed", "No Yarn or pnpm lockfile was found.");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function validatePackageLock(packageLock, result) {
|
|
548
|
+
if (!packageLock) {
|
|
549
|
+
addCheck(result, "package-lock.json", "warning", "No lockfile exists; npm install will create one.");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const version = packageLock.lockfileVersion;
|
|
553
|
+
if (typeof version !== "number" || !supportedLockfileVersions.has(version)) {
|
|
554
|
+
addCheck(result, "package-lock.json", "failed", "Supported lockfileVersion values are 1 and 2; found " +
|
|
555
|
+
String(version) +
|
|
556
|
+
".");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
addCheck(result, "package-lock.json", "passed", "Using npm lockfileVersion " + String(version) + ".");
|
|
560
|
+
}
|
|
561
|
+
function validateManagedArchivePaths(vendoredArchivePath, currentManagedArchivePath, result) {
|
|
562
|
+
if (!vendoredArchivePath && !currentManagedArchivePath) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const archivePaths = [vendoredArchivePath, currentManagedArchivePath].filter((value) => Boolean(value));
|
|
566
|
+
const vendorDirectories = Array.from(new Set(archivePaths.map((filePath) => path.dirname(filePath))));
|
|
567
|
+
try {
|
|
568
|
+
for (const directory of vendorDirectories) {
|
|
569
|
+
if (!fs.existsSync(directory)) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const stat = fs.lstatSync(directory);
|
|
573
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
574
|
+
throw new Error("Managed SDK vendor path must be a regular directory and cannot be a symbolic link: " +
|
|
575
|
+
directory);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const archivePath of archivePaths) {
|
|
579
|
+
if (fs.existsSync(archivePath)) {
|
|
580
|
+
assertRegularFile(archivePath, "Managed SDK archive", ".tgz");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (currentManagedArchivePath &&
|
|
584
|
+
!fs.existsSync(currentManagedArchivePath)) {
|
|
585
|
+
throw new Error("The current vendored SDK archive is missing: " +
|
|
586
|
+
currentManagedArchivePath);
|
|
587
|
+
}
|
|
588
|
+
addCheck(result, "managed SDK archive", "passed", "Vendor directory and managed archive paths are regular filesystem entries.");
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
addCheck(result, "managed SDK archive", "failed", errorMessage(error));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function resolveManagedArchivePath(dependencySpec, projectDirectory) {
|
|
595
|
+
const normalizedSpec = dependencySpec.replace(/\\/g, "/");
|
|
596
|
+
const match = /^file:vendor\/(aiot-nexus-plugin-sdk(?:-[0-9]+\.[0-9]+\.[0-9]+)?\.tgz)$/.exec(normalizedSpec);
|
|
597
|
+
if (!match) {
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
const archivePath = path.join(projectDirectory, "vendor", match[1]);
|
|
601
|
+
assertProjectPath(projectDirectory, archivePath, "Managed SDK archive");
|
|
602
|
+
return archivePath;
|
|
603
|
+
}
|
|
604
|
+
function resolveBuiltPackagePath(packageJson, projectDirectory, result) {
|
|
605
|
+
const scripts = asRecord(packageJson.scripts);
|
|
606
|
+
if (!scripts || typeof scripts.build !== "string") {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
const sourceManifestPath = path.join(projectDirectory, "src", "manifest.json");
|
|
610
|
+
if (!fs.existsSync(sourceManifestPath)) {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
assertRegularFile(sourceManifestPath, "Source plugin manifest", ".json");
|
|
615
|
+
const sourceManifest = readJson(sourceManifestPath, "Source plugin manifest");
|
|
616
|
+
if (typeof sourceManifest.pluginId !== "string" ||
|
|
617
|
+
!pluginIdPattern.test(sourceManifest.pluginId)) {
|
|
618
|
+
throw new Error("Source plugin manifest pluginId must match " +
|
|
619
|
+
pluginIdPattern.source +
|
|
620
|
+
".");
|
|
621
|
+
}
|
|
622
|
+
const packageDirectory = path.join(projectDirectory, "dist", sourceManifest.pluginId);
|
|
623
|
+
assertProjectPath(projectDirectory, packageDirectory, "Built plugin package");
|
|
624
|
+
return packageDirectory;
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
addCheck(result, "package output", "failed", errorMessage(error));
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function findSdkDependency(packageJson, result) {
|
|
632
|
+
const found = [];
|
|
633
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
634
|
+
const dependencies = asRecord(packageJson[section]);
|
|
635
|
+
if (dependencies && sdkPackageName in dependencies) {
|
|
636
|
+
const spec = dependencies[sdkPackageName];
|
|
637
|
+
if (typeof spec !== "string" || !spec.trim()) {
|
|
638
|
+
addCheck(result, "SDK dependency", "failed", section + " contains an invalid SDK dependency spec.");
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
found.push({ section, spec });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (found.length === 0) {
|
|
646
|
+
addCheck(result, "SDK dependency", "failed", "Project does not declare " + sdkPackageName + ".");
|
|
647
|
+
return { section: "devDependencies", spec: "" };
|
|
648
|
+
}
|
|
649
|
+
if (found.length > 1) {
|
|
650
|
+
addCheck(result, "SDK dependency", "failed", "SDK is declared in both dependencies and devDependencies.");
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
addCheck(result, "SDK dependency location", "passed", "SDK is declared in " + found[0].section + ".");
|
|
654
|
+
}
|
|
655
|
+
return found[0];
|
|
656
|
+
}
|
|
657
|
+
function resolveCurrentVersion(projectDirectory, dependencySpec, packageLock, result) {
|
|
658
|
+
const candidates = [
|
|
659
|
+
readInstalledSdkVersion(projectDirectory),
|
|
660
|
+
readLockedSdkVersion(packageLock),
|
|
661
|
+
isStrictVersion(dependencySpec) ? dependencySpec : undefined,
|
|
662
|
+
].filter((value) => Boolean(value));
|
|
663
|
+
const versions = Array.from(new Set(candidates));
|
|
664
|
+
if (versions.length > 1) {
|
|
665
|
+
addCheck(result, "current SDK", "failed", "SDK dependency state is inconsistent: " + versions.join(", ") + ".");
|
|
666
|
+
return versions[0];
|
|
667
|
+
}
|
|
668
|
+
if (versions.length === 1) {
|
|
669
|
+
addCheck(result, "current SDK", "passed", "Resolved current SDK " + versions[0] + ".");
|
|
670
|
+
return versions[0];
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
function readInstalledSdkVersion(projectDirectory) {
|
|
675
|
+
const packagePath = path.join(projectDirectory, "node_modules", "@aiot-nexus", "plugin-sdk", "package.json");
|
|
676
|
+
if (!fs.existsSync(packagePath)) {
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const packageJson = readJson(packagePath, "Installed SDK package.json");
|
|
681
|
+
return packageJson.name === sdkPackageName &&
|
|
682
|
+
isStrictVersion(packageJson.version)
|
|
683
|
+
? packageJson.version
|
|
684
|
+
: undefined;
|
|
685
|
+
}
|
|
686
|
+
catch (_error) {
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function readLockedSdkVersion(packageLock) {
|
|
691
|
+
if (!packageLock) {
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
const packages = asRecord(packageLock.packages);
|
|
695
|
+
const installed = packages && asRecord(packages["node_modules/@aiot-nexus/plugin-sdk"]);
|
|
696
|
+
if (installed && isStrictVersion(installed.version)) {
|
|
697
|
+
return installed.version;
|
|
698
|
+
}
|
|
699
|
+
const dependencies = asRecord(packageLock.dependencies);
|
|
700
|
+
const sdk = dependencies && asRecord(dependencies[sdkPackageName]);
|
|
701
|
+
return sdk && isStrictVersion(sdk.version) ? sdk.version : undefined;
|
|
702
|
+
}
|
|
703
|
+
function prepareLocalArchive(source) {
|
|
704
|
+
if (!source.localArchivePath || !source.vendoredArchivePath) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (samePath(source.localArchivePath, source.vendoredArchivePath)) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
fs.mkdirSync(path.dirname(source.vendoredArchivePath), { recursive: true });
|
|
711
|
+
fs.copyFileSync(source.localArchivePath, source.vendoredArchivePath);
|
|
712
|
+
}
|
|
713
|
+
function removeCurrentManagedArchive(inspection) {
|
|
714
|
+
if (!inspection.currentManagedArchivePath) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const targetArchivePath = inspection.normalizedSource.vendoredArchivePath;
|
|
718
|
+
if (!targetArchivePath ||
|
|
719
|
+
!samePath(inspection.currentManagedArchivePath, targetArchivePath)) {
|
|
720
|
+
fs.rmSync(inspection.currentManagedArchivePath, { force: true });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function plannedChangedFiles(inspection) {
|
|
724
|
+
const files = [
|
|
725
|
+
"package.json",
|
|
726
|
+
"package-lock.json",
|
|
727
|
+
"node_modules/@aiot-nexus/plugin-sdk",
|
|
728
|
+
];
|
|
729
|
+
const projectDirectory = inspection.result.projectDirectory;
|
|
730
|
+
if (inspection.normalizedSource.vendoredArchivePath) {
|
|
731
|
+
files.push(relativePath(projectDirectory, inspection.normalizedSource.vendoredArchivePath));
|
|
732
|
+
}
|
|
733
|
+
if (inspection.currentManagedArchivePath) {
|
|
734
|
+
files.push(relativePath(projectDirectory, inspection.currentManagedArchivePath));
|
|
735
|
+
}
|
|
736
|
+
if (inspection.builtPackagePath) {
|
|
737
|
+
files.push(relativePath(projectDirectory, inspection.builtPackagePath));
|
|
738
|
+
}
|
|
739
|
+
return Array.from(new Set(files));
|
|
740
|
+
}
|
|
741
|
+
function restoreSnapshot(snapshot) {
|
|
742
|
+
removePath(snapshot.filePath);
|
|
743
|
+
if (snapshot.kind === "missing") {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (!snapshot.backupPath) {
|
|
747
|
+
throw new Error("Recovery snapshot is missing its backup path: " + snapshot.filePath);
|
|
748
|
+
}
|
|
749
|
+
fs.mkdirSync(path.dirname(snapshot.filePath), { recursive: true });
|
|
750
|
+
if (snapshot.kind === "file") {
|
|
751
|
+
fs.copyFileSync(snapshot.backupPath, snapshot.filePath);
|
|
752
|
+
if (snapshot.mode !== undefined) {
|
|
753
|
+
fs.chmodSync(snapshot.filePath, snapshot.mode);
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
copyDirectory(snapshot.backupPath, snapshot.filePath);
|
|
758
|
+
}
|
|
759
|
+
function changedFiles(snapshots, projectDirectory) {
|
|
760
|
+
return snapshots
|
|
761
|
+
.filter(snapshotChanged)
|
|
762
|
+
.map((snapshot) => relativePath(projectDirectory, snapshot.filePath));
|
|
763
|
+
}
|
|
764
|
+
function assertNoPendingTransaction(projectDirectory) {
|
|
765
|
+
const transactionDirectory = path.join(projectDirectory, recoveryDirectoryName);
|
|
766
|
+
if (!fs.existsSync(transactionDirectory)) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const stat = fs.lstatSync(transactionDirectory);
|
|
770
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
771
|
+
throw new Error("SDK upgrade recovery path is not a regular directory: " +
|
|
772
|
+
transactionDirectory);
|
|
773
|
+
}
|
|
774
|
+
const journalPath = path.join(transactionDirectory, "recovery.json");
|
|
775
|
+
let phase = "unknown";
|
|
776
|
+
if (fs.existsSync(journalPath)) {
|
|
777
|
+
try {
|
|
778
|
+
const journal = readJson(journalPath, "SDK upgrade recovery journal");
|
|
779
|
+
if (typeof journal.phase === "string") {
|
|
780
|
+
phase = journal.phase;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
catch (_error) {
|
|
784
|
+
phase = "invalid-journal";
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const recoveryInstruction = phase === "committed"
|
|
788
|
+
? "The upgrade reached the committed phase. Verify the target SDK, lockfile, and project checks, then remove the recovery directory; do not restore the backups unless you intentionally revert."
|
|
789
|
+
: phase === "rolled-back"
|
|
790
|
+
? "The rollback completed. Verify the original SDK and project checks, then remove the recovery directory."
|
|
791
|
+
: "Restore the files listed in recovery.json from its backups, then run npm ci when the original lockfile existed or npm install otherwise.";
|
|
792
|
+
throw new Error("A retained SDK upgrade recovery transaction was found at " +
|
|
793
|
+
transactionDirectory +
|
|
794
|
+
" (phase: " +
|
|
795
|
+
phase +
|
|
796
|
+
"). " +
|
|
797
|
+
recoveryInstruction +
|
|
798
|
+
" Do not delete the directory until the project is verified.");
|
|
799
|
+
}
|
|
800
|
+
function createUpgradeTransaction(inspection, snapshotPaths) {
|
|
801
|
+
const projectDirectory = inspection.result.projectDirectory;
|
|
802
|
+
const directory = path.join(projectDirectory, recoveryDirectoryName);
|
|
803
|
+
assertProjectPath(projectDirectory, directory, "SDK upgrade recovery directory");
|
|
804
|
+
if (fs.existsSync(directory)) {
|
|
805
|
+
throw new Error("SDK upgrade recovery directory already exists: " + directory);
|
|
806
|
+
}
|
|
807
|
+
fs.mkdirSync(directory);
|
|
808
|
+
const backupRoot = path.join(directory, "backups");
|
|
809
|
+
fs.mkdirSync(backupRoot);
|
|
810
|
+
const transaction = {
|
|
811
|
+
directory,
|
|
812
|
+
journalPath: path.join(directory, "recovery.json"),
|
|
813
|
+
projectDirectory,
|
|
814
|
+
currentVersion: inspection.result.currentVersion,
|
|
815
|
+
targetVersion: inspection.result.targetVersion,
|
|
816
|
+
packageLockExisted: inspection.packageLockExisted,
|
|
817
|
+
snapshots: [],
|
|
818
|
+
};
|
|
819
|
+
try {
|
|
820
|
+
transaction.snapshots = snapshotPaths.map((filePath, index) => snapshotPath(projectDirectory, filePath, path.join(backupRoot, String(index))));
|
|
821
|
+
writeTransactionJournal(transaction, "prepared");
|
|
822
|
+
return transaction;
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
let retainedDetail = "";
|
|
826
|
+
try {
|
|
827
|
+
cleanupUpgradeTransaction(transaction);
|
|
828
|
+
}
|
|
829
|
+
catch (cleanupError) {
|
|
830
|
+
retainedDetail =
|
|
831
|
+
" Recovery data remains at " +
|
|
832
|
+
directory +
|
|
833
|
+
": " +
|
|
834
|
+
errorMessage(cleanupError) +
|
|
835
|
+
".";
|
|
836
|
+
}
|
|
837
|
+
throw new Error("Could not create the durable SDK upgrade recovery snapshot: " +
|
|
838
|
+
errorMessage(error) +
|
|
839
|
+
"." +
|
|
840
|
+
retainedDetail);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function snapshotPath(projectDirectory, filePath, backupPath) {
|
|
844
|
+
assertProjectPath(projectDirectory, filePath, "SDK upgrade snapshot target");
|
|
845
|
+
if (!fs.existsSync(filePath)) {
|
|
846
|
+
return { filePath, kind: "missing" };
|
|
847
|
+
}
|
|
848
|
+
const stat = fs.lstatSync(filePath);
|
|
849
|
+
if (stat.isSymbolicLink()) {
|
|
850
|
+
throw new Error("SDK upgrade snapshot targets cannot be symbolic links: " + filePath);
|
|
851
|
+
}
|
|
852
|
+
if (stat.isFile()) {
|
|
853
|
+
fs.copyFileSync(filePath, backupPath);
|
|
854
|
+
fs.chmodSync(backupPath, stat.mode);
|
|
855
|
+
flushFile(backupPath);
|
|
856
|
+
return { filePath, kind: "file", backupPath, mode: stat.mode };
|
|
857
|
+
}
|
|
858
|
+
if (stat.isDirectory()) {
|
|
859
|
+
copyDirectory(filePath, backupPath);
|
|
860
|
+
return { filePath, kind: "directory", backupPath, mode: stat.mode };
|
|
861
|
+
}
|
|
862
|
+
throw new Error("SDK upgrade snapshot targets must be regular files or directories: " +
|
|
863
|
+
filePath);
|
|
864
|
+
}
|
|
865
|
+
function writeTransactionJournal(transaction, phase, detail) {
|
|
866
|
+
const journal = {
|
|
867
|
+
schemaVersion: 1,
|
|
868
|
+
phase,
|
|
869
|
+
detail: detail || null,
|
|
870
|
+
projectDirectory: transaction.projectDirectory,
|
|
871
|
+
currentVersion: transaction.currentVersion,
|
|
872
|
+
targetVersion: transaction.targetVersion,
|
|
873
|
+
packageLockExisted: transaction.packageLockExisted,
|
|
874
|
+
recoveryInstallCommand: transaction.packageLockExisted
|
|
875
|
+
? "npm ci"
|
|
876
|
+
: "npm install",
|
|
877
|
+
snapshots: transaction.snapshots.map((snapshot) => ({
|
|
878
|
+
path: relativePath(transaction.projectDirectory, snapshot.filePath),
|
|
879
|
+
kind: snapshot.kind,
|
|
880
|
+
backup: snapshot.backupPath
|
|
881
|
+
? relativePath(transaction.directory, snapshot.backupPath)
|
|
882
|
+
: null,
|
|
883
|
+
mode: snapshot.mode === undefined ? null : snapshot.mode,
|
|
884
|
+
})),
|
|
885
|
+
};
|
|
886
|
+
const temporaryPath = transaction.journalPath + ".tmp";
|
|
887
|
+
fs.writeFileSync(temporaryPath, JSON.stringify(journal, null, 2) + "\n", "utf8");
|
|
888
|
+
flushFile(temporaryPath);
|
|
889
|
+
fs.rmSync(transaction.journalPath, { force: true });
|
|
890
|
+
fs.renameSync(temporaryPath, transaction.journalPath);
|
|
891
|
+
}
|
|
892
|
+
function tryWriteTransactionJournal(transaction, phase, detail) {
|
|
893
|
+
try {
|
|
894
|
+
writeTransactionJournal(transaction, phase, detail);
|
|
895
|
+
}
|
|
896
|
+
catch (_error) {
|
|
897
|
+
// The recovery directory and its backups remain the primary recovery evidence.
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function cleanupUpgradeTransaction(transaction) {
|
|
901
|
+
const expectedDirectory = path.join(transaction.projectDirectory, recoveryDirectoryName);
|
|
902
|
+
if (!samePath(transaction.directory, expectedDirectory)) {
|
|
903
|
+
throw new Error("Refusing to remove an unexpected SDK recovery directory: " +
|
|
904
|
+
transaction.directory);
|
|
905
|
+
}
|
|
906
|
+
if (!fs.existsSync(transaction.directory)) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const stat = fs.lstatSync(transaction.directory);
|
|
910
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
911
|
+
throw new Error("Refusing to remove a non-directory SDK recovery path: " +
|
|
912
|
+
transaction.directory);
|
|
913
|
+
}
|
|
914
|
+
fs.rmSync(transaction.directory, { recursive: true, force: true });
|
|
915
|
+
}
|
|
916
|
+
function snapshotChanged(snapshot) {
|
|
917
|
+
if (snapshot.kind === "missing") {
|
|
918
|
+
return fs.existsSync(snapshot.filePath);
|
|
919
|
+
}
|
|
920
|
+
if (!fs.existsSync(snapshot.filePath) || !snapshot.backupPath) {
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
const stat = fs.lstatSync(snapshot.filePath);
|
|
924
|
+
if (stat.isSymbolicLink()) {
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
if (snapshot.kind === "file") {
|
|
928
|
+
return (!stat.isFile() || !filesEqual(snapshot.filePath, snapshot.backupPath));
|
|
929
|
+
}
|
|
930
|
+
return (!stat.isDirectory() ||
|
|
931
|
+
!directoriesEqual(snapshot.filePath, snapshot.backupPath));
|
|
932
|
+
}
|
|
933
|
+
function filesEqual(left, right) {
|
|
934
|
+
const leftStat = fs.statSync(left);
|
|
935
|
+
const rightStat = fs.statSync(right);
|
|
936
|
+
return (leftStat.size === rightStat.size &&
|
|
937
|
+
fs.readFileSync(left).equals(fs.readFileSync(right)));
|
|
938
|
+
}
|
|
939
|
+
function directoriesEqual(left, right) {
|
|
940
|
+
const leftEntries = fs
|
|
941
|
+
.readdirSync(left, { withFileTypes: true })
|
|
942
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
943
|
+
const rightEntries = fs
|
|
944
|
+
.readdirSync(right, { withFileTypes: true })
|
|
945
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
946
|
+
if (leftEntries.length !== rightEntries.length) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
for (let index = 0; index < leftEntries.length; index += 1) {
|
|
950
|
+
const leftEntry = leftEntries[index];
|
|
951
|
+
const rightEntry = rightEntries[index];
|
|
952
|
+
if (leftEntry.name !== rightEntry.name ||
|
|
953
|
+
leftEntry.isDirectory() !== rightEntry.isDirectory() ||
|
|
954
|
+
leftEntry.isFile() !== rightEntry.isFile()) {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
const leftPath = path.join(left, leftEntry.name);
|
|
958
|
+
const rightPath = path.join(right, rightEntry.name);
|
|
959
|
+
if (leftEntry.isDirectory()) {
|
|
960
|
+
if (!directoriesEqual(leftPath, rightPath)) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
else if (!leftEntry.isFile() || !filesEqual(leftPath, rightPath)) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
function copyDirectory(sourceDirectory, targetDirectory) {
|
|
971
|
+
const sourceStat = fs.lstatSync(sourceDirectory);
|
|
972
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
973
|
+
throw new Error("SDK recovery can only copy regular directories: " + sourceDirectory);
|
|
974
|
+
}
|
|
975
|
+
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
976
|
+
fs.chmodSync(targetDirectory, sourceStat.mode);
|
|
977
|
+
for (const entry of fs.readdirSync(sourceDirectory, {
|
|
978
|
+
withFileTypes: true,
|
|
979
|
+
})) {
|
|
980
|
+
const sourcePath = path.join(sourceDirectory, entry.name);
|
|
981
|
+
const targetPath = path.join(targetDirectory, entry.name);
|
|
982
|
+
if (entry.isSymbolicLink()) {
|
|
983
|
+
throw new Error("SDK recovery directories cannot contain symbolic links: " + sourcePath);
|
|
984
|
+
}
|
|
985
|
+
if (entry.isDirectory()) {
|
|
986
|
+
copyDirectory(sourcePath, targetPath);
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (!entry.isFile()) {
|
|
990
|
+
throw new Error("SDK recovery encountered an unsupported filesystem entry: " +
|
|
991
|
+
sourcePath);
|
|
992
|
+
}
|
|
993
|
+
const stat = fs.lstatSync(sourcePath);
|
|
994
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
995
|
+
fs.chmodSync(targetPath, stat.mode);
|
|
996
|
+
flushFile(targetPath);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function flushFile(filePath) {
|
|
1000
|
+
const descriptor = fs.openSync(filePath, "r+");
|
|
1001
|
+
try {
|
|
1002
|
+
fs.fsyncSync(descriptor);
|
|
1003
|
+
}
|
|
1004
|
+
finally {
|
|
1005
|
+
fs.closeSync(descriptor);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function removePath(filePath) {
|
|
1009
|
+
if (!fs.existsSync(filePath)) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const stat = fs.lstatSync(filePath);
|
|
1013
|
+
fs.rmSync(filePath, {
|
|
1014
|
+
recursive: stat.isDirectory() && !stat.isSymbolicLink(),
|
|
1015
|
+
force: true,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
function assertProjectPath(projectDirectory, filePath, label) {
|
|
1019
|
+
const relative = path.relative(projectDirectory, path.resolve(filePath));
|
|
1020
|
+
if (!relative ||
|
|
1021
|
+
relative === ".." ||
|
|
1022
|
+
relative.startsWith(".." + path.sep) ||
|
|
1023
|
+
path.isAbsolute(relative)) {
|
|
1024
|
+
throw new Error(label + " must stay inside the plugin project: " + filePath);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function addChangedFile(result, filePath) {
|
|
1028
|
+
if (!result.changedFiles.includes(filePath)) {
|
|
1029
|
+
result.changedFiles.push(filePath);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function createResult(projectDirectory, source) {
|
|
1033
|
+
return {
|
|
1034
|
+
status: "blocked",
|
|
1035
|
+
projectDirectory,
|
|
1036
|
+
currentVersion: null,
|
|
1037
|
+
targetVersion: sdk_info_1.PLUGIN_SDK_VERSION,
|
|
1038
|
+
source,
|
|
1039
|
+
dependencySection: null,
|
|
1040
|
+
changedFiles: [],
|
|
1041
|
+
checks: [],
|
|
1042
|
+
rollback: null,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function addCheck(result, name, status, detail) {
|
|
1046
|
+
result.checks.push({ name, status, detail });
|
|
1047
|
+
}
|
|
1048
|
+
function hasFailedCheck(result) {
|
|
1049
|
+
return result.checks.some((check) => check.status === "failed");
|
|
1050
|
+
}
|
|
1051
|
+
function commandSucceeded(result) {
|
|
1052
|
+
return !result.error && result.status === 0;
|
|
1053
|
+
}
|
|
1054
|
+
function commandFailure(label, result) {
|
|
1055
|
+
if (result.error) {
|
|
1056
|
+
return label + " could not start: " + result.error.message;
|
|
1057
|
+
}
|
|
1058
|
+
const output = (result.stderr || result.stdout).trim();
|
|
1059
|
+
const detail = output.length > 4000 ? output.slice(output.length - 4000) : output;
|
|
1060
|
+
return (label +
|
|
1061
|
+
" failed with exit code " +
|
|
1062
|
+
String(result.status) +
|
|
1063
|
+
(detail ? ": " + detail : "."));
|
|
1064
|
+
}
|
|
1065
|
+
function defaultRuntime() {
|
|
1066
|
+
return {
|
|
1067
|
+
nodeVersion: process.version,
|
|
1068
|
+
runNpm: (args, cwd) => runNpm(args, cwd),
|
|
1069
|
+
runNode: (args, cwd) => runProcess(process.execPath, args, cwd),
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function runNpm(args, cwd) {
|
|
1073
|
+
const npmExecPath = process.env.npm_execpath;
|
|
1074
|
+
if (npmExecPath) {
|
|
1075
|
+
return runProcess(process.execPath, [npmExecPath, ...args], cwd);
|
|
1076
|
+
}
|
|
1077
|
+
return runProcess(process.platform === "win32" ? "npm.cmd" : "npm", args, cwd);
|
|
1078
|
+
}
|
|
1079
|
+
function runProcess(command, args, cwd) {
|
|
1080
|
+
const result = (0, child_process_1.spawnSync)(command, args, {
|
|
1081
|
+
cwd,
|
|
1082
|
+
encoding: "utf8",
|
|
1083
|
+
shell: false,
|
|
1084
|
+
maxBuffer: commandOutputLimit,
|
|
1085
|
+
env: {
|
|
1086
|
+
...process.env,
|
|
1087
|
+
NEXUS_PLUGIN_SDK_UPDATE_CHECK: "0",
|
|
1088
|
+
},
|
|
1089
|
+
});
|
|
1090
|
+
return {
|
|
1091
|
+
status: result.status,
|
|
1092
|
+
stdout: result.stdout || "",
|
|
1093
|
+
stderr: result.stderr || "",
|
|
1094
|
+
error: result.error,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function readJson(filePath, label) {
|
|
1098
|
+
try {
|
|
1099
|
+
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1100
|
+
const record = asRecord(value);
|
|
1101
|
+
if (!record) {
|
|
1102
|
+
throw new Error("root value must be an object");
|
|
1103
|
+
}
|
|
1104
|
+
return record;
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
throw new Error(label + " is invalid: " + errorMessage(error));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function parseJsonText(value, label) {
|
|
1111
|
+
try {
|
|
1112
|
+
const parsed = JSON.parse(value);
|
|
1113
|
+
const record = asRecord(parsed);
|
|
1114
|
+
if (!record) {
|
|
1115
|
+
throw new Error("root value must be an object");
|
|
1116
|
+
}
|
|
1117
|
+
return record;
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
throw new Error(label + " is invalid: " + errorMessage(error));
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function asRecord(value) {
|
|
1124
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1125
|
+
? value
|
|
1126
|
+
: undefined;
|
|
1127
|
+
}
|
|
1128
|
+
function assertRegularFile(filePath, label, extension) {
|
|
1129
|
+
if (extension && path.extname(filePath).toLowerCase() !== extension) {
|
|
1130
|
+
throw new Error(label + " must use the " + extension + " extension: " + filePath);
|
|
1131
|
+
}
|
|
1132
|
+
if (!fs.existsSync(filePath)) {
|
|
1133
|
+
throw new Error(label + " does not exist: " + filePath);
|
|
1134
|
+
}
|
|
1135
|
+
const stat = fs.lstatSync(filePath);
|
|
1136
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1137
|
+
throw new Error(label +
|
|
1138
|
+
" must be a regular file and cannot be a symbolic link: " +
|
|
1139
|
+
filePath);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
function isPathLike(value) {
|
|
1143
|
+
return (path.isAbsolute(value) ||
|
|
1144
|
+
value.startsWith(".") ||
|
|
1145
|
+
value.includes("/") ||
|
|
1146
|
+
value.includes("\\") ||
|
|
1147
|
+
value.toLowerCase().endsWith(".tgz"));
|
|
1148
|
+
}
|
|
1149
|
+
function isStrictVersion(value) {
|
|
1150
|
+
if (typeof value !== "string") {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
const match = strictVersionPattern.exec(value);
|
|
1154
|
+
return Boolean(match &&
|
|
1155
|
+
[match[1], match[2], match[3]].every((segment) => Number.isSafeInteger(Number(segment))));
|
|
1156
|
+
}
|
|
1157
|
+
function compareVersions(left, right) {
|
|
1158
|
+
const leftParts = versionParts(left);
|
|
1159
|
+
const rightParts = versionParts(right);
|
|
1160
|
+
for (let index = 0; index < 3; index += 1) {
|
|
1161
|
+
if (leftParts[index] !== rightParts[index]) {
|
|
1162
|
+
return leftParts[index] > rightParts[index] ? 1 : -1;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return 0;
|
|
1166
|
+
}
|
|
1167
|
+
function majorVersion(value) {
|
|
1168
|
+
return versionParts(value)[0];
|
|
1169
|
+
}
|
|
1170
|
+
function isAtLeastVersion(value, minimum) {
|
|
1171
|
+
return isStrictVersion(value) && compareVersions(value, minimum) >= 0;
|
|
1172
|
+
}
|
|
1173
|
+
function isSupportedNodeVersion(value) {
|
|
1174
|
+
return (isStrictVersion(value) &&
|
|
1175
|
+
majorVersion(value) === 16 &&
|
|
1176
|
+
compareVersions(value, "16.20.0") >= 0);
|
|
1177
|
+
}
|
|
1178
|
+
function versionParts(value) {
|
|
1179
|
+
const match = strictVersionPattern.exec(value);
|
|
1180
|
+
if (!match) {
|
|
1181
|
+
throw new Error("Version must use X.Y.Z format: " + value);
|
|
1182
|
+
}
|
|
1183
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
1184
|
+
}
|
|
1185
|
+
function samePath(left, right) {
|
|
1186
|
+
const normalize = (value) => {
|
|
1187
|
+
const resolved = path.resolve(value).replace(/\\/g, "/");
|
|
1188
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
1189
|
+
};
|
|
1190
|
+
return normalize(left) === normalize(right);
|
|
1191
|
+
}
|
|
1192
|
+
function relativePath(projectDirectory, filePath) {
|
|
1193
|
+
return (path.relative(projectDirectory, filePath).replace(/\\/g, "/") ||
|
|
1194
|
+
path.basename(filePath));
|
|
1195
|
+
}
|
|
1196
|
+
function errorMessage(error) {
|
|
1197
|
+
return error instanceof Error ? error.message : String(error);
|
|
1198
|
+
}
|