@jskit-ai/jskit-cli 0.2.115 → 0.2.116
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/package.json +4 -4
- package/src/server/cliRuntime/descriptorValidation.js +58 -0
- package/src/server/cliRuntime/mutationApplication.js +6 -0
- package/src/server/cliRuntime/mutations/sourceMutations.js +458 -0
- package/src/server/cliRuntime/packageInstallFlow.js +63 -1
- package/src/server/commandHandlers/app.js +4 -0
- package/src/server/commandHandlers/appCommandCatalog.js +17 -0
- package/src/server/commandHandlers/appCommands/migrateSourceMutations.js +538 -0
- package/src/server/commandHandlers/show/renderPackageText.js +18 -0
- package/src/server/core/commandCatalog.js +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/jskit-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.116",
|
|
4
4
|
"description": "Bundle and package orchestration CLI for JSKIT apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"test": "node --test"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@jskit-ai/jskit-catalog": "0.1.
|
|
24
|
-
"@jskit-ai/kernel": "0.1.
|
|
25
|
-
"@jskit-ai/shell-web": "0.1.
|
|
23
|
+
"@jskit-ai/jskit-catalog": "0.1.111",
|
|
24
|
+
"@jskit-ai/kernel": "0.1.104",
|
|
25
|
+
"@jskit-ai/shell-web": "0.1.103",
|
|
26
26
|
"@vue/compiler-sfc": "^3.5.29",
|
|
27
27
|
"ts-morph": "^28.0.0"
|
|
28
28
|
},
|
|
@@ -61,6 +61,62 @@ function validateFileMutationShape(descriptor, descriptorPath) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function validateSourceMutationShape(descriptor, descriptorPath) {
|
|
65
|
+
const packageId = String(ensureObject(descriptor).packageId || "").trim() || "unknown-package";
|
|
66
|
+
const mutations = ensureObject(ensureObject(descriptor).mutations);
|
|
67
|
+
const sourceMutations = ensureArray(mutations.source);
|
|
68
|
+
const supportedOps = new Set([
|
|
69
|
+
"ensure-assignment",
|
|
70
|
+
"ensure-call",
|
|
71
|
+
"ensure-export-const",
|
|
72
|
+
"ensure-import"
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
for (const rawMutation of sourceMutations) {
|
|
76
|
+
const mutation = ensureObject(rawMutation);
|
|
77
|
+
const operation = String(mutation.op || "").trim();
|
|
78
|
+
const file = String(mutation.file || "").trim();
|
|
79
|
+
if (!supportedOps.has(operation)) {
|
|
80
|
+
throw createCliError(
|
|
81
|
+
`Invalid package descriptor at ${descriptorPath}: source mutation in ${packageId} has unsupported op "${operation}".`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (!file) {
|
|
85
|
+
throw createCliError(
|
|
86
|
+
`Invalid package descriptor at ${descriptorPath}: source mutation in ${packageId} requires "file".`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (operation === "ensure-import") {
|
|
90
|
+
const hasImportBinding =
|
|
91
|
+
String(mutation.defaultImport || "").trim() ||
|
|
92
|
+
String(mutation.namespaceImport || "").trim() ||
|
|
93
|
+
ensureArray(mutation.namedImports).length > 0;
|
|
94
|
+
if (!String(mutation.from || "").trim() || !hasImportBinding) {
|
|
95
|
+
throw createCliError(
|
|
96
|
+
`Invalid package descriptor at ${descriptorPath}: ensure-import in ${packageId} requires "from" and an import binding.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (operation === "ensure-call" && !String(mutation.callee || "").trim()) {
|
|
101
|
+
throw createCliError(
|
|
102
|
+
`Invalid package descriptor at ${descriptorPath}: ensure-call in ${packageId} requires "callee".`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (operation === "ensure-assignment") {
|
|
106
|
+
if (!String(mutation.target || "").trim() || !String(mutation.value || "").trim()) {
|
|
107
|
+
throw createCliError(
|
|
108
|
+
`Invalid package descriptor at ${descriptorPath}: ensure-assignment in ${packageId} requires "target" and "value".`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (operation === "ensure-export-const" && !String(mutation.name || "").trim()) {
|
|
113
|
+
throw createCliError(
|
|
114
|
+
`Invalid package descriptor at ${descriptorPath}: ensure-export-const in ${packageId} requires "name".`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
64
120
|
function validateLifecycleHookSpec(spec = {}, descriptorPath, label = "lifecycle hook") {
|
|
65
121
|
const normalized = ensureObject(spec);
|
|
66
122
|
if (Object.keys(normalized).length < 1) {
|
|
@@ -133,6 +189,7 @@ function validatePackageDescriptorShape(descriptor, descriptorPath) {
|
|
|
133
189
|
}
|
|
134
190
|
|
|
135
191
|
validateFileMutationShape(normalized, descriptorPath);
|
|
192
|
+
validateSourceMutationShape(normalized, descriptorPath);
|
|
136
193
|
const lifecycle = validateLifecycleShape(normalized, descriptorPath);
|
|
137
194
|
|
|
138
195
|
return {
|
|
@@ -165,6 +222,7 @@ function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { ex
|
|
|
165
222
|
}
|
|
166
223
|
|
|
167
224
|
validateFileMutationShape(normalized, descriptorPath);
|
|
225
|
+
validateSourceMutationShape(normalized, descriptorPath);
|
|
168
226
|
const lifecycle = validateLifecycleShape(normalized, descriptorPath);
|
|
169
227
|
|
|
170
228
|
return {
|
|
@@ -8,3 +8,9 @@ export {
|
|
|
8
8
|
partitionPreFileConfigTextMutations,
|
|
9
9
|
resolvePositioningMutations
|
|
10
10
|
} from "./mutations/textMutations.js";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
applySourceMutations,
|
|
14
|
+
partitionPreFileConfigSourceMutations,
|
|
15
|
+
resolvePositioningSourceMutations
|
|
16
|
+
} from "./mutations/sourceMutations.js";
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdir,
|
|
3
|
+
writeFile
|
|
4
|
+
} from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
Project,
|
|
8
|
+
SyntaxKind
|
|
9
|
+
} from "ts-morph";
|
|
10
|
+
import { createCliError } from "../../shared/cliError.js";
|
|
11
|
+
import {
|
|
12
|
+
ensureArray,
|
|
13
|
+
ensureObject
|
|
14
|
+
} from "../../shared/collectionUtils.js";
|
|
15
|
+
import {
|
|
16
|
+
interpolateOptionValue
|
|
17
|
+
} from "../../shared/optionInterpolation.js";
|
|
18
|
+
import {
|
|
19
|
+
normalizeMutationWhen,
|
|
20
|
+
shouldApplyMutationWhen
|
|
21
|
+
} from "../mutationWhen.js";
|
|
22
|
+
import {
|
|
23
|
+
loadMutationWhenConfigContext,
|
|
24
|
+
normalizeRelativePath,
|
|
25
|
+
readFileBufferIfExists
|
|
26
|
+
} from "../ioAndMigrations.js";
|
|
27
|
+
import { normalizeMutationRelativeFilePath } from "./mutationPathUtils.js";
|
|
28
|
+
|
|
29
|
+
const PRE_FILE_CONFIG_MUTATION_TARGETS = new Set([
|
|
30
|
+
"config/public.js",
|
|
31
|
+
"config/server.js"
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function createSourceFile(relativeFile, sourceText) {
|
|
35
|
+
const project = new Project({
|
|
36
|
+
useInMemoryFileSystem: true,
|
|
37
|
+
skipAddingFilesFromTsConfig: true
|
|
38
|
+
});
|
|
39
|
+
return project.createSourceFile(`/${relativeFile}`, sourceText, { overwrite: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeSourceText(value = "") {
|
|
43
|
+
return String(value || "").replace(/\r\n/g, "\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function insertText(sourceText, index, text) {
|
|
47
|
+
return `${sourceText.slice(0, index)}${text}${sourceText.slice(index)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function appendBlock(sourceText, block) {
|
|
51
|
+
const normalized = normalizeSourceText(sourceText);
|
|
52
|
+
const trimmed = normalized.replace(/\s*$/u, "");
|
|
53
|
+
if (!trimmed) {
|
|
54
|
+
return `${block.trim()}\n`;
|
|
55
|
+
}
|
|
56
|
+
return `${trimmed}\n\n${block.trim()}\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderImportMutation(mutation, options, packageId, mutationId) {
|
|
60
|
+
const defaultImport = interpolateOptionValue(
|
|
61
|
+
mutation.defaultImport || "",
|
|
62
|
+
options,
|
|
63
|
+
packageId,
|
|
64
|
+
`${mutationId}.defaultImport`
|
|
65
|
+
).trim();
|
|
66
|
+
const namespaceImport = interpolateOptionValue(
|
|
67
|
+
mutation.namespaceImport || "",
|
|
68
|
+
options,
|
|
69
|
+
packageId,
|
|
70
|
+
`${mutationId}.namespaceImport`
|
|
71
|
+
).trim();
|
|
72
|
+
const namedImports = ensureArray(mutation.namedImports)
|
|
73
|
+
.map((entry, index) =>
|
|
74
|
+
interpolateOptionValue(entry, options, packageId, `${mutationId}.namedImports.${index}`).trim()
|
|
75
|
+
)
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
const from = interpolateOptionValue(mutation.from || "", options, packageId, `${mutationId}.from`).trim();
|
|
78
|
+
|
|
79
|
+
if (!from) {
|
|
80
|
+
throw createCliError(`Invalid ensure-import source mutation in ${packageId}: "from" is required.`);
|
|
81
|
+
}
|
|
82
|
+
if (!defaultImport && !namespaceImport && namedImports.length < 1) {
|
|
83
|
+
throw createCliError(
|
|
84
|
+
`Invalid ensure-import source mutation in ${packageId}: defaultImport, namespaceImport, or namedImports is required.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let importClause = "";
|
|
89
|
+
if (namespaceImport) {
|
|
90
|
+
importClause = `* as ${namespaceImport}`;
|
|
91
|
+
} else {
|
|
92
|
+
const namedClause = namedImports.length > 0 ? `{ ${namedImports.join(", ")} }` : "";
|
|
93
|
+
importClause = [defaultImport, namedClause].filter(Boolean).join(", ");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
defaultImport,
|
|
98
|
+
namespaceImport,
|
|
99
|
+
namedImports,
|
|
100
|
+
from,
|
|
101
|
+
statement: `import ${importClause} from "${from}";`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sourceAlreadyHasImport(sourceFile, rendered) {
|
|
106
|
+
return sourceFile.getImportDeclarations().some((importDeclaration) => {
|
|
107
|
+
if (importDeclaration.getModuleSpecifierValue() !== rendered.from) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const defaultImport = importDeclaration.getDefaultImport()?.getText() || "";
|
|
112
|
+
if (rendered.defaultImport && defaultImport !== rendered.defaultImport) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const namespaceImport = importDeclaration.getNamespaceImport()?.getText() || "";
|
|
117
|
+
if (rendered.namespaceImport && namespaceImport !== rendered.namespaceImport) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const existingNamedImports = new Set(
|
|
122
|
+
importDeclaration.getNamedImports().map((namedImport) => namedImport.getName())
|
|
123
|
+
);
|
|
124
|
+
return rendered.namedImports.every((name) => existingNamedImports.has(name));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ensureImport(sourceText, relativeFile, mutation, options, packageId, mutationId) {
|
|
129
|
+
const sourceFile = createSourceFile(relativeFile, sourceText);
|
|
130
|
+
const rendered = renderImportMutation(mutation, options, packageId, mutationId);
|
|
131
|
+
|
|
132
|
+
if (sourceAlreadyHasImport(sourceFile, rendered)) {
|
|
133
|
+
return {
|
|
134
|
+
changed: false,
|
|
135
|
+
content: sourceText
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
140
|
+
if (importDeclarations.length > 0) {
|
|
141
|
+
const lastImport = importDeclarations[importDeclarations.length - 1];
|
|
142
|
+
return {
|
|
143
|
+
changed: true,
|
|
144
|
+
content: insertText(sourceText, lastImport.getEnd(), `\n${rendered.statement}`)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const normalized = normalizeSourceText(sourceText);
|
|
149
|
+
return {
|
|
150
|
+
changed: true,
|
|
151
|
+
content: normalized.trim() ? `${rendered.statement}\n\n${normalized}` : `${rendered.statement}\n`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeSourceArg(value = "") {
|
|
156
|
+
const raw = String(value || "").trim();
|
|
157
|
+
const quotedMatch = raw.match(/^(['"])(.*)\1$/u);
|
|
158
|
+
if (!quotedMatch) {
|
|
159
|
+
return raw;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(`"${quotedMatch[2].replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
|
|
163
|
+
} catch {
|
|
164
|
+
return quotedMatch[2];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function callExpressionMatches(callExpression, callee, expectedArgs, uniqueArgIndex) {
|
|
169
|
+
if (callExpression.getExpression().getText() !== callee) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const args = callExpression.getArguments();
|
|
174
|
+
if (uniqueArgIndex < 0 || uniqueArgIndex >= expectedArgs.length) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (uniqueArgIndex >= args.length) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return normalizeSourceArg(args[uniqueArgIndex].getText()) === normalizeSourceArg(expectedArgs[uniqueArgIndex]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderCallMutation(mutation, options, packageId, mutationId) {
|
|
185
|
+
const callee = interpolateOptionValue(mutation.callee || "", options, packageId, `${mutationId}.callee`).trim();
|
|
186
|
+
const args = ensureArray(mutation.args)
|
|
187
|
+
.map((entry, index) => interpolateOptionValue(entry, options, packageId, `${mutationId}.args.${index}`).trim())
|
|
188
|
+
.filter(Boolean);
|
|
189
|
+
|
|
190
|
+
if (!callee) {
|
|
191
|
+
throw createCliError(`Invalid ensure-call source mutation in ${packageId}: "callee" is required.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
callee,
|
|
196
|
+
args,
|
|
197
|
+
statement: `${callee}(${args.join(", ")});`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function ensureCall(sourceText, relativeFile, mutation, options, packageId, mutationId) {
|
|
202
|
+
const sourceFile = createSourceFile(relativeFile, sourceText);
|
|
203
|
+
const rendered = renderCallMutation(mutation, options, packageId, mutationId);
|
|
204
|
+
const uniqueArgIndex = Number.isInteger(mutation.uniqueArgIndex)
|
|
205
|
+
? mutation.uniqueArgIndex
|
|
206
|
+
: 0;
|
|
207
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
208
|
+
if (callExpressions.some((callExpression) =>
|
|
209
|
+
callExpressionMatches(callExpression, rendered.callee, rendered.args, uniqueArgIndex)
|
|
210
|
+
)) {
|
|
211
|
+
return {
|
|
212
|
+
changed: false,
|
|
213
|
+
content: sourceText
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const beforeClass = String(mutation.beforeClass || "").trim();
|
|
218
|
+
if (beforeClass) {
|
|
219
|
+
const classDeclaration = sourceFile.getClass(beforeClass);
|
|
220
|
+
if (!classDeclaration) {
|
|
221
|
+
throw createCliError(
|
|
222
|
+
`Invalid ensure-call source mutation in ${packageId}: beforeClass "${beforeClass}" was not found in ${relativeFile}.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
changed: true,
|
|
227
|
+
content: insertText(sourceText, classDeclaration.getStart(), `${rendered.statement}\n\n`)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
changed: true,
|
|
233
|
+
content: appendBlock(sourceText, rendered.statement)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function sourceHasAssignmentToTarget(sourceFile, target) {
|
|
238
|
+
return sourceFile
|
|
239
|
+
.getDescendantsOfKind(SyntaxKind.BinaryExpression)
|
|
240
|
+
.some((expression) => expression.getLeft().getText() === target);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderAssignmentStatement(target, value) {
|
|
244
|
+
return `${target} = ${value};`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function ensureAssignment(sourceText, relativeFile, mutation, options, packageId, mutationId) {
|
|
248
|
+
const sourceFile = createSourceFile(relativeFile, sourceText);
|
|
249
|
+
const target = interpolateOptionValue(mutation.target || "", options, packageId, `${mutationId}.target`).trim();
|
|
250
|
+
const value = interpolateOptionValue(mutation.value || "", options, packageId, `${mutationId}.value`).trim();
|
|
251
|
+
if (!target) {
|
|
252
|
+
throw createCliError(`Invalid ensure-assignment source mutation in ${packageId}: "target" is required.`);
|
|
253
|
+
}
|
|
254
|
+
if (!value) {
|
|
255
|
+
throw createCliError(`Invalid ensure-assignment source mutation in ${packageId}: "value" is required.`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const statements = [];
|
|
259
|
+
for (const rawEnsureTarget of ensureArray(mutation.ensureObjects)) {
|
|
260
|
+
const ensureTarget = interpolateOptionValue(
|
|
261
|
+
rawEnsureTarget,
|
|
262
|
+
options,
|
|
263
|
+
packageId,
|
|
264
|
+
`${mutationId}.ensureObjects`
|
|
265
|
+
).trim();
|
|
266
|
+
if (!ensureTarget || sourceHasAssignmentToTarget(sourceFile, ensureTarget)) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
statements.push(`${ensureTarget} ||= {};`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!sourceHasAssignmentToTarget(sourceFile, target)) {
|
|
273
|
+
statements.push(renderAssignmentStatement(target, value));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (statements.length < 1) {
|
|
277
|
+
return {
|
|
278
|
+
changed: false,
|
|
279
|
+
content: sourceText
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
changed: true,
|
|
285
|
+
content: appendBlock(sourceText, statements.join("\n"))
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function ensureExportConst(sourceText, relativeFile, mutation, options, packageId, mutationId) {
|
|
290
|
+
const sourceFile = createSourceFile(relativeFile, sourceText);
|
|
291
|
+
const name = interpolateOptionValue(mutation.name || "", options, packageId, `${mutationId}.name`).trim();
|
|
292
|
+
const value = interpolateOptionValue(mutation.value || "{}", options, packageId, `${mutationId}.value`).trim() || "{}";
|
|
293
|
+
if (!name) {
|
|
294
|
+
throw createCliError(`Invalid ensure-export-const source mutation in ${packageId}: "name" is required.`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (sourceFile.getVariableDeclaration(name)) {
|
|
298
|
+
return {
|
|
299
|
+
changed: false,
|
|
300
|
+
content: sourceText
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const statement = `export const ${name} = ${value};`;
|
|
305
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
306
|
+
if (importDeclarations.length > 0) {
|
|
307
|
+
const lastImport = importDeclarations[importDeclarations.length - 1];
|
|
308
|
+
return {
|
|
309
|
+
changed: true,
|
|
310
|
+
content: insertText(sourceText, lastImport.getEnd(), `\n\n${statement}`)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const normalized = normalizeSourceText(sourceText);
|
|
315
|
+
return {
|
|
316
|
+
changed: true,
|
|
317
|
+
content: normalized.trim() ? `${statement}\n\n${normalized}` : `${statement}\n`
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function applySourceMutationToContent(sourceText, relativeFile, mutation, options, packageId) {
|
|
322
|
+
const operation = String(mutation?.op || "").trim();
|
|
323
|
+
const mutationId = String(mutation?.id || "").trim() || operation || "source";
|
|
324
|
+
|
|
325
|
+
if (operation === "ensure-import") {
|
|
326
|
+
return ensureImport(sourceText, relativeFile, mutation, options, packageId, mutationId);
|
|
327
|
+
}
|
|
328
|
+
if (operation === "ensure-call") {
|
|
329
|
+
return ensureCall(sourceText, relativeFile, mutation, options, packageId, mutationId);
|
|
330
|
+
}
|
|
331
|
+
if (operation === "ensure-assignment") {
|
|
332
|
+
return ensureAssignment(sourceText, relativeFile, mutation, options, packageId, mutationId);
|
|
333
|
+
}
|
|
334
|
+
if (operation === "ensure-export-const") {
|
|
335
|
+
return ensureExportConst(sourceText, relativeFile, mutation, options, packageId, mutationId);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
throw createCliError(`Unsupported source mutation op "${operation}" in ${packageId}.`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function createManagedSourceRecord(relativeFile, mutation) {
|
|
342
|
+
const mutationId = String(mutation?.id || "").trim() || String(mutation?.op || "source").trim();
|
|
343
|
+
const recordKey = `${relativeFile}::${mutationId}`;
|
|
344
|
+
return {
|
|
345
|
+
recordKey,
|
|
346
|
+
record: {
|
|
347
|
+
file: relativeFile,
|
|
348
|
+
op: String(mutation?.op || "").trim(),
|
|
349
|
+
id: String(mutation?.id || ""),
|
|
350
|
+
reason: String(mutation?.reason || ""),
|
|
351
|
+
category: String(mutation?.category || "")
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function applySourceMutations(
|
|
357
|
+
packageEntry,
|
|
358
|
+
appRoot,
|
|
359
|
+
sourceMutations,
|
|
360
|
+
options,
|
|
361
|
+
managedSource,
|
|
362
|
+
touchedFiles,
|
|
363
|
+
{ dryRun = false } = {}
|
|
364
|
+
) {
|
|
365
|
+
for (const mutation of sourceMutations) {
|
|
366
|
+
const when = normalizeMutationWhen(mutation?.when);
|
|
367
|
+
const configContext = when?.config ? await loadMutationWhenConfigContext(appRoot) : {};
|
|
368
|
+
if (
|
|
369
|
+
!shouldApplyMutationWhen(when, {
|
|
370
|
+
options,
|
|
371
|
+
configContext,
|
|
372
|
+
packageId: packageEntry.packageId,
|
|
373
|
+
mutationContext: "source mutation"
|
|
374
|
+
})
|
|
375
|
+
) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const relativeFile = normalizeMutationRelativeFilePath(mutation?.file || "");
|
|
380
|
+
if (!relativeFile) {
|
|
381
|
+
throw createCliError(`Invalid source mutation in ${packageEntry.packageId}: "file" is required.`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const absoluteFile = path.join(appRoot, relativeFile);
|
|
385
|
+
const previous = await readFileBufferIfExists(absoluteFile);
|
|
386
|
+
const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
|
|
387
|
+
const applied = applySourceMutationToContent(
|
|
388
|
+
normalizeSourceText(previousContent),
|
|
389
|
+
relativeFile,
|
|
390
|
+
mutation,
|
|
391
|
+
options,
|
|
392
|
+
packageEntry.packageId
|
|
393
|
+
);
|
|
394
|
+
const {
|
|
395
|
+
recordKey,
|
|
396
|
+
record
|
|
397
|
+
} = createManagedSourceRecord(relativeFile, mutation);
|
|
398
|
+
if (!applied.changed) {
|
|
399
|
+
managedSource[recordKey] = record;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!dryRun) {
|
|
404
|
+
await mkdir(path.dirname(absoluteFile), { recursive: true });
|
|
405
|
+
await writeFile(absoluteFile, applied.content, "utf8");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
managedSource[recordKey] = record;
|
|
409
|
+
touchedFiles.add(normalizeRelativePath(appRoot, absoluteFile));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isPositioningSourceMutation(value = {}) {
|
|
414
|
+
const mutation = ensureObject(value);
|
|
415
|
+
const operation = String(mutation.op || "").trim();
|
|
416
|
+
if (operation !== "ensure-call") {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
return normalizeMutationRelativeFilePath(mutation.file) === "src/placement.js";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isPreFileConfigSourceMutation(value = {}) {
|
|
423
|
+
const mutation = ensureObject(value);
|
|
424
|
+
const operation = String(mutation.op || "").trim();
|
|
425
|
+
if (operation !== "ensure-assignment" && operation !== "ensure-export-const") {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
return PRE_FILE_CONFIG_MUTATION_TARGETS.has(normalizeMutationRelativeFilePath(mutation.file));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function partitionPreFileConfigSourceMutations(sourceMutations = []) {
|
|
432
|
+
const preFileSourceMutations = [];
|
|
433
|
+
const postFileSourceMutations = [];
|
|
434
|
+
|
|
435
|
+
for (const mutation of ensureArray(sourceMutations)) {
|
|
436
|
+
if (isPreFileConfigSourceMutation(mutation)) {
|
|
437
|
+
preFileSourceMutations.push(mutation);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
postFileSourceMutations.push(mutation);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
preFileSourceMutations,
|
|
445
|
+
postFileSourceMutations
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function resolvePositioningSourceMutations(descriptorMutations = {}) {
|
|
450
|
+
const mutations = ensureObject(descriptorMutations);
|
|
451
|
+
return ensureArray(mutations.source).filter((mutationValue) => isPositioningSourceMutation(mutationValue));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export {
|
|
455
|
+
applySourceMutations,
|
|
456
|
+
partitionPreFileConfigSourceMutations,
|
|
457
|
+
resolvePositioningSourceMutations
|
|
458
|
+
};
|
|
@@ -36,10 +36,13 @@ import {
|
|
|
36
36
|
} from "./packageTemplateResolution.js";
|
|
37
37
|
import {
|
|
38
38
|
applyFileMutations,
|
|
39
|
+
applySourceMutations,
|
|
39
40
|
applyTextMutations,
|
|
41
|
+
partitionPreFileConfigSourceMutations,
|
|
40
42
|
partitionPreFileConfigTextMutations,
|
|
41
43
|
prepareFileMutations,
|
|
42
|
-
resolvePositioningMutations
|
|
44
|
+
resolvePositioningMutations,
|
|
45
|
+
resolvePositioningSourceMutations
|
|
43
46
|
} from "./mutationApplication.js";
|
|
44
47
|
function createManagedRecordBase(packageEntry, options) {
|
|
45
48
|
const sourceRecord = {
|
|
@@ -61,6 +64,7 @@ function createManagedRecordBase(packageEntry, options) {
|
|
|
61
64
|
scripts: {}
|
|
62
65
|
},
|
|
63
66
|
text: {},
|
|
67
|
+
source: {},
|
|
64
68
|
vite: {},
|
|
65
69
|
files: [],
|
|
66
70
|
migrations: []
|
|
@@ -172,6 +176,7 @@ async function applyPackagePositioning({
|
|
|
172
176
|
scripts: cloneManagedMap(existingPackageJsonManaged.scripts)
|
|
173
177
|
},
|
|
174
178
|
text: cloneManagedMap(existingManaged.text),
|
|
179
|
+
source: cloneManagedMap(existingManaged.source),
|
|
175
180
|
vite: cloneManagedMap(existingManaged.vite),
|
|
176
181
|
files: cloneManagedArray(existingManaged.files),
|
|
177
182
|
migrations: cloneManagedArray(existingManaged.migrations)
|
|
@@ -188,8 +193,10 @@ async function applyPackagePositioning({
|
|
|
188
193
|
|
|
189
194
|
const mutations = ensureObject(packageEntry.descriptor.mutations);
|
|
190
195
|
const positioningMutations = resolvePositioningMutations(mutations);
|
|
196
|
+
const positioningSourceMutations = resolvePositioningSourceMutations(mutations);
|
|
191
197
|
const appliedManagedFiles = [];
|
|
192
198
|
const appliedManagedText = {};
|
|
199
|
+
const appliedManagedSource = {};
|
|
193
200
|
const preparedFileMutations = await prepareFileMutations(
|
|
194
201
|
packageEntryForMutations,
|
|
195
202
|
packageOptions,
|
|
@@ -225,6 +232,19 @@ async function applyPackagePositioning({
|
|
|
225
232
|
}
|
|
226
233
|
);
|
|
227
234
|
}
|
|
235
|
+
if (positioningSourceMutations.length > 0) {
|
|
236
|
+
await applySourceMutations(
|
|
237
|
+
packageEntryForMutations,
|
|
238
|
+
appRoot,
|
|
239
|
+
positioningSourceMutations,
|
|
240
|
+
packageOptions,
|
|
241
|
+
appliedManagedSource,
|
|
242
|
+
touchedFiles,
|
|
243
|
+
{
|
|
244
|
+
dryRun
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
228
248
|
|
|
229
249
|
if (appliedManagedFiles.length > 0) {
|
|
230
250
|
const replacedPaths = new Set(
|
|
@@ -245,6 +265,12 @@ async function applyPackagePositioning({
|
|
|
245
265
|
...appliedManagedText
|
|
246
266
|
};
|
|
247
267
|
}
|
|
268
|
+
if (Object.keys(appliedManagedSource).length > 0) {
|
|
269
|
+
nextManaged.source = {
|
|
270
|
+
...nextManaged.source,
|
|
271
|
+
...appliedManagedSource
|
|
272
|
+
};
|
|
273
|
+
}
|
|
248
274
|
|
|
249
275
|
const managedRecord = {
|
|
250
276
|
...existingInstall,
|
|
@@ -283,6 +309,7 @@ async function applyPackageMigrationsOnly({
|
|
|
283
309
|
scripts: cloneManagedMap(existingPackageJsonManaged.scripts)
|
|
284
310
|
},
|
|
285
311
|
text: cloneManagedMap(existingManaged.text),
|
|
312
|
+
source: cloneManagedMap(existingManaged.source),
|
|
286
313
|
vite: cloneManagedMap(existingManaged.vite),
|
|
287
314
|
files: cloneManagedArray(existingManaged.files),
|
|
288
315
|
migrations: cloneManagedArray(existingManaged.migrations)
|
|
@@ -363,6 +390,7 @@ async function applyPackageInstall({
|
|
|
363
390
|
const mutations = ensureObject(packageEntry.descriptor.mutations);
|
|
364
391
|
const fileMutations = ensureArray(mutations.files);
|
|
365
392
|
const textMutations = ensureArray(mutations.text);
|
|
393
|
+
const sourceMutations = ensureArray(mutations.source);
|
|
366
394
|
const hasSurfaceTargetedFileMutations = fileMutations.some((mutationValue) =>
|
|
367
395
|
Boolean(normalizeFileMutationRecord(mutationValue).toSurface)
|
|
368
396
|
);
|
|
@@ -375,6 +403,15 @@ async function applyPackageInstall({
|
|
|
375
403
|
preFileTextMutations: [],
|
|
376
404
|
postFileTextMutations: textMutations
|
|
377
405
|
};
|
|
406
|
+
const {
|
|
407
|
+
preFileSourceMutations,
|
|
408
|
+
postFileSourceMutations
|
|
409
|
+
} = hasSurfaceTargetedFileMutations
|
|
410
|
+
? partitionPreFileConfigSourceMutations(sourceMutations)
|
|
411
|
+
: {
|
|
412
|
+
preFileSourceMutations: [],
|
|
413
|
+
postFileSourceMutations: sourceMutations
|
|
414
|
+
};
|
|
378
415
|
const templateRoot = await resolvePackageTemplateRoot({
|
|
379
416
|
packageEntry,
|
|
380
417
|
appRoot,
|
|
@@ -403,6 +440,19 @@ async function applyPackageInstall({
|
|
|
403
440
|
}
|
|
404
441
|
);
|
|
405
442
|
}
|
|
443
|
+
if (preFileSourceMutations.length > 0) {
|
|
444
|
+
await applySourceMutations(
|
|
445
|
+
packageEntryForMutations,
|
|
446
|
+
appRoot,
|
|
447
|
+
preFileSourceMutations,
|
|
448
|
+
packageOptions,
|
|
449
|
+
managedRecord.managed.source,
|
|
450
|
+
touchedFiles,
|
|
451
|
+
{
|
|
452
|
+
dryRun
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
}
|
|
406
456
|
|
|
407
457
|
const preparedFileMutations = await prepareFileMutations(
|
|
408
458
|
packageEntryForMutations,
|
|
@@ -576,6 +626,18 @@ async function applyPackageInstall({
|
|
|
576
626
|
}
|
|
577
627
|
);
|
|
578
628
|
|
|
629
|
+
await applySourceMutations(
|
|
630
|
+
packageEntryForMutations,
|
|
631
|
+
appRoot,
|
|
632
|
+
postFileSourceMutations,
|
|
633
|
+
packageOptions,
|
|
634
|
+
managedRecord.managed.source,
|
|
635
|
+
touchedFiles,
|
|
636
|
+
{
|
|
637
|
+
dryRun
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
|
|
579
641
|
await applyViteMutations(
|
|
580
642
|
packageEntryForMutations,
|
|
581
643
|
appRoot,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./appCommandCatalog.js";
|
|
11
11
|
import { runAppAdoptManagedScriptsCommand } from "./appCommands/adoptManagedScripts.js";
|
|
12
12
|
import { runAppLinkLocalPackagesCommand } from "./appCommands/linkLocalPackages.js";
|
|
13
|
+
import { runAppMigrateSourceMutationsCommand } from "./appCommands/migrateSourceMutations.js";
|
|
13
14
|
import { runAppPreparePreviewUserCommand } from "./appCommands/preparePreviewUser.js";
|
|
14
15
|
import { runAppReleaseCommand } from "./appCommands/release.js";
|
|
15
16
|
import { runAppUpdatePackagesCommand } from "./appCommands/updatePackages.js";
|
|
@@ -152,6 +153,9 @@ function createAppCommands(ctx = {}) {
|
|
|
152
153
|
if (definition.name === "adopt-managed-scripts") {
|
|
153
154
|
return runAppAdoptManagedScriptsCommand(ctx, { appRoot, options, stdout, stderr });
|
|
154
155
|
}
|
|
156
|
+
if (definition.name === "migrate-source-mutations") {
|
|
157
|
+
return runAppMigrateSourceMutationsCommand(ctx, { appRoot, options, stdout, stderr });
|
|
158
|
+
}
|
|
155
159
|
if (definition.name === "prepare-preview-user") {
|
|
156
160
|
return runAppPreparePreviewUserCommand(ctx, { appRoot, options, stdout, stderr });
|
|
157
161
|
}
|
|
@@ -188,6 +188,22 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
188
188
|
"Customized script values are reported and left alone unless --force is used.",
|
|
189
189
|
"This command is for apps that still carry copied JSKIT maintenance scripts."
|
|
190
190
|
])
|
|
191
|
+
}),
|
|
192
|
+
"migrate-source-mutations": Object.freeze({
|
|
193
|
+
name: "migrate-source-mutations",
|
|
194
|
+
summary: "Rewrite legacy append-text source edits into the current source-mutation layout.",
|
|
195
|
+
usage: "jskit app migrate-source-mutations [--dry-run]",
|
|
196
|
+
options: Object.freeze([
|
|
197
|
+
Object.freeze({
|
|
198
|
+
label: "--dry-run",
|
|
199
|
+
description: "Preview source rewrites without writing app files."
|
|
200
|
+
})
|
|
201
|
+
]),
|
|
202
|
+
defaults: Object.freeze([
|
|
203
|
+
"Moves legacy MainClientProvider component registrations before the MainClientProvider class.",
|
|
204
|
+
"Leaves apps unchanged when they already use the current source-mutation layout.",
|
|
205
|
+
"Run this after updating JSKIT packages in older apps that were installed before source mutations."
|
|
206
|
+
])
|
|
191
207
|
})
|
|
192
208
|
});
|
|
193
209
|
|
|
@@ -217,6 +233,7 @@ function buildAppCommandOptionMeta(subcommandName = "") {
|
|
|
217
233
|
if (
|
|
218
234
|
definition.name === "update-packages" ||
|
|
219
235
|
definition.name === "adopt-managed-scripts" ||
|
|
236
|
+
definition.name === "migrate-source-mutations" ||
|
|
220
237
|
definition.name === "release"
|
|
221
238
|
) {
|
|
222
239
|
optionMeta["dry-run"] = { inputType: "flag" };
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
Node,
|
|
5
|
+
Project
|
|
6
|
+
} from "ts-morph";
|
|
7
|
+
import { fileExists } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
const MAIN_CLIENT_PROVIDER_FILE = "packages/main/src/client/providers/MainClientProvider.js";
|
|
10
|
+
const CRUD_FORM_FIELD_ARRAY_NAMES = Object.freeze([
|
|
11
|
+
"UI_CREATE_FORM_FIELDS",
|
|
12
|
+
"UI_EDIT_FORM_FIELDS"
|
|
13
|
+
]);
|
|
14
|
+
const CRUD_FORM_FIELD_MARKERS_BY_ARRAY = Object.freeze({
|
|
15
|
+
UI_CREATE_FORM_FIELDS: "// jskit:crud-ui-form-fields:new",
|
|
16
|
+
UI_EDIT_FORM_FIELDS: "// jskit:crud-ui-form-fields:edit"
|
|
17
|
+
});
|
|
18
|
+
const SOURCE_FILE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".vue"]);
|
|
19
|
+
const SOURCE_SCAN_SKIP_DIRS = new Set([
|
|
20
|
+
".git",
|
|
21
|
+
".jskit",
|
|
22
|
+
"build",
|
|
23
|
+
"coverage",
|
|
24
|
+
"dist",
|
|
25
|
+
"node_modules"
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function createSourceFile(sourceText = "") {
|
|
29
|
+
const project = new Project({
|
|
30
|
+
useInMemoryFileSystem: true,
|
|
31
|
+
skipAddingFilesFromTsConfig: true
|
|
32
|
+
});
|
|
33
|
+
return project.createSourceFile("/MainClientProvider.js", String(sourceText || ""), { overwrite: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readStringLiteralValue(node) {
|
|
37
|
+
if (!node) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
41
|
+
return node.getLiteralText();
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readRegisterMainClientComponentToken(statement) {
|
|
47
|
+
if (!Node.isExpressionStatement(statement)) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
const expression = statement.getExpression();
|
|
51
|
+
if (!Node.isCallExpression(expression)) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
if (expression.getExpression().getText() !== "registerMainClientComponent") {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
return readStringLiteralValue(expression.getArguments()[0]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildMainClientProviderMigration(sourceText = "") {
|
|
61
|
+
const sourceFile = createSourceFile(sourceText);
|
|
62
|
+
const classDeclaration = sourceFile.getClass("MainClientProvider");
|
|
63
|
+
if (!classDeclaration) {
|
|
64
|
+
return {
|
|
65
|
+
changed: false,
|
|
66
|
+
content: sourceText,
|
|
67
|
+
moved: 0,
|
|
68
|
+
deduped: 0,
|
|
69
|
+
reason: "missing-class"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const classStart = classDeclaration.getStart();
|
|
74
|
+
const registerRecords = sourceFile
|
|
75
|
+
.getStatements()
|
|
76
|
+
.map((statement, index) => ({
|
|
77
|
+
statement,
|
|
78
|
+
index,
|
|
79
|
+
token: readRegisterMainClientComponentToken(statement),
|
|
80
|
+
text: statement.getText(),
|
|
81
|
+
beforeClass: statement.getStart() < classStart
|
|
82
|
+
}))
|
|
83
|
+
.filter((record) => record.token);
|
|
84
|
+
|
|
85
|
+
const movedRecords = registerRecords.filter((record) => !record.beforeClass);
|
|
86
|
+
const seenTokens = new Set();
|
|
87
|
+
const desiredRecords = [];
|
|
88
|
+
const duplicates = [];
|
|
89
|
+
for (const record of [
|
|
90
|
+
...registerRecords.filter((entry) => entry.beforeClass),
|
|
91
|
+
...movedRecords
|
|
92
|
+
]) {
|
|
93
|
+
if (seenTokens.has(record.token)) {
|
|
94
|
+
duplicates.push(record);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
seenTokens.add(record.token);
|
|
98
|
+
desiredRecords.push(record);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (movedRecords.length < 1 && duplicates.length < 1) {
|
|
102
|
+
return {
|
|
103
|
+
changed: false,
|
|
104
|
+
content: sourceText,
|
|
105
|
+
moved: 0,
|
|
106
|
+
deduped: 0,
|
|
107
|
+
reason: "already-current"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const record of [...registerRecords].sort((left, right) => right.index - left.index)) {
|
|
112
|
+
record.statement.remove();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const nextClassIndex = sourceFile.getStatements().findIndex((statement) =>
|
|
116
|
+
Node.isClassDeclaration(statement) && statement.getName() === "MainClientProvider"
|
|
117
|
+
);
|
|
118
|
+
if (nextClassIndex < 0) {
|
|
119
|
+
return {
|
|
120
|
+
changed: false,
|
|
121
|
+
content: sourceText,
|
|
122
|
+
moved: 0,
|
|
123
|
+
deduped: 0,
|
|
124
|
+
reason: "missing-class"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
sourceFile.insertStatements(nextClassIndex, desiredRecords.map((record) => record.text));
|
|
129
|
+
return {
|
|
130
|
+
changed: true,
|
|
131
|
+
content: sourceFile.getFullText(),
|
|
132
|
+
moved: movedRecords.length,
|
|
133
|
+
deduped: duplicates.length,
|
|
134
|
+
reason: "rewritten"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function escapeRegExp(value = "") {
|
|
139
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function findMatchingDelimiter(sourceText = "", openIndex = -1, openChar = "(", closeChar = ")") {
|
|
143
|
+
const source = String(sourceText || "");
|
|
144
|
+
if (openIndex < 0 || source[openIndex] !== openChar) {
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let depth = 0;
|
|
149
|
+
let quote = "";
|
|
150
|
+
let inLineComment = false;
|
|
151
|
+
let inBlockComment = false;
|
|
152
|
+
for (let index = openIndex; index < source.length; index += 1) {
|
|
153
|
+
const char = source[index];
|
|
154
|
+
const nextChar = source[index + 1] || "";
|
|
155
|
+
const previousChar = source[index - 1] || "";
|
|
156
|
+
|
|
157
|
+
if (inLineComment) {
|
|
158
|
+
if (char === "\n") {
|
|
159
|
+
inLineComment = false;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (inBlockComment) {
|
|
164
|
+
if (char === "*" && nextChar === "/") {
|
|
165
|
+
inBlockComment = false;
|
|
166
|
+
index += 1;
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (quote) {
|
|
171
|
+
if (char === "\\" && quote !== "`") {
|
|
172
|
+
index += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (char === quote && previousChar !== "\\") {
|
|
176
|
+
quote = "";
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (char === "/" && nextChar === "/") {
|
|
182
|
+
inLineComment = true;
|
|
183
|
+
index += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (char === "/" && nextChar === "*") {
|
|
187
|
+
inBlockComment = true;
|
|
188
|
+
index += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
192
|
+
quote = char;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (char === openChar) {
|
|
196
|
+
depth += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (char === closeChar) {
|
|
200
|
+
depth -= 1;
|
|
201
|
+
if (depth === 0) {
|
|
202
|
+
return index;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return -1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function readLineIndent(sourceText = "", index = 0) {
|
|
211
|
+
const source = String(sourceText || "");
|
|
212
|
+
const lineStart = source.lastIndexOf("\n", Math.max(0, index - 1)) + 1;
|
|
213
|
+
const linePrefix = source.slice(lineStart, index);
|
|
214
|
+
const match = linePrefix.match(/^[ \t]*/);
|
|
215
|
+
return match?.[0] || "";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readFormFieldKey(sourceText = "") {
|
|
219
|
+
const match = String(sourceText || "").match(/(?:^|[,{]\s*)(?:"key"|key)\s*:\s*["']([^"']+)["']/);
|
|
220
|
+
return String(match?.[1] || "");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readFormFieldKeys(sourceText = "") {
|
|
224
|
+
const keys = new Set();
|
|
225
|
+
const keyPattern = /(?:^|[,{]\s*)(?:"key"|key)\s*:\s*["']([^"']+)["']/g;
|
|
226
|
+
let match = null;
|
|
227
|
+
while ((match = keyPattern.exec(String(sourceText || ""))) != null) {
|
|
228
|
+
if (match[1]) {
|
|
229
|
+
keys.add(String(match[1]));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return keys;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatArrayEntry(entrySource = "", indent = " ") {
|
|
236
|
+
return String(entrySource || "")
|
|
237
|
+
.trim()
|
|
238
|
+
.split(/\r?\n/)
|
|
239
|
+
.map((line) => `${indent}${line}`)
|
|
240
|
+
.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function findFormFieldArrayDeclaration(sourceText = "", arrayName = "") {
|
|
244
|
+
const source = String(sourceText || "");
|
|
245
|
+
const declarationPattern = new RegExp(`\\b(?:const|let)\\s+${escapeRegExp(arrayName)}\\s*=\\s*\\[`, "g");
|
|
246
|
+
const match = declarationPattern.exec(source);
|
|
247
|
+
if (!match) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const openIndex = source.indexOf("[", match.index);
|
|
252
|
+
const closeIndex = findMatchingDelimiter(source, openIndex, "[", "]");
|
|
253
|
+
if (closeIndex < 0) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
openIndex,
|
|
259
|
+
closeIndex,
|
|
260
|
+
indent: readLineIndent(source, match.index),
|
|
261
|
+
content: source.slice(openIndex + 1, closeIndex)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function findLineRangeContaining(sourceText = "", searchText = "", { afterIndex = -1 } = {}) {
|
|
266
|
+
const source = String(sourceText || "");
|
|
267
|
+
const target = String(searchText || "");
|
|
268
|
+
if (!target) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const startSearchIndex = Number.isInteger(afterIndex) && afterIndex >= 0 ? afterIndex : 0;
|
|
273
|
+
const matchIndex = source.indexOf(target, startSearchIndex);
|
|
274
|
+
if (matchIndex < 0) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const lineStart = source.lastIndexOf("\n", Math.max(0, matchIndex - 1)) + 1;
|
|
279
|
+
let lineEnd = source.indexOf("\n", matchIndex);
|
|
280
|
+
if (lineEnd < 0) {
|
|
281
|
+
lineEnd = source.length;
|
|
282
|
+
} else {
|
|
283
|
+
lineEnd += 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
startIndex: lineStart,
|
|
288
|
+
endIndex: lineEnd,
|
|
289
|
+
text: source.slice(lineStart, lineEnd)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function findFormFieldPushCalls(sourceText = "", arrayName = "") {
|
|
294
|
+
const source = String(sourceText || "");
|
|
295
|
+
const callPattern = new RegExp(`^[ \\t]*${escapeRegExp(arrayName)}\\.push\\s*\\(`, "gm");
|
|
296
|
+
const calls = [];
|
|
297
|
+
let match = null;
|
|
298
|
+
while ((match = callPattern.exec(source)) != null) {
|
|
299
|
+
const openIndex = source.indexOf("(", match.index);
|
|
300
|
+
const closeIndex = findMatchingDelimiter(source, openIndex, "(", ")");
|
|
301
|
+
if (closeIndex < 0) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let endIndex = closeIndex + 1;
|
|
306
|
+
while (endIndex < source.length && /[ \t]/.test(source[endIndex])) {
|
|
307
|
+
endIndex += 1;
|
|
308
|
+
}
|
|
309
|
+
if (source[endIndex] === ";") {
|
|
310
|
+
endIndex += 1;
|
|
311
|
+
}
|
|
312
|
+
while (endIndex < source.length && /[ \t]/.test(source[endIndex])) {
|
|
313
|
+
endIndex += 1;
|
|
314
|
+
}
|
|
315
|
+
if (source.slice(endIndex, endIndex + 2) === "\r\n") {
|
|
316
|
+
endIndex += 2;
|
|
317
|
+
} else if (source[endIndex] === "\n") {
|
|
318
|
+
endIndex += 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const argumentSource = source.slice(openIndex + 1, closeIndex).trim();
|
|
322
|
+
if (!argumentSource.startsWith("{") || !argumentSource.endsWith("}")) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
calls.push({
|
|
327
|
+
startIndex: match.index,
|
|
328
|
+
endIndex,
|
|
329
|
+
argumentSource,
|
|
330
|
+
key: readFormFieldKey(argumentSource)
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return calls;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildArrayContentWithAddedEntries(existingContent = "", entries = [], indent = "", { marker = "" } = {}) {
|
|
338
|
+
const entriesBlock = entries
|
|
339
|
+
.map((entry) => formatArrayEntry(entry, `${indent} `))
|
|
340
|
+
.join(",\n");
|
|
341
|
+
const markerLine = String(marker || "").trim() ? `${indent} ${String(marker || "").trim()}` : "";
|
|
342
|
+
const normalizedExistingContent = markerLine
|
|
343
|
+
? String(existingContent || "").replace(new RegExp(`^[ \\t]*${escapeRegExp(marker)}[ \\t]*\\r?\\n?`, "m"), "")
|
|
344
|
+
: String(existingContent || "");
|
|
345
|
+
if (!entriesBlock && !markerLine) {
|
|
346
|
+
return normalizedExistingContent;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const trimmedExisting = normalizedExistingContent.trim();
|
|
350
|
+
if (!trimmedExisting) {
|
|
351
|
+
const lines = [entriesBlock, markerLine].filter(Boolean);
|
|
352
|
+
return `\n${lines.join("\n")}\n${indent}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const existingWithoutTrailingWhitespace = normalizedExistingContent.trimEnd();
|
|
356
|
+
const existingWithComma = existingWithoutTrailingWhitespace.endsWith(",")
|
|
357
|
+
? existingWithoutTrailingWhitespace
|
|
358
|
+
: `${existingWithoutTrailingWhitespace},`;
|
|
359
|
+
const appendedLines = [entriesBlock, markerLine].filter(Boolean).join("\n");
|
|
360
|
+
return `${existingWithComma}\n${appendedLines}\n${indent}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function applyTextReplacements(sourceText = "", replacements = []) {
|
|
364
|
+
let content = String(sourceText || "");
|
|
365
|
+
for (const replacement of [...replacements].sort((left, right) => right.start - left.start)) {
|
|
366
|
+
content = `${content.slice(0, replacement.start)}${replacement.text}${content.slice(replacement.end)}`;
|
|
367
|
+
}
|
|
368
|
+
return content;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function buildCrudFormFieldPushMigration(sourceText = "") {
|
|
372
|
+
const source = String(sourceText || "");
|
|
373
|
+
if (!source.includes("UI_")) {
|
|
374
|
+
return {
|
|
375
|
+
changed: false,
|
|
376
|
+
content: source,
|
|
377
|
+
folded: 0,
|
|
378
|
+
movedMarkers: 0,
|
|
379
|
+
reason: "no-form-field-pushes"
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const replacements = [];
|
|
384
|
+
let folded = 0;
|
|
385
|
+
let movedMarkers = 0;
|
|
386
|
+
for (const arrayName of CRUD_FORM_FIELD_ARRAY_NAMES) {
|
|
387
|
+
const declaration = findFormFieldArrayDeclaration(source, arrayName);
|
|
388
|
+
const calls = findFormFieldPushCalls(source, arrayName);
|
|
389
|
+
const marker = CRUD_FORM_FIELD_MARKERS_BY_ARRAY[arrayName] || "";
|
|
390
|
+
const markerInArray = marker ? declaration?.content?.includes(marker) === true : true;
|
|
391
|
+
const markerLineRange = marker && !markerInArray && declaration
|
|
392
|
+
? findLineRangeContaining(source, marker, { afterIndex: declaration.closeIndex })
|
|
393
|
+
: null;
|
|
394
|
+
if (!declaration || (calls.length < 1 && !markerLineRange)) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const knownKeys = readFormFieldKeys(declaration.content);
|
|
399
|
+
const entriesToAdd = [];
|
|
400
|
+
for (const call of calls) {
|
|
401
|
+
const callKey = call.key;
|
|
402
|
+
if (callKey && knownKeys.has(callKey)) {
|
|
403
|
+
replacements.push({ start: call.startIndex, end: call.endIndex, text: "" });
|
|
404
|
+
folded += 1;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (callKey) {
|
|
408
|
+
knownKeys.add(callKey);
|
|
409
|
+
}
|
|
410
|
+
entriesToAdd.push(call.argumentSource);
|
|
411
|
+
replacements.push({ start: call.startIndex, end: call.endIndex, text: "" });
|
|
412
|
+
folded += 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (markerLineRange) {
|
|
416
|
+
replacements.push({ start: markerLineRange.startIndex, end: markerLineRange.endIndex, text: "" });
|
|
417
|
+
movedMarkers += 1;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (entriesToAdd.length > 0 || markerLineRange) {
|
|
421
|
+
const shouldAddMarker = marker && !markerInArray;
|
|
422
|
+
replacements.push({
|
|
423
|
+
start: declaration.openIndex + 1,
|
|
424
|
+
end: declaration.closeIndex,
|
|
425
|
+
text: buildArrayContentWithAddedEntries(declaration.content, entriesToAdd, declaration.indent, {
|
|
426
|
+
marker: shouldAddMarker ? marker : ""
|
|
427
|
+
})
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (folded < 1 && movedMarkers < 1) {
|
|
433
|
+
return {
|
|
434
|
+
changed: false,
|
|
435
|
+
content: source,
|
|
436
|
+
folded: 0,
|
|
437
|
+
movedMarkers: 0,
|
|
438
|
+
reason: "no-foldable-form-field-pushes"
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
changed: true,
|
|
444
|
+
content: applyTextReplacements(source, replacements),
|
|
445
|
+
folded,
|
|
446
|
+
movedMarkers,
|
|
447
|
+
reason: "rewritten"
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function collectCrudFormFieldCandidateFiles(appRoot = "") {
|
|
452
|
+
const pagesRoot = path.join(appRoot, "src", "pages");
|
|
453
|
+
if (!(await fileExists(pagesRoot))) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const files = [];
|
|
458
|
+
async function visit(directory) {
|
|
459
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
460
|
+
for (const entry of entries) {
|
|
461
|
+
if (entry.name.startsWith(".") || SOURCE_SCAN_SKIP_DIRS.has(entry.name)) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const absolutePath = path.join(directory, entry.name);
|
|
465
|
+
if (entry.isDirectory()) {
|
|
466
|
+
await visit(absolutePath);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (!entry.isFile() || !SOURCE_FILE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
files.push(absolutePath);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
await visit(pagesRoot);
|
|
477
|
+
return files.sort();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function runAppMigrateSourceMutationsCommand(_ctx = {}, { appRoot = "", options = {}, stdout }) {
|
|
481
|
+
const dryRun = options?.dryRun === true;
|
|
482
|
+
let changedCount = 0;
|
|
483
|
+
const absolutePath = path.join(appRoot, MAIN_CLIENT_PROVIDER_FILE);
|
|
484
|
+
if (await fileExists(absolutePath)) {
|
|
485
|
+
const previousContent = await readFile(absolutePath, "utf8");
|
|
486
|
+
const migrated = buildMainClientProviderMigration(previousContent);
|
|
487
|
+
if (migrated.changed) {
|
|
488
|
+
if (!dryRun) {
|
|
489
|
+
await writeFile(absolutePath, migrated.content, "utf8");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
changedCount += 1;
|
|
493
|
+
const action = dryRun ? "would rewrite" : "rewrote";
|
|
494
|
+
const movedSuffix = migrated.moved === 1 ? "1 component registration" : `${migrated.moved} component registrations`;
|
|
495
|
+
const dedupeSuffix = migrated.deduped > 0
|
|
496
|
+
? ` and removed ${migrated.deduped} duplicate registration${migrated.deduped === 1 ? "" : "s"}`
|
|
497
|
+
: "";
|
|
498
|
+
stdout.write(
|
|
499
|
+
`[migrate-source-mutations] ${action} ${MAIN_CLIENT_PROVIDER_FILE}: moved ${movedSuffix} before MainClientProvider${dedupeSuffix}.\n`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const crudFormFieldFiles = await collectCrudFormFieldCandidateFiles(appRoot);
|
|
505
|
+
for (const filePath of crudFormFieldFiles) {
|
|
506
|
+
const previousContent = await readFile(filePath, "utf8");
|
|
507
|
+
const migrated = buildCrudFormFieldPushMigration(previousContent);
|
|
508
|
+
if (!migrated.changed) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (!dryRun) {
|
|
512
|
+
await writeFile(filePath, migrated.content, "utf8");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
changedCount += 1;
|
|
516
|
+
const relativePath = path.relative(appRoot, filePath).replaceAll(path.sep, "/");
|
|
517
|
+
const action = dryRun ? "would rewrite" : "rewrote";
|
|
518
|
+
const foldedSuffix = migrated.folded === 1 ? "1 form field push" : `${migrated.folded} form field pushes`;
|
|
519
|
+
const markerSuffix = migrated.movedMarkers > 0
|
|
520
|
+
? ` and moved ${migrated.movedMarkers} form-field marker${migrated.movedMarkers === 1 ? "" : "s"} into array literals`
|
|
521
|
+
: "";
|
|
522
|
+
stdout.write(
|
|
523
|
+
`[migrate-source-mutations] ${action} ${relativePath}: folded ${foldedSuffix} into array literals${markerSuffix}.\n`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (changedCount < 1) {
|
|
528
|
+
stdout.write("[migrate-source-mutations] source files are already current.\n");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export {
|
|
535
|
+
buildCrudFormFieldPushMigration,
|
|
536
|
+
buildMainClientProviderMigration,
|
|
537
|
+
runAppMigrateSourceMutationsCommand
|
|
538
|
+
};
|
|
@@ -78,6 +78,7 @@ function renderPackagePayloadText({
|
|
|
78
78
|
const devMutations = ensureObject(ensureObject(payload.mutations).dependencies).dev || {};
|
|
79
79
|
const scriptMutations = ensureObject(ensureObject(payload.mutations).packageJson).scripts || {};
|
|
80
80
|
const textMutations = ensureArray(ensureObject(payload.mutations).text);
|
|
81
|
+
const sourceMutations = ensureArray(ensureObject(payload.mutations).source);
|
|
81
82
|
const runtimeMutationEntries = Object.entries(ensureObject(runtimeMutations));
|
|
82
83
|
const devMutationEntries = Object.entries(ensureObject(devMutations));
|
|
83
84
|
const scriptMutationEntries = Object.entries(ensureObject(scriptMutations));
|
|
@@ -371,6 +372,23 @@ function renderPackagePayloadText({
|
|
|
371
372
|
}
|
|
372
373
|
}
|
|
373
374
|
|
|
375
|
+
if (sourceMutations.length > 0) {
|
|
376
|
+
stdout.write(`${color.heading(`Source mutations (${sourceMutations.length}):`)}\n`);
|
|
377
|
+
for (const mutation of sourceMutations) {
|
|
378
|
+
const record = ensureObject(mutation);
|
|
379
|
+
const op = String(record.op || "").trim();
|
|
380
|
+
const file = String(record.file || "").trim();
|
|
381
|
+
const reason = String(record.reason || "").trim();
|
|
382
|
+
const reasonSuffix = reason ? `: ${reason}` : "";
|
|
383
|
+
let target = String(record.target || record.callee || record.name || record.from || "").trim();
|
|
384
|
+
if (op === "ensure-import") {
|
|
385
|
+
target = String(record.from || "").trim();
|
|
386
|
+
}
|
|
387
|
+
const mutationLabel = `${op} ${file} ${target}`.trim();
|
|
388
|
+
stdout.write(`- ${color.item(mutationLabel)}${reasonSuffix}\n`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
374
392
|
if (payload.fileWritePlan.fileCount > 0) {
|
|
375
393
|
stdout.write(`${color.heading(`File writes (${payload.fileWritePlan.fileCount}):`)}\n`);
|
|
376
394
|
for (const group of payload.fileWritePlan.groups) {
|
|
@@ -136,13 +136,13 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
136
136
|
parameters: Object.freeze([
|
|
137
137
|
Object.freeze({
|
|
138
138
|
name: "<subcommand>",
|
|
139
|
-
description: "verify | update-packages | link-local-packages | release | adopt-managed-scripts."
|
|
139
|
+
description: "verify | update-packages | link-local-packages | release | adopt-managed-scripts | migrate-source-mutations."
|
|
140
140
|
})
|
|
141
141
|
]),
|
|
142
142
|
defaults: Object.freeze([
|
|
143
143
|
"The scaffold keeps npm run shortcuts such as verify and jskit:update, but their maintained behavior lives under jskit app.",
|
|
144
144
|
"Use jskit app <subcommand> help for subcommand-specific usage.",
|
|
145
|
-
"--dry-run is accepted by update-packages, adopt-managed-scripts, and release."
|
|
145
|
+
"--dry-run is accepted by update-packages, adopt-managed-scripts, migrate-source-mutations, and release."
|
|
146
146
|
]),
|
|
147
147
|
examples: Object.freeze([
|
|
148
148
|
Object.freeze({
|
|
@@ -156,7 +156,9 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
156
156
|
label: "Existing app migration",
|
|
157
157
|
lines: Object.freeze([
|
|
158
158
|
"jskit app adopt-managed-scripts --dry-run",
|
|
159
|
-
"jskit app adopt-managed-scripts --force"
|
|
159
|
+
"jskit app adopt-managed-scripts --force",
|
|
160
|
+
"jskit app migrate-source-mutations --dry-run",
|
|
161
|
+
"jskit app migrate-source-mutations"
|
|
160
162
|
])
|
|
161
163
|
})
|
|
162
164
|
]),
|