@jskit-ai/jskit-cli 0.2.25 → 0.2.27

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.
@@ -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,13 +53,15 @@ 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 PACKAGE_INSTALL_MODE_INSTALLABLE = "installable";
59
- const PACKAGE_INSTALL_MODE_CLONE_ONLY = "clone-only";
60
- const PACKAGE_INSTALL_MODES = Object.freeze([PACKAGE_INSTALL_MODE_INSTALLABLE, PACKAGE_INSTALL_MODE_CLONE_ONLY]);
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();
64
62
  const MATERIALIZED_PACKAGE_TEMP_DIRECTORIES = new Set();
63
+ const LOCAL_WORKSPACE_PACKAGE_ROOTS = new Map();
64
+ let LOCAL_WORKSPACE_PACKAGE_ID_INDEX = null;
65
65
  const BUILTIN_CAPABILITY_PROVIDERS = Object.freeze({
66
66
  "runtime.actions": Object.freeze(["@jskit-ai/kernel"])
67
67
  });
@@ -95,6 +95,20 @@ function normalizeMutationExtension(value) {
95
95
  return `.${extension}`;
96
96
  }
97
97
 
98
+ function normalizeTemplateContextRecord(value) {
99
+ const record = ensureObject(value);
100
+ const entrypoint = String(record.entrypoint || "").trim();
101
+ const exportName = String(record.export || "").trim();
102
+ if (!entrypoint && !exportName) {
103
+ return null;
104
+ }
105
+
106
+ return Object.freeze({
107
+ entrypoint,
108
+ export: exportName || "buildTemplateContext"
109
+ });
110
+ }
111
+
98
112
  function normalizeFileMutationRecord(value) {
99
113
  const record = ensureObject(value);
100
114
  const op = String(record.op || "copy-file").trim().toLowerCase() || "copy-file";
@@ -111,6 +125,7 @@ function normalizeFileMutationRecord(value) {
111
125
  id: String(record.id || "").trim(),
112
126
  category: String(record.category || "").trim(),
113
127
  reason: String(record.reason || "").trim(),
128
+ templateContext: normalizeTemplateContextRecord(record.templateContext),
114
129
  when: normalizeMutationWhen(record.when)
115
130
  };
116
131
  }
@@ -184,6 +199,44 @@ function normalizeWhenSourceValue(value) {
184
199
  return "";
185
200
  }
186
201
 
202
+ function normalizeWhenComparisonValue(value) {
203
+ const normalizedValue = normalizeWhenSourceValue(value);
204
+ if (!normalizedValue.includes(",")) {
205
+ return normalizedValue;
206
+ }
207
+
208
+ return normalizedValue
209
+ .split(",")
210
+ .map((entry) => String(entry || "").trim())
211
+ .filter(Boolean)
212
+ .join(",");
213
+ }
214
+
215
+ function splitWhenComparisonTokens(value) {
216
+ return normalizeWhenComparisonValue(value)
217
+ .split(",")
218
+ .map((entry) => String(entry || "").trim())
219
+ .filter(Boolean);
220
+ }
221
+
222
+ function matchesWhenComparisonValue(optionValue, optionTokens, expectedValue) {
223
+ const expected = normalizeWhenComparisonValue(expectedValue);
224
+ if (!expected) {
225
+ return false;
226
+ }
227
+
228
+ const expectedTokens = splitWhenComparisonTokens(expected);
229
+ if (expectedTokens.length > 1) {
230
+ return optionValue === expected;
231
+ }
232
+
233
+ if (optionTokens.length > 1) {
234
+ return optionTokens.includes(expectedTokens[0]);
235
+ }
236
+
237
+ return optionValue === expectedTokens[0];
238
+ }
239
+
187
240
  function resolveWhenConfigValue(configContext = {}, configPath = "") {
188
241
  const normalizedPath = String(configPath || "").trim();
189
242
  if (!normalizedPath) {
@@ -244,11 +297,12 @@ function shouldApplyMutationWhen(
244
297
  const sourceValue = optionName
245
298
  ? readObjectPath(options, optionName)
246
299
  : resolveWhenConfigValue(configContext, configPath);
247
- const optionValue = normalizeWhenSourceValue(sourceValue);
248
- const equals = String(when.equals || "").trim();
249
- const notEquals = String(when.notEquals || "").trim();
250
- const includes = ensureArray(when.includes).map((entry) => String(entry || "").trim()).filter(Boolean);
251
- const excludes = ensureArray(when.excludes).map((entry) => String(entry || "").trim()).filter(Boolean);
300
+ const optionValue = normalizeWhenComparisonValue(sourceValue);
301
+ const optionTokens = splitWhenComparisonTokens(optionValue);
302
+ const equals = normalizeWhenComparisonValue(when.equals);
303
+ const notEquals = normalizeWhenComparisonValue(when.notEquals);
304
+ const includes = ensureArray(when.includes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
305
+ const excludes = ensureArray(when.excludes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
252
306
 
253
307
  if (equals && optionValue !== equals) {
254
308
  return false;
@@ -256,10 +310,10 @@ function shouldApplyMutationWhen(
256
310
  if (notEquals && optionValue === notEquals) {
257
311
  return false;
258
312
  }
259
- if (includes.length > 0 && !includes.includes(optionValue)) {
313
+ if (includes.length > 0 && !includes.some((entry) => matchesWhenComparisonValue(optionValue, optionTokens, entry))) {
260
314
  return false;
261
315
  }
262
- if (excludes.length > 0 && excludes.includes(optionValue)) {
316
+ if (excludes.length > 0 && excludes.some((entry) => matchesWhenComparisonValue(optionValue, optionTokens, entry))) {
263
317
  return false;
264
318
  }
265
319
 
@@ -1113,16 +1167,16 @@ function removeEnvValue(content, key, expectedValue, previous) {
1113
1167
  };
1114
1168
  }
1115
1169
 
1116
- function normalizePackageInstallationMode(rawValue, descriptorPath) {
1117
- const normalized = String(rawValue || "")
1118
- .trim()
1119
- .toLowerCase();
1170
+ function normalizePackageKind(rawValue, descriptorPath) {
1171
+ const normalized = String(rawValue || "").trim().toLowerCase();
1120
1172
  if (!normalized) {
1121
- return PACKAGE_INSTALL_MODE_INSTALLABLE;
1173
+ throw createCliError(
1174
+ `Invalid package descriptor at ${descriptorPath}: missing kind (expected ${PACKAGE_KINDS.join(" | ")}).`
1175
+ );
1122
1176
  }
1123
- if (!PACKAGE_INSTALL_MODES.includes(normalized)) {
1177
+ if (!PACKAGE_KINDS.includes(normalized)) {
1124
1178
  throw createCliError(
1125
- `Invalid package descriptor at ${descriptorPath}: installationMode must be one of: ${PACKAGE_INSTALL_MODES.join(", ")}.`
1179
+ `Invalid package descriptor at ${descriptorPath}: kind must be one of: ${PACKAGE_KINDS.join(", ")}.`
1126
1180
  );
1127
1181
  }
1128
1182
  return normalized;
@@ -1177,13 +1231,13 @@ function validatePackageDescriptorShape(descriptor, descriptorPath) {
1177
1231
 
1178
1232
  return {
1179
1233
  ...normalized,
1180
- installationMode: normalizePackageInstallationMode(normalized.installationMode, descriptorPath)
1234
+ kind: normalizePackageKind(normalized.kind, descriptorPath)
1181
1235
  };
1182
1236
  }
1183
1237
 
1184
- function isCloneOnlyPackageEntry(packageEntry) {
1238
+ function isGeneratorPackageEntry(packageEntry) {
1185
1239
  const descriptor = ensureObject(packageEntry?.descriptor);
1186
- return String(descriptor.installationMode || "").trim().toLowerCase() === PACKAGE_INSTALL_MODE_CLONE_ONLY;
1240
+ return String(descriptor.kind || "").trim().toLowerCase() === PACKAGE_KIND_GENERATOR;
1187
1241
  }
1188
1242
 
1189
1243
  function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { expectedPackageId = "", fallbackVersion = "" } = {}) {
@@ -1208,7 +1262,8 @@ function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { ex
1208
1262
  return {
1209
1263
  ...normalized,
1210
1264
  packageId,
1211
- version
1265
+ version,
1266
+ kind: normalizePackageKind(normalized.kind, descriptorPath)
1212
1267
  };
1213
1268
  }
1214
1269
 
@@ -1277,85 +1332,6 @@ function validateBundleDescriptorShape(descriptor, descriptorPath) {
1277
1332
  return normalized;
1278
1333
  }
1279
1334
 
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
1335
  async function loadAppLocalPackageRegistry(appRoot) {
1360
1336
  const localPackagesRoot = path.join(appRoot, "packages");
1361
1337
  if (!(await fileExists(localPackagesRoot))) {
@@ -1468,17 +1444,14 @@ async function loadCatalogPackageRegistry() {
1468
1444
  }
1469
1445
 
1470
1446
  async function loadPackageRegistry() {
1471
- const workspaceRegistry = await loadWorkspacePackageRegistry();
1472
1447
  const catalogRegistry = await loadCatalogPackageRegistry();
1473
- const merged = mergePackageRegistries(catalogRegistry, workspaceRegistry);
1474
-
1475
- if (merged.size === 0) {
1448
+ if (catalogRegistry.size === 0) {
1476
1449
  throw createCliError(
1477
- "Unable to load package registry. Provide JSKIT_REPO_ROOT for workspace mode or ensure @jskit-ai/jskit-catalog is installed (or set JSKIT_CATALOG_PACKAGES_PATH)."
1450
+ "Unable to load package registry from @jskit-ai/jskit-catalog. Install it alongside @jskit-ai/jskit-cli or set JSKIT_CATALOG_PACKAGES_PATH."
1478
1451
  );
1479
1452
  }
1480
1453
 
1481
- return merged;
1454
+ return catalogRegistry;
1482
1455
  }
1483
1456
 
1484
1457
  async function loadInstalledNodeModulePackageEntry({ appRoot, packageId }) {
@@ -1745,6 +1718,7 @@ function createLocalPackageDescriptorTemplate({ packageId, description }) {
1745
1718
  packageVersion: 1,
1746
1719
  packageId: "${packageId}",
1747
1720
  version: "0.1.0",
1721
+ kind: "runtime",
1748
1722
  description: ${JSON.stringify(String(description || ""))},
1749
1723
  dependsOn: [
1750
1724
  // "@jskit-ai/kernel"
@@ -3511,11 +3485,110 @@ async function materializePackageRootFromRegistry({ packageEntry, appRoot }) {
3511
3485
  return packageRoot;
3512
3486
  }
3513
3487
 
3488
+ async function resolvePackageRootFromNodeModules({ appRoot, packageId }) {
3489
+ const normalizedAppRoot = path.resolve(String(appRoot || "").trim());
3490
+ const normalizedPackageId = String(packageId || "").trim();
3491
+ if (!normalizedAppRoot || !normalizedPackageId) {
3492
+ return "";
3493
+ }
3494
+
3495
+ const candidateRoot = path.resolve(normalizedAppRoot, "node_modules", ...normalizedPackageId.split("/"));
3496
+ const candidateDescriptorPath = path.join(candidateRoot, "package.descriptor.mjs");
3497
+ if (!(await fileExists(candidateDescriptorPath))) {
3498
+ return "";
3499
+ }
3500
+
3501
+ return candidateRoot;
3502
+ }
3503
+
3504
+ async function loadLocalWorkspacePackageIdIndex() {
3505
+ if (LOCAL_WORKSPACE_PACKAGE_ID_INDEX instanceof Map) {
3506
+ return LOCAL_WORKSPACE_PACKAGE_ID_INDEX;
3507
+ }
3508
+
3509
+ const repoRoot = path.resolve(CLI_PACKAGE_ROOT, "..", "..");
3510
+ const parentDirectories = [
3511
+ path.join(repoRoot, "packages"),
3512
+ path.join(repoRoot, "tooling")
3513
+ ];
3514
+ const packageIdIndex = new Map();
3515
+
3516
+ for (const parentDirectory of parentDirectories) {
3517
+ if (!(await fileExists(parentDirectory))) {
3518
+ continue;
3519
+ }
3520
+
3521
+ const entries = await readdir(parentDirectory, { withFileTypes: true });
3522
+ for (const entry of entries) {
3523
+ if (!entry.isDirectory() || entry.name.startsWith(".")) {
3524
+ continue;
3525
+ }
3526
+
3527
+ const candidateRoot = path.join(parentDirectory, entry.name);
3528
+ const packageJsonPath = path.join(candidateRoot, "package.json");
3529
+ const descriptorPath = path.join(candidateRoot, "package.descriptor.mjs");
3530
+ if (!(await fileExists(packageJsonPath)) || !(await fileExists(descriptorPath))) {
3531
+ continue;
3532
+ }
3533
+
3534
+ let packageJson = {};
3535
+ try {
3536
+ packageJson = await readJsonFile(packageJsonPath);
3537
+ } catch {
3538
+ continue;
3539
+ }
3540
+
3541
+ const packageId = String(packageJson?.name || "").trim();
3542
+ if (!packageId.startsWith("@jskit-ai/")) {
3543
+ continue;
3544
+ }
3545
+ if (packageIdIndex.has(packageId)) {
3546
+ continue;
3547
+ }
3548
+ packageIdIndex.set(packageId, candidateRoot);
3549
+ }
3550
+ }
3551
+
3552
+ LOCAL_WORKSPACE_PACKAGE_ID_INDEX = packageIdIndex;
3553
+ return packageIdIndex;
3554
+ }
3555
+
3556
+ async function resolvePackageRootFromLocalWorkspace({ packageId }) {
3557
+ const normalizedPackageId = String(packageId || "").trim();
3558
+ if (!normalizedPackageId.startsWith("@jskit-ai/")) {
3559
+ return "";
3560
+ }
3561
+ if (LOCAL_WORKSPACE_PACKAGE_ROOTS.has(normalizedPackageId)) {
3562
+ return LOCAL_WORKSPACE_PACKAGE_ROOTS.get(normalizedPackageId);
3563
+ }
3564
+
3565
+ const packageIdIndex = await loadLocalWorkspacePackageIdIndex();
3566
+ const packageRoot = String(packageIdIndex.get(normalizedPackageId) || "").trim();
3567
+ LOCAL_WORKSPACE_PACKAGE_ROOTS.set(normalizedPackageId, packageRoot);
3568
+ return packageRoot;
3569
+ }
3570
+
3514
3571
  async function resolvePackageTemplateRoot({ packageEntry, appRoot }) {
3515
3572
  const packageRoot = String(packageEntry?.rootDir || "").trim();
3516
3573
  if (packageRoot) {
3517
3574
  return packageRoot;
3518
3575
  }
3576
+
3577
+ const installedPackageRoot = await resolvePackageRootFromNodeModules({
3578
+ appRoot,
3579
+ packageId: packageEntry?.packageId
3580
+ });
3581
+ if (installedPackageRoot) {
3582
+ return installedPackageRoot;
3583
+ }
3584
+
3585
+ const localWorkspacePackageRoot = await resolvePackageRootFromLocalWorkspace({
3586
+ packageId: packageEntry?.packageId
3587
+ });
3588
+ if (localWorkspacePackageRoot) {
3589
+ return localWorkspacePackageRoot;
3590
+ }
3591
+
3519
3592
  return await materializePackageRootFromRegistry({ packageEntry, appRoot });
3520
3593
  }
3521
3594
 
@@ -3525,6 +3598,8 @@ async function cleanupMaterializedPackageRoots() {
3525
3598
  }
3526
3599
  MATERIALIZED_PACKAGE_TEMP_DIRECTORIES.clear();
3527
3600
  MATERIALIZED_PACKAGE_ROOTS.clear();
3601
+ LOCAL_WORKSPACE_PACKAGE_ROOTS.clear();
3602
+ LOCAL_WORKSPACE_PACKAGE_ID_INDEX = null;
3528
3603
  }
3529
3604
 
3530
3605
  function interpolateFileMutationRecord(mutation, options, packageId) {
@@ -3544,20 +3619,134 @@ function interpolateFileMutationRecord(mutation, options, packageId) {
3544
3619
  extension: interpolate(mutation.extension, "extension"),
3545
3620
  id: interpolate(mutation.id, "id"),
3546
3621
  category: interpolate(mutation.category, "category"),
3547
- reason: interpolate(mutation.reason, "reason")
3622
+ reason: interpolate(mutation.reason, "reason"),
3623
+ templateContext: mutation.templateContext
3624
+ ? {
3625
+ entrypoint: interpolate(mutation.templateContext.entrypoint, "templateContext.entrypoint"),
3626
+ export: interpolate(mutation.templateContext.export, "templateContext.export")
3627
+ }
3628
+ : null
3548
3629
  };
3549
3630
  }
3550
3631
 
3551
- async function copyTemplateFile(sourcePath, targetPath, options, packageId, interpolationKey) {
3632
+ function applyTemplateContextReplacements(sourceContent, replacements) {
3633
+ let output = String(sourceContent || "");
3634
+ for (const [placeholder, value] of Object.entries(ensureObject(replacements))) {
3635
+ const normalizedPlaceholder = String(placeholder || "");
3636
+ if (!normalizedPlaceholder) {
3637
+ continue;
3638
+ }
3639
+ output = output.split(normalizedPlaceholder).join(String(value == null ? "" : value));
3640
+ }
3641
+ return output;
3642
+ }
3643
+
3644
+ async function copyTemplateFile(
3645
+ sourcePath,
3646
+ targetPath,
3647
+ options,
3648
+ packageId,
3649
+ interpolationKey,
3650
+ templateContextReplacements = null
3651
+ ) {
3552
3652
  const sourceContent = await readFile(sourcePath, "utf8");
3553
- const renderedContent = sourceContent.includes("${")
3653
+ let renderedContent = sourceContent.includes("${")
3554
3654
  ? interpolateOptionValue(sourceContent, options, packageId, interpolationKey)
3555
3655
  : sourceContent;
3656
+ if (templateContextReplacements) {
3657
+ renderedContent = applyTemplateContextReplacements(renderedContent, templateContextReplacements);
3658
+ }
3556
3659
 
3557
3660
  await mkdir(path.dirname(targetPath), { recursive: true });
3558
3661
  await writeFile(targetPath, renderedContent, "utf8");
3559
3662
  }
3560
3663
 
3664
+ async function resolveTemplateContextReplacementsForMutation({
3665
+ packageEntry,
3666
+ mutation,
3667
+ options,
3668
+ appRoot,
3669
+ sourcePath,
3670
+ targetPaths
3671
+ } = {}) {
3672
+ const templateContext = ensureObject(mutation?.templateContext);
3673
+ const hasTemplateContext = Object.keys(templateContext).length > 0;
3674
+ const entrypoint = String(templateContext.entrypoint || "").trim();
3675
+ if (!hasTemplateContext) {
3676
+ return null;
3677
+ }
3678
+ if (!entrypoint) {
3679
+ throw createCliError(
3680
+ `Invalid files mutation in ${packageEntry.packageId}: templateContext.entrypoint is required when templateContext is set.`
3681
+ );
3682
+ }
3683
+ const exportName = String(templateContext.export || "").trim() || "buildTemplateContext";
3684
+ const resolvedEntrypointPath = resolveAppRelativePathWithinRoot(
3685
+ packageEntry.rootDir,
3686
+ entrypoint,
3687
+ `${packageEntry.packageId} files mutation templateContext.entrypoint`
3688
+ );
3689
+ const absoluteEntrypointPath = resolvedEntrypointPath.absolutePath;
3690
+ if (!(await fileExists(absoluteEntrypointPath))) {
3691
+ throw createCliError(
3692
+ `Invalid files mutation in ${packageEntry.packageId}: templateContext.entrypoint not found at ${entrypoint}.`
3693
+ );
3694
+ }
3695
+
3696
+ let moduleNamespace = null;
3697
+ try {
3698
+ moduleNamespace = await import(`${pathToFileURL(absoluteEntrypointPath).href}?t=${Date.now()}_${Math.random()}`);
3699
+ } catch (error) {
3700
+ throw createCliError(
3701
+ `Unable to load templateContext entrypoint ${entrypoint} for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
3702
+ );
3703
+ }
3704
+
3705
+ const resolver = moduleNamespace?.[exportName];
3706
+ if (typeof resolver !== "function") {
3707
+ throw createCliError(
3708
+ `Invalid files mutation in ${packageEntry.packageId}: templateContext export "${exportName}" is not a function.`
3709
+ );
3710
+ }
3711
+
3712
+ let replacements = null;
3713
+ try {
3714
+ replacements = await resolver({
3715
+ packageId: packageEntry.packageId,
3716
+ packageRoot: packageEntry.rootDir,
3717
+ appRoot,
3718
+ options: Object.freeze({ ...ensureObject(options) }),
3719
+ mutation: Object.freeze({ ...ensureObject(mutation) }),
3720
+ sourcePath,
3721
+ targetPaths: Object.freeze([...ensureArray(targetPaths)])
3722
+ });
3723
+ } catch (error) {
3724
+ throw createCliError(
3725
+ `templateContext export "${exportName}" failed for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
3726
+ );
3727
+ }
3728
+
3729
+ if (replacements == null) {
3730
+ return null;
3731
+ }
3732
+ if (!replacements || typeof replacements !== "object" || Array.isArray(replacements)) {
3733
+ throw createCliError(
3734
+ `Invalid files mutation in ${packageEntry.packageId}: templateContext export "${exportName}" must return an object map of placeholder replacements.`
3735
+ );
3736
+ }
3737
+
3738
+ const normalizedReplacements = {};
3739
+ for (const [placeholder, value] of Object.entries(replacements)) {
3740
+ const normalizedPlaceholder = String(placeholder || "").trim();
3741
+ if (!normalizedPlaceholder) {
3742
+ continue;
3743
+ }
3744
+ normalizedReplacements[normalizedPlaceholder] = String(value == null ? "" : value);
3745
+ }
3746
+
3747
+ return Object.freeze(normalizedReplacements);
3748
+ }
3749
+
3561
3750
  function normalizeSurfaceIdForMutation(value = "") {
3562
3751
  return String(value || "")
3563
3752
  .trim()
@@ -3738,8 +3927,10 @@ async function applyFileMutations(
3738
3927
  managedFiles,
3739
3928
  managedMigrations,
3740
3929
  touchedFiles,
3741
- warnings = []
3930
+ warnings = [],
3931
+ precomputedTemplateContextByMutationIndex = null
3742
3932
  ) {
3933
+ const mutationList = ensureArray(fileMutations);
3743
3934
  const managedMigrationById = new Map();
3744
3935
  for (const managedMigrationValue of ensureArray(managedMigrations)) {
3745
3936
  const managedMigration = ensureObject(managedMigrationValue);
@@ -3750,7 +3941,7 @@ async function applyFileMutations(
3750
3941
  managedMigrationById.set(migrationId, managedMigration);
3751
3942
  }
3752
3943
 
3753
- for (const mutationValue of fileMutations) {
3944
+ for (const [mutationIndex, mutationValue] of mutationList.entries()) {
3754
3945
  const normalizedMutation = normalizeFileMutationRecord(mutationValue);
3755
3946
  const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
3756
3947
  const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
@@ -3788,10 +3979,27 @@ async function applyFileMutations(
3788
3979
  throw createCliError(`Missing migration template source ${sourcePath} for ${packageEntry.packageId}.`);
3789
3980
  }
3790
3981
 
3982
+ const hasPrecomputedTemplateContext =
3983
+ precomputedTemplateContextByMutationIndex instanceof Map &&
3984
+ precomputedTemplateContextByMutationIndex.has(mutationIndex);
3985
+ const templateContextReplacements = hasPrecomputedTemplateContext
3986
+ ? precomputedTemplateContextByMutationIndex.get(mutationIndex)
3987
+ : await resolveTemplateContextReplacementsForMutation({
3988
+ packageEntry,
3989
+ mutation,
3990
+ options,
3991
+ appRoot,
3992
+ sourcePath,
3993
+ targetPaths: [path.join(appRoot, toDir)]
3994
+ });
3995
+
3791
3996
  const sourceContent = await readFile(sourcePath, "utf8");
3792
- const renderedSourceContent = sourceContent.includes("${")
3997
+ let renderedSourceContent = sourceContent.includes("${")
3793
3998
  ? interpolateOptionValue(sourceContent, options, packageEntry.packageId, `${mutation.id || from}.source`)
3794
3999
  : sourceContent;
4000
+ if (templateContextReplacements) {
4001
+ renderedSourceContent = applyTemplateContextReplacements(renderedSourceContent, templateContextReplacements);
4002
+ }
3795
4003
  const sourceExtension = normalizeMigrationExtension(path.extname(from), ".cjs");
3796
4004
  const extension = normalizeMigrationExtension(mutation.extension, sourceExtension);
3797
4005
  const sourceHash = hashBuffer(Buffer.from(renderedSourceContent, "utf8"));
@@ -3956,6 +4164,19 @@ async function applyFileMutations(
3956
4164
  configContext
3957
4165
  })
3958
4166
  : [path.join(appRoot, to)];
4167
+ const hasPrecomputedTemplateContext =
4168
+ precomputedTemplateContextByMutationIndex instanceof Map &&
4169
+ precomputedTemplateContextByMutationIndex.has(mutationIndex);
4170
+ const templateContextReplacements = hasPrecomputedTemplateContext
4171
+ ? precomputedTemplateContextByMutationIndex.get(mutationIndex)
4172
+ : await resolveTemplateContextReplacementsForMutation({
4173
+ packageEntry,
4174
+ mutation,
4175
+ options,
4176
+ appRoot,
4177
+ sourcePath,
4178
+ targetPaths
4179
+ });
3959
4180
  for (const targetPath of targetPaths) {
3960
4181
  const previous = await readFileBufferIfExists(targetPath);
3961
4182
  await copyTemplateFile(
@@ -3963,7 +4184,8 @@ async function applyFileMutations(
3963
4184
  targetPath,
3964
4185
  options,
3965
4186
  packageEntry.packageId,
3966
- `${mutation.id || to || from}.source`
4187
+ `${mutation.id || to || from}.source`,
4188
+ templateContextReplacements
3967
4189
  );
3968
4190
  const nextBuffer = await readFile(targetPath);
3969
4191
 
@@ -3982,6 +4204,91 @@ async function applyFileMutations(
3982
4204
  }
3983
4205
  }
3984
4206
 
4207
+ async function preflightFileMutationTemplateContexts(
4208
+ packageEntry,
4209
+ options,
4210
+ appRoot,
4211
+ fileMutations
4212
+ ) {
4213
+ const mutationList = ensureArray(fileMutations);
4214
+ const replacementsByMutationIndex = new Map();
4215
+
4216
+ for (const [mutationIndex, mutationValue] of mutationList.entries()) {
4217
+ const normalizedMutation = normalizeFileMutationRecord(mutationValue);
4218
+ const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
4219
+ const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
4220
+ if (
4221
+ !shouldApplyMutationWhen(normalizedMutation.when, {
4222
+ options,
4223
+ configContext,
4224
+ packageId: packageEntry.packageId,
4225
+ mutationContext: "files mutation"
4226
+ })
4227
+ ) {
4228
+ continue;
4229
+ }
4230
+
4231
+ const mutation = interpolateFileMutationRecord(normalizedMutation, options, packageEntry.packageId);
4232
+ const templateContext = ensureObject(mutation.templateContext);
4233
+ if (Object.keys(templateContext).length < 1) {
4234
+ continue;
4235
+ }
4236
+
4237
+ const operation = mutation.op || "copy-file";
4238
+ if (operation !== "copy-file" && operation !== "install-migration") {
4239
+ continue;
4240
+ }
4241
+
4242
+ const from = mutation.from;
4243
+ const to = mutation.to;
4244
+ const toSurface = mutation.toSurface;
4245
+ if (!from) {
4246
+ throw createCliError(
4247
+ `Invalid files mutation in ${packageEntry.packageId}: "from" is required.`
4248
+ );
4249
+ }
4250
+ if (operation === "copy-file") {
4251
+ if (to && toSurface) {
4252
+ throw createCliError(
4253
+ `Invalid files mutation in ${packageEntry.packageId}: "to" and "toSurface" cannot both be set.`
4254
+ );
4255
+ }
4256
+ if (!to && !toSurface) {
4257
+ throw createCliError(
4258
+ `Invalid files mutation in ${packageEntry.packageId}: "from" plus one destination ("to" or "toSurface") are required.`
4259
+ );
4260
+ }
4261
+ }
4262
+
4263
+ const sourcePath = path.join(packageEntry.rootDir, from);
4264
+ if (!(await fileExists(sourcePath))) {
4265
+ throw createCliError(`Missing template source ${sourcePath} for ${packageEntry.packageId}.`);
4266
+ }
4267
+
4268
+ const targetPaths = operation === "copy-file"
4269
+ ? toSurface
4270
+ ? resolveSurfaceTargetPathsForMutation({
4271
+ appRoot,
4272
+ packageId: packageEntry.packageId,
4273
+ mutation,
4274
+ configContext
4275
+ })
4276
+ : [path.join(appRoot, to)]
4277
+ : [path.join(appRoot, mutation.toDir || "migrations")];
4278
+ const replacements = await resolveTemplateContextReplacementsForMutation({
4279
+ packageEntry,
4280
+ mutation,
4281
+ options,
4282
+ appRoot,
4283
+ sourcePath,
4284
+ targetPaths
4285
+ });
4286
+ replacementsByMutationIndex.set(mutationIndex, replacements);
4287
+ }
4288
+
4289
+ return replacementsByMutationIndex;
4290
+ }
4291
+
3985
4292
  async function applyTextMutations(packageEntry, appRoot, textMutations, options, managedText, touchedFiles) {
3986
4293
  for (const mutation of textMutations) {
3987
4294
  const when = normalizeMutationWhen(mutation?.when);
@@ -4391,9 +4698,10 @@ async function applyPackageInstall({
4391
4698
 
4392
4699
  const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
4393
4700
  managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
4394
- const cloneOnlyPackage = isCloneOnlyPackageEntry(packageEntry);
4701
+ const generatorPackage = isGeneratorPackageEntry(packageEntry);
4395
4702
  const mutationWarnings = [];
4396
4703
  const mutations = ensureObject(packageEntry.descriptor.mutations);
4704
+ const fileMutations = ensureArray(mutations.files);
4397
4705
  const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
4398
4706
  const packageEntryForMutations =
4399
4707
  templateRoot === packageEntry.rootDir
@@ -4402,6 +4710,14 @@ async function applyPackageInstall({
4402
4710
  ...packageEntry,
4403
4711
  rootDir: templateRoot
4404
4712
  };
4713
+
4714
+ const precomputedTemplateContextByMutationIndex = await preflightFileMutationTemplateContexts(
4715
+ packageEntryForMutations,
4716
+ packageOptions,
4717
+ appRoot,
4718
+ fileMutations
4719
+ );
4720
+
4405
4721
  const mutationDependencies = ensureObject(mutations.dependencies);
4406
4722
  const runtimeDependencies = ensureObject(mutationDependencies.runtime);
4407
4723
  const devDependencies = ensureObject(mutationDependencies.dev);
@@ -4471,7 +4787,7 @@ async function applyPackageInstall({
4471
4787
  }
4472
4788
  }
4473
4789
 
4474
- if (cloneOnlyPackage) {
4790
+ if (generatorPackage) {
4475
4791
  const removedRuntimeDependency = removePackageJsonField(appPackageJson, "dependencies", packageEntry.packageId);
4476
4792
  const removedDevDependency = removePackageJsonField(appPackageJson, "devDependencies", packageEntry.packageId);
4477
4793
  if (removedRuntimeDependency || removedDevDependency) {
@@ -4507,11 +4823,12 @@ async function applyPackageInstall({
4507
4823
  packageEntryForMutations,
4508
4824
  packageOptions,
4509
4825
  appRoot,
4510
- ensureArray(mutations.files),
4826
+ fileMutations,
4511
4827
  managedRecord.managed.files,
4512
4828
  managedRecord.managed.migrations,
4513
4829
  touchedFiles,
4514
- mutationWarnings
4830
+ mutationWarnings,
4831
+ precomputedTemplateContextByMutationIndex
4515
4832
  );
4516
4833
 
4517
4834
  await applyTextMutations(
@@ -4532,7 +4849,7 @@ async function applyPackageInstall({
4532
4849
  touchedFiles
4533
4850
  );
4534
4851
 
4535
- if (cloneOnlyPackage) {
4852
+ if (generatorPackage) {
4536
4853
  delete lock.installedPackages[packageEntry.packageId];
4537
4854
  } else {
4538
4855
  managedRecord.migrationSyncVersion = packageEntry.version;