kintone-migrator 0.34.4 → 0.34.5

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/dist/index.mjs CHANGED
@@ -10428,6 +10428,128 @@ const CustomizationConfigSerializer = { serialize: (config) => {
10428
10428
  };
10429
10429
  } };
10430
10430
  //#endregion
10431
+ //#region src/core/domain/customization/services/customizationMerge.ts
10432
+ const PLATFORMS = ["desktop", "mobile"];
10433
+ const CATEGORIES = ["js", "css"];
10434
+ const SCOPE_KEY = "config:scope";
10435
+ /**
10436
+ * Bucket-qualified resource identity: `platform:category:name`. Shared with the
10437
+ * force/firstTime pull path (`preservePaths`) so path preservation matches the
10438
+ * merge on the same identity and cross-bucket same-basename files stay distinct.
10439
+ */
10440
+ function resourceKey(platform, category, name) {
10441
+ return `${platform}:${category}:${name}`;
10442
+ }
10443
+ function platformList(config, platform, category) {
10444
+ const p = config[platform];
10445
+ return category === "js" ? p.js : p.css;
10446
+ }
10447
+ /**
10448
+ * Builds the per-side map keyed by `platform:category:name`. `contentTag`
10449
+ * returns the content marker for a same-name FILE so divergent content surfaces
10450
+ * as a conflict. `side` distinguishes local vs remote tags for modified files.
10451
+ */
10452
+ function toMap(config, contentTag) {
10453
+ const map = /* @__PURE__ */ new Map();
10454
+ for (const platform of PLATFORMS) for (const category of CATEGORIES) for (const resource of platformList(config, platform, category)) {
10455
+ const name = resourceName(resource);
10456
+ const tag = resource.type === "FILE" ? `FILE:${contentTag(name)}` : "URL";
10457
+ map.set(resourceKey(platform, category, name), {
10458
+ resource,
10459
+ tag
10460
+ });
10461
+ }
10462
+ const scope = config.scope ?? "ALL";
10463
+ map.set(SCOPE_KEY, { scope });
10464
+ return map;
10465
+ }
10466
+ function isScopeValue(v) {
10467
+ return "scope" in v;
10468
+ }
10469
+ function eq(a, b) {
10470
+ if (isScopeValue(a) || isScopeValue(b)) return isScopeValue(a) && isScopeValue(b) && a.scope === b.scope;
10471
+ return a.tag === b.tag;
10472
+ }
10473
+ /**
10474
+ * Computes the file-name-keyed 3-way merge of base/local/remote customization.
10475
+ *
10476
+ * `modifiedFileNames` is the set of same-name FILE basenames whose local
10477
+ * content differs from the remote content (reused from the diffDetector flow).
10478
+ * Such files get distinct local/remote tags so they classify as conflicts; all
10479
+ * other matched files share the `"same"` tag and classify as unchanged.
10480
+ */
10481
+ function computeCustomizationThreeWayMerge(base, local, remote, modifiedFileNames) {
10482
+ const tagFor = (side) => (name) => modifiedFileNames.has(name) ? side : "same";
10483
+ return classifyThreeWay(toMap(base, () => "same"), toMap(local, tagFor("local")), toMap(remote, tagFor("remote")), eq);
10484
+ }
10485
+ /**
10486
+ * Applies a resolved 3-way merge, returning the merged customization config.
10487
+ *
10488
+ * Auto-merged entries take their `merged` value; conflicts take the chosen
10489
+ * side. The resulting resources are flattened back into per-platform js/css
10490
+ * lists, preserving the order they appear in the chosen/merged side. Throws a
10491
+ * BusinessRuleError when a conflict is left unresolved (programmer invariant).
10492
+ */
10493
+ function resolveCustomizationMerge(merge, resolution, local, remote) {
10494
+ const chosen = /* @__PURE__ */ new Map();
10495
+ let scope = "ALL";
10496
+ for (const entry of merge.entries) {
10497
+ let value;
10498
+ if (entry.change.kind === "conflict") {
10499
+ const choice = resolution.get(entry.key);
10500
+ if (choice === void 0) throw new BusinessRuleError(CustomizationErrorCode.CzUnresolvedConflict, `Unresolved conflict for "${entry.key}"`);
10501
+ value = choice === "local" ? entry.local : entry.remote;
10502
+ } else value = entry.merged;
10503
+ if (value === void 0) continue;
10504
+ if (isScopeValue(value)) scope = value.scope;
10505
+ else chosen.set(entry.key, value.resource);
10506
+ }
10507
+ return rebuild(chosen, scope, local, remote);
10508
+ }
10509
+ /**
10510
+ * Rebuilds the config from the chosen resource keys, ordering each bucket by the
10511
+ * resource's position in local first, then any remote-only resources, so the
10512
+ * resulting list is stable and matches the user's local ordering intent.
10513
+ *
10514
+ * `path` is a local-owned concern (the build-output location) that is orthogonal
10515
+ * to which content wins the merge: whenever a key exists locally, the local
10516
+ * resource (its declared path) is kept regardless of the chosen content side.
10517
+ * Only remote-only keys fall back to the chosen (remote/basename) resource.
10518
+ */
10519
+ function rebuild(chosen, scope, local, remote) {
10520
+ function bucket(platform, category) {
10521
+ const seen = /* @__PURE__ */ new Set();
10522
+ const out = [];
10523
+ const localByKey = /* @__PURE__ */ new Map();
10524
+ for (const resource of platformList(local, platform, category)) localByKey.set(resourceKey(platform, category, resourceName(resource)), resource);
10525
+ const append = (resources) => {
10526
+ for (const resource of resources) {
10527
+ const key = resourceKey(platform, category, resourceName(resource));
10528
+ if (seen.has(key)) continue;
10529
+ const picked = chosen.get(key);
10530
+ if (picked !== void 0) {
10531
+ out.push(localByKey.get(key) ?? picked);
10532
+ seen.add(key);
10533
+ }
10534
+ }
10535
+ };
10536
+ append(platformList(local, platform, category));
10537
+ append(platformList(remote, platform, category));
10538
+ return out;
10539
+ }
10540
+ return {
10541
+ scope,
10542
+ desktop: {
10543
+ js: bucket("desktop", "js"),
10544
+ css: bucket("desktop", "css")
10545
+ },
10546
+ mobile: {
10547
+ js: bucket("mobile", "js"),
10548
+ css: bucket("mobile", "css")
10549
+ }
10550
+ };
10551
+ }
10552
+ //#endregion
10431
10553
  //#region src/lib/deduplicateName.ts
10432
10554
  const DEFAULT_MAX_COUNTER = 1e4;
10433
10555
  /**
@@ -10500,7 +10622,7 @@ function usedStemsFor(usedNames, ext) {
10500
10622
  for (const name of usedNames) if (name.endsWith(ext)) stems.add(name.slice(0, name.length - ext.length));
10501
10623
  return stems;
10502
10624
  }
10503
- function planResources(resources, platformDir, resourceType, relativeBaseDir) {
10625
+ function planResources(resources, platformName, platformDir, resourceType, relativeBaseDir, contentBase, preservePaths) {
10504
10626
  const planned = [];
10505
10627
  const filesToDownload = [];
10506
10628
  const dir = join(platformDir, resourceType);
@@ -10511,28 +10633,42 @@ function planResources(resources, platformDir, resourceType, relativeBaseDir) {
10511
10633
  });
10512
10634
  else {
10513
10635
  const fileName = deduplicateFileName(sanitizeFileName(resource.file.name), usedNames);
10514
- const absolutePath = join(dir, fileName);
10515
- const relativePath = join(relativeBaseDir, resourceType, fileName);
10516
- planned.push({
10517
- type: "FILE",
10518
- path: relativePath
10519
- });
10520
- filesToDownload.push({
10521
- fileName,
10522
- fileKey: resource.file.fileKey,
10523
- absolutePath
10524
- });
10636
+ const declaredPath = preservePaths?.get(resourceKey(platformName, resourceType, resource.file.name));
10637
+ if (declaredPath !== void 0) {
10638
+ planned.push({
10639
+ type: "FILE",
10640
+ path: declaredPath
10641
+ });
10642
+ filesToDownload.push({
10643
+ fileName,
10644
+ fileKey: resource.file.fileKey,
10645
+ absolutePath: join(contentBase, declaredPath)
10646
+ });
10647
+ } else {
10648
+ const absolutePath = join(dir, fileName);
10649
+ const relativePath = join(relativeBaseDir, resourceType, fileName);
10650
+ planned.push({
10651
+ type: "FILE",
10652
+ path: relativePath
10653
+ });
10654
+ filesToDownload.push({
10655
+ fileName,
10656
+ fileKey: resource.file.fileKey,
10657
+ absolutePath
10658
+ });
10659
+ }
10525
10660
  }
10526
10661
  return {
10527
10662
  resources: planned,
10528
10663
  filesToDownload
10529
10664
  };
10530
10665
  }
10531
- function planPlatform(remotePlatform, platformName, basePath, filePrefix) {
10532
- const platformDir = join(basePath, filePrefix, platformName);
10666
+ function planPlatform(remotePlatform, platformName, basePath, filePrefix, preservePaths) {
10667
+ const contentBase = join(basePath, filePrefix);
10668
+ const platformDir = join(contentBase, platformName);
10533
10669
  const platformPrefix = platformName;
10534
- const jsPlan = planResources(remotePlatform.js, platformDir, "js", platformPrefix);
10535
- const cssPlan = planResources(remotePlatform.css, platformDir, "css", platformPrefix);
10670
+ const jsPlan = planResources(remotePlatform.js, platformName, platformDir, "js", platformPrefix, contentBase, preservePaths);
10671
+ const cssPlan = planResources(remotePlatform.css, platformName, platformDir, "css", platformPrefix, contentBase, preservePaths);
10536
10672
  const fileCount = remotePlatform.js.filter((r) => r.type === "FILE").length + remotePlatform.css.filter((r) => r.type === "FILE").length;
10537
10673
  return {
10538
10674
  platform: {
@@ -10560,8 +10696,8 @@ async function downloadFiles(files, container) {
10560
10696
  async function captureCustomization({ container, input }) {
10561
10697
  const existing = await container.customizationStorage.get();
10562
10698
  const { scope, desktop, mobile } = await container.customizationConfigurator.getCustomization();
10563
- const desktopPlan = planPlatform(desktop, "desktop", input.basePath, input.filePrefix);
10564
- const mobilePlan = planPlatform(mobile, "mobile", input.basePath, input.filePrefix);
10699
+ const desktopPlan = planPlatform(desktop, "desktop", input.basePath, input.filePrefix, input.preservePaths);
10700
+ const mobilePlan = planPlatform(mobile, "mobile", input.basePath, input.filePrefix, input.preservePaths);
10565
10701
  const config = {
10566
10702
  scope,
10567
10703
  desktop: desktopPlan.platform,
@@ -10571,6 +10707,7 @@ async function captureCustomization({ container, input }) {
10571
10707
  await downloadFiles([...desktopPlan.filesToDownload, ...mobilePlan.filesToDownload], container);
10572
10708
  return {
10573
10709
  configText,
10710
+ config,
10574
10711
  hasExistingConfig: existing.exists,
10575
10712
  fileResourceCount: desktopPlan.fileCount + mobilePlan.fileCount
10576
10713
  };
@@ -11565,116 +11702,6 @@ var apply_default$9 = define({
11565
11702
  }
11566
11703
  });
11567
11704
  //#endregion
11568
- //#region src/core/domain/customization/services/customizationMerge.ts
11569
- const PLATFORMS = ["desktop", "mobile"];
11570
- const CATEGORIES = ["js", "css"];
11571
- const SCOPE_KEY = "config:scope";
11572
- function resourceKey(platform, category, name) {
11573
- return `${platform}:${category}:${name}`;
11574
- }
11575
- function platformList(config, platform, category) {
11576
- const p = config[platform];
11577
- return category === "js" ? p.js : p.css;
11578
- }
11579
- /**
11580
- * Builds the per-side map keyed by `platform:category:name`. `contentTag`
11581
- * returns the content marker for a same-name FILE so divergent content surfaces
11582
- * as a conflict. `side` distinguishes local vs remote tags for modified files.
11583
- */
11584
- function toMap(config, contentTag) {
11585
- const map = /* @__PURE__ */ new Map();
11586
- for (const platform of PLATFORMS) for (const category of CATEGORIES) for (const resource of platformList(config, platform, category)) {
11587
- const name = resourceName(resource);
11588
- const tag = resource.type === "FILE" ? `FILE:${contentTag(name)}` : "URL";
11589
- map.set(resourceKey(platform, category, name), {
11590
- resource,
11591
- tag
11592
- });
11593
- }
11594
- const scope = config.scope ?? "ALL";
11595
- map.set(SCOPE_KEY, { scope });
11596
- return map;
11597
- }
11598
- function isScopeValue(v) {
11599
- return "scope" in v;
11600
- }
11601
- function eq(a, b) {
11602
- if (isScopeValue(a) || isScopeValue(b)) return isScopeValue(a) && isScopeValue(b) && a.scope === b.scope;
11603
- return a.tag === b.tag;
11604
- }
11605
- /**
11606
- * Computes the file-name-keyed 3-way merge of base/local/remote customization.
11607
- *
11608
- * `modifiedFileNames` is the set of same-name FILE basenames whose local
11609
- * content differs from the remote content (reused from the diffDetector flow).
11610
- * Such files get distinct local/remote tags so they classify as conflicts; all
11611
- * other matched files share the `"same"` tag and classify as unchanged.
11612
- */
11613
- function computeCustomizationThreeWayMerge(base, local, remote, modifiedFileNames) {
11614
- const tagFor = (side) => (name) => modifiedFileNames.has(name) ? side : "same";
11615
- return classifyThreeWay(toMap(base, () => "same"), toMap(local, tagFor("local")), toMap(remote, tagFor("remote")), eq);
11616
- }
11617
- /**
11618
- * Applies a resolved 3-way merge, returning the merged customization config.
11619
- *
11620
- * Auto-merged entries take their `merged` value; conflicts take the chosen
11621
- * side. The resulting resources are flattened back into per-platform js/css
11622
- * lists, preserving the order they appear in the chosen/merged side. Throws a
11623
- * BusinessRuleError when a conflict is left unresolved (programmer invariant).
11624
- */
11625
- function resolveCustomizationMerge(merge, resolution, local, remote) {
11626
- const chosen = /* @__PURE__ */ new Map();
11627
- let scope = "ALL";
11628
- for (const entry of merge.entries) {
11629
- let value;
11630
- if (entry.change.kind === "conflict") {
11631
- const choice = resolution.get(entry.key);
11632
- if (choice === void 0) throw new BusinessRuleError(CustomizationErrorCode.CzUnresolvedConflict, `Unresolved conflict for "${entry.key}"`);
11633
- value = choice === "local" ? entry.local : entry.remote;
11634
- } else value = entry.merged;
11635
- if (value === void 0) continue;
11636
- if (isScopeValue(value)) scope = value.scope;
11637
- else chosen.set(entry.key, value.resource);
11638
- }
11639
- return rebuild(chosen, scope, local, remote);
11640
- }
11641
- /**
11642
- * Rebuilds the config from the chosen resource keys, ordering each bucket by the
11643
- * resource's position in local first, then any remote-only resources, so the
11644
- * resulting list is stable and matches the user's local ordering intent.
11645
- */
11646
- function rebuild(chosen, scope, local, remote) {
11647
- function bucket(platform, category) {
11648
- const seen = /* @__PURE__ */ new Set();
11649
- const out = [];
11650
- const append = (resources) => {
11651
- for (const resource of resources) {
11652
- const key = resourceKey(platform, category, resourceName(resource));
11653
- if (seen.has(key)) continue;
11654
- const picked = chosen.get(key);
11655
- if (picked !== void 0) {
11656
- out.push(picked);
11657
- seen.add(key);
11658
- }
11659
- }
11660
- };
11661
- append(platformList(local, platform, category));
11662
- append(platformList(remote, platform, category));
11663
- return out;
11664
- }
11665
- return {
11666
- scope,
11667
- desktop: {
11668
- js: bucket("desktop", "js"),
11669
- css: bucket("desktop", "css")
11670
- },
11671
- mobile: {
11672
- js: bucket("mobile", "js"),
11673
- css: bucket("mobile", "css")
11674
- }
11675
- };
11676
- }
11677
- //#endregion
11678
11705
  //#region src/core/application/customization/customizationRemote.ts
11679
11706
  /**
11680
11707
  * Converts the kintone remote customization (file metadata) into a
@@ -11899,15 +11926,17 @@ async function saveCustomizationSnapshotAndRevision(container, config, revision)
11899
11926
  async function pullCustomization({ container, input }) {
11900
11927
  const { state, local, remote } = await loadCustomizationThreeWayInputs(container);
11901
11928
  if (input.force || state === void 0 || local === void 0) {
11929
+ const preservePaths = local !== void 0 ? buildPreservePaths(local) : void 0;
11902
11930
  const captured = await captureCustomization({
11903
11931
  container,
11904
11932
  input: {
11905
11933
  basePath: input.captureBasePath,
11906
- filePrefix: input.filePrefix
11934
+ filePrefix: input.filePrefix,
11935
+ preservePaths
11907
11936
  }
11908
11937
  });
11909
11938
  await container.customizationStorage.update(captured.configText);
11910
- await saveCustomizationSnapshotAndRevision(container, remote.config, remote.revision);
11939
+ await saveCustomizationSnapshotAndRevision(container, captured.config, remote.revision);
11911
11940
  return { mode: input.force ? "force" : "firstTime" };
11912
11941
  }
11913
11942
  const modifiedFileNames = await computeModifiedFileNames(local, remote.raw, input.basePath, container);
@@ -11955,7 +11984,26 @@ async function applyPulledCustomizationMerge({ container, input }) {
11955
11984
  await Promise.all(downloads);
11956
11985
  const configText = stringifyConfig(container.configCodec, CustomizationConfigSerializer.serialize(merged));
11957
11986
  await container.customizationStorage.update(configText);
11958
- await saveCustomizationSnapshotAndRevision(container, input.remoteConfig, input.remoteRevision);
11987
+ await saveCustomizationSnapshotAndRevision(container, merged, input.remoteRevision);
11988
+ }
11989
+ /**
11990
+ * Builds the path-preservation map for `captureCustomization`: bucket-qualified
11991
+ * key (`resourceKey(platform, category, FILE basename)`) → declared local
11992
+ * relative path. The composite key matches the merge identity so cross-bucket
11993
+ * same-basename files (e.g. `app/desktop/js/main.js` vs `app/mobile/js/main.js`)
11994
+ * stay distinct instead of colliding under a flat basename map. Only FILE
11995
+ * resources carry a path.
11996
+ */
11997
+ function buildPreservePaths(config) {
11998
+ const map = /* @__PURE__ */ new Map();
11999
+ const add = (platform, category, resources) => {
12000
+ for (const resource of resources) if (resource.type === "FILE") map.set(resourceKey(platform, category, resourceName(resource)), resource.path);
12001
+ };
12002
+ add("desktop", "js", config.desktop.js);
12003
+ add("desktop", "css", config.desktop.css);
12004
+ add("mobile", "js", config.mobile.js);
12005
+ add("mobile", "css", config.mobile.css);
12006
+ return map;
11959
12007
  }
11960
12008
  function allFileResources(config) {
11961
12009
  return [
@@ -11987,6 +12035,25 @@ function remoteFileKeyMap(remote) {
11987
12035
  }
11988
12036
  //#endregion
11989
12037
  //#region src/cli/commands/customize/pull.ts
12038
+ /**
12039
+ * Derives the path bases `runPull` feeds to the application layer.
12040
+ *
12041
+ * - `basePath` (= push's {@link computeBasePath}) is where merged/downloaded
12042
+ * content resolves and push uploads from.
12043
+ * - `captureBasePath` + `filePrefix` are passed to the capture-equivalent
12044
+ * (force / first-time) path.
12045
+ *
12046
+ * Invariant: `join(captureBasePath, filePrefix) === basePath`, so pull writes
12047
+ * exactly where push uploads from. Exported so the invariant test pins these
12048
+ * exact derivations rather than a hand-mirrored copy.
12049
+ */
12050
+ function derivePullPaths(customizeFilePath) {
12051
+ return {
12052
+ basePath: computeBasePath(customizeFilePath),
12053
+ captureBasePath: dirname(resolve(customizeFilePath)),
12054
+ filePrefix: deriveFilePrefix(customizeFilePath)
12055
+ };
12056
+ }
11990
12057
  async function resolveConflicts$1(merged, options) {
11991
12058
  const resolution = /* @__PURE__ */ new Map();
11992
12059
  for (const conflict of merged.merge.conflicts) {
@@ -12015,9 +12082,7 @@ async function resolveConflicts$1(merged, options) {
12015
12082
  }
12016
12083
  async function runPull$1(containerConfig, options) {
12017
12084
  const container = createCustomizationCliContainer(containerConfig);
12018
- const basePath = computeBasePath(containerConfig.customizeFilePath);
12019
- const captureBasePath = dirname(resolve(containerConfig.customizeFilePath));
12020
- const filePrefix = deriveFilePrefix(containerConfig.customizeFilePath);
12085
+ const { basePath, captureBasePath, filePrefix } = derivePullPaths(containerConfig.customizeFilePath);
12021
12086
  const s = p.spinner();
12022
12087
  s.start("Pulling customization from kintone...");
12023
12088
  let result;