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.
@@ -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
- * @param {string} configPath - 项目 config.js 的路径
152
- */
153
- async function getD1Config(configPath) {
154
- // 优先从环境变量读取
155
- const accountId = process.env.D1_ACCOUNT_ID || process.env.CF_ACCOUNT_ID;
156
- const databaseId = process.env.D1_DATABASE_ID;
157
- const apiToken = process.env.CLOUDFLARE_API_TOKEN || process.env.CF_API_TOKEN;
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
- if (accountId && databaseId && apiToken) {
160
- return { accountId, databaseId, apiToken };
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
- // 如果环境变量未配置,尝试从项目的 SITE_CONFIGS_JSON 读取
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
- // 确保路径是有效的 file:// URL 格式(Windows 需要)
166
- let importPath = configPath;
167
- if (!configPath.startsWith('file://') && !configPath.startsWith('http://') && !configPath.startsWith('https://')) {
168
- // 如果是绝对路径,转换为 file:// URL
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
- const siteConfig = SITE_CONFIGS_JSON[siteName];
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 getDatabaseConnection(configPath) {
230
- const config = await getD1Config(configPath);
231
-
232
- if (!config) {
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
- // 验证 API Token 是否有效(可选,仅用于诊断)
237
- const isValid = await verifyApiToken(config.apiToken);
238
- if (!isValid) {
239
- console.warn('⚠️ API Token 验证失败,但将继续尝试连接数据库...');
240
- console.warn(' 如果后续数据库查询失败,请检查:');
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
- return await createD1RemoteConnection(config.accountId, config.databaseId, config.apiToken);
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 { getDatabaseConnection } from "./dbConnection.js";
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 dbConnection = null;
39
+ let dbBindings = { db: null };
33
40
  if (config.devServer?.db) {
34
41
  try {
35
42
  if (typeof config.devServer.db === 'function') {
36
- dbConnection = await config.devServer.db();
43
+ dbBindings = normalizeDbBindings(await config.devServer.db());
37
44
  } else if (config.devServer.db === true) {
38
45
  // 如果设置为 true,使用内置的数据库连接功能
39
46
  try {
40
- dbConnection = await getDatabaseConnection(paths.config);
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
- dbConnection = config.devServer.db;
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]){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pug-site-core",
3
- "version": "3.0.35",
3
+ "version": "3.0.36",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {