@gravito/constellation 1.0.0-alpha.6 → 1.0.0
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/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/DiskSitemapStorage-7ZZMGC4K.js +6 -0
- package/dist/chunk-7WHLC3OJ.js +56 -0
- package/dist/index.cjs +515 -254
- package/dist/index.d.cts +821 -0
- package/dist/index.d.ts +821 -0
- package/dist/{index.mjs → index.js} +413 -258
- package/package.json +21 -11
|
@@ -1,76 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
-
for (let key of __getOwnPropNames(mod))
|
|
11
|
-
if (!__hasOwnProp.call(to, key))
|
|
12
|
-
__defProp(to, key, {
|
|
13
|
-
get: () => mod[key],
|
|
14
|
-
enumerable: true
|
|
15
|
-
});
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __export = (target, all) => {
|
|
19
|
-
for (var name in all)
|
|
20
|
-
__defProp(target, name, {
|
|
21
|
-
get: all[name],
|
|
22
|
-
enumerable: true,
|
|
23
|
-
configurable: true,
|
|
24
|
-
set: (newValue) => all[name] = () => newValue
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
28
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
29
|
-
|
|
30
|
-
// src/storage/DiskSitemapStorage.ts
|
|
31
|
-
var exports_DiskSitemapStorage = {};
|
|
32
|
-
__export(exports_DiskSitemapStorage, {
|
|
33
|
-
DiskSitemapStorage: () => DiskSitemapStorage
|
|
34
|
-
});
|
|
35
|
-
import fs from "node:fs/promises";
|
|
36
|
-
import path from "node:path";
|
|
37
|
-
|
|
38
|
-
class DiskSitemapStorage {
|
|
39
|
-
outDir;
|
|
40
|
-
baseUrl;
|
|
41
|
-
constructor(outDir, baseUrl) {
|
|
42
|
-
this.outDir = outDir;
|
|
43
|
-
this.baseUrl = baseUrl;
|
|
44
|
-
}
|
|
45
|
-
async write(filename, content) {
|
|
46
|
-
await fs.mkdir(this.outDir, { recursive: true });
|
|
47
|
-
await fs.writeFile(path.join(this.outDir, filename), content);
|
|
48
|
-
}
|
|
49
|
-
async read(filename) {
|
|
50
|
-
try {
|
|
51
|
-
return await fs.readFile(path.join(this.outDir, filename), "utf-8");
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async exists(filename) {
|
|
57
|
-
try {
|
|
58
|
-
await fs.access(path.join(this.outDir, filename));
|
|
59
|
-
return true;
|
|
60
|
-
} catch {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
getUrl(filename) {
|
|
65
|
-
const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
66
|
-
const file = filename.startsWith("/") ? filename.slice(1) : filename;
|
|
67
|
-
return `${base}/${file}`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
var init_DiskSitemapStorage = () => {};
|
|
1
|
+
import {
|
|
2
|
+
DiskSitemapStorage
|
|
3
|
+
} from "./chunk-7WHLC3OJ.js";
|
|
71
4
|
|
|
72
5
|
// src/core/ChangeTracker.ts
|
|
73
|
-
|
|
6
|
+
var MemoryChangeTracker = class {
|
|
74
7
|
changes = [];
|
|
75
8
|
maxChanges;
|
|
76
9
|
constructor(options = {}) {
|
|
@@ -98,9 +31,8 @@ class MemoryChangeTracker {
|
|
|
98
31
|
}
|
|
99
32
|
this.changes = this.changes.filter((change) => change.timestamp < since);
|
|
100
33
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
class RedisChangeTracker {
|
|
34
|
+
};
|
|
35
|
+
var RedisChangeTracker = class {
|
|
104
36
|
client;
|
|
105
37
|
keyPrefix;
|
|
106
38
|
ttl;
|
|
@@ -175,18 +107,23 @@ class RedisChangeTracker {
|
|
|
175
107
|
await this.client.zrem(listKey, url);
|
|
176
108
|
}
|
|
177
109
|
}
|
|
178
|
-
} catch {
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
179
112
|
}
|
|
180
|
-
}
|
|
113
|
+
};
|
|
114
|
+
|
|
181
115
|
// src/core/DiffCalculator.ts
|
|
182
|
-
|
|
116
|
+
var DiffCalculator = class {
|
|
183
117
|
batchSize;
|
|
184
118
|
constructor(options = {}) {
|
|
185
119
|
this.batchSize = options.batchSize || 1e4;
|
|
186
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* 計算兩個 sitemap 狀態的差異
|
|
123
|
+
*/
|
|
187
124
|
calculate(oldEntries, newEntries) {
|
|
188
|
-
const oldMap = new Map;
|
|
189
|
-
const newMap = new Map;
|
|
125
|
+
const oldMap = /* @__PURE__ */ new Map();
|
|
126
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
190
127
|
for (const entry of oldEntries) {
|
|
191
128
|
oldMap.set(entry.url, entry);
|
|
192
129
|
}
|
|
@@ -211,9 +148,12 @@ class DiffCalculator {
|
|
|
211
148
|
}
|
|
212
149
|
return { added, updated, removed };
|
|
213
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* 批次計算差異(用於大量 URL)
|
|
153
|
+
*/
|
|
214
154
|
async calculateBatch(oldEntries, newEntries) {
|
|
215
|
-
const oldMap = new Map;
|
|
216
|
-
const newMap = new Map;
|
|
155
|
+
const oldMap = /* @__PURE__ */ new Map();
|
|
156
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
217
157
|
for await (const entry of oldEntries) {
|
|
218
158
|
oldMap.set(entry.url, entry);
|
|
219
159
|
}
|
|
@@ -222,8 +162,11 @@ class DiffCalculator {
|
|
|
222
162
|
}
|
|
223
163
|
return this.calculate(Array.from(oldMap.values()), Array.from(newMap.values()));
|
|
224
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* 從變更記錄計算差異
|
|
167
|
+
*/
|
|
225
168
|
calculateFromChanges(baseEntries, changes) {
|
|
226
|
-
const entryMap = new Map;
|
|
169
|
+
const entryMap = /* @__PURE__ */ new Map();
|
|
227
170
|
for (const entry of baseEntries) {
|
|
228
171
|
entryMap.set(entry.url, entry);
|
|
229
172
|
}
|
|
@@ -239,6 +182,9 @@ class DiffCalculator {
|
|
|
239
182
|
const newEntries = Array.from(entryMap.values());
|
|
240
183
|
return this.calculate(baseEntries, newEntries);
|
|
241
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* 檢查 entry 是否有變更
|
|
187
|
+
*/
|
|
242
188
|
hasChanged(oldEntry, newEntry) {
|
|
243
189
|
if (oldEntry.lastmod !== newEntry.lastmod) {
|
|
244
190
|
return true;
|
|
@@ -256,9 +202,10 @@ class DiffCalculator {
|
|
|
256
202
|
}
|
|
257
203
|
return false;
|
|
258
204
|
}
|
|
259
|
-
}
|
|
205
|
+
};
|
|
206
|
+
|
|
260
207
|
// src/core/ShadowProcessor.ts
|
|
261
|
-
|
|
208
|
+
var ShadowProcessor = class {
|
|
262
209
|
options;
|
|
263
210
|
shadowId;
|
|
264
211
|
operations = [];
|
|
@@ -266,6 +213,9 @@ class ShadowProcessor {
|
|
|
266
213
|
this.options = options;
|
|
267
214
|
this.shadowId = `shadow-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
268
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* 添加一個影子操作
|
|
218
|
+
*/
|
|
269
219
|
async addOperation(operation) {
|
|
270
220
|
if (!this.options.enabled) {
|
|
271
221
|
await this.options.storage.write(operation.filename, operation.content);
|
|
@@ -281,6 +231,9 @@ class ShadowProcessor {
|
|
|
281
231
|
await this.options.storage.write(operation.filename, operation.content);
|
|
282
232
|
}
|
|
283
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* 提交所有影子操作
|
|
236
|
+
*/
|
|
284
237
|
async commit() {
|
|
285
238
|
if (!this.options.enabled) {
|
|
286
239
|
return;
|
|
@@ -298,22 +251,31 @@ class ShadowProcessor {
|
|
|
298
251
|
}
|
|
299
252
|
this.operations = [];
|
|
300
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* 取消所有影子操作
|
|
256
|
+
*/
|
|
301
257
|
async rollback() {
|
|
302
258
|
if (!this.options.enabled) {
|
|
303
259
|
return;
|
|
304
260
|
}
|
|
305
261
|
this.operations = [];
|
|
306
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* 獲取當前影子 ID
|
|
265
|
+
*/
|
|
307
266
|
getShadowId() {
|
|
308
267
|
return this.shadowId;
|
|
309
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* 獲取所有操作
|
|
271
|
+
*/
|
|
310
272
|
getOperations() {
|
|
311
273
|
return [...this.operations];
|
|
312
274
|
}
|
|
313
|
-
}
|
|
275
|
+
};
|
|
314
276
|
|
|
315
277
|
// src/core/SitemapIndex.ts
|
|
316
|
-
|
|
278
|
+
var SitemapIndex = class {
|
|
317
279
|
options;
|
|
318
280
|
entries = [];
|
|
319
281
|
constructor(options) {
|
|
@@ -344,8 +306,7 @@ class SitemapIndex {
|
|
|
344
306
|
`;
|
|
345
307
|
const indent = pretty ? " " : "";
|
|
346
308
|
const subIndent = pretty ? " " : "";
|
|
347
|
-
const nl = pretty ?
|
|
348
|
-
` : "";
|
|
309
|
+
const nl = pretty ? "\n" : "";
|
|
349
310
|
for (const entry of this.entries) {
|
|
350
311
|
let loc = entry.url;
|
|
351
312
|
if (!loc.startsWith("http")) {
|
|
@@ -368,10 +329,10 @@ class SitemapIndex {
|
|
|
368
329
|
escape(str) {
|
|
369
330
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
370
331
|
}
|
|
371
|
-
}
|
|
332
|
+
};
|
|
372
333
|
|
|
373
334
|
// src/core/SitemapStream.ts
|
|
374
|
-
|
|
335
|
+
var SitemapStream = class {
|
|
375
336
|
options;
|
|
376
337
|
entries = [];
|
|
377
338
|
constructor(options) {
|
|
@@ -422,8 +383,7 @@ class SitemapStream {
|
|
|
422
383
|
renderUrl(entry, baseUrl, pretty) {
|
|
423
384
|
const indent = pretty ? " " : "";
|
|
424
385
|
const subIndent = pretty ? " " : "";
|
|
425
|
-
const nl = pretty ?
|
|
426
|
-
` : "";
|
|
386
|
+
const nl = pretty ? "\n" : "";
|
|
427
387
|
let loc = entry.url;
|
|
428
388
|
if (!loc.startsWith("http")) {
|
|
429
389
|
if (!loc.startsWith("/")) {
|
|
@@ -440,7 +400,7 @@ class SitemapStream {
|
|
|
440
400
|
if (entry.changefreq) {
|
|
441
401
|
item += `${subIndent}<changefreq>${entry.changefreq}</changefreq>${nl}`;
|
|
442
402
|
}
|
|
443
|
-
if (entry.priority !==
|
|
403
|
+
if (entry.priority !== void 0) {
|
|
444
404
|
item += `${subIndent}<priority>${entry.priority.toFixed(1)}</priority>${nl}`;
|
|
445
405
|
}
|
|
446
406
|
if (entry.alternates) {
|
|
@@ -466,7 +426,7 @@ class SitemapStream {
|
|
|
466
426
|
item += `${subIndent}<xhtml:link rel="canonical" href="${this.escape(canonicalUrl)}"/>${nl}`;
|
|
467
427
|
}
|
|
468
428
|
if (entry.redirect && !entry.redirect.canonical) {
|
|
469
|
-
item += `${subIndent}<!-- Redirect: ${entry.redirect.from}
|
|
429
|
+
item += `${subIndent}<!-- Redirect: ${entry.redirect.from} \u2192 ${entry.redirect.to} (${entry.redirect.type}) -->${nl}`;
|
|
470
430
|
}
|
|
471
431
|
if (entry.images) {
|
|
472
432
|
for (const img of entry.images) {
|
|
@@ -562,15 +522,15 @@ class SitemapStream {
|
|
|
562
522
|
escape(str) {
|
|
563
523
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
564
524
|
}
|
|
565
|
-
}
|
|
525
|
+
};
|
|
566
526
|
|
|
567
527
|
// src/core/SitemapGenerator.ts
|
|
568
|
-
|
|
528
|
+
var SitemapGenerator = class {
|
|
569
529
|
options;
|
|
570
530
|
shadowProcessor = null;
|
|
571
531
|
constructor(options) {
|
|
572
532
|
this.options = {
|
|
573
|
-
maxEntriesPerFile:
|
|
533
|
+
maxEntriesPerFile: 5e4,
|
|
574
534
|
filename: "sitemap.xml",
|
|
575
535
|
...options
|
|
576
536
|
};
|
|
@@ -608,7 +568,7 @@ class SitemapGenerator {
|
|
|
608
568
|
const url = this.options.storage.getUrl(filename);
|
|
609
569
|
index.add({
|
|
610
570
|
url,
|
|
611
|
-
lastmod: new Date
|
|
571
|
+
lastmod: /* @__PURE__ */ new Date()
|
|
612
572
|
});
|
|
613
573
|
shardIndex++;
|
|
614
574
|
currentCount = 0;
|
|
@@ -649,13 +609,16 @@ class SitemapGenerator {
|
|
|
649
609
|
await this.options.storage.write(this.options.filename, indexXml);
|
|
650
610
|
}
|
|
651
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* 獲取影子處理器(如果啟用)
|
|
614
|
+
*/
|
|
652
615
|
getShadowProcessor() {
|
|
653
616
|
return this.shadowProcessor;
|
|
654
617
|
}
|
|
655
|
-
}
|
|
618
|
+
};
|
|
656
619
|
|
|
657
620
|
// src/core/IncrementalGenerator.ts
|
|
658
|
-
|
|
621
|
+
var IncrementalGenerator = class {
|
|
659
622
|
options;
|
|
660
623
|
changeTracker;
|
|
661
624
|
diffCalculator;
|
|
@@ -663,9 +626,12 @@ class IncrementalGenerator {
|
|
|
663
626
|
constructor(options) {
|
|
664
627
|
this.options = options;
|
|
665
628
|
this.changeTracker = options.changeTracker;
|
|
666
|
-
this.diffCalculator = options.diffCalculator || new DiffCalculator;
|
|
629
|
+
this.diffCalculator = options.diffCalculator || new DiffCalculator();
|
|
667
630
|
this.generator = new SitemapGenerator(options);
|
|
668
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* 生成完整的 sitemap(首次生成)
|
|
634
|
+
*/
|
|
669
635
|
async generateFull() {
|
|
670
636
|
await this.generator.run();
|
|
671
637
|
if (this.options.autoTrack) {
|
|
@@ -678,12 +644,15 @@ class IncrementalGenerator {
|
|
|
678
644
|
type: "add",
|
|
679
645
|
url: entry.url,
|
|
680
646
|
entry,
|
|
681
|
-
timestamp: new Date
|
|
647
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
682
648
|
});
|
|
683
649
|
}
|
|
684
650
|
}
|
|
685
651
|
}
|
|
686
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* 增量生成(只更新變更的部分)
|
|
655
|
+
*/
|
|
687
656
|
async generateIncremental(since) {
|
|
688
657
|
const changes = await this.changeTracker.getChanges(since);
|
|
689
658
|
if (changes.length === 0) {
|
|
@@ -693,12 +662,21 @@ class IncrementalGenerator {
|
|
|
693
662
|
const diff = this.diffCalculator.calculateFromChanges(baseEntries, changes);
|
|
694
663
|
await this.generateDiff(diff);
|
|
695
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* 手動追蹤變更
|
|
667
|
+
*/
|
|
696
668
|
async trackChange(change) {
|
|
697
669
|
await this.changeTracker.track(change);
|
|
698
670
|
}
|
|
671
|
+
/**
|
|
672
|
+
* 獲取變更記錄
|
|
673
|
+
*/
|
|
699
674
|
async getChanges(since) {
|
|
700
675
|
return this.changeTracker.getChanges(since);
|
|
701
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* 載入基礎 entries(從現有 sitemap)
|
|
679
|
+
*/
|
|
702
680
|
async loadBaseEntries() {
|
|
703
681
|
const entries = [];
|
|
704
682
|
const { providers } = this.options;
|
|
@@ -709,9 +687,15 @@ class IncrementalGenerator {
|
|
|
709
687
|
}
|
|
710
688
|
return entries;
|
|
711
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* 生成差異部分
|
|
692
|
+
*/
|
|
712
693
|
async generateDiff(_diff) {
|
|
713
694
|
await this.generator.run();
|
|
714
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* 將 AsyncIterable 轉換為陣列
|
|
698
|
+
*/
|
|
715
699
|
async toArray(iterable) {
|
|
716
700
|
const array = [];
|
|
717
701
|
for await (const item of iterable) {
|
|
@@ -719,17 +703,21 @@ class IncrementalGenerator {
|
|
|
719
703
|
}
|
|
720
704
|
return array;
|
|
721
705
|
}
|
|
722
|
-
}
|
|
706
|
+
};
|
|
707
|
+
|
|
723
708
|
// src/core/ProgressTracker.ts
|
|
724
|
-
|
|
709
|
+
var ProgressTracker = class {
|
|
725
710
|
storage;
|
|
726
711
|
updateInterval;
|
|
727
712
|
currentProgress = null;
|
|
728
713
|
updateTimer = null;
|
|
729
714
|
constructor(options) {
|
|
730
715
|
this.storage = options.storage;
|
|
731
|
-
this.updateInterval = options.updateInterval ||
|
|
716
|
+
this.updateInterval = options.updateInterval || 1e3;
|
|
732
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* 初始化進度追蹤
|
|
720
|
+
*/
|
|
733
721
|
async init(jobId, total) {
|
|
734
722
|
this.currentProgress = {
|
|
735
723
|
jobId,
|
|
@@ -737,10 +725,13 @@ class ProgressTracker {
|
|
|
737
725
|
total,
|
|
738
726
|
processed: 0,
|
|
739
727
|
percentage: 0,
|
|
740
|
-
startTime: new Date
|
|
728
|
+
startTime: /* @__PURE__ */ new Date()
|
|
741
729
|
};
|
|
742
730
|
await this.storage.set(jobId, this.currentProgress);
|
|
743
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* 更新進度
|
|
734
|
+
*/
|
|
744
735
|
async update(processed, status) {
|
|
745
736
|
if (!this.currentProgress) {
|
|
746
737
|
return;
|
|
@@ -760,26 +751,35 @@ class ProgressTracker {
|
|
|
760
751
|
}, this.updateInterval);
|
|
761
752
|
}
|
|
762
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* 完成進度追蹤
|
|
756
|
+
*/
|
|
763
757
|
async complete() {
|
|
764
758
|
if (!this.currentProgress) {
|
|
765
759
|
return;
|
|
766
760
|
}
|
|
767
761
|
this.currentProgress.status = "completed";
|
|
768
|
-
this.currentProgress.endTime = new Date;
|
|
762
|
+
this.currentProgress.endTime = /* @__PURE__ */ new Date();
|
|
769
763
|
this.currentProgress.percentage = 100;
|
|
770
764
|
await this.flush();
|
|
771
765
|
this.stop();
|
|
772
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* 標記為失敗
|
|
769
|
+
*/
|
|
773
770
|
async fail(error) {
|
|
774
771
|
if (!this.currentProgress) {
|
|
775
772
|
return;
|
|
776
773
|
}
|
|
777
774
|
this.currentProgress.status = "failed";
|
|
778
|
-
this.currentProgress.endTime = new Date;
|
|
775
|
+
this.currentProgress.endTime = /* @__PURE__ */ new Date();
|
|
779
776
|
this.currentProgress.error = error;
|
|
780
777
|
await this.flush();
|
|
781
778
|
this.stop();
|
|
782
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* 刷新進度到儲存
|
|
782
|
+
*/
|
|
783
783
|
async flush() {
|
|
784
784
|
if (!this.currentProgress) {
|
|
785
785
|
return;
|
|
@@ -792,16 +792,23 @@ class ProgressTracker {
|
|
|
792
792
|
error: this.currentProgress.error
|
|
793
793
|
});
|
|
794
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* 停止更新計時器
|
|
797
|
+
*/
|
|
795
798
|
stop() {
|
|
796
799
|
if (this.updateTimer) {
|
|
797
800
|
clearInterval(this.updateTimer);
|
|
798
801
|
this.updateTimer = null;
|
|
799
802
|
}
|
|
800
803
|
}
|
|
804
|
+
/**
|
|
805
|
+
* 獲取當前進度
|
|
806
|
+
*/
|
|
801
807
|
getCurrentProgress() {
|
|
802
808
|
return this.currentProgress ? { ...this.currentProgress } : null;
|
|
803
809
|
}
|
|
804
|
-
}
|
|
810
|
+
};
|
|
811
|
+
|
|
805
812
|
// src/helpers/I18nSitemap.ts
|
|
806
813
|
function generateI18nEntries(path, locales, baseUrl = "", options = {}) {
|
|
807
814
|
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -820,9 +827,10 @@ function generateI18nEntries(path, locales, baseUrl = "", options = {}) {
|
|
|
820
827
|
};
|
|
821
828
|
});
|
|
822
829
|
}
|
|
830
|
+
|
|
823
831
|
// src/jobs/GenerateSitemapJob.ts
|
|
824
832
|
import { Job } from "@gravito/stream";
|
|
825
|
-
|
|
833
|
+
var GenerateSitemapJob = class extends Job {
|
|
826
834
|
options;
|
|
827
835
|
generator;
|
|
828
836
|
totalEntries = 0;
|
|
@@ -861,6 +869,9 @@ class GenerateSitemapJob extends Job {
|
|
|
861
869
|
throw err;
|
|
862
870
|
}
|
|
863
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* 計算總 URL 數
|
|
874
|
+
*/
|
|
864
875
|
async calculateTotal() {
|
|
865
876
|
let total = 0;
|
|
866
877
|
const { providers } = this.options.generatorOptions;
|
|
@@ -876,11 +887,14 @@ class GenerateSitemapJob extends Job {
|
|
|
876
887
|
}
|
|
877
888
|
return total;
|
|
878
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* 帶進度追蹤的生成
|
|
892
|
+
*/
|
|
879
893
|
async generateWithProgress() {
|
|
880
894
|
const { progressTracker, shadowProcessor, onProgress } = this.options;
|
|
881
895
|
const {
|
|
882
896
|
providers,
|
|
883
|
-
maxEntriesPerFile =
|
|
897
|
+
maxEntriesPerFile = 5e4,
|
|
884
898
|
storage,
|
|
885
899
|
baseUrl,
|
|
886
900
|
pretty,
|
|
@@ -899,20 +913,24 @@ class GenerateSitemapJob extends Job {
|
|
|
899
913
|
});
|
|
900
914
|
}
|
|
901
915
|
}
|
|
902
|
-
}
|
|
916
|
+
};
|
|
917
|
+
|
|
903
918
|
// src/OrbitSitemap.ts
|
|
904
|
-
import { randomUUID } from "
|
|
919
|
+
import { randomUUID } from "crypto";
|
|
905
920
|
|
|
906
921
|
// src/redirect/RedirectHandler.ts
|
|
907
|
-
|
|
922
|
+
var RedirectHandler = class {
|
|
908
923
|
options;
|
|
909
924
|
constructor(options) {
|
|
910
925
|
this.options = options;
|
|
911
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* 處理 entries 中的轉址
|
|
929
|
+
*/
|
|
912
930
|
async processEntries(entries) {
|
|
913
931
|
const { manager, strategy, followChains, maxChainLength } = this.options;
|
|
914
932
|
const _processedEntries = [];
|
|
915
|
-
const redirectMap = new Map;
|
|
933
|
+
const redirectMap = /* @__PURE__ */ new Map();
|
|
916
934
|
for (const entry of entries) {
|
|
917
935
|
const redirectTarget = await manager.resolve(entry.url, followChains, maxChainLength);
|
|
918
936
|
if (redirectTarget && entry.url !== redirectTarget) {
|
|
@@ -920,6 +938,7 @@ class RedirectHandler {
|
|
|
920
938
|
from: entry.url,
|
|
921
939
|
to: redirectTarget,
|
|
922
940
|
type: 301
|
|
941
|
+
// Default to 301 for resolved chains
|
|
923
942
|
});
|
|
924
943
|
}
|
|
925
944
|
}
|
|
@@ -936,9 +955,12 @@ class RedirectHandler {
|
|
|
936
955
|
return entries;
|
|
937
956
|
}
|
|
938
957
|
}
|
|
958
|
+
/**
|
|
959
|
+
* 策略一:移除舊 URL,加入新 URL
|
|
960
|
+
*/
|
|
939
961
|
handleRemoveOldAddNew(entries, redirectMap) {
|
|
940
962
|
const processed = [];
|
|
941
|
-
const redirectedUrls = new Set;
|
|
963
|
+
const redirectedUrls = /* @__PURE__ */ new Set();
|
|
942
964
|
for (const entry of entries) {
|
|
943
965
|
const redirect = redirectMap.get(entry.url);
|
|
944
966
|
if (redirect) {
|
|
@@ -958,6 +980,9 @@ class RedirectHandler {
|
|
|
958
980
|
}
|
|
959
981
|
return processed;
|
|
960
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* 策略二:保留關聯,使用 canonical link
|
|
985
|
+
*/
|
|
961
986
|
handleKeepRelation(entries, redirectMap) {
|
|
962
987
|
const processed = [];
|
|
963
988
|
for (const entry of entries) {
|
|
@@ -978,6 +1003,9 @@ class RedirectHandler {
|
|
|
978
1003
|
}
|
|
979
1004
|
return processed;
|
|
980
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* 策略三:僅更新 URL
|
|
1008
|
+
*/
|
|
981
1009
|
handleUpdateUrl(entries, redirectMap) {
|
|
982
1010
|
return entries.map((entry) => {
|
|
983
1011
|
const redirect = redirectMap.get(entry.url);
|
|
@@ -995,9 +1023,12 @@ class RedirectHandler {
|
|
|
995
1023
|
return entry;
|
|
996
1024
|
});
|
|
997
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* 策略四:雙重標記
|
|
1028
|
+
*/
|
|
998
1029
|
handleDualMark(entries, redirectMap) {
|
|
999
1030
|
const processed = [];
|
|
1000
|
-
const addedUrls = new Set;
|
|
1031
|
+
const addedUrls = /* @__PURE__ */ new Set();
|
|
1001
1032
|
for (const entry of entries) {
|
|
1002
1033
|
const redirect = redirectMap.get(entry.url);
|
|
1003
1034
|
if (redirect) {
|
|
@@ -1027,15 +1058,14 @@ class RedirectHandler {
|
|
|
1027
1058
|
}
|
|
1028
1059
|
return processed;
|
|
1029
1060
|
}
|
|
1030
|
-
}
|
|
1061
|
+
};
|
|
1031
1062
|
|
|
1032
1063
|
// src/storage/MemorySitemapStorage.ts
|
|
1033
|
-
|
|
1034
|
-
baseUrl;
|
|
1035
|
-
files = new Map;
|
|
1064
|
+
var MemorySitemapStorage = class {
|
|
1036
1065
|
constructor(baseUrl) {
|
|
1037
1066
|
this.baseUrl = baseUrl;
|
|
1038
1067
|
}
|
|
1068
|
+
files = /* @__PURE__ */ new Map();
|
|
1039
1069
|
async write(filename, content) {
|
|
1040
1070
|
this.files.set(filename, content);
|
|
1041
1071
|
}
|
|
@@ -1050,28 +1080,60 @@ class MemorySitemapStorage {
|
|
|
1050
1080
|
const file = filename.startsWith("/") ? filename.slice(1) : filename;
|
|
1051
1081
|
return `${base}/${file}`;
|
|
1052
1082
|
}
|
|
1053
|
-
}
|
|
1083
|
+
};
|
|
1054
1084
|
|
|
1055
1085
|
// src/OrbitSitemap.ts
|
|
1056
|
-
|
|
1086
|
+
function sanitizeFilename(value) {
|
|
1087
|
+
if (!value) {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
if (value.includes("\0")) {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
if (value.includes("..")) {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
return value;
|
|
1100
|
+
}
|
|
1101
|
+
var OrbitSitemap = class _OrbitSitemap {
|
|
1057
1102
|
options;
|
|
1058
1103
|
mode;
|
|
1059
1104
|
constructor(mode, options) {
|
|
1060
1105
|
this.mode = mode;
|
|
1061
1106
|
this.options = options;
|
|
1062
1107
|
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Create a dynamic sitemap configuration.
|
|
1110
|
+
*
|
|
1111
|
+
* @param options - The dynamic sitemap options.
|
|
1112
|
+
* @returns An OrbitSitemap instance configured for dynamic generation.
|
|
1113
|
+
*/
|
|
1063
1114
|
static dynamic(options) {
|
|
1064
|
-
return new
|
|
1115
|
+
return new _OrbitSitemap("dynamic", {
|
|
1065
1116
|
path: "/sitemap.xml",
|
|
1066
1117
|
...options
|
|
1067
1118
|
});
|
|
1068
1119
|
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Create a static sitemap configuration.
|
|
1122
|
+
*
|
|
1123
|
+
* @param options - The static sitemap options.
|
|
1124
|
+
* @returns An OrbitSitemap instance configured for static generation.
|
|
1125
|
+
*/
|
|
1069
1126
|
static static(options) {
|
|
1070
|
-
return new
|
|
1127
|
+
return new _OrbitSitemap("static", {
|
|
1071
1128
|
filename: "sitemap.xml",
|
|
1072
1129
|
...options
|
|
1073
1130
|
});
|
|
1074
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Install the sitemap module into PlanetCore.
|
|
1134
|
+
*
|
|
1135
|
+
* @param core - The PlanetCore instance.
|
|
1136
|
+
*/
|
|
1075
1137
|
install(core) {
|
|
1076
1138
|
if (this.mode === "dynamic") {
|
|
1077
1139
|
this.installDynamic(core);
|
|
@@ -1083,10 +1145,14 @@ class OrbitSitemap {
|
|
|
1083
1145
|
const opts = this.options;
|
|
1084
1146
|
const storage = opts.storage ?? new MemorySitemapStorage(opts.baseUrl);
|
|
1085
1147
|
const indexFilename = opts.path?.split("/").pop() ?? "sitemap.xml";
|
|
1086
|
-
const baseDir = opts.path ? opts.path.substring(0, opts.path.lastIndexOf("/")) :
|
|
1148
|
+
const baseDir = opts.path ? opts.path.substring(0, opts.path.lastIndexOf("/")) : void 0;
|
|
1087
1149
|
const handler = async (ctx) => {
|
|
1088
1150
|
const reqPath = ctx.req.path;
|
|
1089
|
-
const
|
|
1151
|
+
const rawName = reqPath.split("/").pop() || indexFilename;
|
|
1152
|
+
const filename = sanitizeFilename(rawName);
|
|
1153
|
+
if (!filename) {
|
|
1154
|
+
return ctx.text("Not Found", 404);
|
|
1155
|
+
}
|
|
1090
1156
|
const isIndex = filename === indexFilename;
|
|
1091
1157
|
let content = await storage.read(filename);
|
|
1092
1158
|
if (!content && isIndex) {
|
|
@@ -1131,6 +1197,12 @@ class OrbitSitemap {
|
|
|
1131
1197
|
const shardRoute = `${baseDir}/${basename}-:shard.xml`;
|
|
1132
1198
|
core.router.get(shardRoute, handler);
|
|
1133
1199
|
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Generate the sitemap (static mode only).
|
|
1202
|
+
*
|
|
1203
|
+
* @returns A promise that resolves when generation is complete.
|
|
1204
|
+
* @throws {Error} If called in dynamic mode.
|
|
1205
|
+
*/
|
|
1134
1206
|
async generate() {
|
|
1135
1207
|
if (this.mode !== "static") {
|
|
1136
1208
|
throw new Error("generate() can only be called in static mode");
|
|
@@ -1138,7 +1210,7 @@ class OrbitSitemap {
|
|
|
1138
1210
|
const opts = this.options;
|
|
1139
1211
|
let storage = opts.storage;
|
|
1140
1212
|
if (!storage) {
|
|
1141
|
-
const { DiskSitemapStorage: DiskSitemapStorage2 } = await
|
|
1213
|
+
const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-7ZZMGC4K.js");
|
|
1142
1214
|
storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
|
|
1143
1215
|
}
|
|
1144
1216
|
let providers = opts.providers;
|
|
@@ -1167,6 +1239,13 @@ class OrbitSitemap {
|
|
|
1167
1239
|
await generator.run();
|
|
1168
1240
|
console.log(`[OrbitSitemap] Generated sitemap in ${opts.outDir}`);
|
|
1169
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Generate incremental sitemap updates (static mode only).
|
|
1244
|
+
*
|
|
1245
|
+
* @param since - Only include items modified since this date.
|
|
1246
|
+
* @returns A promise that resolves when incremental generation is complete.
|
|
1247
|
+
* @throws {Error} If called in dynamic mode, or if incremental generation is not enabled/configured.
|
|
1248
|
+
*/
|
|
1170
1249
|
async generateIncremental(since) {
|
|
1171
1250
|
if (this.mode !== "static") {
|
|
1172
1251
|
throw new Error("generateIncremental() can only be called in static mode");
|
|
@@ -1177,7 +1256,7 @@ class OrbitSitemap {
|
|
|
1177
1256
|
}
|
|
1178
1257
|
let storage = opts.storage;
|
|
1179
1258
|
if (!storage) {
|
|
1180
|
-
const { DiskSitemapStorage: DiskSitemapStorage2 } = await
|
|
1259
|
+
const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-7ZZMGC4K.js");
|
|
1181
1260
|
storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
|
|
1182
1261
|
}
|
|
1183
1262
|
const incrementalGenerator = new IncrementalGenerator({
|
|
@@ -1191,6 +1270,13 @@ class OrbitSitemap {
|
|
|
1191
1270
|
await incrementalGenerator.generateIncremental(since);
|
|
1192
1271
|
console.log(`[OrbitSitemap] Generated incremental sitemap in ${opts.outDir}`);
|
|
1193
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Generate sitemap asynchronously in the background (static mode only).
|
|
1275
|
+
*
|
|
1276
|
+
* @param options - Options for the async generation job.
|
|
1277
|
+
* @returns A promise resolving to the job ID.
|
|
1278
|
+
* @throws {Error} If called in dynamic mode.
|
|
1279
|
+
*/
|
|
1194
1280
|
async generateAsync(options) {
|
|
1195
1281
|
if (this.mode !== "static") {
|
|
1196
1282
|
throw new Error("generateAsync() can only be called in static mode");
|
|
@@ -1199,7 +1285,7 @@ class OrbitSitemap {
|
|
|
1199
1285
|
const jobId = randomUUID();
|
|
1200
1286
|
let storage = opts.storage;
|
|
1201
1287
|
if (!storage) {
|
|
1202
|
-
const { DiskSitemapStorage: DiskSitemapStorage2 } = await
|
|
1288
|
+
const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-7ZZMGC4K.js");
|
|
1203
1289
|
storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
|
|
1204
1290
|
}
|
|
1205
1291
|
let providers = opts.providers;
|
|
@@ -1245,6 +1331,12 @@ class OrbitSitemap {
|
|
|
1245
1331
|
});
|
|
1246
1332
|
return jobId;
|
|
1247
1333
|
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Install API endpoints for triggering and monitoring sitemap generation.
|
|
1336
|
+
*
|
|
1337
|
+
* @param core - The PlanetCore instance.
|
|
1338
|
+
* @param basePath - The base path for the API endpoints (default: '/admin/sitemap').
|
|
1339
|
+
*/
|
|
1248
1340
|
installApiEndpoints(core, basePath = "/admin/sitemap") {
|
|
1249
1341
|
const opts = this.options;
|
|
1250
1342
|
core.router.post(`${basePath}/generate`, async (ctx) => {
|
|
@@ -1252,7 +1344,7 @@ class OrbitSitemap {
|
|
|
1252
1344
|
const body = await ctx.req.json().catch(() => ({}));
|
|
1253
1345
|
const jobId = await this.generateAsync({
|
|
1254
1346
|
incremental: body.incremental,
|
|
1255
|
-
since: body.since ? new Date(body.since) :
|
|
1347
|
+
since: body.since ? new Date(body.since) : void 0
|
|
1256
1348
|
});
|
|
1257
1349
|
return ctx.json({ jobId, status: "started" });
|
|
1258
1350
|
} catch (error) {
|
|
@@ -1282,6 +1374,9 @@ class OrbitSitemap {
|
|
|
1282
1374
|
return ctx.json(history);
|
|
1283
1375
|
});
|
|
1284
1376
|
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Convert an AsyncIterable to an array.
|
|
1379
|
+
*/
|
|
1285
1380
|
async toArray(iterable) {
|
|
1286
1381
|
const array = [];
|
|
1287
1382
|
for await (const item of iterable) {
|
|
@@ -1289,16 +1384,17 @@ class OrbitSitemap {
|
|
|
1289
1384
|
}
|
|
1290
1385
|
return array;
|
|
1291
1386
|
}
|
|
1292
|
-
}
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1293
1389
|
// src/providers/RouteScanner.ts
|
|
1294
1390
|
function matchGlob(str, pattern) {
|
|
1295
1391
|
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1296
1392
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
1297
1393
|
return regex.test(str);
|
|
1298
1394
|
}
|
|
1299
|
-
|
|
1300
|
-
class RouteScanner {
|
|
1395
|
+
var RouteScanner = class {
|
|
1301
1396
|
router;
|
|
1397
|
+
// Using any to key access internal routes
|
|
1302
1398
|
options;
|
|
1303
1399
|
constructor(router, options = {}) {
|
|
1304
1400
|
this.router = router;
|
|
@@ -1335,10 +1431,10 @@ class RouteScanner {
|
|
|
1335
1431
|
}
|
|
1336
1432
|
return routes;
|
|
1337
1433
|
}
|
|
1338
|
-
shouldInclude(
|
|
1434
|
+
shouldInclude(path) {
|
|
1339
1435
|
if (this.options.exclude) {
|
|
1340
1436
|
for (const pattern of this.options.exclude) {
|
|
1341
|
-
if (matchGlob(
|
|
1437
|
+
if (matchGlob(path, pattern)) {
|
|
1342
1438
|
return false;
|
|
1343
1439
|
}
|
|
1344
1440
|
}
|
|
@@ -1346,7 +1442,7 @@ class RouteScanner {
|
|
|
1346
1442
|
if (this.options.include) {
|
|
1347
1443
|
let matched = false;
|
|
1348
1444
|
for (const pattern of this.options.include) {
|
|
1349
|
-
if (matchGlob(
|
|
1445
|
+
if (matchGlob(path, pattern)) {
|
|
1350
1446
|
matched = true;
|
|
1351
1447
|
break;
|
|
1352
1448
|
}
|
|
@@ -1355,17 +1451,21 @@ class RouteScanner {
|
|
|
1355
1451
|
}
|
|
1356
1452
|
return true;
|
|
1357
1453
|
}
|
|
1358
|
-
}
|
|
1454
|
+
};
|
|
1359
1455
|
function routeScanner(router, options) {
|
|
1360
1456
|
return new RouteScanner(router, options);
|
|
1361
1457
|
}
|
|
1458
|
+
|
|
1362
1459
|
// src/redirect/RedirectDetector.ts
|
|
1363
|
-
|
|
1460
|
+
var RedirectDetector = class {
|
|
1364
1461
|
options;
|
|
1365
|
-
cache = new Map;
|
|
1462
|
+
cache = /* @__PURE__ */ new Map();
|
|
1366
1463
|
constructor(options) {
|
|
1367
1464
|
this.options = options;
|
|
1368
1465
|
}
|
|
1466
|
+
/**
|
|
1467
|
+
* 偵測單一 URL 的轉址
|
|
1468
|
+
*/
|
|
1369
1469
|
async detect(url) {
|
|
1370
1470
|
if (this.options.autoDetect?.cache) {
|
|
1371
1471
|
const cached = this.cache.get(url);
|
|
@@ -1395,21 +1495,29 @@ class RedirectDetector {
|
|
|
1395
1495
|
}
|
|
1396
1496
|
return null;
|
|
1397
1497
|
}
|
|
1498
|
+
/**
|
|
1499
|
+
* 批次偵測轉址
|
|
1500
|
+
*/
|
|
1398
1501
|
async detectBatch(urls) {
|
|
1399
|
-
const results = new Map;
|
|
1502
|
+
const results = /* @__PURE__ */ new Map();
|
|
1400
1503
|
const maxConcurrent = this.options.autoDetect?.maxConcurrent || 10;
|
|
1401
1504
|
const batches = [];
|
|
1402
|
-
for (let i = 0;i < urls.length; i += maxConcurrent) {
|
|
1505
|
+
for (let i = 0; i < urls.length; i += maxConcurrent) {
|
|
1403
1506
|
batches.push(urls.slice(i, i + maxConcurrent));
|
|
1404
1507
|
}
|
|
1405
1508
|
for (const batch of batches) {
|
|
1406
|
-
const promises = batch.map(
|
|
1407
|
-
|
|
1408
|
-
|
|
1509
|
+
const promises = batch.map(
|
|
1510
|
+
(url) => this.detect(url).then((rule) => {
|
|
1511
|
+
results.set(url, rule);
|
|
1512
|
+
})
|
|
1513
|
+
);
|
|
1409
1514
|
await Promise.all(promises);
|
|
1410
1515
|
}
|
|
1411
1516
|
return results;
|
|
1412
1517
|
}
|
|
1518
|
+
/**
|
|
1519
|
+
* 從資料庫偵測
|
|
1520
|
+
*/
|
|
1413
1521
|
async detectFromDatabase(url) {
|
|
1414
1522
|
const { database } = this.options;
|
|
1415
1523
|
if (!database?.enabled) {
|
|
@@ -1432,14 +1540,17 @@ class RedirectDetector {
|
|
|
1432
1540
|
return null;
|
|
1433
1541
|
}
|
|
1434
1542
|
}
|
|
1543
|
+
/**
|
|
1544
|
+
* 從設定檔偵測
|
|
1545
|
+
*/
|
|
1435
1546
|
async detectFromConfig(url) {
|
|
1436
1547
|
const { config } = this.options;
|
|
1437
1548
|
if (!config?.enabled) {
|
|
1438
1549
|
return null;
|
|
1439
1550
|
}
|
|
1440
1551
|
try {
|
|
1441
|
-
const
|
|
1442
|
-
const data = await
|
|
1552
|
+
const fs = await import("fs/promises");
|
|
1553
|
+
const data = await fs.readFile(config.path, "utf-8");
|
|
1443
1554
|
const redirects = JSON.parse(data);
|
|
1444
1555
|
const rule = redirects.find((r) => r.from === url);
|
|
1445
1556
|
return rule || null;
|
|
@@ -1447,6 +1558,9 @@ class RedirectDetector {
|
|
|
1447
1558
|
return null;
|
|
1448
1559
|
}
|
|
1449
1560
|
}
|
|
1561
|
+
/**
|
|
1562
|
+
* 自動偵測(透過 HTTP 請求)
|
|
1563
|
+
*/
|
|
1450
1564
|
async detectAuto(url) {
|
|
1451
1565
|
const { autoDetect, baseUrl } = this.options;
|
|
1452
1566
|
if (!autoDetect?.enabled) {
|
|
@@ -1454,14 +1568,15 @@ class RedirectDetector {
|
|
|
1454
1568
|
}
|
|
1455
1569
|
try {
|
|
1456
1570
|
const fullUrl = url.startsWith("http") ? url : `${baseUrl}${url}`;
|
|
1457
|
-
const timeout = autoDetect.timeout ||
|
|
1458
|
-
const controller = new AbortController;
|
|
1571
|
+
const timeout = autoDetect.timeout || 5e3;
|
|
1572
|
+
const controller = new AbortController();
|
|
1459
1573
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1460
1574
|
try {
|
|
1461
1575
|
const response = await fetch(fullUrl, {
|
|
1462
1576
|
method: "HEAD",
|
|
1463
1577
|
signal: controller.signal,
|
|
1464
1578
|
redirect: "manual"
|
|
1579
|
+
// 手動處理轉址
|
|
1465
1580
|
});
|
|
1466
1581
|
clearTimeout(timeoutId);
|
|
1467
1582
|
if (response.status === 301 || response.status === 302) {
|
|
@@ -1480,23 +1595,28 @@ class RedirectDetector {
|
|
|
1480
1595
|
throw error;
|
|
1481
1596
|
}
|
|
1482
1597
|
}
|
|
1483
|
-
} catch {
|
|
1598
|
+
} catch {
|
|
1599
|
+
}
|
|
1484
1600
|
return null;
|
|
1485
1601
|
}
|
|
1602
|
+
/**
|
|
1603
|
+
* 快取結果
|
|
1604
|
+
*/
|
|
1486
1605
|
cacheResult(url, rule) {
|
|
1487
1606
|
if (!this.options.autoDetect?.cache) {
|
|
1488
1607
|
return;
|
|
1489
1608
|
}
|
|
1490
|
-
const ttl = (this.options.autoDetect.cacheTtl || 3600) *
|
|
1609
|
+
const ttl = (this.options.autoDetect.cacheTtl || 3600) * 1e3;
|
|
1491
1610
|
this.cache.set(url, {
|
|
1492
1611
|
rule,
|
|
1493
1612
|
expires: Date.now() + ttl
|
|
1494
1613
|
});
|
|
1495
1614
|
}
|
|
1496
|
-
}
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1497
1617
|
// src/redirect/RedirectManager.ts
|
|
1498
|
-
|
|
1499
|
-
rules = new Map;
|
|
1618
|
+
var MemoryRedirectManager = class {
|
|
1619
|
+
rules = /* @__PURE__ */ new Map();
|
|
1500
1620
|
maxRules;
|
|
1501
1621
|
constructor(options = {}) {
|
|
1502
1622
|
this.maxRules = options.maxRules || 1e5;
|
|
@@ -1537,9 +1657,8 @@ class MemoryRedirectManager {
|
|
|
1537
1657
|
}
|
|
1538
1658
|
return current;
|
|
1539
1659
|
}
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
class RedisRedirectManager {
|
|
1660
|
+
};
|
|
1661
|
+
var RedisRedirectManager = class {
|
|
1543
1662
|
client;
|
|
1544
1663
|
keyPrefix;
|
|
1545
1664
|
ttl;
|
|
@@ -1618,19 +1737,17 @@ class RedisRedirectManager {
|
|
|
1618
1737
|
}
|
|
1619
1738
|
return current;
|
|
1620
1739
|
}
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// src/index.ts
|
|
1624
|
-
init_DiskSitemapStorage();
|
|
1740
|
+
};
|
|
1625
1741
|
|
|
1626
1742
|
// src/storage/GCPSitemapStorage.ts
|
|
1627
|
-
|
|
1743
|
+
var GCPSitemapStorage = class {
|
|
1628
1744
|
bucket;
|
|
1629
1745
|
prefix;
|
|
1630
1746
|
baseUrl;
|
|
1631
1747
|
shadowEnabled;
|
|
1632
1748
|
shadowMode;
|
|
1633
1749
|
storageClient;
|
|
1750
|
+
// 動態載入 @google-cloud/storage
|
|
1634
1751
|
bucketInstance;
|
|
1635
1752
|
constructor(options) {
|
|
1636
1753
|
this.bucket = options.bucket;
|
|
@@ -1646,12 +1763,15 @@ class GCPSitemapStorage {
|
|
|
1646
1763
|
try {
|
|
1647
1764
|
const { Storage } = await import("@google-cloud/storage");
|
|
1648
1765
|
const clientOptions = {};
|
|
1649
|
-
if (this.constructor.name === "GCPSitemapStorage") {
|
|
1766
|
+
if (this.constructor.name === "GCPSitemapStorage") {
|
|
1767
|
+
}
|
|
1650
1768
|
this.storageClient = new Storage(clientOptions);
|
|
1651
1769
|
this.bucketInstance = this.storageClient.bucket(this.bucket);
|
|
1652
1770
|
return { client: this.storageClient, bucket: this.bucketInstance };
|
|
1653
1771
|
} catch (error) {
|
|
1654
|
-
throw new Error(
|
|
1772
|
+
throw new Error(
|
|
1773
|
+
`Failed to load Google Cloud Storage. Please install @google-cloud/storage: ${error instanceof Error ? error.message : String(error)}`
|
|
1774
|
+
);
|
|
1655
1775
|
}
|
|
1656
1776
|
}
|
|
1657
1777
|
getKey(filename) {
|
|
@@ -1703,6 +1823,7 @@ class GCPSitemapStorage {
|
|
|
1703
1823
|
const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
1704
1824
|
return `${base}/${key}`;
|
|
1705
1825
|
}
|
|
1826
|
+
// 影子處理方法
|
|
1706
1827
|
async writeShadow(filename, content, shadowId) {
|
|
1707
1828
|
if (!this.shadowEnabled) {
|
|
1708
1829
|
return this.write(filename, content);
|
|
@@ -1779,10 +1900,11 @@ class GCPSitemapStorage {
|
|
|
1779
1900
|
}
|
|
1780
1901
|
await versionedFile.copy(bucket.file(key));
|
|
1781
1902
|
}
|
|
1782
|
-
}
|
|
1903
|
+
};
|
|
1904
|
+
|
|
1783
1905
|
// src/storage/MemoryProgressStorage.ts
|
|
1784
|
-
|
|
1785
|
-
storage = new Map;
|
|
1906
|
+
var MemoryProgressStorage = class {
|
|
1907
|
+
storage = /* @__PURE__ */ new Map();
|
|
1786
1908
|
async get(jobId) {
|
|
1787
1909
|
const progress = this.storage.get(jobId);
|
|
1788
1910
|
return progress ? { ...progress } : null;
|
|
@@ -1808,9 +1930,10 @@ class MemoryProgressStorage {
|
|
|
1808
1930
|
});
|
|
1809
1931
|
return limit ? sorted.slice(0, limit) : sorted;
|
|
1810
1932
|
}
|
|
1811
|
-
}
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1812
1935
|
// src/storage/RedisProgressStorage.ts
|
|
1813
|
-
|
|
1936
|
+
var RedisProgressStorage = class {
|
|
1814
1937
|
client;
|
|
1815
1938
|
keyPrefix;
|
|
1816
1939
|
ttl;
|
|
@@ -1880,9 +2003,10 @@ class RedisProgressStorage {
|
|
|
1880
2003
|
return [];
|
|
1881
2004
|
}
|
|
1882
2005
|
}
|
|
1883
|
-
}
|
|
2006
|
+
};
|
|
2007
|
+
|
|
1884
2008
|
// src/storage/S3SitemapStorage.ts
|
|
1885
|
-
|
|
2009
|
+
var S3SitemapStorage = class {
|
|
1886
2010
|
bucket;
|
|
1887
2011
|
region;
|
|
1888
2012
|
prefix;
|
|
@@ -1890,6 +2014,7 @@ class S3SitemapStorage {
|
|
|
1890
2014
|
shadowEnabled;
|
|
1891
2015
|
shadowMode;
|
|
1892
2016
|
s3Client;
|
|
2017
|
+
// 動態載入 @aws-sdk/client-s3
|
|
1893
2018
|
constructor(options) {
|
|
1894
2019
|
this.bucket = options.bucket;
|
|
1895
2020
|
this.region = options.region || "us-east-1";
|
|
@@ -1915,7 +2040,8 @@ class S3SitemapStorage {
|
|
|
1915
2040
|
const clientOptions = {
|
|
1916
2041
|
region: this.region
|
|
1917
2042
|
};
|
|
1918
|
-
if (this.constructor.name === "S3SitemapStorage") {
|
|
2043
|
+
if (this.constructor.name === "S3SitemapStorage") {
|
|
2044
|
+
}
|
|
1919
2045
|
this.s3Client = {
|
|
1920
2046
|
S3Client,
|
|
1921
2047
|
PutObjectCommand,
|
|
@@ -1928,7 +2054,9 @@ class S3SitemapStorage {
|
|
|
1928
2054
|
};
|
|
1929
2055
|
return this.s3Client;
|
|
1930
2056
|
} catch (error) {
|
|
1931
|
-
throw new Error(
|
|
2057
|
+
throw new Error(
|
|
2058
|
+
`Failed to load AWS SDK. Please install @aws-sdk/client-s3: ${error instanceof Error ? error.message : String(error)}`
|
|
2059
|
+
);
|
|
1932
2060
|
}
|
|
1933
2061
|
}
|
|
1934
2062
|
getKey(filename) {
|
|
@@ -1938,21 +2066,25 @@ class S3SitemapStorage {
|
|
|
1938
2066
|
async write(filename, content) {
|
|
1939
2067
|
const s3 = await this.getS3Client();
|
|
1940
2068
|
const key = this.getKey(filename);
|
|
1941
|
-
await s3.client.send(
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2069
|
+
await s3.client.send(
|
|
2070
|
+
new s3.PutObjectCommand({
|
|
2071
|
+
Bucket: this.bucket,
|
|
2072
|
+
Key: key,
|
|
2073
|
+
Body: content,
|
|
2074
|
+
ContentType: "application/xml"
|
|
2075
|
+
})
|
|
2076
|
+
);
|
|
1947
2077
|
}
|
|
1948
2078
|
async read(filename) {
|
|
1949
2079
|
try {
|
|
1950
2080
|
const s3 = await this.getS3Client();
|
|
1951
2081
|
const key = this.getKey(filename);
|
|
1952
|
-
const response = await s3.client.send(
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2082
|
+
const response = await s3.client.send(
|
|
2083
|
+
new s3.GetObjectCommand({
|
|
2084
|
+
Bucket: this.bucket,
|
|
2085
|
+
Key: key
|
|
2086
|
+
})
|
|
2087
|
+
);
|
|
1956
2088
|
if (!response.Body) {
|
|
1957
2089
|
return null;
|
|
1958
2090
|
}
|
|
@@ -1973,10 +2105,12 @@ class S3SitemapStorage {
|
|
|
1973
2105
|
try {
|
|
1974
2106
|
const s3 = await this.getS3Client();
|
|
1975
2107
|
const key = this.getKey(filename);
|
|
1976
|
-
await s3.client.send(
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2108
|
+
await s3.client.send(
|
|
2109
|
+
new s3.HeadObjectCommand({
|
|
2110
|
+
Bucket: this.bucket,
|
|
2111
|
+
Key: key
|
|
2112
|
+
})
|
|
2113
|
+
);
|
|
1980
2114
|
return true;
|
|
1981
2115
|
} catch (error) {
|
|
1982
2116
|
if (error.name === "NotFound" || error.$metadata?.httpStatusCode === 404) {
|
|
@@ -1990,6 +2124,7 @@ class S3SitemapStorage {
|
|
|
1990
2124
|
const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
1991
2125
|
return `${base}/${key}`;
|
|
1992
2126
|
}
|
|
2127
|
+
// 影子處理方法
|
|
1993
2128
|
async writeShadow(filename, content, shadowId) {
|
|
1994
2129
|
if (!this.shadowEnabled) {
|
|
1995
2130
|
return this.write(filename, content);
|
|
@@ -1997,12 +2132,14 @@ class S3SitemapStorage {
|
|
|
1997
2132
|
const s3 = await this.getS3Client();
|
|
1998
2133
|
const id = shadowId || `shadow-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
1999
2134
|
const shadowKey = this.getKey(`${filename}.shadow.${id}`);
|
|
2000
|
-
await s3.client.send(
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2135
|
+
await s3.client.send(
|
|
2136
|
+
new s3.PutObjectCommand({
|
|
2137
|
+
Bucket: this.bucket,
|
|
2138
|
+
Key: shadowKey,
|
|
2139
|
+
Body: content,
|
|
2140
|
+
ContentType: "application/xml"
|
|
2141
|
+
})
|
|
2142
|
+
);
|
|
2006
2143
|
}
|
|
2007
2144
|
async commitShadow(shadowId) {
|
|
2008
2145
|
if (!this.shadowEnabled) {
|
|
@@ -2010,14 +2147,18 @@ class S3SitemapStorage {
|
|
|
2010
2147
|
}
|
|
2011
2148
|
const s3 = await this.getS3Client();
|
|
2012
2149
|
const prefix = this.prefix ? `${this.prefix}/` : "";
|
|
2013
|
-
const listResponse = await s3.client.send(
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2150
|
+
const listResponse = await s3.client.send(
|
|
2151
|
+
new s3.ListObjectsV2Command({
|
|
2152
|
+
Bucket: this.bucket,
|
|
2153
|
+
Prefix: prefix
|
|
2154
|
+
})
|
|
2155
|
+
);
|
|
2017
2156
|
if (!listResponse.Contents) {
|
|
2018
2157
|
return;
|
|
2019
2158
|
}
|
|
2020
|
-
const shadowFiles = listResponse.Contents.filter(
|
|
2159
|
+
const shadowFiles = listResponse.Contents.filter(
|
|
2160
|
+
(obj) => obj.Key?.includes(`.shadow.${shadowId}`)
|
|
2161
|
+
);
|
|
2021
2162
|
for (const shadowFile of shadowFiles) {
|
|
2022
2163
|
if (!shadowFile.Key) {
|
|
2023
2164
|
continue;
|
|
@@ -2025,35 +2166,45 @@ class S3SitemapStorage {
|
|
|
2025
2166
|
const originalKey = shadowFile.Key.replace(/\.shadow\.[^/]+$/, "");
|
|
2026
2167
|
const _originalFilename = originalKey.replace(prefix, "");
|
|
2027
2168
|
if (this.shadowMode === "atomic") {
|
|
2028
|
-
await s3.client.send(
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2169
|
+
await s3.client.send(
|
|
2170
|
+
new s3.CopyObjectCommand({
|
|
2171
|
+
Bucket: this.bucket,
|
|
2172
|
+
CopySource: `${this.bucket}/${shadowFile.Key}`,
|
|
2173
|
+
Key: originalKey,
|
|
2174
|
+
ContentType: "application/xml"
|
|
2175
|
+
})
|
|
2176
|
+
);
|
|
2177
|
+
await s3.client.send(
|
|
2178
|
+
new s3.DeleteObjectCommand({
|
|
2179
|
+
Bucket: this.bucket,
|
|
2180
|
+
Key: shadowFile.Key
|
|
2181
|
+
})
|
|
2182
|
+
);
|
|
2038
2183
|
} else {
|
|
2039
2184
|
const version = shadowId;
|
|
2040
2185
|
const versionedKey = `${originalKey}.v${version}`;
|
|
2041
|
-
await s3.client.send(
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2186
|
+
await s3.client.send(
|
|
2187
|
+
new s3.CopyObjectCommand({
|
|
2188
|
+
Bucket: this.bucket,
|
|
2189
|
+
CopySource: `${this.bucket}/${shadowFile.Key}`,
|
|
2190
|
+
Key: versionedKey,
|
|
2191
|
+
ContentType: "application/xml"
|
|
2192
|
+
})
|
|
2193
|
+
);
|
|
2194
|
+
await s3.client.send(
|
|
2195
|
+
new s3.CopyObjectCommand({
|
|
2196
|
+
Bucket: this.bucket,
|
|
2197
|
+
CopySource: `${this.bucket}/${shadowFile.Key}`,
|
|
2198
|
+
Key: originalKey,
|
|
2199
|
+
ContentType: "application/xml"
|
|
2200
|
+
})
|
|
2201
|
+
);
|
|
2202
|
+
await s3.client.send(
|
|
2203
|
+
new s3.DeleteObjectCommand({
|
|
2204
|
+
Bucket: this.bucket,
|
|
2205
|
+
Key: shadowFile.Key
|
|
2206
|
+
})
|
|
2207
|
+
);
|
|
2057
2208
|
}
|
|
2058
2209
|
}
|
|
2059
2210
|
}
|
|
@@ -2065,10 +2216,12 @@ class S3SitemapStorage {
|
|
|
2065
2216
|
const s3 = await this.getS3Client();
|
|
2066
2217
|
const key = this.getKey(filename);
|
|
2067
2218
|
const prefix = key.replace(/\.xml$/, "");
|
|
2068
|
-
const listResponse = await s3.client.send(
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2219
|
+
const listResponse = await s3.client.send(
|
|
2220
|
+
new s3.ListObjectsV2Command({
|
|
2221
|
+
Bucket: this.bucket,
|
|
2222
|
+
Prefix: prefix
|
|
2223
|
+
})
|
|
2224
|
+
);
|
|
2072
2225
|
if (!listResponse.Contents) {
|
|
2073
2226
|
return [];
|
|
2074
2227
|
}
|
|
@@ -2098,37 +2251,39 @@ class S3SitemapStorage {
|
|
|
2098
2251
|
if (!exists) {
|
|
2099
2252
|
throw new Error(`Version ${version} not found for ${filename}`);
|
|
2100
2253
|
}
|
|
2101
|
-
await s3.client.send(
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2254
|
+
await s3.client.send(
|
|
2255
|
+
new s3.CopyObjectCommand({
|
|
2256
|
+
Bucket: this.bucket,
|
|
2257
|
+
CopySource: `${this.bucket}/${versionedKey}`,
|
|
2258
|
+
Key: key,
|
|
2259
|
+
ContentType: "application/xml"
|
|
2260
|
+
})
|
|
2261
|
+
);
|
|
2107
2262
|
}
|
|
2108
|
-
}
|
|
2263
|
+
};
|
|
2109
2264
|
export {
|
|
2110
|
-
|
|
2111
|
-
generateI18nEntries,
|
|
2112
|
-
SitemapStream,
|
|
2113
|
-
SitemapIndex,
|
|
2114
|
-
SitemapGenerator,
|
|
2115
|
-
ShadowProcessor,
|
|
2116
|
-
S3SitemapStorage,
|
|
2117
|
-
RouteScanner,
|
|
2118
|
-
RedisRedirectManager,
|
|
2119
|
-
RedisProgressStorage,
|
|
2120
|
-
RedisChangeTracker,
|
|
2121
|
-
RedirectHandler,
|
|
2122
|
-
RedirectDetector,
|
|
2123
|
-
ProgressTracker,
|
|
2124
|
-
OrbitSitemap,
|
|
2125
|
-
MemorySitemapStorage,
|
|
2126
|
-
MemoryRedirectManager,
|
|
2127
|
-
MemoryProgressStorage,
|
|
2128
|
-
MemoryChangeTracker,
|
|
2129
|
-
IncrementalGenerator,
|
|
2130
|
-
GenerateSitemapJob,
|
|
2131
|
-
GCPSitemapStorage,
|
|
2265
|
+
DiffCalculator,
|
|
2132
2266
|
DiskSitemapStorage,
|
|
2133
|
-
|
|
2267
|
+
GCPSitemapStorage,
|
|
2268
|
+
GenerateSitemapJob,
|
|
2269
|
+
IncrementalGenerator,
|
|
2270
|
+
MemoryChangeTracker,
|
|
2271
|
+
MemoryProgressStorage,
|
|
2272
|
+
MemoryRedirectManager,
|
|
2273
|
+
MemorySitemapStorage,
|
|
2274
|
+
OrbitSitemap,
|
|
2275
|
+
ProgressTracker,
|
|
2276
|
+
RedirectDetector,
|
|
2277
|
+
RedirectHandler,
|
|
2278
|
+
RedisChangeTracker,
|
|
2279
|
+
RedisProgressStorage,
|
|
2280
|
+
RedisRedirectManager,
|
|
2281
|
+
RouteScanner,
|
|
2282
|
+
S3SitemapStorage,
|
|
2283
|
+
ShadowProcessor,
|
|
2284
|
+
SitemapGenerator,
|
|
2285
|
+
SitemapIndex,
|
|
2286
|
+
SitemapStream,
|
|
2287
|
+
generateI18nEntries,
|
|
2288
|
+
routeScanner
|
|
2134
2289
|
};
|