@jskit-ai/jskit-cli 0.2.24 → 0.2.26
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 +2 -2
- package/src/server/argParser.js +23 -4
- package/src/server/cliRuntime.js +410 -121
- package/src/server/commandHandlers.js +462 -13
- package/src/server/pathResolution.js +0 -64
- package/src/server/runCli.js +16 -0
- package/src/server/runtimeDeps.js +1 -0
package/src/server/cliRuntime.js
CHANGED
|
@@ -28,9 +28,7 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
BUNDLES_ROOT,
|
|
30
30
|
CATALOG_PACKAGES_PATH,
|
|
31
|
-
CLI_PACKAGE_ROOT
|
|
32
|
-
MODULES_ROOT,
|
|
33
|
-
WORKSPACE_ROOT
|
|
31
|
+
CLI_PACKAGE_ROOT
|
|
34
32
|
} from "./pathResolution.js";
|
|
35
33
|
import {
|
|
36
34
|
appendTextSnippet,
|
|
@@ -55,9 +53,9 @@ const VITE_DEV_PROXY_CONFIG_RELATIVE_PATH = ".jskit/vite.dev.proxy.json";
|
|
|
55
53
|
const VITE_DEV_PROXY_CONFIG_VERSION = 1;
|
|
56
54
|
const PUBLIC_APP_CONFIG_RELATIVE_PATH = "config/public.js";
|
|
57
55
|
const SERVER_APP_CONFIG_RELATIVE_PATH = "config/server.js";
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
56
|
+
const PACKAGE_KIND_RUNTIME = "runtime";
|
|
57
|
+
const PACKAGE_KIND_GENERATOR = "generator";
|
|
58
|
+
const PACKAGE_KINDS = Object.freeze([PACKAGE_KIND_RUNTIME, PACKAGE_KIND_GENERATOR]);
|
|
61
59
|
const WORKSPACE_VISIBILITY_LEVELS = Object.freeze(["workspace", "workspace_user"]);
|
|
62
60
|
const WORKSPACE_VISIBILITY_SET = new Set(WORKSPACE_VISIBILITY_LEVELS);
|
|
63
61
|
const MATERIALIZED_PACKAGE_ROOTS = new Map();
|
|
@@ -95,6 +93,20 @@ function normalizeMutationExtension(value) {
|
|
|
95
93
|
return `.${extension}`;
|
|
96
94
|
}
|
|
97
95
|
|
|
96
|
+
function normalizeTemplateContextRecord(value) {
|
|
97
|
+
const record = ensureObject(value);
|
|
98
|
+
const entrypoint = String(record.entrypoint || "").trim();
|
|
99
|
+
const exportName = String(record.export || "").trim();
|
|
100
|
+
if (!entrypoint && !exportName) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
entrypoint,
|
|
106
|
+
export: exportName || "buildTemplateContext"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
function normalizeFileMutationRecord(value) {
|
|
99
111
|
const record = ensureObject(value);
|
|
100
112
|
const op = String(record.op || "copy-file").trim().toLowerCase() || "copy-file";
|
|
@@ -111,6 +123,7 @@ function normalizeFileMutationRecord(value) {
|
|
|
111
123
|
id: String(record.id || "").trim(),
|
|
112
124
|
category: String(record.category || "").trim(),
|
|
113
125
|
reason: String(record.reason || "").trim(),
|
|
126
|
+
templateContext: normalizeTemplateContextRecord(record.templateContext),
|
|
114
127
|
when: normalizeMutationWhen(record.when)
|
|
115
128
|
};
|
|
116
129
|
}
|
|
@@ -184,6 +197,44 @@ function normalizeWhenSourceValue(value) {
|
|
|
184
197
|
return "";
|
|
185
198
|
}
|
|
186
199
|
|
|
200
|
+
function normalizeWhenComparisonValue(value) {
|
|
201
|
+
const normalizedValue = normalizeWhenSourceValue(value);
|
|
202
|
+
if (!normalizedValue.includes(",")) {
|
|
203
|
+
return normalizedValue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return normalizedValue
|
|
207
|
+
.split(",")
|
|
208
|
+
.map((entry) => String(entry || "").trim())
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.join(",");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function splitWhenComparisonTokens(value) {
|
|
214
|
+
return normalizeWhenComparisonValue(value)
|
|
215
|
+
.split(",")
|
|
216
|
+
.map((entry) => String(entry || "").trim())
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function matchesWhenComparisonValue(optionValue, optionTokens, expectedValue) {
|
|
221
|
+
const expected = normalizeWhenComparisonValue(expectedValue);
|
|
222
|
+
if (!expected) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const expectedTokens = splitWhenComparisonTokens(expected);
|
|
227
|
+
if (expectedTokens.length > 1) {
|
|
228
|
+
return optionValue === expected;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (optionTokens.length > 1) {
|
|
232
|
+
return optionTokens.includes(expectedTokens[0]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return optionValue === expectedTokens[0];
|
|
236
|
+
}
|
|
237
|
+
|
|
187
238
|
function resolveWhenConfigValue(configContext = {}, configPath = "") {
|
|
188
239
|
const normalizedPath = String(configPath || "").trim();
|
|
189
240
|
if (!normalizedPath) {
|
|
@@ -244,11 +295,12 @@ function shouldApplyMutationWhen(
|
|
|
244
295
|
const sourceValue = optionName
|
|
245
296
|
? readObjectPath(options, optionName)
|
|
246
297
|
: resolveWhenConfigValue(configContext, configPath);
|
|
247
|
-
const optionValue =
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
298
|
+
const optionValue = normalizeWhenComparisonValue(sourceValue);
|
|
299
|
+
const optionTokens = splitWhenComparisonTokens(optionValue);
|
|
300
|
+
const equals = normalizeWhenComparisonValue(when.equals);
|
|
301
|
+
const notEquals = normalizeWhenComparisonValue(when.notEquals);
|
|
302
|
+
const includes = ensureArray(when.includes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
|
|
303
|
+
const excludes = ensureArray(when.excludes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
|
|
252
304
|
|
|
253
305
|
if (equals && optionValue !== equals) {
|
|
254
306
|
return false;
|
|
@@ -256,10 +308,10 @@ function shouldApplyMutationWhen(
|
|
|
256
308
|
if (notEquals && optionValue === notEquals) {
|
|
257
309
|
return false;
|
|
258
310
|
}
|
|
259
|
-
if (includes.length > 0 && !includes.
|
|
311
|
+
if (includes.length > 0 && !includes.some((entry) => matchesWhenComparisonValue(optionValue, optionTokens, entry))) {
|
|
260
312
|
return false;
|
|
261
313
|
}
|
|
262
|
-
if (excludes.length > 0 && excludes.
|
|
314
|
+
if (excludes.length > 0 && excludes.some((entry) => matchesWhenComparisonValue(optionValue, optionTokens, entry))) {
|
|
263
315
|
return false;
|
|
264
316
|
}
|
|
265
317
|
|
|
@@ -1113,16 +1165,16 @@ function removeEnvValue(content, key, expectedValue, previous) {
|
|
|
1113
1165
|
};
|
|
1114
1166
|
}
|
|
1115
1167
|
|
|
1116
|
-
function
|
|
1117
|
-
const normalized = String(rawValue || "")
|
|
1118
|
-
.trim()
|
|
1119
|
-
.toLowerCase();
|
|
1168
|
+
function normalizePackageKind(rawValue, descriptorPath) {
|
|
1169
|
+
const normalized = String(rawValue || "").trim().toLowerCase();
|
|
1120
1170
|
if (!normalized) {
|
|
1121
|
-
|
|
1171
|
+
throw createCliError(
|
|
1172
|
+
`Invalid package descriptor at ${descriptorPath}: missing kind (expected ${PACKAGE_KINDS.join(" | ")}).`
|
|
1173
|
+
);
|
|
1122
1174
|
}
|
|
1123
|
-
if (!
|
|
1175
|
+
if (!PACKAGE_KINDS.includes(normalized)) {
|
|
1124
1176
|
throw createCliError(
|
|
1125
|
-
`Invalid package descriptor at ${descriptorPath}:
|
|
1177
|
+
`Invalid package descriptor at ${descriptorPath}: kind must be one of: ${PACKAGE_KINDS.join(", ")}.`
|
|
1126
1178
|
);
|
|
1127
1179
|
}
|
|
1128
1180
|
return normalized;
|
|
@@ -1177,13 +1229,13 @@ function validatePackageDescriptorShape(descriptor, descriptorPath) {
|
|
|
1177
1229
|
|
|
1178
1230
|
return {
|
|
1179
1231
|
...normalized,
|
|
1180
|
-
|
|
1232
|
+
kind: normalizePackageKind(normalized.kind, descriptorPath)
|
|
1181
1233
|
};
|
|
1182
1234
|
}
|
|
1183
1235
|
|
|
1184
|
-
function
|
|
1236
|
+
function isGeneratorPackageEntry(packageEntry) {
|
|
1185
1237
|
const descriptor = ensureObject(packageEntry?.descriptor);
|
|
1186
|
-
return String(descriptor.
|
|
1238
|
+
return String(descriptor.kind || "").trim().toLowerCase() === PACKAGE_KIND_GENERATOR;
|
|
1187
1239
|
}
|
|
1188
1240
|
|
|
1189
1241
|
function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { expectedPackageId = "", fallbackVersion = "" } = {}) {
|
|
@@ -1208,7 +1260,8 @@ function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { ex
|
|
|
1208
1260
|
return {
|
|
1209
1261
|
...normalized,
|
|
1210
1262
|
packageId,
|
|
1211
|
-
version
|
|
1263
|
+
version,
|
|
1264
|
+
kind: normalizePackageKind(normalized.kind, descriptorPath)
|
|
1212
1265
|
};
|
|
1213
1266
|
}
|
|
1214
1267
|
|
|
@@ -1277,85 +1330,6 @@ function validateBundleDescriptorShape(descriptor, descriptorPath) {
|
|
|
1277
1330
|
return normalized;
|
|
1278
1331
|
}
|
|
1279
1332
|
|
|
1280
|
-
async function loadWorkspacePackageRegistry() {
|
|
1281
|
-
if (!MODULES_ROOT || !(await fileExists(MODULES_ROOT))) {
|
|
1282
|
-
return new Map();
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
const directories = [];
|
|
1286
|
-
const levelOne = await readdir(MODULES_ROOT, { withFileTypes: true });
|
|
1287
|
-
|
|
1288
|
-
for (const entry of levelOne) {
|
|
1289
|
-
if (!entry.isDirectory()) {
|
|
1290
|
-
continue;
|
|
1291
|
-
}
|
|
1292
|
-
if (entry.name.startsWith(".") || entry.name.endsWith(".LEGACY")) {
|
|
1293
|
-
continue;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
const absolute = path.join(MODULES_ROOT, entry.name);
|
|
1297
|
-
const descriptorPath = path.join(absolute, "package.descriptor.mjs");
|
|
1298
|
-
if (await fileExists(descriptorPath)) {
|
|
1299
|
-
directories.push(absolute);
|
|
1300
|
-
continue;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const nested = await readdir(absolute, { withFileTypes: true }).catch(() => []);
|
|
1304
|
-
for (const child of nested) {
|
|
1305
|
-
if (!child.isDirectory() || child.name.startsWith(".")) {
|
|
1306
|
-
continue;
|
|
1307
|
-
}
|
|
1308
|
-
const nestedAbsolute = path.join(absolute, child.name);
|
|
1309
|
-
const nestedDescriptor = path.join(nestedAbsolute, "package.descriptor.mjs");
|
|
1310
|
-
if (await fileExists(nestedDescriptor)) {
|
|
1311
|
-
directories.push(nestedAbsolute);
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
const uniqueDirectories = sortStrings([...new Set(directories)]);
|
|
1317
|
-
const registry = new Map();
|
|
1318
|
-
|
|
1319
|
-
for (const packageRoot of uniqueDirectories) {
|
|
1320
|
-
const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
|
|
1321
|
-
const descriptorModule = await import(pathToFileURL(descriptorPath).href);
|
|
1322
|
-
const descriptor = validatePackageDescriptorShape(descriptorModule?.default, descriptorPath);
|
|
1323
|
-
|
|
1324
|
-
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
1325
|
-
if (!(await fileExists(packageJsonPath))) {
|
|
1326
|
-
throw createCliError(`Missing package.json for ${descriptor.packageId} at ${packageRoot}.`);
|
|
1327
|
-
}
|
|
1328
|
-
const packageJson = await readJsonFile(packageJsonPath);
|
|
1329
|
-
const packageName = String(packageJson?.name || "").trim();
|
|
1330
|
-
if (packageName !== descriptor.packageId) {
|
|
1331
|
-
throw createCliError(
|
|
1332
|
-
`Descriptor/package mismatch at ${packageRoot}: package.descriptor.mjs has ${descriptor.packageId} but package.json has ${packageName || "(empty)"}.`
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
const relativeDir = normalizeRelativePath(WORKSPACE_ROOT || MODULES_ROOT, packageRoot);
|
|
1337
|
-
const descriptorRelativePath = normalizeRelativePath(WORKSPACE_ROOT || MODULES_ROOT, descriptorPath);
|
|
1338
|
-
registry.set(
|
|
1339
|
-
descriptor.packageId,
|
|
1340
|
-
createPackageEntry({
|
|
1341
|
-
packageId: descriptor.packageId,
|
|
1342
|
-
version: descriptor.version,
|
|
1343
|
-
descriptor,
|
|
1344
|
-
rootDir: packageRoot,
|
|
1345
|
-
relativeDir,
|
|
1346
|
-
descriptorRelativePath,
|
|
1347
|
-
packageJson,
|
|
1348
|
-
sourceType: "packages-directory",
|
|
1349
|
-
source: {
|
|
1350
|
-
descriptorPath: descriptorRelativePath
|
|
1351
|
-
}
|
|
1352
|
-
})
|
|
1353
|
-
);
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
return registry;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
1333
|
async function loadAppLocalPackageRegistry(appRoot) {
|
|
1360
1334
|
const localPackagesRoot = path.join(appRoot, "packages");
|
|
1361
1335
|
if (!(await fileExists(localPackagesRoot))) {
|
|
@@ -1468,17 +1442,14 @@ async function loadCatalogPackageRegistry() {
|
|
|
1468
1442
|
}
|
|
1469
1443
|
|
|
1470
1444
|
async function loadPackageRegistry() {
|
|
1471
|
-
const workspaceRegistry = await loadWorkspacePackageRegistry();
|
|
1472
1445
|
const catalogRegistry = await loadCatalogPackageRegistry();
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
if (merged.size === 0) {
|
|
1446
|
+
if (catalogRegistry.size === 0) {
|
|
1476
1447
|
throw createCliError(
|
|
1477
|
-
"Unable to load package registry
|
|
1448
|
+
"Unable to load package registry from @jskit-ai/jskit-catalog. Install it alongside @jskit-ai/jskit-cli or set JSKIT_CATALOG_PACKAGES_PATH."
|
|
1478
1449
|
);
|
|
1479
1450
|
}
|
|
1480
1451
|
|
|
1481
|
-
return
|
|
1452
|
+
return catalogRegistry;
|
|
1482
1453
|
}
|
|
1483
1454
|
|
|
1484
1455
|
async function loadInstalledNodeModulePackageEntry({ appRoot, packageId }) {
|
|
@@ -1745,6 +1716,7 @@ function createLocalPackageDescriptorTemplate({ packageId, description }) {
|
|
|
1745
1716
|
packageVersion: 1,
|
|
1746
1717
|
packageId: "${packageId}",
|
|
1747
1718
|
version: "0.1.0",
|
|
1719
|
+
kind: "runtime",
|
|
1748
1720
|
description: ${JSON.stringify(String(description || ""))},
|
|
1749
1721
|
dependsOn: [
|
|
1750
1722
|
// "@jskit-ai/kernel"
|
|
@@ -3544,20 +3516,134 @@ function interpolateFileMutationRecord(mutation, options, packageId) {
|
|
|
3544
3516
|
extension: interpolate(mutation.extension, "extension"),
|
|
3545
3517
|
id: interpolate(mutation.id, "id"),
|
|
3546
3518
|
category: interpolate(mutation.category, "category"),
|
|
3547
|
-
reason: interpolate(mutation.reason, "reason")
|
|
3519
|
+
reason: interpolate(mutation.reason, "reason"),
|
|
3520
|
+
templateContext: mutation.templateContext
|
|
3521
|
+
? {
|
|
3522
|
+
entrypoint: interpolate(mutation.templateContext.entrypoint, "templateContext.entrypoint"),
|
|
3523
|
+
export: interpolate(mutation.templateContext.export, "templateContext.export")
|
|
3524
|
+
}
|
|
3525
|
+
: null
|
|
3548
3526
|
};
|
|
3549
3527
|
}
|
|
3550
3528
|
|
|
3551
|
-
|
|
3529
|
+
function applyTemplateContextReplacements(sourceContent, replacements) {
|
|
3530
|
+
let output = String(sourceContent || "");
|
|
3531
|
+
for (const [placeholder, value] of Object.entries(ensureObject(replacements))) {
|
|
3532
|
+
const normalizedPlaceholder = String(placeholder || "");
|
|
3533
|
+
if (!normalizedPlaceholder) {
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3536
|
+
output = output.split(normalizedPlaceholder).join(String(value == null ? "" : value));
|
|
3537
|
+
}
|
|
3538
|
+
return output;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
async function copyTemplateFile(
|
|
3542
|
+
sourcePath,
|
|
3543
|
+
targetPath,
|
|
3544
|
+
options,
|
|
3545
|
+
packageId,
|
|
3546
|
+
interpolationKey,
|
|
3547
|
+
templateContextReplacements = null
|
|
3548
|
+
) {
|
|
3552
3549
|
const sourceContent = await readFile(sourcePath, "utf8");
|
|
3553
|
-
|
|
3550
|
+
let renderedContent = sourceContent.includes("${")
|
|
3554
3551
|
? interpolateOptionValue(sourceContent, options, packageId, interpolationKey)
|
|
3555
3552
|
: sourceContent;
|
|
3553
|
+
if (templateContextReplacements) {
|
|
3554
|
+
renderedContent = applyTemplateContextReplacements(renderedContent, templateContextReplacements);
|
|
3555
|
+
}
|
|
3556
3556
|
|
|
3557
3557
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
3558
3558
|
await writeFile(targetPath, renderedContent, "utf8");
|
|
3559
3559
|
}
|
|
3560
3560
|
|
|
3561
|
+
async function resolveTemplateContextReplacementsForMutation({
|
|
3562
|
+
packageEntry,
|
|
3563
|
+
mutation,
|
|
3564
|
+
options,
|
|
3565
|
+
appRoot,
|
|
3566
|
+
sourcePath,
|
|
3567
|
+
targetPaths
|
|
3568
|
+
} = {}) {
|
|
3569
|
+
const templateContext = ensureObject(mutation?.templateContext);
|
|
3570
|
+
const hasTemplateContext = Object.keys(templateContext).length > 0;
|
|
3571
|
+
const entrypoint = String(templateContext.entrypoint || "").trim();
|
|
3572
|
+
if (!hasTemplateContext) {
|
|
3573
|
+
return null;
|
|
3574
|
+
}
|
|
3575
|
+
if (!entrypoint) {
|
|
3576
|
+
throw createCliError(
|
|
3577
|
+
`Invalid files mutation in ${packageEntry.packageId}: templateContext.entrypoint is required when templateContext is set.`
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
const exportName = String(templateContext.export || "").trim() || "buildTemplateContext";
|
|
3581
|
+
const resolvedEntrypointPath = resolveAppRelativePathWithinRoot(
|
|
3582
|
+
packageEntry.rootDir,
|
|
3583
|
+
entrypoint,
|
|
3584
|
+
`${packageEntry.packageId} files mutation templateContext.entrypoint`
|
|
3585
|
+
);
|
|
3586
|
+
const absoluteEntrypointPath = resolvedEntrypointPath.absolutePath;
|
|
3587
|
+
if (!(await fileExists(absoluteEntrypointPath))) {
|
|
3588
|
+
throw createCliError(
|
|
3589
|
+
`Invalid files mutation in ${packageEntry.packageId}: templateContext.entrypoint not found at ${entrypoint}.`
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
let moduleNamespace = null;
|
|
3594
|
+
try {
|
|
3595
|
+
moduleNamespace = await import(`${pathToFileURL(absoluteEntrypointPath).href}?t=${Date.now()}_${Math.random()}`);
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
throw createCliError(
|
|
3598
|
+
`Unable to load templateContext entrypoint ${entrypoint} for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
|
|
3599
|
+
);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
const resolver = moduleNamespace?.[exportName];
|
|
3603
|
+
if (typeof resolver !== "function") {
|
|
3604
|
+
throw createCliError(
|
|
3605
|
+
`Invalid files mutation in ${packageEntry.packageId}: templateContext export "${exportName}" is not a function.`
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
let replacements = null;
|
|
3610
|
+
try {
|
|
3611
|
+
replacements = await resolver({
|
|
3612
|
+
packageId: packageEntry.packageId,
|
|
3613
|
+
packageRoot: packageEntry.rootDir,
|
|
3614
|
+
appRoot,
|
|
3615
|
+
options: Object.freeze({ ...ensureObject(options) }),
|
|
3616
|
+
mutation: Object.freeze({ ...ensureObject(mutation) }),
|
|
3617
|
+
sourcePath,
|
|
3618
|
+
targetPaths: Object.freeze([...ensureArray(targetPaths)])
|
|
3619
|
+
});
|
|
3620
|
+
} catch (error) {
|
|
3621
|
+
throw createCliError(
|
|
3622
|
+
`templateContext export "${exportName}" failed for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
|
|
3623
|
+
);
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
if (replacements == null) {
|
|
3627
|
+
return null;
|
|
3628
|
+
}
|
|
3629
|
+
if (!replacements || typeof replacements !== "object" || Array.isArray(replacements)) {
|
|
3630
|
+
throw createCliError(
|
|
3631
|
+
`Invalid files mutation in ${packageEntry.packageId}: templateContext export "${exportName}" must return an object map of placeholder replacements.`
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
const normalizedReplacements = {};
|
|
3636
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
3637
|
+
const normalizedPlaceholder = String(placeholder || "").trim();
|
|
3638
|
+
if (!normalizedPlaceholder) {
|
|
3639
|
+
continue;
|
|
3640
|
+
}
|
|
3641
|
+
normalizedReplacements[normalizedPlaceholder] = String(value == null ? "" : value);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
return Object.freeze(normalizedReplacements);
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3561
3647
|
function normalizeSurfaceIdForMutation(value = "") {
|
|
3562
3648
|
return String(value || "")
|
|
3563
3649
|
.trim()
|
|
@@ -3738,8 +3824,10 @@ async function applyFileMutations(
|
|
|
3738
3824
|
managedFiles,
|
|
3739
3825
|
managedMigrations,
|
|
3740
3826
|
touchedFiles,
|
|
3741
|
-
warnings = []
|
|
3827
|
+
warnings = [],
|
|
3828
|
+
precomputedTemplateContextByMutationIndex = null
|
|
3742
3829
|
) {
|
|
3830
|
+
const mutationList = ensureArray(fileMutations);
|
|
3743
3831
|
const managedMigrationById = new Map();
|
|
3744
3832
|
for (const managedMigrationValue of ensureArray(managedMigrations)) {
|
|
3745
3833
|
const managedMigration = ensureObject(managedMigrationValue);
|
|
@@ -3750,7 +3838,7 @@ async function applyFileMutations(
|
|
|
3750
3838
|
managedMigrationById.set(migrationId, managedMigration);
|
|
3751
3839
|
}
|
|
3752
3840
|
|
|
3753
|
-
for (const mutationValue of
|
|
3841
|
+
for (const [mutationIndex, mutationValue] of mutationList.entries()) {
|
|
3754
3842
|
const normalizedMutation = normalizeFileMutationRecord(mutationValue);
|
|
3755
3843
|
const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
|
|
3756
3844
|
const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
|
|
@@ -3788,10 +3876,27 @@ async function applyFileMutations(
|
|
|
3788
3876
|
throw createCliError(`Missing migration template source ${sourcePath} for ${packageEntry.packageId}.`);
|
|
3789
3877
|
}
|
|
3790
3878
|
|
|
3879
|
+
const hasPrecomputedTemplateContext =
|
|
3880
|
+
precomputedTemplateContextByMutationIndex instanceof Map &&
|
|
3881
|
+
precomputedTemplateContextByMutationIndex.has(mutationIndex);
|
|
3882
|
+
const templateContextReplacements = hasPrecomputedTemplateContext
|
|
3883
|
+
? precomputedTemplateContextByMutationIndex.get(mutationIndex)
|
|
3884
|
+
: await resolveTemplateContextReplacementsForMutation({
|
|
3885
|
+
packageEntry,
|
|
3886
|
+
mutation,
|
|
3887
|
+
options,
|
|
3888
|
+
appRoot,
|
|
3889
|
+
sourcePath,
|
|
3890
|
+
targetPaths: [path.join(appRoot, toDir)]
|
|
3891
|
+
});
|
|
3892
|
+
|
|
3791
3893
|
const sourceContent = await readFile(sourcePath, "utf8");
|
|
3792
|
-
|
|
3894
|
+
let renderedSourceContent = sourceContent.includes("${")
|
|
3793
3895
|
? interpolateOptionValue(sourceContent, options, packageEntry.packageId, `${mutation.id || from}.source`)
|
|
3794
3896
|
: sourceContent;
|
|
3897
|
+
if (templateContextReplacements) {
|
|
3898
|
+
renderedSourceContent = applyTemplateContextReplacements(renderedSourceContent, templateContextReplacements);
|
|
3899
|
+
}
|
|
3795
3900
|
const sourceExtension = normalizeMigrationExtension(path.extname(from), ".cjs");
|
|
3796
3901
|
const extension = normalizeMigrationExtension(mutation.extension, sourceExtension);
|
|
3797
3902
|
const sourceHash = hashBuffer(Buffer.from(renderedSourceContent, "utf8"));
|
|
@@ -3956,6 +4061,19 @@ async function applyFileMutations(
|
|
|
3956
4061
|
configContext
|
|
3957
4062
|
})
|
|
3958
4063
|
: [path.join(appRoot, to)];
|
|
4064
|
+
const hasPrecomputedTemplateContext =
|
|
4065
|
+
precomputedTemplateContextByMutationIndex instanceof Map &&
|
|
4066
|
+
precomputedTemplateContextByMutationIndex.has(mutationIndex);
|
|
4067
|
+
const templateContextReplacements = hasPrecomputedTemplateContext
|
|
4068
|
+
? precomputedTemplateContextByMutationIndex.get(mutationIndex)
|
|
4069
|
+
: await resolveTemplateContextReplacementsForMutation({
|
|
4070
|
+
packageEntry,
|
|
4071
|
+
mutation,
|
|
4072
|
+
options,
|
|
4073
|
+
appRoot,
|
|
4074
|
+
sourcePath,
|
|
4075
|
+
targetPaths
|
|
4076
|
+
});
|
|
3959
4077
|
for (const targetPath of targetPaths) {
|
|
3960
4078
|
const previous = await readFileBufferIfExists(targetPath);
|
|
3961
4079
|
await copyTemplateFile(
|
|
@@ -3963,7 +4081,8 @@ async function applyFileMutations(
|
|
|
3963
4081
|
targetPath,
|
|
3964
4082
|
options,
|
|
3965
4083
|
packageEntry.packageId,
|
|
3966
|
-
`${mutation.id || to || from}.source
|
|
4084
|
+
`${mutation.id || to || from}.source`,
|
|
4085
|
+
templateContextReplacements
|
|
3967
4086
|
);
|
|
3968
4087
|
const nextBuffer = await readFile(targetPath);
|
|
3969
4088
|
|
|
@@ -3982,6 +4101,91 @@ async function applyFileMutations(
|
|
|
3982
4101
|
}
|
|
3983
4102
|
}
|
|
3984
4103
|
|
|
4104
|
+
async function preflightFileMutationTemplateContexts(
|
|
4105
|
+
packageEntry,
|
|
4106
|
+
options,
|
|
4107
|
+
appRoot,
|
|
4108
|
+
fileMutations
|
|
4109
|
+
) {
|
|
4110
|
+
const mutationList = ensureArray(fileMutations);
|
|
4111
|
+
const replacementsByMutationIndex = new Map();
|
|
4112
|
+
|
|
4113
|
+
for (const [mutationIndex, mutationValue] of mutationList.entries()) {
|
|
4114
|
+
const normalizedMutation = normalizeFileMutationRecord(mutationValue);
|
|
4115
|
+
const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
|
|
4116
|
+
const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
|
|
4117
|
+
if (
|
|
4118
|
+
!shouldApplyMutationWhen(normalizedMutation.when, {
|
|
4119
|
+
options,
|
|
4120
|
+
configContext,
|
|
4121
|
+
packageId: packageEntry.packageId,
|
|
4122
|
+
mutationContext: "files mutation"
|
|
4123
|
+
})
|
|
4124
|
+
) {
|
|
4125
|
+
continue;
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
const mutation = interpolateFileMutationRecord(normalizedMutation, options, packageEntry.packageId);
|
|
4129
|
+
const templateContext = ensureObject(mutation.templateContext);
|
|
4130
|
+
if (Object.keys(templateContext).length < 1) {
|
|
4131
|
+
continue;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
const operation = mutation.op || "copy-file";
|
|
4135
|
+
if (operation !== "copy-file" && operation !== "install-migration") {
|
|
4136
|
+
continue;
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
const from = mutation.from;
|
|
4140
|
+
const to = mutation.to;
|
|
4141
|
+
const toSurface = mutation.toSurface;
|
|
4142
|
+
if (!from) {
|
|
4143
|
+
throw createCliError(
|
|
4144
|
+
`Invalid files mutation in ${packageEntry.packageId}: "from" is required.`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
if (operation === "copy-file") {
|
|
4148
|
+
if (to && toSurface) {
|
|
4149
|
+
throw createCliError(
|
|
4150
|
+
`Invalid files mutation in ${packageEntry.packageId}: "to" and "toSurface" cannot both be set.`
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
4153
|
+
if (!to && !toSurface) {
|
|
4154
|
+
throw createCliError(
|
|
4155
|
+
`Invalid files mutation in ${packageEntry.packageId}: "from" plus one destination ("to" or "toSurface") are required.`
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
const sourcePath = path.join(packageEntry.rootDir, from);
|
|
4161
|
+
if (!(await fileExists(sourcePath))) {
|
|
4162
|
+
throw createCliError(`Missing template source ${sourcePath} for ${packageEntry.packageId}.`);
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
const targetPaths = operation === "copy-file"
|
|
4166
|
+
? toSurface
|
|
4167
|
+
? resolveSurfaceTargetPathsForMutation({
|
|
4168
|
+
appRoot,
|
|
4169
|
+
packageId: packageEntry.packageId,
|
|
4170
|
+
mutation,
|
|
4171
|
+
configContext
|
|
4172
|
+
})
|
|
4173
|
+
: [path.join(appRoot, to)]
|
|
4174
|
+
: [path.join(appRoot, mutation.toDir || "migrations")];
|
|
4175
|
+
const replacements = await resolveTemplateContextReplacementsForMutation({
|
|
4176
|
+
packageEntry,
|
|
4177
|
+
mutation,
|
|
4178
|
+
options,
|
|
4179
|
+
appRoot,
|
|
4180
|
+
sourcePath,
|
|
4181
|
+
targetPaths
|
|
4182
|
+
});
|
|
4183
|
+
replacementsByMutationIndex.set(mutationIndex, replacements);
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
return replacementsByMutationIndex;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
3985
4189
|
async function applyTextMutations(packageEntry, appRoot, textMutations, options, managedText, touchedFiles) {
|
|
3986
4190
|
for (const mutation of textMutations) {
|
|
3987
4191
|
const when = normalizeMutationWhen(mutation?.when);
|
|
@@ -4298,6 +4502,79 @@ async function applyPackagePositioning({
|
|
|
4298
4502
|
return managedRecord;
|
|
4299
4503
|
}
|
|
4300
4504
|
|
|
4505
|
+
async function applyPackageMigrationsOnly({
|
|
4506
|
+
packageEntry,
|
|
4507
|
+
packageOptions,
|
|
4508
|
+
appRoot,
|
|
4509
|
+
lock,
|
|
4510
|
+
touchedFiles
|
|
4511
|
+
}) {
|
|
4512
|
+
const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
|
|
4513
|
+
if (Object.keys(existingInstall).length < 1) {
|
|
4514
|
+
throw createCliError(`Package is not installed: ${packageEntry.packageId}`);
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
const existingManaged = ensureObject(existingInstall.managed);
|
|
4518
|
+
const existingPackageJsonManaged = ensureObject(existingManaged.packageJson);
|
|
4519
|
+
const nextManaged = {
|
|
4520
|
+
packageJson: {
|
|
4521
|
+
dependencies: cloneManagedMap(existingPackageJsonManaged.dependencies),
|
|
4522
|
+
devDependencies: cloneManagedMap(existingPackageJsonManaged.devDependencies),
|
|
4523
|
+
scripts: cloneManagedMap(existingPackageJsonManaged.scripts)
|
|
4524
|
+
},
|
|
4525
|
+
text: cloneManagedMap(existingManaged.text),
|
|
4526
|
+
vite: cloneManagedMap(existingManaged.vite),
|
|
4527
|
+
files: cloneManagedArray(existingManaged.files),
|
|
4528
|
+
migrations: cloneManagedArray(existingManaged.migrations)
|
|
4529
|
+
};
|
|
4530
|
+
|
|
4531
|
+
const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
|
|
4532
|
+
const packageEntryForMutations =
|
|
4533
|
+
templateRoot === packageEntry.rootDir
|
|
4534
|
+
? packageEntry
|
|
4535
|
+
: {
|
|
4536
|
+
...packageEntry,
|
|
4537
|
+
rootDir: templateRoot
|
|
4538
|
+
};
|
|
4539
|
+
const mutations = ensureObject(packageEntry.descriptor.mutations);
|
|
4540
|
+
const migrationFileMutations = ensureArray(mutations.files).filter((mutationValue) => {
|
|
4541
|
+
const normalized = normalizeFileMutationRecord(mutationValue);
|
|
4542
|
+
const operation = String(normalized.op || "copy-file").trim();
|
|
4543
|
+
return operation === "install-migration";
|
|
4544
|
+
});
|
|
4545
|
+
const mutationWarnings = [];
|
|
4546
|
+
|
|
4547
|
+
if (migrationFileMutations.length > 0) {
|
|
4548
|
+
await applyFileMutations(
|
|
4549
|
+
packageEntryForMutations,
|
|
4550
|
+
packageOptions,
|
|
4551
|
+
appRoot,
|
|
4552
|
+
migrationFileMutations,
|
|
4553
|
+
[],
|
|
4554
|
+
nextManaged.migrations,
|
|
4555
|
+
touchedFiles,
|
|
4556
|
+
mutationWarnings
|
|
4557
|
+
);
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
const managedRecord = {
|
|
4561
|
+
...existingInstall,
|
|
4562
|
+
packageId: packageEntry.packageId,
|
|
4563
|
+
source: resolveManagedSourceRecord(packageEntry, existingInstall),
|
|
4564
|
+
managed: nextManaged,
|
|
4565
|
+
options: {
|
|
4566
|
+
...ensureObject(packageOptions)
|
|
4567
|
+
},
|
|
4568
|
+
migrationSyncVersion: packageEntry.version,
|
|
4569
|
+
installedAt: String(existingInstall.installedAt || new Date().toISOString())
|
|
4570
|
+
};
|
|
4571
|
+
lock.installedPackages[packageEntry.packageId] = managedRecord;
|
|
4572
|
+
if (mutationWarnings.length > 0) {
|
|
4573
|
+
managedRecord.warnings = mutationWarnings;
|
|
4574
|
+
}
|
|
4575
|
+
return managedRecord;
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4301
4578
|
async function applyPackageInstall({
|
|
4302
4579
|
packageEntry,
|
|
4303
4580
|
packageOptions,
|
|
@@ -4318,9 +4595,10 @@ async function applyPackageInstall({
|
|
|
4318
4595
|
|
|
4319
4596
|
const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
|
|
4320
4597
|
managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
|
|
4321
|
-
const
|
|
4598
|
+
const generatorPackage = isGeneratorPackageEntry(packageEntry);
|
|
4322
4599
|
const mutationWarnings = [];
|
|
4323
4600
|
const mutations = ensureObject(packageEntry.descriptor.mutations);
|
|
4601
|
+
const fileMutations = ensureArray(mutations.files);
|
|
4324
4602
|
const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
|
|
4325
4603
|
const packageEntryForMutations =
|
|
4326
4604
|
templateRoot === packageEntry.rootDir
|
|
@@ -4329,6 +4607,14 @@ async function applyPackageInstall({
|
|
|
4329
4607
|
...packageEntry,
|
|
4330
4608
|
rootDir: templateRoot
|
|
4331
4609
|
};
|
|
4610
|
+
|
|
4611
|
+
const precomputedTemplateContextByMutationIndex = await preflightFileMutationTemplateContexts(
|
|
4612
|
+
packageEntryForMutations,
|
|
4613
|
+
packageOptions,
|
|
4614
|
+
appRoot,
|
|
4615
|
+
fileMutations
|
|
4616
|
+
);
|
|
4617
|
+
|
|
4332
4618
|
const mutationDependencies = ensureObject(mutations.dependencies);
|
|
4333
4619
|
const runtimeDependencies = ensureObject(mutationDependencies.runtime);
|
|
4334
4620
|
const devDependencies = ensureObject(mutationDependencies.dev);
|
|
@@ -4398,7 +4684,7 @@ async function applyPackageInstall({
|
|
|
4398
4684
|
}
|
|
4399
4685
|
}
|
|
4400
4686
|
|
|
4401
|
-
if (
|
|
4687
|
+
if (generatorPackage) {
|
|
4402
4688
|
const removedRuntimeDependency = removePackageJsonField(appPackageJson, "dependencies", packageEntry.packageId);
|
|
4403
4689
|
const removedDevDependency = removePackageJsonField(appPackageJson, "devDependencies", packageEntry.packageId);
|
|
4404
4690
|
if (removedRuntimeDependency || removedDevDependency) {
|
|
@@ -4434,11 +4720,12 @@ async function applyPackageInstall({
|
|
|
4434
4720
|
packageEntryForMutations,
|
|
4435
4721
|
packageOptions,
|
|
4436
4722
|
appRoot,
|
|
4437
|
-
|
|
4723
|
+
fileMutations,
|
|
4438
4724
|
managedRecord.managed.files,
|
|
4439
4725
|
managedRecord.managed.migrations,
|
|
4440
4726
|
touchedFiles,
|
|
4441
|
-
mutationWarnings
|
|
4727
|
+
mutationWarnings,
|
|
4728
|
+
precomputedTemplateContextByMutationIndex
|
|
4442
4729
|
);
|
|
4443
4730
|
|
|
4444
4731
|
await applyTextMutations(
|
|
@@ -4459,9 +4746,10 @@ async function applyPackageInstall({
|
|
|
4459
4746
|
touchedFiles
|
|
4460
4747
|
);
|
|
4461
4748
|
|
|
4462
|
-
if (
|
|
4749
|
+
if (generatorPackage) {
|
|
4463
4750
|
delete lock.installedPackages[packageEntry.packageId];
|
|
4464
4751
|
} else {
|
|
4752
|
+
managedRecord.migrationSyncVersion = packageEntry.version;
|
|
4465
4753
|
lock.installedPackages[packageEntry.packageId] = managedRecord;
|
|
4466
4754
|
}
|
|
4467
4755
|
if (mutationWarnings.length > 0) {
|
|
@@ -4522,6 +4810,7 @@ const commandHandlers = createCommandHandlers(
|
|
|
4522
4810
|
validatePlannedCapabilityClosure,
|
|
4523
4811
|
resolvePackageOptions,
|
|
4524
4812
|
applyPackageInstall,
|
|
4813
|
+
applyPackageMigrationsOnly,
|
|
4525
4814
|
applyPackagePositioning,
|
|
4526
4815
|
adoptAppLocalPackageDependencies,
|
|
4527
4816
|
loadAppPackageJson,
|