@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 +740 -333
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +736 -333
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +738 -333
- package/dist/index.mjs.map +4 -4
- package/dist/src/cache/constants.d.ts +7 -0
- package/dist/src/cache/index.d.ts +4 -0
- package/dist/src/cache/kv-variety.d.ts +10 -0
- package/dist/src/cache/theme-file-storage.d.ts +88 -0
- package/dist/src/cache/worker-cache-proxy.d.ts +31 -0
- package/dist/src/theme/theme-loader.d.ts +36 -44
- package/dist/src/theme.d.ts +22 -7
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/src/utils/kv-flavor.d.ts +7 -0
- package/dist/types/cloudflare.d.ts +14 -3
- package/package.json +1 -1
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
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
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
|
|
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.
|
|
19409
|
-
|
|
19410
|
-
|
|
19411
|
-
|
|
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
|
-
*
|
|
19888
|
+
* Get a single config by file path (synchronous).
|
|
19889
|
+
* Returns null if config not found.
|
|
19416
19890
|
*/
|
|
19417
|
-
|
|
19418
|
-
|
|
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
|
-
*
|
|
19895
|
+
* Get all loaded configs.
|
|
19896
|
+
* Used by theme getter to expose configs to editor/tests.
|
|
19430
19897
|
*/
|
|
19431
|
-
|
|
19432
|
-
|
|
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
|
-
*
|
|
19902
|
+
* Get multiple configs by path pattern (synchronous).
|
|
19903
|
+
* Filters configs by prefix and optional suffix.
|
|
19441
19904
|
*/
|
|
19442
|
-
|
|
19443
|
-
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
|
|
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
|
-
|
|
19912
|
+
return results;
|
|
19449
19913
|
}
|
|
19450
19914
|
/**
|
|
19451
|
-
*
|
|
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
|
-
|
|
19454
|
-
|
|
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
|
-
*
|
|
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
|
|
19488
|
-
const
|
|
19489
|
-
if (
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
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
|
|
19497
|
-
const
|
|
19498
|
-
|
|
19499
|
-
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
19505
|
-
|
|
19506
|
-
|
|
19507
|
-
|
|
19508
|
-
|
|
19509
|
-
|
|
19510
|
-
|
|
19511
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
19530
|
-
const
|
|
19531
|
-
|
|
19532
|
-
|
|
19533
|
-
|
|
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
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
);
|
|
19539
|
-
|
|
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
|
-
*
|
|
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
|
|
19549
|
-
const
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
19557
|
-
|
|
19558
|
-
|
|
19559
|
-
|
|
19560
|
-
|
|
19561
|
-
|
|
19562
|
-
|
|
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
|
-
|
|
19587
|
-
|
|
19588
|
-
|
|
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
|
-
|
|
19592
|
-
|
|
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
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
19608
|
-
|
|
19609
|
-
|
|
19610
|
-
|
|
19611
|
-
|
|
19612
|
-
|
|
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
|
-
*
|
|
20039
|
+
* Helper to ensure all configs have file_data.
|
|
20040
|
+
* Fetches missing data from API and updates KV cache.
|
|
19617
20041
|
*/
|
|
19618
|
-
async
|
|
19619
|
-
|
|
19620
|
-
|
|
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
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
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
|
-
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
|
|
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
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
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
|
|
20089
|
+
return mergedConfigs;
|
|
19657
20090
|
}
|
|
19658
20091
|
/**
|
|
19659
|
-
* Fetches
|
|
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
|
-
|
|
19667
|
-
`
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
20012
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
20537
|
-
return this.themeConfigs.get(filePath) ?? null;
|
|
20538
|
-
}
|
|
20539
|
-
return this.themeLoader.fetchThemeConfig(filePath);
|
|
20954
|
+
return this.themeLoader.getConfig(filePath);
|
|
20540
20955
|
}
|
|
20541
|
-
|
|
20542
|
-
const configs =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
21153
|
+
templateConfig = this._getTemplateConfigByType(
|
|
20746
21154
|
"templates",
|
|
20747
21155
|
name,
|
|
20748
21156
|
altTemplateId
|
|
20749
21157
|
);
|
|
20750
21158
|
}
|
|
20751
21159
|
if (!templateConfig) {
|
|
20752
|
-
templateConfig =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|