@gravito/constellation 3.0.2 → 3.1.1
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 +96 -251
- package/README.zh-TW.md +130 -13
- package/dist/{DiskSitemapStorage-WP6RITUN.js → DiskSitemapStorage-VLN5I24C.js} +1 -1
- package/dist/chunk-3IZTXYU7.js +166 -0
- package/dist/index.cjs +1400 -67
- package/dist/index.d.cts +1579 -148
- package/dist/index.d.ts +1579 -148
- package/dist/index.js +1293 -70
- package/package.json +11 -9
- package/dist/chunk-IS2H7U6M.js +0 -68
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/storage/DiskSitemapStorage.ts
|
|
2
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { Readable as Readable2 } from "stream";
|
|
6
|
+
import { pipeline } from "stream/promises";
|
|
7
|
+
|
|
8
|
+
// src/utils/Compression.ts
|
|
9
|
+
import { Readable } from "stream";
|
|
10
|
+
import { createGzip } from "zlib";
|
|
11
|
+
async function compressToBuffer(source, config) {
|
|
12
|
+
const level = config?.level ?? 6;
|
|
13
|
+
if (level < 1 || level > 9) {
|
|
14
|
+
throw new Error(`Invalid compression level: ${level}. Must be between 1 and 9.`);
|
|
15
|
+
}
|
|
16
|
+
const chunks = [];
|
|
17
|
+
const gzip = createGzip({ level });
|
|
18
|
+
const readable = Readable.from(source, { encoding: "utf-8" });
|
|
19
|
+
try {
|
|
20
|
+
readable.pipe(gzip);
|
|
21
|
+
for await (const chunk of gzip) {
|
|
22
|
+
chunks.push(chunk);
|
|
23
|
+
}
|
|
24
|
+
return Buffer.concat(chunks);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
readable.destroy();
|
|
27
|
+
gzip.destroy();
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function createCompressionStream(config) {
|
|
32
|
+
const level = config?.level ?? 6;
|
|
33
|
+
if (level < 1 || level > 9) {
|
|
34
|
+
throw new Error(`Invalid compression level: ${level}. Must be between 1 and 9.`);
|
|
35
|
+
}
|
|
36
|
+
return createGzip({ level });
|
|
37
|
+
}
|
|
38
|
+
function toGzipFilename(filename) {
|
|
39
|
+
return filename.endsWith(".gz") ? filename : `${filename}.gz`;
|
|
40
|
+
}
|
|
41
|
+
function fromGzipFilename(filename) {
|
|
42
|
+
return filename.endsWith(".gz") ? filename.slice(0, -3) : filename;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/storage/DiskSitemapStorage.ts
|
|
46
|
+
function sanitizeFilename(filename) {
|
|
47
|
+
if (!filename) {
|
|
48
|
+
throw new Error("Invalid sitemap filename.");
|
|
49
|
+
}
|
|
50
|
+
if (filename.includes("\0")) {
|
|
51
|
+
throw new Error("Invalid sitemap filename.");
|
|
52
|
+
}
|
|
53
|
+
if (filename.includes("/") || filename.includes("\\")) {
|
|
54
|
+
throw new Error("Invalid sitemap filename.");
|
|
55
|
+
}
|
|
56
|
+
if (filename.includes("..")) {
|
|
57
|
+
throw new Error("Invalid sitemap filename.");
|
|
58
|
+
}
|
|
59
|
+
return filename;
|
|
60
|
+
}
|
|
61
|
+
var DiskSitemapStorage = class {
|
|
62
|
+
constructor(outDir, baseUrl) {
|
|
63
|
+
this.outDir = outDir;
|
|
64
|
+
this.baseUrl = baseUrl;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Writes sitemap content to a file on the local disk.
|
|
68
|
+
*
|
|
69
|
+
* @param filename - The name of the file to write.
|
|
70
|
+
* @param content - The XML or JSON content.
|
|
71
|
+
*/
|
|
72
|
+
async write(filename, content) {
|
|
73
|
+
const safeName = sanitizeFilename(filename);
|
|
74
|
+
await fs.mkdir(this.outDir, { recursive: true });
|
|
75
|
+
await fs.writeFile(path.join(this.outDir, safeName), content);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 使用串流方式寫入 sitemap 檔案,可選擇性啟用 gzip 壓縮。
|
|
79
|
+
* 此方法可大幅降低大型 sitemap 的記憶體峰值。
|
|
80
|
+
*
|
|
81
|
+
* @param filename - 檔案名稱
|
|
82
|
+
* @param stream - XML 內容的 AsyncIterable
|
|
83
|
+
* @param options - 寫入選項(如壓縮、content type)
|
|
84
|
+
*
|
|
85
|
+
* @since 3.1.0
|
|
86
|
+
*/
|
|
87
|
+
async writeStream(filename, stream, options) {
|
|
88
|
+
const safeName = sanitizeFilename(options?.compress ? toGzipFilename(filename) : filename);
|
|
89
|
+
await fs.mkdir(this.outDir, { recursive: true });
|
|
90
|
+
const filePath = path.join(this.outDir, safeName);
|
|
91
|
+
const writeStream = createWriteStream(filePath);
|
|
92
|
+
const readable = Readable2.from(stream, { encoding: "utf-8" });
|
|
93
|
+
if (options?.compress) {
|
|
94
|
+
const gzip = createCompressionStream();
|
|
95
|
+
await pipeline(readable, gzip, writeStream);
|
|
96
|
+
} else {
|
|
97
|
+
await pipeline(readable, writeStream);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Reads sitemap content from a file on the local disk.
|
|
102
|
+
*
|
|
103
|
+
* @param filename - The name of the file to read.
|
|
104
|
+
* @returns A promise resolving to the file content as a string, or null if not found.
|
|
105
|
+
*/
|
|
106
|
+
async read(filename) {
|
|
107
|
+
try {
|
|
108
|
+
const safeName = sanitizeFilename(filename);
|
|
109
|
+
return await fs.readFile(path.join(this.outDir, safeName), "utf-8");
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns a readable stream for a sitemap file on the local disk.
|
|
116
|
+
*
|
|
117
|
+
* @param filename - The name of the file to stream.
|
|
118
|
+
* @returns A promise resolving to an async iterable of file chunks, or null if not found.
|
|
119
|
+
*/
|
|
120
|
+
async readStream(filename) {
|
|
121
|
+
try {
|
|
122
|
+
const safeName = sanitizeFilename(filename);
|
|
123
|
+
const fullPath = path.join(this.outDir, safeName);
|
|
124
|
+
await fs.access(fullPath);
|
|
125
|
+
const stream = createReadStream(fullPath, { encoding: "utf-8" });
|
|
126
|
+
return stream;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Checks if a sitemap file exists on the local disk.
|
|
133
|
+
*
|
|
134
|
+
* @param filename - The name of the file to check.
|
|
135
|
+
* @returns A promise resolving to true if the file exists, false otherwise.
|
|
136
|
+
*/
|
|
137
|
+
async exists(filename) {
|
|
138
|
+
try {
|
|
139
|
+
const safeName = sanitizeFilename(filename);
|
|
140
|
+
await fs.access(path.join(this.outDir, safeName));
|
|
141
|
+
return true;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns the full public URL for a sitemap file.
|
|
148
|
+
*
|
|
149
|
+
* @param filename - The name of the sitemap file.
|
|
150
|
+
* @returns The public URL as a string.
|
|
151
|
+
*/
|
|
152
|
+
getUrl(filename) {
|
|
153
|
+
const safeName = sanitizeFilename(filename);
|
|
154
|
+
const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
155
|
+
const file = safeName.startsWith("/") ? safeName.slice(1) : safeName;
|
|
156
|
+
return `${base}/${file}`;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
compressToBuffer,
|
|
162
|
+
createCompressionStream,
|
|
163
|
+
toGzipFilename,
|
|
164
|
+
fromGzipFilename,
|
|
165
|
+
DiskSitemapStorage
|
|
166
|
+
};
|