pug-site-core 3.0.35 → 3.0.36
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/lib/dbConnection.js +138 -48
- package/lib/devServer.js +13 -8
- package/package.json +1 -1
package/lib/dbConnection.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
// Cloudflare D1 API 基础 URL
|
|
15
15
|
const CLOUDFLARE_API_BASE = 'https://api.cloudflare.com/client/v4';
|
|
16
|
+
const DEFAULT_D1_BINDING = 'db';
|
|
17
|
+
const D1_BINDING_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* 创建 D1 远程连接
|
|
@@ -146,56 +148,122 @@ function createD1RemoteAdapter(accountId, databaseId, apiToken) {
|
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
function normalizeConfigImportPath(configPath) {
|
|
152
|
+
if (configPath.startsWith('file://') || configPath.startsWith('http://') || configPath.startsWith('https://')) {
|
|
153
|
+
return configPath;
|
|
154
|
+
}
|
|
155
|
+
if (configPath.match(/^[A-Za-z]:/)) {
|
|
156
|
+
return `file:///${configPath.replace(/\\/g, '/')}`;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
158
160
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
function getEnvD1Parts() {
|
|
162
|
+
return {
|
|
163
|
+
accountId: process.env.D1_ACCOUNT_ID || process.env.CF_ACCOUNT_ID,
|
|
164
|
+
databaseId: process.env.D1_DATABASE_ID,
|
|
165
|
+
apiToken: process.env.CLOUDFLARE_API_TOKEN || process.env.CF_API_TOKEN,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function assertValidBindingName(binding) {
|
|
170
|
+
if (!D1_BINDING_NAME_RE.test(binding)) {
|
|
171
|
+
throw new Error(`D1 binding 名称无效: ${binding}`);
|
|
161
172
|
}
|
|
173
|
+
}
|
|
162
174
|
|
|
163
|
-
|
|
175
|
+
function getDatabaseIdFromValue(binding, value) {
|
|
176
|
+
if (typeof value === 'string') {
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
if (value && typeof value === 'object') {
|
|
180
|
+
return value.database_id || value.databaseId;
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`D1 binding ${binding} 的配置必须是 database id 字符串或 { database_id } 对象`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeD1Bindings(siteConfig, envDatabaseId) {
|
|
186
|
+
const bindings = {};
|
|
187
|
+
const d1Databases = siteConfig.d1_databases && typeof siteConfig.d1_databases === 'object'
|
|
188
|
+
? siteConfig.d1_databases
|
|
189
|
+
: {};
|
|
190
|
+
|
|
191
|
+
const defaultDbConfig = Object.prototype.hasOwnProperty.call(d1Databases, DEFAULT_D1_BINDING)
|
|
192
|
+
? d1Databases[DEFAULT_D1_BINDING]
|
|
193
|
+
: siteConfig.d1_database_id;
|
|
194
|
+
|
|
195
|
+
if (defaultDbConfig || envDatabaseId) {
|
|
196
|
+
bindings[DEFAULT_D1_BINDING] = envDatabaseId || getDatabaseIdFromValue(DEFAULT_D1_BINDING, defaultDbConfig);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const [binding, value] of Object.entries(d1Databases)) {
|
|
200
|
+
assertValidBindingName(binding);
|
|
201
|
+
if (binding === DEFAULT_D1_BINDING) continue;
|
|
202
|
+
const databaseId = getDatabaseIdFromValue(binding, value);
|
|
203
|
+
if (!databaseId) {
|
|
204
|
+
throw new Error(`D1 binding ${binding} 缺少 database_id`);
|
|
205
|
+
}
|
|
206
|
+
bindings[binding] = databaseId;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return bindings;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function loadSiteConfig(configPath) {
|
|
164
213
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (configPath.match(/^[A-Za-z]:/)) {
|
|
170
|
-
importPath = `file:///${configPath.replace(/\\/g, '/')}`;
|
|
171
|
-
} else {
|
|
172
|
-
// 相对路径,使用 pathToFileURL
|
|
173
|
-
const { pathToFileURL } = await import('url');
|
|
174
|
-
importPath = pathToFileURL(configPath).href;
|
|
175
|
-
}
|
|
214
|
+
let importPath = normalizeConfigImportPath(configPath);
|
|
215
|
+
if (!importPath) {
|
|
216
|
+
const { pathToFileURL } = await import('url');
|
|
217
|
+
importPath = pathToFileURL(configPath).href;
|
|
176
218
|
}
|
|
219
|
+
|
|
177
220
|
const { config, SITE_CONFIGS_JSON } = await import(importPath);
|
|
178
221
|
const siteName = config.siteConfig?.siteName || 'default';
|
|
179
|
-
|
|
180
|
-
// 检查是否有 SITE_CONFIGS_JSON 配置
|
|
181
222
|
if (SITE_CONFIGS_JSON && SITE_CONFIGS_JSON[siteName]) {
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
accountId: siteConfig.cf_account_id,
|
|
185
|
-
databaseId: siteConfig.d1_database_id,
|
|
186
|
-
apiToken: siteConfig.cf_api_token,
|
|
187
|
-
};
|
|
188
|
-
} else {
|
|
189
|
-
console.warn(`SITE_CONFIGS_JSON 中未找到 ${siteName} 的配置`);
|
|
223
|
+
return SITE_CONFIGS_JSON[siteName];
|
|
190
224
|
}
|
|
225
|
+
console.warn(`SITE_CONFIGS_JSON 中未找到 ${siteName} 的配置`);
|
|
191
226
|
} catch (error) {
|
|
192
|
-
// 忽略配置读取错误
|
|
193
227
|
console.warn('读取 SITE_CONFIGS_JSON 配置失败:', error.message);
|
|
194
228
|
}
|
|
195
229
|
|
|
196
230
|
return null;
|
|
197
231
|
}
|
|
198
232
|
|
|
233
|
+
/**
|
|
234
|
+
* 从配置中获取所有 D1 binding 连接信息
|
|
235
|
+
* @param {string} configPath - 项目 config.js 的路径
|
|
236
|
+
*/
|
|
237
|
+
async function getD1Configs(configPath) {
|
|
238
|
+
const envConfig = getEnvD1Parts();
|
|
239
|
+
const siteConfig = await loadSiteConfig(configPath);
|
|
240
|
+
|
|
241
|
+
if (!siteConfig) {
|
|
242
|
+
if (envConfig.accountId && envConfig.databaseId && envConfig.apiToken) {
|
|
243
|
+
return {
|
|
244
|
+
[DEFAULT_D1_BINDING]: {
|
|
245
|
+
accountId: envConfig.accountId,
|
|
246
|
+
databaseId: envConfig.databaseId,
|
|
247
|
+
apiToken: envConfig.apiToken,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const accountId = envConfig.accountId || siteConfig.cf_account_id;
|
|
255
|
+
const apiToken = envConfig.apiToken || siteConfig.cf_api_token;
|
|
256
|
+
const bindings = normalizeD1Bindings(siteConfig, envConfig.databaseId);
|
|
257
|
+
|
|
258
|
+
const configs = {};
|
|
259
|
+
for (const [binding, databaseId] of Object.entries(bindings)) {
|
|
260
|
+
assertValidBindingName(binding);
|
|
261
|
+
configs[binding] = { accountId, databaseId, apiToken };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return Object.keys(configs).length ? configs : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
199
267
|
/**
|
|
200
268
|
* 验证 API Token 是否有效
|
|
201
269
|
* @param {string} apiToken - Cloudflare API Token
|
|
@@ -222,26 +290,48 @@ async function verifyApiToken(apiToken) {
|
|
|
222
290
|
}
|
|
223
291
|
}
|
|
224
292
|
|
|
293
|
+
async function verifyD1Tokens(configs) {
|
|
294
|
+
const apiTokens = [...new Set(Object.values(configs).map((config) => config.apiToken).filter(Boolean))];
|
|
295
|
+
for (const apiToken of apiTokens) {
|
|
296
|
+
const isValid = await verifyApiToken(apiToken);
|
|
297
|
+
if (!isValid) {
|
|
298
|
+
console.warn('⚠️ API Token 验证失败,但将继续尝试连接数据库...');
|
|
299
|
+
console.warn(' 如果后续数据库查询失败,请检查:');
|
|
300
|
+
console.warn(' 1. Token 是否正确(检查 config.js 中的 cf_api_token)');
|
|
301
|
+
console.warn(' 2. Token 是否在 Cloudflare Dashboard 中仍然有效');
|
|
302
|
+
console.warn(' 3. Token 是否有 D1 数据库的访问权限');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
225
307
|
/**
|
|
226
|
-
*
|
|
308
|
+
* 创建所有 D1 binding 连接
|
|
227
309
|
* @param {string} configPath - 项目 config.js 的路径
|
|
228
310
|
*/
|
|
229
|
-
export async function
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
if (!
|
|
311
|
+
export async function getDatabaseConnections(configPath) {
|
|
312
|
+
const configs = await getD1Configs(configPath);
|
|
313
|
+
|
|
314
|
+
if (!configs) {
|
|
233
315
|
throw new Error('未配置 D1 数据库连接信息。请设置环境变量或配置 SITE_CONFIGS_JSON');
|
|
234
316
|
}
|
|
235
317
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.warn(' 1. Token 是否正确(检查 config.js 中的 cf_api_token)');
|
|
242
|
-
console.warn(' 2. Token 是否在 Cloudflare Dashboard 中仍然有效');
|
|
243
|
-
console.warn(' 3. Token 是否有 D1 数据库的访问权限');
|
|
318
|
+
await verifyD1Tokens(configs);
|
|
319
|
+
|
|
320
|
+
const connections = {};
|
|
321
|
+
for (const [binding, config] of Object.entries(configs)) {
|
|
322
|
+
connections[binding] = await createD1RemoteConnection(config.accountId, config.databaseId, config.apiToken);
|
|
244
323
|
}
|
|
324
|
+
return connections;
|
|
325
|
+
}
|
|
245
326
|
|
|
246
|
-
|
|
327
|
+
/**
|
|
328
|
+
* 创建默认数据库连接
|
|
329
|
+
* @param {string} configPath - 项目 config.js 的路径
|
|
330
|
+
*/
|
|
331
|
+
export async function getDatabaseConnection(configPath) {
|
|
332
|
+
const connections = await getDatabaseConnections(configPath);
|
|
333
|
+
if (!connections[DEFAULT_D1_BINDING]) {
|
|
334
|
+
throw new Error(`未配置默认 D1 binding: ${DEFAULT_D1_BINDING}`);
|
|
335
|
+
}
|
|
336
|
+
return connections[DEFAULT_D1_BINDING];
|
|
247
337
|
}
|
package/lib/devServer.js
CHANGED
|
@@ -19,7 +19,7 @@ import { Worker } from "worker_threads";
|
|
|
19
19
|
import { paths } from "./paths.js";
|
|
20
20
|
import { injectDebugScript } from "./debug/pugDebug.js";
|
|
21
21
|
import { setupDebugRoutes } from "./debug/debugRouter.js";
|
|
22
|
-
import {
|
|
22
|
+
import { getDatabaseConnections } from "./dbConnection.js";
|
|
23
23
|
|
|
24
24
|
const { config } = await import(paths.config);
|
|
25
25
|
const pagsTemplatePath = config.devServer.isDebug ? paths.template.debugPages : paths.template.pages;
|
|
@@ -28,22 +28,29 @@ const port = await getIdleProt(config.devServer.port);
|
|
|
28
28
|
process.env._port = port;
|
|
29
29
|
process.env._localIp = localIp;
|
|
30
30
|
|
|
31
|
+
function normalizeDbBindings(dbConfig) {
|
|
32
|
+
if (dbConfig && typeof dbConfig === 'object' && typeof dbConfig.prepare !== 'function') {
|
|
33
|
+
return dbConfig;
|
|
34
|
+
}
|
|
35
|
+
return { db: dbConfig };
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
// 初始化数据库连接(如果配置了)- 在模块加载时初始化一次
|
|
32
|
-
let
|
|
39
|
+
let dbBindings = { db: null };
|
|
33
40
|
if (config.devServer?.db) {
|
|
34
41
|
try {
|
|
35
42
|
if (typeof config.devServer.db === 'function') {
|
|
36
|
-
|
|
43
|
+
dbBindings = normalizeDbBindings(await config.devServer.db());
|
|
37
44
|
} else if (config.devServer.db === true) {
|
|
38
45
|
// 如果设置为 true,使用内置的数据库连接功能
|
|
39
46
|
try {
|
|
40
|
-
|
|
47
|
+
dbBindings = await getDatabaseConnections(paths.config);
|
|
41
48
|
} catch (error) {
|
|
42
49
|
console.error('❌ 数据库连接失败:', error.message);
|
|
43
50
|
console.warn('将使用 null 作为数据库连接');
|
|
44
51
|
}
|
|
45
52
|
} else {
|
|
46
|
-
|
|
53
|
+
dbBindings = normalizeDbBindings(config.devServer.db);
|
|
47
54
|
}
|
|
48
55
|
} catch (error) {
|
|
49
56
|
console.error('初始化数据库连接失败:', error);
|
|
@@ -282,9 +289,7 @@ async function matchRouter(url, language, device) {
|
|
|
282
289
|
language,
|
|
283
290
|
device,
|
|
284
291
|
getR2Data,
|
|
285
|
-
env:
|
|
286
|
-
db: dbConnection // 使用预初始化的数据库连接,模拟线上环境的 this.env.db
|
|
287
|
-
}
|
|
292
|
+
env: dbBindings
|
|
288
293
|
};
|
|
289
294
|
|
|
290
295
|
if(config.abtest && config.abtest.enabled && abtestRouter && abtestRouter[config.abtest.curVariant]){
|