@swell/apps-sdk 1.0.149 → 1.0.150

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.cjs CHANGED
@@ -112,6 +112,7 @@ __export(index_exports, {
112
112
  getEasyblocksComponentDefinitions: () => getEasyblocksComponentDefinitions,
113
113
  getEasyblocksPagePropsWithConfigs: () => getEasyblocksPagePropsWithConfigs,
114
114
  getEasyblocksPageTemplate: () => getEasyblocksPageTemplate,
115
+ getKVFlavor: () => getKVFlavor,
115
116
  getLayoutSectionGroups: () => getLayoutSectionGroups,
116
117
  getMenuItemStorefrontUrl: () => getMenuItemStorefrontUrl,
117
118
  getMenuItemUrlAndResource: () => getMenuItemUrlAndResource,
@@ -127,6 +128,7 @@ __export(index_exports, {
127
128
  isObject: () => isObject2,
128
129
  md5: () => md5,
129
130
  removeCircularReferences: () => removeCircularReferences,
131
+ resetKVFlavorCache: () => resetKVFlavorCache,
130
132
  resolveAsyncResources: () => resolveAsyncResources,
131
133
  resolveLookupCollection: () => resolveLookupCollection,
132
134
  resolveMenuItemUrlAndResource: () => resolveMenuItemUrlAndResource,
@@ -6832,6 +6834,50 @@ function md5(inputString) {
6832
6834
  return rh(a) + rh(b) + rh(c) + rh(d);
6833
6835
  }
6834
6836
 
6837
+ // src/utils/kv-flavor.ts
6838
+ var cachedKVFlavor;
6839
+ function getKVFlavor(workerEnv) {
6840
+ if (cachedKVFlavor) {
6841
+ return cachedKVFlavor;
6842
+ }
6843
+ if (!workerEnv?.THEME) {
6844
+ cachedKVFlavor = "memory";
6845
+ return cachedKVFlavor;
6846
+ }
6847
+ const kvFlavor = workerEnv.KV_FLAVOR;
6848
+ if (kvFlavor) {
6849
+ const flavorLower = String(kvFlavor).toLowerCase();
6850
+ switch (flavorLower) {
6851
+ case "cloudflare":
6852
+ case "cf":
6853
+ cachedKVFlavor = "cf";
6854
+ break;
6855
+ case "miniflare":
6856
+ cachedKVFlavor = "miniflare";
6857
+ break;
6858
+ case "memory":
6859
+ cachedKVFlavor = "memory";
6860
+ break;
6861
+ default:
6862
+ logger.warn(`[KV] Unknown KV_FLAVOR: ${kvFlavor}, using miniflare`);
6863
+ cachedKVFlavor = "miniflare";
6864
+ }
6865
+ return cachedKVFlavor;
6866
+ }
6867
+ try {
6868
+ if (typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers") {
6869
+ cachedKVFlavor = "cf";
6870
+ return cachedKVFlavor;
6871
+ }
6872
+ } catch {
6873
+ }
6874
+ cachedKVFlavor = "miniflare";
6875
+ return cachedKVFlavor;
6876
+ }
6877
+ function resetKVFlavorCache() {
6878
+ cachedKVFlavor = void 0;
6879
+ }
6880
+
6835
6881
  // src/utils/index.ts
6836
6882
  function isSectionConfig(config, themeConfigs) {
6837
6883
  if (!config.file_path.startsWith("theme/sections/")) {
@@ -7361,12 +7407,441 @@ function buildStores2() {
7361
7407
 
7362
7408
  // src/cache/theme-cache.ts
7363
7409
  var TTL = 90 * 24 * 60 * 60 * 1e3;
7364
- var ThemeCache = class extends Cache {
7365
- constructor(options) {
7366
- super({
7367
- ttl: TTL,
7368
- ...options
7410
+
7411
+ // src/cache/theme-file-storage.ts
7412
+ var import_bluebird2 = __toESM(require("bluebird"), 1);
7413
+
7414
+ // src/cache/kv-variety.ts
7415
+ var import_bluebird = __toESM(require("bluebird"), 1);
7416
+ var { Promise: Promise2 } = import_bluebird.default;
7417
+ var CFKV = class {
7418
+ constructor(kv) {
7419
+ this.kv = kv;
7420
+ }
7421
+ async get(keys) {
7422
+ if (keys.length === 0) {
7423
+ return /* @__PURE__ */ new Map();
7424
+ }
7425
+ const result = await this.kv.get(keys, "text");
7426
+ if (!(result instanceof Map)) {
7427
+ const map = /* @__PURE__ */ new Map();
7428
+ for (const key of keys) {
7429
+ map.set(key, null);
7430
+ }
7431
+ return map;
7432
+ }
7433
+ return result;
7434
+ }
7435
+ async put(key, value, metadata) {
7436
+ await this.kv.put(key, value, { metadata });
7437
+ }
7438
+ };
7439
+ var MiniflareKV = class {
7440
+ constructor(kv) {
7441
+ this.kv = kv;
7442
+ }
7443
+ async get(keys) {
7444
+ if (keys.length === 0) {
7445
+ return /* @__PURE__ */ new Map();
7446
+ }
7447
+ const result = /* @__PURE__ */ new Map();
7448
+ await Promise2.map(
7449
+ keys,
7450
+ async (key) => {
7451
+ const value = await this.kv.get(key, "text");
7452
+ result.set(key, value);
7453
+ },
7454
+ { concurrency: 50 }
7455
+ );
7456
+ return result;
7457
+ }
7458
+ async put(key, value, metadata) {
7459
+ await this.kv.put(key, value, { metadata });
7460
+ }
7461
+ };
7462
+ var MemoryKV = class {
7463
+ store = /* @__PURE__ */ new Map();
7464
+ async get(keys) {
7465
+ const result = /* @__PURE__ */ new Map();
7466
+ for (const key of keys) {
7467
+ const entry = this.store.get(key);
7468
+ result.set(key, entry?.value ?? null);
7469
+ }
7470
+ return result;
7471
+ }
7472
+ async put(key, value, metadata) {
7473
+ this.store.set(key, { value, metadata });
7474
+ }
7475
+ };
7476
+ function createClientKV(env, flavor = "cf") {
7477
+ if (env?.THEME) {
7478
+ if (flavor === "miniflare") {
7479
+ return new MiniflareKV(env.THEME);
7480
+ }
7481
+ return new CFKV(env.THEME);
7482
+ }
7483
+ return new MemoryKV();
7484
+ }
7485
+
7486
+ // src/cache/theme-file-storage.ts
7487
+ var { Promise: Promise3 } = import_bluebird2.default;
7488
+ var ThemeFileStorage = class {
7489
+ kv;
7490
+ maxConcurrency;
7491
+ maxBatchSize = 20 * 1024 * 1024;
7492
+ // 20MB safety margin
7493
+ constructor(env, flavor = "cf") {
7494
+ this.kv = createClientKV(env, flavor);
7495
+ this.maxConcurrency = flavor === "miniflare" ? 50 : 6;
7496
+ }
7497
+ /**
7498
+ * Build a KV storage key from a file hash
7499
+ */
7500
+ buildKey(hash) {
7501
+ return `file_data:${hash}`;
7502
+ }
7503
+ /**
7504
+ * Extract hash from a KV storage key
7505
+ */
7506
+ extractHashFromKey(key) {
7507
+ return key.replace("file_data:", "");
7508
+ }
7509
+ /**
7510
+ * Plan GET batches based on file sizes to avoid 413 errors
7511
+ * Uses round-robin distribution for even batch sizes
7512
+ */
7513
+ planGetBatches(configs) {
7514
+ if (configs.length === 0) {
7515
+ return [];
7516
+ }
7517
+ const sorted = [...configs].sort((a, b) => {
7518
+ const sizeA = a.file?.length || 0;
7519
+ const sizeB = b.file?.length || 0;
7520
+ return sizeB - sizeA;
7369
7521
  });
7522
+ const totalSize = sorted.reduce((sum, config) => {
7523
+ return sum + (config.file?.length || 0);
7524
+ }, 0);
7525
+ const sizeBatches = Math.ceil(totalSize / this.maxBatchSize);
7526
+ const keyBatches = Math.ceil(sorted.length / 100);
7527
+ const targetBatches = Math.max(sizeBatches, keyBatches);
7528
+ const batches = Array.from(
7529
+ { length: targetBatches },
7530
+ () => ({
7531
+ configs: [],
7532
+ keys: [],
7533
+ estimatedSize: 0
7534
+ })
7535
+ );
7536
+ sorted.forEach((config, index) => {
7537
+ const batchIndex = index % targetBatches;
7538
+ const batch = batches[batchIndex];
7539
+ batch.configs.push(config);
7540
+ batch.keys.push(this.buildKey(config.hash));
7541
+ batch.estimatedSize += config.file?.length || 0;
7542
+ });
7543
+ return batches.filter((batch) => batch.configs.length > 0);
7544
+ }
7545
+ /**
7546
+ * Load a single batch from KV storage
7547
+ */
7548
+ async loadBatch(batch) {
7549
+ return this.kv.get(batch.keys);
7550
+ }
7551
+ /**
7552
+ * Merge batch results with original configs
7553
+ */
7554
+ mergeResults(configs, batchResults) {
7555
+ const allData = /* @__PURE__ */ new Map();
7556
+ for (const batchResult of batchResults) {
7557
+ for (const [key, value] of batchResult.entries()) {
7558
+ allData.set(key, value);
7559
+ }
7560
+ }
7561
+ return configs.map((config) => {
7562
+ const key = this.buildKey(config.hash);
7563
+ const fileData = allData.get(key);
7564
+ if (fileData) {
7565
+ return {
7566
+ ...config,
7567
+ file_data: fileData
7568
+ };
7569
+ }
7570
+ return config;
7571
+ });
7572
+ }
7573
+ async getFiles(configs) {
7574
+ if (configs.length === 0) {
7575
+ return [];
7576
+ }
7577
+ const trace = createTraceId();
7578
+ const batches = this.planGetBatches(configs);
7579
+ const totalSize = batches.reduce((sum, b) => sum + b.estimatedSize, 0);
7580
+ const maxBatchSize = Math.max(...batches.map((b) => b.estimatedSize));
7581
+ logger.debug("[ThemeFileStorage] Loading files start", {
7582
+ totalConfigs: configs.length,
7583
+ batchCount: batches.length,
7584
+ maxBatchSize,
7585
+ totalSize,
7586
+ trace
7587
+ });
7588
+ const results = await Promise3.map(
7589
+ batches,
7590
+ (batch) => this.loadBatch(batch),
7591
+ { concurrency: Math.min(this.maxConcurrency, batches.length) }
7592
+ );
7593
+ const mergedConfigs = this.mergeResults(configs, results);
7594
+ const loadedCount = mergedConfigs.filter((c) => c.file_data).length;
7595
+ logger.debug("[ThemeFileStorage] Loading files end", {
7596
+ requested: configs.length,
7597
+ loaded: loadedCount,
7598
+ missing: configs.length - loadedCount,
7599
+ batches: batches.length,
7600
+ trace
7601
+ });
7602
+ return mergedConfigs;
7603
+ }
7604
+ /**
7605
+ * Validate file sizes and categorize by threshold
7606
+ */
7607
+ validateFiles(configs) {
7608
+ const valid = [];
7609
+ const warnings = [];
7610
+ for (const config of configs) {
7611
+ if (!config.file_data) {
7612
+ continue;
7613
+ }
7614
+ const size = config.file?.length || 0;
7615
+ if (size >= 25 * 1024 * 1024) {
7616
+ warnings.push({
7617
+ hash: config.hash,
7618
+ filePath: config.file_path,
7619
+ size,
7620
+ reason: "exceeded_25mb",
7621
+ action: "rejected"
7622
+ });
7623
+ } else if (size >= 5 * 1024 * 1024) {
7624
+ warnings.push({
7625
+ hash: config.hash,
7626
+ filePath: config.file_path,
7627
+ size,
7628
+ reason: "rejected_5mb",
7629
+ action: "rejected"
7630
+ });
7631
+ } else {
7632
+ if (size >= 1024 * 1024) {
7633
+ warnings.push({
7634
+ hash: config.hash,
7635
+ filePath: config.file_path,
7636
+ size,
7637
+ reason: "warning_1mb",
7638
+ action: "stored"
7639
+ });
7640
+ }
7641
+ valid.push(config);
7642
+ }
7643
+ }
7644
+ return { valid, warnings };
7645
+ }
7646
+ /**
7647
+ * Check which files already exist in KV storage
7648
+ * Uses batch planning to avoid 413 errors when checking existence
7649
+ */
7650
+ async checkExistence(configs) {
7651
+ if (configs.length === 0) {
7652
+ return /* @__PURE__ */ new Set();
7653
+ }
7654
+ const existing = /* @__PURE__ */ new Set();
7655
+ const batches = this.planGetBatches(configs);
7656
+ const results = await Promise3.map(
7657
+ batches,
7658
+ (batch) => this.kv.get(batch.keys),
7659
+ { concurrency: this.maxConcurrency }
7660
+ );
7661
+ for (const batchResult of results) {
7662
+ for (const [key, value] of batchResult.entries()) {
7663
+ if (value !== null) {
7664
+ const hash = this.extractHashFromKey(key);
7665
+ existing.add(hash);
7666
+ }
7667
+ }
7668
+ }
7669
+ return existing;
7670
+ }
7671
+ async putFiles(configs) {
7672
+ const result = {
7673
+ written: 0,
7674
+ skipped: 0,
7675
+ skippedExisting: 0,
7676
+ warnings: []
7677
+ };
7678
+ if (configs.length === 0) {
7679
+ return result;
7680
+ }
7681
+ const trace = createTraceId();
7682
+ logger.debug("[ThemeFileStorage] Put files start", {
7683
+ totalConfigs: configs.length,
7684
+ trace
7685
+ });
7686
+ const { valid, warnings } = this.validateFiles(configs);
7687
+ result.warnings = warnings;
7688
+ if (warnings.length > 0) {
7689
+ const rejectedCount = warnings.filter(
7690
+ (w) => w.action === "rejected"
7691
+ ).length;
7692
+ const warnedCount = warnings.filter((w) => w.action === "stored").length;
7693
+ logger.warn("[ThemeFileStorage] File size validation issues", {
7694
+ totalWarnings: warnings.length,
7695
+ rejected: rejectedCount,
7696
+ warned: warnedCount,
7697
+ trace
7698
+ });
7699
+ warnings.filter((w) => w.action === "rejected").forEach((w) => {
7700
+ logger.error("[ThemeFileStorage] File rejected due to size", {
7701
+ filePath: w.filePath,
7702
+ size: w.size,
7703
+ reason: w.reason,
7704
+ trace
7705
+ });
7706
+ });
7707
+ }
7708
+ const rejected = warnings.filter((w) => w.action === "rejected").length;
7709
+ result.skipped = rejected + (configs.length - configs.filter((c) => c.file_data).length);
7710
+ logger.debug("[ThemeFileStorage] Checking existence", {
7711
+ validFiles: valid.length,
7712
+ trace
7713
+ });
7714
+ const existing = await this.checkExistence(valid);
7715
+ result.skippedExisting = existing.size;
7716
+ const toWrite = valid.filter((config) => !existing.has(config.hash));
7717
+ if (toWrite.length > 0) {
7718
+ logger.debug("[ThemeFileStorage] Writing new files", {
7719
+ toWrite: toWrite.length,
7720
+ skippedExisting: existing.size,
7721
+ trace
7722
+ });
7723
+ await Promise3.map(
7724
+ toWrite,
7725
+ async (config) => {
7726
+ const key = this.buildKey(config.hash);
7727
+ const metadata = config.file?.content_type ? { content_type: config.file.content_type } : void 0;
7728
+ await this.kv.put(key, config.file_data, metadata);
7729
+ result.written++;
7730
+ },
7731
+ { concurrency: this.maxConcurrency }
7732
+ );
7733
+ }
7734
+ logger.info("[ThemeFileStorage] Put files complete", {
7735
+ written: result.written,
7736
+ skipped: result.skipped,
7737
+ skippedExisting: result.skippedExisting,
7738
+ warnings: result.warnings.length,
7739
+ trace
7740
+ });
7741
+ return result;
7742
+ }
7743
+ };
7744
+
7745
+ // src/cache/constants.ts
7746
+ var SECOND = 1e3;
7747
+ var MINUTE = 60 * SECOND;
7748
+ var HOUR = 60 * MINUTE;
7749
+ var DAY = 24 * HOUR;
7750
+ var YEAR = 365 * DAY;
7751
+ var MAX_TTL = YEAR;
7752
+ var SHORT_TTL = 5 * SECOND;
7753
+
7754
+ // src/cache/worker-cache-proxy.ts
7755
+ var CACHE_NAME = "swell-cache-v1";
7756
+ var CACHE_KEY_ORIGIN = "https://cache.swell.store";
7757
+ var WorkerCacheProxy = class {
7758
+ swell;
7759
+ constructor(swell) {
7760
+ this.swell = swell;
7761
+ }
7762
+ /**
7763
+ * Reads a JSON value from Worker Cache using a key built from path+query.
7764
+ * Returns null on miss or if running outside of a Worker environment.
7765
+ */
7766
+ async get(path, query, opts) {
7767
+ if (typeof caches === "undefined") {
7768
+ return null;
7769
+ }
7770
+ const { keyUrl } = await this.buildKeyUrl(path, query, opts?.version);
7771
+ try {
7772
+ const cache = await caches.open(CACHE_NAME);
7773
+ const match = await cache.match(keyUrl);
7774
+ if (!match) return null;
7775
+ const data = await match.json();
7776
+ return data;
7777
+ } catch {
7778
+ return null;
7779
+ }
7780
+ }
7781
+ /**
7782
+ * Stores a JSON value in Worker Cache under key built from path+query.
7783
+ * No-ops outside of a Worker environment.
7784
+ */
7785
+ async put(path, query, value, opts) {
7786
+ if (typeof caches === "undefined") {
7787
+ return;
7788
+ }
7789
+ const { keyUrl, hasVersion } = await this.buildKeyUrl(
7790
+ path,
7791
+ query,
7792
+ opts?.version
7793
+ );
7794
+ const ttlMs = hasVersion ? MAX_TTL : SHORT_TTL;
7795
+ try {
7796
+ const cache = await caches.open(CACHE_NAME);
7797
+ const response = new Response(JSON.stringify(value), {
7798
+ headers: {
7799
+ "Content-Type": "application/json",
7800
+ "Cache-Control": `public, max-age=${Math.floor(ttlMs / 1e3)}`
7801
+ }
7802
+ });
7803
+ await cache.put(keyUrl, response);
7804
+ } catch {
7805
+ }
7806
+ }
7807
+ /**
7808
+ * Builds a deterministic key URL for Worker Cache from the backend API URL
7809
+ * composed using path and query. Includes tenant and auth isolation and an
7810
+ * optional version segment.
7811
+ */
7812
+ async buildKeyUrl(path, query, explicitVersion) {
7813
+ const apiHost = this.swell.backend?.apiHost;
7814
+ const endpointPath = String(path).startsWith("/") ? String(path).substring(1) : String(path);
7815
+ let queryString = "";
7816
+ if (query && this.swell.backend) {
7817
+ queryString = this.swell.backend.stringifyQuery(query);
7818
+ }
7819
+ const fullUrl = `${apiHost}/${endpointPath}${queryString ? `?${queryString}` : ""}`;
7820
+ const instanceId = this.swell.instanceId || "";
7821
+ const authKey = String(this.swell.swellHeaders?.["swell-auth-key"] || "");
7822
+ const tenantHash = await this.sha256Hex(`${instanceId}|${authKey}`);
7823
+ const version = explicitVersion !== void 0 ? explicitVersion : this.swell.swellHeaders?.["theme-version-hash"] || null;
7824
+ const hasVersion = Boolean(version);
7825
+ const versionHash = hasVersion ? await this.sha256Hex(String(version)) : null;
7826
+ const urlHash = await this.sha256Hex(fullUrl);
7827
+ const keyUrl = versionHash ? `${CACHE_KEY_ORIGIN}/v1/${tenantHash}/${versionHash}/${urlHash}` : `${CACHE_KEY_ORIGIN}/v1/${tenantHash}/${urlHash}`;
7828
+ return { keyUrl, hasVersion };
7829
+ }
7830
+ /**
7831
+ * SHA-256 digest with hex encoding. Requires Worker crypto; callers
7832
+ * should avoid invoking this outside of Worker code paths.
7833
+ */
7834
+ async sha256Hex(input) {
7835
+ if (typeof crypto !== "undefined" && crypto.subtle && crypto.subtle.digest) {
7836
+ const encoder = new TextEncoder();
7837
+ const digest = await crypto.subtle.digest(
7838
+ "SHA-256",
7839
+ encoder.encode(input)
7840
+ );
7841
+ const bytes = new Uint8Array(digest);
7842
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
7843
+ }
7844
+ return md5(input);
7370
7845
  }
7371
7846
  };
7372
7847
 
@@ -7731,7 +8206,7 @@ var Swell = class _Swell {
7731
8206
  this.workerEnv = workerEnv;
7732
8207
  this.resourceLoadingIndicator = params.resourceLoadingIndicator;
7733
8208
  logger.info(
7734
- `[SDK] KV cache: ${this.workerEnv?.THEME ? "enabled" : "disabled"}`
8209
+ `[SDK] KV cache: ${this.workerEnv?.THEME ? "enabled" : "disabled"}, flavor: ${getKVFlavor(this.workerEnv)}`
7735
8210
  );
7736
8211
  if (serverHeaders) {
7737
8212
  const { headers: headers2, swellHeaders: swellHeaders2 } = _Swell.formatHeaders(serverHeaders);
@@ -19382,296 +19857,255 @@ function getLiquidFS(getThemeConfig, extName) {
19382
19857
  }
19383
19858
 
19384
19859
  // src/theme/theme-loader.ts
19385
- var import_bluebird = __toESM(require("bluebird"), 1);
19386
- var { Promise: Promise2 } = import_bluebird.default;
19387
19860
  var MAX_INDIVIDUAL_CONFIGS_TO_FETCH = 50;
19388
- var ThemeLoader = class _ThemeLoader {
19389
- static cache = null;
19861
+ var ThemeLoader = class {
19390
19862
  swell;
19391
- manifest;
19392
19863
  configs;
19393
- configPaths;
19394
19864
  constructor(swell) {
19395
19865
  this.swell = swell;
19396
- this.manifest = null;
19397
19866
  this.configs = /* @__PURE__ */ new Map();
19398
- this.configPaths = [];
19399
19867
  }
19868
+ /**
19869
+ * Initialize the theme loader with all configurations.
19870
+ * Either uses provided configs (editor mode) or loads from storage.
19871
+ */
19400
19872
  async init(themeConfigs) {
19401
19873
  if (themeConfigs) {
19402
19874
  this.setConfigs(themeConfigs);
19403
19875
  return;
19404
19876
  }
19405
19877
  if (!this.getThemeId()) {
19878
+ logger.debug("[ThemeLoader] No theme ID, skipping init");
19406
19879
  return;
19407
19880
  }
19408
- await this.fetchManifest();
19409
- if (this.manifest === null) {
19410
- console.log("ThemeLoader.init - version manifest not found");
19411
- await this.loadTheme();
19412
- }
19881
+ await this.loadAllConfigs();
19882
+ logger.info("[ThemeLoader] Initialization complete", {
19883
+ configCount: this.configs.size,
19884
+ themeId: this.getThemeId()
19885
+ });
19413
19886
  }
19414
19887
  /**
19415
- * Loads theme configs for this version.
19888
+ * Get a single config by file path (synchronous).
19889
+ * Returns null if config not found.
19416
19890
  */
19417
- async loadTheme() {
19418
- const { swellHeaders } = this.swell;
19419
- console.log("ThemeLoader.loadTheme", swellHeaders["theme-version-hash"]);
19420
- if (swellHeaders["theme-version-hash"]) {
19421
- const configs = await this.loadThemeFromManifest();
19422
- if (configs) {
19423
- return configs;
19424
- }
19425
- }
19426
- return this.loadThemeAllConfigs();
19891
+ getConfig(filePath) {
19892
+ return this.configs.get(filePath) ?? null;
19427
19893
  }
19428
19894
  /**
19429
- * Returns the cache instance for this theme loader.
19895
+ * Get all loaded configs.
19896
+ * Used by theme getter to expose configs to editor/tests.
19430
19897
  */
19431
- getCache() {
19432
- if (_ThemeLoader.cache === null) {
19433
- _ThemeLoader.cache = new ThemeCache({
19434
- kvStore: this.swell.workerEnv?.THEME
19435
- });
19436
- }
19437
- return _ThemeLoader.cache;
19898
+ getConfigs() {
19899
+ return this.configs;
19438
19900
  }
19439
19901
  /**
19440
- * Load theme configs from internal data, typically in the editor.
19902
+ * Get multiple configs by path pattern (synchronous).
19903
+ * Filters configs by prefix and optional suffix.
19441
19904
  */
19442
- setConfigs(themeConfigs) {
19443
- this.configs = new Map(themeConfigs);
19444
- this.manifest = /* @__PURE__ */ new Map();
19445
- for (const { file_path, hash } of this.configs.values()) {
19446
- this.manifest.set(file_path, hash);
19905
+ getConfigsByPath(pathPrefix, pathSuffix) {
19906
+ const results = [];
19907
+ for (const [path, config] of this.configs) {
19908
+ if (path.startsWith(pathPrefix) && (!pathSuffix || path.endsWith(pathSuffix))) {
19909
+ results.push(config);
19910
+ }
19447
19911
  }
19448
- this.configPaths = Array.from(this.configs.keys());
19912
+ return results;
19449
19913
  }
19450
19914
  /**
19451
- * Preloads a theme version and configs. This is used to optimize initial theme load.
19915
+ * Load theme configs from internal data, typically in the editor.
19916
+ * Used when configs are provided externally (e.g., from editor).
19452
19917
  */
19453
- async preloadTheme(payload) {
19454
- const { version, configs } = payload;
19455
- console.log(
19456
- `ThemeLoader.preloadTheme${version?.hash ? ` - manifest: ${version.hash}` : ""}${configs?.length ? ` - configs: ${configs.length}` : ""}`
19457
- );
19458
- const promises = [];
19459
- if (version) {
19460
- promises.push(this.cacheManifest(version));
19461
- }
19462
- if (configs) {
19463
- const themeId = this.getThemeId();
19464
- promises.push(
19465
- Promise2.map(
19466
- configs,
19467
- async (config) => {
19468
- const promises2 = [
19469
- this.cacheThemeConfig(config)
19470
- ];
19471
- if (themeId && config.file?.url) {
19472
- promises2.push(
19473
- this.cacheThemeFileUrl(themeId, config.hash, config.file.url)
19474
- );
19475
- }
19476
- await Promise2.all(promises2);
19477
- },
19478
- { concurrency: 10 }
19479
- )
19480
- );
19481
- }
19482
- await Promise2.all(promises);
19918
+ setConfigs(themeConfigs) {
19919
+ this.configs = new Map(themeConfigs);
19483
19920
  }
19484
19921
  /**
19485
- * Fetches a theme config by file path.
19922
+ * Updates KV with file_data for provided theme configs (warmup path).
19923
+ * Uses the new ThemeFileStorage abstraction for optimized operations.
19486
19924
  */
19487
- async fetchThemeConfig(filePath) {
19488
- const config = this.configs.get(filePath);
19489
- if (config !== void 0) {
19490
- return config;
19491
- }
19492
- const hash = this.manifest?.get(filePath);
19493
- if (!hash) {
19494
- return null;
19925
+ async updateThemeCache(payload) {
19926
+ const configs = payload?.configs || [];
19927
+ if (configs.length === 0) {
19928
+ logger.debug("[ThemeLoader] No configs to cache");
19929
+ return {
19930
+ written: 0,
19931
+ skipped: 0,
19932
+ skippedExisting: 0,
19933
+ warnings: []
19934
+ };
19495
19935
  }
19496
- const cache = this.getCache();
19497
- const themeId = this.getThemeId();
19498
- const [themeConfig, fileUrl] = await Promise2.all([
19499
- cache.get(`config:${hash}`),
19500
- themeId ? cache.get(`file:${themeId}:${hash}`) : void 0
19501
- ]);
19502
- if (themeConfig) {
19503
- let config2 = themeConfig;
19504
- if (fileUrl && themeConfig.file?.url) {
19505
- config2 = {
19506
- ...themeConfig,
19507
- file: { ...themeConfig.file, url: fileUrl }
19508
- };
19509
- }
19510
- this.configs.set(filePath, config2);
19511
- return config2;
19936
+ const flavor = getKVFlavor(this.swell.workerEnv);
19937
+ const trace = createTraceId();
19938
+ logger.info("[ThemeLoader] Starting theme cache update", {
19939
+ totalConfigs: configs.length,
19940
+ flavor,
19941
+ trace
19942
+ });
19943
+ const storage = new ThemeFileStorage(this.swell.workerEnv, flavor);
19944
+ const result = await storage.putFiles(configs);
19945
+ if (result.warnings.length > 0) {
19946
+ logger.warn("[ThemeLoader] Theme cache updated with warnings", {
19947
+ total: configs.length,
19948
+ written: result.written,
19949
+ skipped: result.skipped,
19950
+ skippedExisting: result.skippedExisting,
19951
+ warnings: result.warnings.length,
19952
+ trace
19953
+ });
19954
+ } else {
19955
+ logger.info("[ThemeLoader] Theme cache updated successfully", {
19956
+ total: configs.length,
19957
+ written: result.written,
19958
+ skipped: result.skipped,
19959
+ skippedExisting: result.skippedExisting,
19960
+ trace
19961
+ });
19512
19962
  }
19513
- return this.fetchThemeConfigsFromSourceByPath(filePath, hash);
19514
- }
19515
- async fetchThemeConfigsByPath(pathPrefix, pathSuffix) {
19516
- const paths = this.configPaths.filter(
19517
- (path) => path.startsWith(pathPrefix) && (!pathSuffix || path.endsWith(pathSuffix))
19518
- );
19519
- const configs = await Promise2.map(
19520
- paths,
19521
- (path) => this.fetchThemeConfig(path),
19522
- { concurrency: 10 }
19523
- );
19524
- return configs.filter((config) => config !== null);
19963
+ return result;
19525
19964
  }
19526
19965
  /**
19527
- * Load all theme configs.
19966
+ * Main loading logic - loads all configs at once.
19967
+ * 1. Fetches lightweight metadata (cached when possible)
19968
+ * 2. Batch hydrates file_data from KV
19969
+ * 3. Fetches missing file_data from API if needed
19528
19970
  */
19529
- async loadThemeAllConfigs() {
19530
- const { swellHeaders } = this.swell;
19531
- const configVersion = String(swellHeaders["theme-config-version"]);
19532
- if (!configVersion) {
19533
- throw new Error("Theme version is required");
19971
+ async loadAllConfigs() {
19972
+ const configMetadata = await this.fetchConfigMetadata();
19973
+ if (configMetadata.length === 0) {
19974
+ logger.warn("[ThemeLoader] No configs found");
19975
+ return;
19534
19976
  }
19535
- const configs = await this.getCache().fetch(
19536
- `configs-all:${this.swell.instanceId}:v@${configVersion}2`,
19537
- () => this.fetchThemeConfigsFromSource()
19538
- );
19539
- return configs?.results ?? [];
19977
+ logger.debug("[ThemeLoader] Loading configs", {
19978
+ total: configMetadata.length
19979
+ });
19980
+ const flavor = getKVFlavor(this.swell.workerEnv);
19981
+ const storage = new ThemeFileStorage(this.swell.workerEnv, flavor);
19982
+ const kvHydrated = await storage.getFiles(configMetadata);
19983
+ const completeConfigs = await this.ensureConfigsHaveData(kvHydrated);
19984
+ for (const config of completeConfigs) {
19985
+ this.configs.set(config.file_path, config);
19986
+ }
19987
+ logger.info("[ThemeLoader] All configs loaded", {
19988
+ total: completeConfigs.length,
19989
+ withData: completeConfigs.filter((c) => c.file_data).length
19990
+ });
19540
19991
  }
19541
19992
  /**
19542
- * Load theme configs via manifest.
19543
- *
19544
- * This approach has the following optimizations:
19545
- * - cached manifests and configs can be shared by other clients
19546
- * - when fetching from source, only fetch the missing records
19993
+ * Fetch lightweight config metadata from API or cache.
19994
+ * Does NOT include file_data to minimize payload size.
19547
19995
  */
19548
- async loadThemeFromManifest() {
19549
- const manifest = await this.fetchManifest();
19550
- if (!manifest) {
19551
- return null;
19552
- }
19553
- const configHashesUnresolved = [];
19554
- const configsByHash = /* @__PURE__ */ new Map();
19555
- const themeId = this.getThemeId();
19556
- const cache = this.getCache();
19557
- await Promise2.map(
19558
- manifest.values(),
19559
- async (configHash) => {
19560
- const [themeConfig, fileUrl] = await Promise2.all([
19561
- cache.get(`config:${configHash}`),
19562
- themeId ? cache.get(`file:${themeId}:${configHash}`) : void 0
19563
- ]);
19564
- if (!themeConfig) {
19565
- configHashesUnresolved.push(configHash);
19566
- return;
19567
- }
19568
- let config = themeConfig;
19569
- if (fileUrl && themeConfig.file?.url) {
19570
- config = {
19571
- ...themeConfig,
19572
- file: { ...themeConfig.file, url: fileUrl }
19573
- };
19996
+ async fetchConfigMetadata() {
19997
+ const query = {
19998
+ ...this.themeVersionQueryFilter(),
19999
+ limit: 1e3,
20000
+ type: "theme",
20001
+ fields: "id, name, type, file, file_path, hash"
20002
+ // NO file_data
20003
+ };
20004
+ try {
20005
+ const cache = new WorkerCacheProxy(this.swell);
20006
+ const versionHash = this.swell.swellHeaders["theme-version-hash"];
20007
+ const cached = await cache.get(
20008
+ "/:themes:configs",
20009
+ query,
20010
+ {
20011
+ version: versionHash || null
19574
20012
  }
19575
- configsByHash.set(config.hash, config);
19576
- this.configs.set(config.file_path, config);
19577
- },
19578
- { concurrency: 10 }
19579
- );
19580
- if (configHashesUnresolved.length > 0) {
19581
- const configs = await this.fetchThemeConfigsFromSource(
19582
- // If no configs were resolved, then fetch them all. otherwise fetch
19583
- // the specific subset of configs.
19584
- configsByHash.size === 0 ? void 0 : configHashesUnresolved
19585
20013
  );
19586
- const newConfigs = configs?.results ?? [];
19587
- for (const config of newConfigs) {
19588
- configsByHash.set(config.hash, config);
19589
- this.configs.set(config.file_path, config);
20014
+ if (cached) {
20015
+ logger.debug("[ThemeLoader] Config metadata cache hit");
20016
+ return cached;
19590
20017
  }
19591
- await Promise2.map(
19592
- newConfigs,
19593
- async (config) => {
19594
- const promises = [this.cacheThemeConfig(config)];
19595
- if (themeId && config.file?.url) {
19596
- promises.push(
19597
- this.cacheThemeFileUrl(themeId, config.hash, config.file.url)
19598
- );
19599
- }
19600
- await Promise2.all(promises);
19601
- },
19602
- { concurrency: 10 }
19603
- );
20018
+ } catch (err) {
20019
+ logger.warn("[ThemeLoader] Cache read failed, fetching from API", err);
19604
20020
  }
19605
- return Array.from(configsByHash.values());
19606
- }
19607
- /**
19608
- * Caches a theme version manifest by hash.
19609
- */
19610
- async cacheManifest(version) {
19611
- if (version?.hash) {
19612
- await this.getCache().set(`manifest:${version.hash}`, version.manifest);
20021
+ logger.debug("[ThemeLoader] Fetching config metadata from API");
20022
+ const response = await this.swell.get(
20023
+ "/:themes:configs",
20024
+ query
20025
+ );
20026
+ const configs = response?.results || [];
20027
+ try {
20028
+ const cache = new WorkerCacheProxy(this.swell);
20029
+ const versionHash = this.swell.swellHeaders["theme-version-hash"];
20030
+ await cache.put("/:themes:configs", query, configs, {
20031
+ version: versionHash || null
20032
+ });
20033
+ } catch (err) {
20034
+ logger.warn("[ThemeLoader] Cache write failed", err);
19613
20035
  }
20036
+ return configs;
19614
20037
  }
19615
20038
  /**
19616
- * Caches a theme config by hash.
20039
+ * Helper to ensure all configs have file_data.
20040
+ * Fetches missing data from API and updates KV cache.
19617
20041
  */
19618
- async cacheThemeConfig(config) {
19619
- if (config?.hash) {
19620
- await this.getCache().set(`config:${config.hash}`, config);
20042
+ async ensureConfigsHaveData(configs) {
20043
+ const missingData = configs.filter((c) => !c.file_data);
20044
+ if (missingData.length === 0) {
20045
+ logger.debug("[ThemeLoader] All configs have file_data from KV");
20046
+ return configs;
19621
20047
  }
19622
- }
19623
- /**
19624
- * Caches a CDN file url by config hash.
19625
- */
19626
- async cacheThemeFileUrl(themeId, configHash, fileUrl) {
19627
- await this.getCache().set(`file:${themeId}:${configHash}`, fileUrl);
19628
- }
19629
- /**
19630
- * Fetches the manifest (set of config hashes) for a theme version.
19631
- */
19632
- async fetchManifest() {
19633
- const { swellHeaders } = this.swell;
19634
- const versionHash = swellHeaders["theme-version-hash"];
19635
- console.log("ThemeLoader.fetchManifest", versionHash);
19636
- let manifest = await this.getCache().get(
19637
- `manifest:${versionHash}`
20048
+ const trace = createTraceId();
20049
+ logger.info(
20050
+ `[ThemeLoader] Loading ${missingData.length} missing file_data from API`,
20051
+ {
20052
+ trace
20053
+ }
19638
20054
  );
19639
- if (!manifest) {
19640
- const themeVersion = await this.swell.get(
19641
- "/:themes:versions/:last",
19642
- {
19643
- ...this.themeVersionQueryFilter(),
19644
- fields: "hash, manifest"
19645
- }
19646
- );
19647
- if (themeVersion) {
19648
- await this.cacheManifest(themeVersion);
19649
- manifest = themeVersion.manifest;
20055
+ const hashes = missingData.map((c) => c.hash);
20056
+ const apiResponse = await this.fetchThemeConfigsFromSource(hashes);
20057
+ const fetched = apiResponse.results || [];
20058
+ logger.info(`[ThemeLoader] Fetched ${fetched.length} configs from API`, {
20059
+ trace
20060
+ });
20061
+ if (fetched.length > 0) {
20062
+ const cacheResult = await this.updateThemeCache({
20063
+ api: 1,
20064
+ // Required by SwellThemePreload type
20065
+ configs: fetched
20066
+ });
20067
+ if (cacheResult.warnings.length > 0) {
20068
+ logger.warn("[ThemeLoader] Some files had size issues", {
20069
+ warnings: cacheResult.warnings.length
20070
+ });
19650
20071
  }
19651
20072
  }
19652
- if (manifest) {
19653
- this.manifest = new Map(Object.entries(manifest));
19654
- this.configPaths = [...this.manifest.keys()];
20073
+ const fetchedMap = new Map(fetched.map((c) => [c.hash, c]));
20074
+ const mergedConfigs = configs.map((config) => {
20075
+ if (!config.file_data) {
20076
+ const withData = fetchedMap.get(config.hash);
20077
+ if (withData?.file_data) {
20078
+ return { ...config, file_data: withData.file_data };
20079
+ }
20080
+ }
20081
+ return config;
20082
+ });
20083
+ const stillMissing = mergedConfigs.filter((c) => !c.file_data).length;
20084
+ if (stillMissing > 0) {
20085
+ logger.warn(
20086
+ `[ThemeLoader] ${stillMissing} configs still missing file_data after fetch`
20087
+ );
19655
20088
  }
19656
- return this.manifest;
20089
+ return mergedConfigs;
19657
20090
  }
19658
20091
  /**
19659
- * Fetches many theme configs via Swell Backend API.
20092
+ * Fetches theme configs with file_data from Swell Backend API.
20093
+ * Used to retrieve missing file_data for configs.
19660
20094
  */
19661
20095
  async fetchThemeConfigsFromSource(configHashes = void 0) {
19662
20096
  configHashes = configHashes || [];
19663
20097
  const { swellHeaders } = this.swell;
19664
20098
  const version = String(swellHeaders["theme-config-version"]);
19665
20099
  const fetchAll = configHashes.length === 0 || configHashes.length > MAX_INDIVIDUAL_CONFIGS_TO_FETCH;
19666
- console.log(
19667
- `Retrieving ${fetchAll ? "all" : "some"} theme configurations - version: ${version}`
20100
+ logger.debug(
20101
+ `[ThemeLoader] Fetching ${fetchAll ? "all" : configHashes.length} configs with file_data`,
20102
+ { version }
19668
20103
  );
19669
20104
  const configs = await this.swell.get(
19670
20105
  "/:themes:configs",
19671
20106
  {
19672
20107
  ...this.themeVersionQueryFilter(),
19673
20108
  ...fetchAll ? void 0 : { hash: { $in: configHashes } },
19674
- // TODO: paginate to support more than 1000 configs
19675
20109
  limit: 1e3,
19676
20110
  type: "theme",
19677
20111
  fields: "name, file, file_path, hash",
@@ -19683,45 +20117,13 @@ var ThemeLoader = class _ThemeLoader {
19683
20117
  return configs;
19684
20118
  }
19685
20119
  /**
19686
- * Fetches one theme config via Swell Backend API.
19687
- * This is used when a hash entry cannot be found.
19688
- * We may override the cached hash in order to ensure it is found on reload,
19689
- * but we probably need to find why that happens in the first place (TODO).
20120
+ * Get the current theme ID from headers.
19690
20121
  */
19691
- async fetchThemeConfigsFromSourceByPath(filePath, hash) {
19692
- console.log(`Retrieving theme config - ${filePath}`);
19693
- const config = await this.swell.get(
19694
- "/:themes:configs/:last",
19695
- {
19696
- ...this.themeVersionQueryFilter(),
19697
- file_path: filePath,
19698
- fields: "name, file, file_path, hash",
19699
- include: {
19700
- file_data: FILE_DATA_INCLUDE_QUERY
19701
- }
19702
- }
19703
- );
19704
- if (config) {
19705
- this.configs.set(filePath, config);
19706
- if (hash) {
19707
- config.hash = hash;
19708
- }
19709
- const themeId = this.getThemeId();
19710
- const promises = [this.cacheThemeConfig(config)];
19711
- if (themeId && config.file?.url) {
19712
- promises.push(
19713
- this.cacheThemeFileUrl(themeId, config.hash, config.file.url)
19714
- );
19715
- }
19716
- await Promise2.all(promises);
19717
- }
19718
- return config ?? null;
19719
- }
19720
20122
  getThemeId() {
19721
20123
  return this.swell.swellHeaders["theme-id"];
19722
20124
  }
19723
20125
  /**
19724
- * Generates a Swell API query filter for this theme version.
20126
+ * Generate a Swell API query filter for this theme version.
19725
20127
  */
19726
20128
  themeVersionQueryFilter() {
19727
20129
  const { swellHeaders } = this.swell;
@@ -19901,7 +20303,6 @@ var SwellTheme3 = class {
19901
20303
  resources;
19902
20304
  liquidSwell;
19903
20305
  themeLoader;
19904
- themeConfigs = null;
19905
20306
  page;
19906
20307
  pageId;
19907
20308
  shopifyCompatibility = null;
@@ -19935,6 +20336,23 @@ var SwellTheme3 = class {
19935
20336
  });
19936
20337
  this.themeLoader = new ThemeLoader(swell);
19937
20338
  }
20339
+ /**
20340
+ * Getter for theme configs - returns the configs from the loader.
20341
+ * Used by editor and tests to access loaded configs.
20342
+ */
20343
+ get themeConfigs() {
20344
+ const configs = this.themeLoader.getConfigs();
20345
+ return configs.size > 0 ? configs : null;
20346
+ }
20347
+ /**
20348
+ * Setter for theme configs - directly sets configs in the loader.
20349
+ * Used by editor and tests to inject configs without API/KV loading.
20350
+ */
20351
+ set themeConfigs(configs) {
20352
+ if (configs) {
20353
+ this.themeLoader.setConfigs(configs);
20354
+ }
20355
+ }
19938
20356
  getSwellAppThemeProps(swellConfig) {
19939
20357
  return swellConfig?.storefront?.theme || {};
19940
20358
  }
@@ -20008,10 +20426,8 @@ var SwellTheme3 = class {
20008
20426
  }
20009
20427
  async getSettingsAndConfigs() {
20010
20428
  const geo = GEO_DATA;
20011
- const [storefrontSettings, settingConfigs] = await Promise.all([
20012
- this.swell.getStorefrontSettings(),
20013
- this.getThemeConfigsByPath("theme/config/", ".json")
20014
- ]);
20429
+ const storefrontSettings = await this.swell.getStorefrontSettings();
20430
+ const settingConfigs = this.getThemeConfigsByPath("theme/config/", ".json");
20015
20431
  const configs = {
20016
20432
  theme: {},
20017
20433
  editor: {},
@@ -20106,7 +20522,7 @@ var SwellTheme3 = class {
20106
20522
  $locale: void 0
20107
20523
  };
20108
20524
  if (pageId) {
20109
- const templateConfig = await this.getThemeTemplateConfigByType(
20525
+ const templateConfig = this._getTemplateConfigByType(
20110
20526
  "templates",
20111
20527
  pageId,
20112
20528
  altTemplate
@@ -20401,7 +20817,7 @@ var SwellTheme3 = class {
20401
20817
  this.shopifyCompatibility.adaptPageData(pageData);
20402
20818
  }
20403
20819
  async getLocaleConfig(localeCode = "en", suffix = ".json") {
20404
- const allLocaleConfigs = await this.getThemeConfigsByPath(
20820
+ const allLocaleConfigs = this.getThemeConfigsByPath(
20405
20821
  "theme/locales/",
20406
20822
  suffix
20407
20823
  );
@@ -20505,22 +20921,24 @@ var SwellTheme3 = class {
20505
20921
  }
20506
20922
  return resolvedUrl;
20507
20923
  }
20508
- async getAllThemeConfigs() {
20509
- if (this.themeConfigs === null) {
20510
- const configs = await this.themeLoader.loadTheme();
20511
- const configsByPath = /* @__PURE__ */ new Map();
20512
- for (const config of configs) {
20513
- configsByPath.set(config.file_path, config);
20514
- }
20515
- this.themeConfigs = configsByPath;
20516
- }
20517
- return this.themeConfigs;
20518
- }
20519
- /**
20520
- * Preloads updated theme configs. Used to optimize initial theme load.
20521
- */
20522
20924
  async preloadThemeConfigs(payload) {
20523
- await this.themeLoader.preloadTheme(payload);
20925
+ const result = await this.themeLoader.updateThemeCache(payload);
20926
+ if (result.warnings && result.warnings.length > 0) {
20927
+ const rejected = result.warnings.filter(
20928
+ (w) => w.reason === "rejected_5mb" || w.reason === "exceeded_25mb"
20929
+ ).length;
20930
+ const warned = result.warnings.filter(
20931
+ (w) => w.reason === "warning_1mb"
20932
+ ).length;
20933
+ logger.warn("[Theme] File size issues detected during cache update", {
20934
+ totalWarnings: result.warnings.length,
20935
+ rejected,
20936
+ warned,
20937
+ details: result.warnings.slice(0, 5)
20938
+ // Log first 5 warnings for debugging
20939
+ });
20940
+ }
20941
+ return result;
20524
20942
  }
20525
20943
  getPageConfigPath(pageId, altTemplate) {
20526
20944
  if (this.shopifyCompatibility) {
@@ -20533,16 +20951,10 @@ var SwellTheme3 = class {
20533
20951
  return `${withSuffix(`theme/templates/${pageId}`, altTemplate)}.json`;
20534
20952
  }
20535
20953
  async getThemeConfig(filePath) {
20536
- if (this.themeConfigs !== null) {
20537
- return this.themeConfigs.get(filePath) ?? null;
20538
- }
20539
- return this.themeLoader.fetchThemeConfig(filePath);
20954
+ return this.themeLoader.getConfig(filePath);
20540
20955
  }
20541
- async getThemeConfigsByPath(pathPrefix, pathSuffix) {
20542
- const configs = await this.themeLoader.fetchThemeConfigsByPath(
20543
- pathPrefix,
20544
- pathSuffix
20545
- );
20956
+ getThemeConfigsByPath(pathPrefix, pathSuffix) {
20957
+ const configs = this.themeLoader.getConfigsByPath(pathPrefix, pathSuffix);
20546
20958
  const configsByPath = /* @__PURE__ */ new Map();
20547
20959
  for (const config of configs) {
20548
20960
  configsByPath.set(config.file_path, config);
@@ -20550,31 +20962,42 @@ var SwellTheme3 = class {
20550
20962
  return configsByPath;
20551
20963
  }
20552
20964
  async getThemeTemplateConfig(filePath) {
20553
- if (filePath.endsWith(".json") || filePath.endsWith(".liquid")) {
20554
- return this.getThemeConfig(filePath);
20555
- }
20556
- const jsonTemplate = await this.getThemeConfig(`${filePath}.json`);
20557
- if (jsonTemplate) {
20558
- return jsonTemplate;
20559
- }
20560
- return this.getThemeConfig(`${filePath}.liquid`);
20965
+ return this._getTemplateConfig(filePath);
20561
20966
  }
20562
- async getThemeTemplateConfigByType(type, name, suffix) {
20967
+ /**
20968
+ * Internal synchronous helper for getting template configs by type.
20969
+ * Used internally within theme.ts to avoid async overhead.
20970
+ */
20971
+ _getTemplateConfigByType(type, name, suffix) {
20563
20972
  const templatesByPriority = [withSuffix(`${type}/${name}`, suffix)];
20564
20973
  if (this.shopifyCompatibility) {
20565
20974
  const path = this.shopifyCompatibility.getThemeFilePath(type, name);
20566
20975
  templatesByPriority.push(withSuffix(path, suffix));
20567
20976
  }
20568
20977
  for (const filePath of templatesByPriority) {
20569
- const templateConfig = await this.getThemeTemplateConfig(
20570
- `theme/${filePath}`
20571
- );
20978
+ const templateConfig = this._getTemplateConfig(`theme/${filePath}`);
20572
20979
  if (templateConfig) {
20573
20980
  return templateConfig;
20574
20981
  }
20575
20982
  }
20576
20983
  return null;
20577
20984
  }
20985
+ /**
20986
+ * Internal synchronous helper for getting template configs.
20987
+ */
20988
+ _getTemplateConfig(filePath) {
20989
+ if (filePath.endsWith(".json") || filePath.endsWith(".liquid")) {
20990
+ return this.themeLoader.getConfig(filePath);
20991
+ }
20992
+ const jsonTemplate = this.themeLoader.getConfig(`${filePath}.json`);
20993
+ if (jsonTemplate) {
20994
+ return jsonTemplate;
20995
+ }
20996
+ return this.themeLoader.getConfig(`${filePath}.liquid`);
20997
+ }
20998
+ async getThemeTemplateConfigByType(type, name, suffix) {
20999
+ return this._getTemplateConfigByType(type, name, suffix);
21000
+ }
20578
21001
  async getAssetConfig(assetName) {
20579
21002
  return await this.getThemeConfig(`theme/assets/${assetName}`) ?? await this.getThemeConfig(`assets/${assetName}`) ?? null;
20580
21003
  }
@@ -20596,17 +21019,8 @@ var SwellTheme3 = class {
20596
21019
  return "";
20597
21020
  }
20598
21021
  template = unescapeLiquidSyntax(template);
20599
- const trace = createTraceId();
20600
21022
  try {
20601
- logger.debug("[SDK] Render template start", {
20602
- config: config.name,
20603
- trace
20604
- });
20605
21023
  const result = await this.liquidSwell.parseAndRender(template, data);
20606
- logger.debug("[SDK] Render template end", {
20607
- config: config.name,
20608
- trace
20609
- });
20610
21024
  return result;
20611
21025
  } catch (err) {
20612
21026
  logger.error(err);
@@ -20623,10 +21037,7 @@ var SwellTheme3 = class {
20623
21037
  }
20624
21038
  async getSectionSchema(sectionName) {
20625
21039
  let result;
20626
- const config = await this.getThemeTemplateConfigByType(
20627
- "sections",
20628
- sectionName
20629
- );
21040
+ const config = this._getTemplateConfigByType("sections", sectionName);
20630
21041
  if (config?.file_path?.endsWith(".json")) {
20631
21042
  try {
20632
21043
  result = import_json56.default.parse(config.file_data) || void 0;
@@ -20704,10 +21115,7 @@ var SwellTheme3 = class {
20704
21115
  return content;
20705
21116
  }
20706
21117
  async renderLayoutTemplate(name, data) {
20707
- const templateConfig = await this.getThemeTemplateConfigByType(
20708
- "layouts",
20709
- name
20710
- );
21118
+ const templateConfig = this._getTemplateConfigByType("layouts", name);
20711
21119
  if (!templateConfig) {
20712
21120
  throw new Error(`Layout template not found: ${name}`);
20713
21121
  }
@@ -20742,17 +21150,14 @@ ${content.slice(pos)}`;
20742
21150
  async renderPageTemplate(name, data, altTemplateId) {
20743
21151
  let templateConfig = null;
20744
21152
  if (altTemplateId) {
20745
- templateConfig = await this.getThemeTemplateConfigByType(
21153
+ templateConfig = this._getTemplateConfigByType(
20746
21154
  "templates",
20747
21155
  name,
20748
21156
  altTemplateId
20749
21157
  );
20750
21158
  }
20751
21159
  if (!templateConfig) {
20752
- templateConfig = await this.getThemeTemplateConfigByType(
20753
- "templates",
20754
- name
20755
- );
21160
+ templateConfig = this._getTemplateConfigByType("templates", name);
20756
21161
  }
20757
21162
  if (templateConfig) {
20758
21163
  const templatePath = name.split("/").splice(1).join("/") || null;
@@ -20853,7 +21258,7 @@ ${content.slice(pos)}`;
20853
21258
  }
20854
21259
  const [sectionKey, originalPageId] = sectionId.split(/__/).reverse();
20855
21260
  const pageId = (originalPageId || "").replaceAll("_", "/");
20856
- const templateConfig = await this.getThemeTemplateConfigByType(
21261
+ const templateConfig = this._getTemplateConfigByType(
20857
21262
  pageId ? "templates" : "sections",
20858
21263
  pageId ? pageId : sectionKey
20859
21264
  );
@@ -21028,7 +21433,7 @@ ${this.shopifyCompatibility.getContentForHeader()}`;
21028
21433
  return defaults;
21029
21434
  }
21030
21435
  async getAllSections() {
21031
- const configs = await this.getThemeConfigsByPath("theme/sections/");
21436
+ const configs = this.getThemeConfigsByPath("theme/sections/");
21032
21437
  return getAllSections(configs, this.getTemplateSchema.bind(this));
21033
21438
  }
21034
21439
  async getPageSections(sectionGroup, resolveSettings = true) {
@@ -21055,7 +21460,7 @@ ${this.shopifyCompatibility.getContentForHeader()}`;
21055
21460
  const sectionName = sectionSchema?.label || sectionFileName;
21056
21461
  let sourcePath = "";
21057
21462
  if (group) {
21058
- const sectionConfig = await this.getThemeTemplateConfigByType(
21463
+ const sectionConfig = this._getTemplateConfigByType(
21059
21464
  "sections",
21060
21465
  `${sectionFileName}.json`
21061
21466
  );
@@ -21072,7 +21477,7 @@ ${this.shopifyCompatibility.getContentForHeader()}`;
21072
21477
  * Get a list of sections and section groups in a page layout.
21073
21478
  */
21074
21479
  async getPageSectionGroups(pageId, altTemplate) {
21075
- const pageConfig = await this.getThemeTemplateConfigByType(
21480
+ const pageConfig = this._getTemplateConfigByType(
21076
21481
  "templates",
21077
21482
  pageId,
21078
21483
  altTemplate
@@ -21139,7 +21544,7 @@ ${this.shopifyCompatibility.getContentForHeader()}`;
21139
21544
  sectionConfigs.map(async (sectionConfig, index) => {
21140
21545
  const { section, schema } = sectionConfig;
21141
21546
  const settings = schema?.fields && this.globals ? resolveSectionSettings(this, sectionConfig, index) : { ...sectionConfig.settings };
21142
- const templateConfig = await this.getThemeTemplateConfigByType(
21547
+ const templateConfig = this._getTemplateConfigByType(
21143
21548
  "sections",
21144
21549
  `${section.type}.liquid`
21145
21550
  );
@@ -21878,6 +22283,7 @@ function getResourceQuery(slug, query) {
21878
22283
  getEasyblocksComponentDefinitions,
21879
22284
  getEasyblocksPagePropsWithConfigs,
21880
22285
  getEasyblocksPageTemplate,
22286
+ getKVFlavor,
21881
22287
  getLayoutSectionGroups,
21882
22288
  getMenuItemStorefrontUrl,
21883
22289
  getMenuItemUrlAndResource,
@@ -21893,6 +22299,7 @@ function getResourceQuery(slug, query) {
21893
22299
  isObject,
21894
22300
  md5,
21895
22301
  removeCircularReferences,
22302
+ resetKVFlavorCache,
21896
22303
  resolveAsyncResources,
21897
22304
  resolveLookupCollection,
21898
22305
  resolveMenuItemUrlAndResource,