jsql-neo 6.5.12 → 6.5.13
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/index.js +1 -1
- package/lib/database.js +8 -1
- package/package.json +2 -1
- package/src/cli/index.js +12 -0
- package/src/cli/plugin.js +346 -0
- package/test/plugin-xql.test.js +23 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.5.
|
|
6
|
+
> **v6.5.13** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
package/index.js
CHANGED
package/lib/database.js
CHANGED
|
@@ -1668,12 +1668,19 @@ class Database {
|
|
|
1668
1668
|
const pluginName = manifest.name || entry.name;
|
|
1669
1669
|
// 查找入口文件:manifest.main || manifest.entry || 'index.js'
|
|
1670
1670
|
const entryFile = manifest.main || manifest.entry || 'index.js';
|
|
1671
|
-
//
|
|
1671
|
+
// 兼容三种布局:
|
|
1672
|
+
// 1. entry 是仓库相对路径(如 "plugins/xql/index.js")且文件在根级 → basename
|
|
1673
|
+
// 2. entry 是根级相对路径(如 "index.js")且文件在根级
|
|
1674
|
+
// 3. plugins/ 子目录(旧布局)
|
|
1672
1675
|
const candidates = [
|
|
1673
1676
|
path.join(dir, entryFile),
|
|
1674
1677
|
path.join(dir, entryFile + '.js'),
|
|
1678
|
+
path.join(dir, path.basename(entryFile)),
|
|
1679
|
+
path.join(dir, path.basename(entryFile) + '.js'),
|
|
1675
1680
|
path.join(dir, 'plugins', entryFile),
|
|
1676
1681
|
path.join(dir, 'plugins', entryFile + '.js'),
|
|
1682
|
+
path.join(dir, 'plugins', path.basename(entryFile)),
|
|
1683
|
+
path.join(dir, 'plugins', path.basename(entryFile) + '.js'),
|
|
1677
1684
|
];
|
|
1678
1685
|
let entryPath = null;
|
|
1679
1686
|
for (const c of candidates) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.5.
|
|
3
|
+
"version": "6.5.13",
|
|
4
4
|
"description": "JSQL-NEO \u2014 Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"index.d.ts",
|
|
36
36
|
"lib/",
|
|
37
37
|
"bin/",
|
|
38
|
+
"src/",
|
|
38
39
|
"wasm/",
|
|
39
40
|
"native/",
|
|
40
41
|
"nativesrc/",
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSQL Plugin Registry CLI
|
|
3
|
+
*
|
|
4
|
+
* 协议文档: /root/.openclaw/workspace/registry-protocol.md
|
|
5
|
+
* Registry API: https://registry-beta.vexify.top/?action=xxx
|
|
6
|
+
*
|
|
7
|
+
* 注意: 当前 registry-beta 实例仅实现了 plugins / search / registry 三个 action,
|
|
8
|
+
* 尚未实现 info 和 download。本模块优先使用实际 API,缺失能力时给出友好提示。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const REGISTRY_BASE = 'https://registry-beta.vexify.top';
|
|
17
|
+
const PLUGIN_DIR = path.join(os.homedir(), '.jsql', 'plugins');
|
|
18
|
+
const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024;
|
|
19
|
+
const MAX_ARCHIVE_ENTRIES = 10000;
|
|
20
|
+
|
|
21
|
+
// ─── 工具函数 ──────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** 插件名只允许字母/数字/点/下划线/连字符,杜绝路径注入 */
|
|
24
|
+
function assertValidPluginName(name) {
|
|
25
|
+
if (typeof name !== 'string' || !name) {
|
|
26
|
+
throw new Error('Plugin name must be a non-empty string');
|
|
27
|
+
}
|
|
28
|
+
if (name.length > 128) {
|
|
29
|
+
throw new Error(`Invalid plugin name: too long (${name.length} > 128)`);
|
|
30
|
+
}
|
|
31
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name.includes('..')) {
|
|
32
|
+
throw new Error(`Invalid plugin name: '${name}'`);
|
|
33
|
+
}
|
|
34
|
+
return name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 校验 tar 清单:拒绝绝对路径、`..`、设备/符号链接/硬链接 */
|
|
38
|
+
function assertSafeTarEntries(archivePath, destDir) {
|
|
39
|
+
const names = execFileSync('tar', ['-tzf', archivePath], {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
42
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
43
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
44
|
+
if (names.length > MAX_ARCHIVE_ENTRIES) {
|
|
45
|
+
throw new Error(`Archive contains too many entries (${names.length} > ${MAX_ARCHIVE_ENTRIES})`);
|
|
46
|
+
}
|
|
47
|
+
const destResolved = path.resolve(destDir);
|
|
48
|
+
for (const entry of names) {
|
|
49
|
+
if (entry.startsWith('/') || entry.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(entry)) {
|
|
50
|
+
throw new Error(`Archive contains absolute path: ${entry}`);
|
|
51
|
+
}
|
|
52
|
+
if (entry.split(/[\\/]+/).includes('..')) {
|
|
53
|
+
throw new Error(`Archive contains path traversal: ${entry}`);
|
|
54
|
+
}
|
|
55
|
+
const target = path.resolve(destResolved, entry);
|
|
56
|
+
if (target !== destResolved && !target.startsWith(destResolved + path.sep)) {
|
|
57
|
+
throw new Error(`Archive entry escapes plugin directory: ${entry}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 类型检查单独用 -tvzf,避免解析带空格的路径
|
|
61
|
+
const verbose = execFileSync('tar', ['-tvzf', archivePath], {
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
64
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
65
|
+
});
|
|
66
|
+
for (const line of verbose.split(/\r?\n/)) {
|
|
67
|
+
if (!line) continue;
|
|
68
|
+
const type = line.charAt(0);
|
|
69
|
+
if (type === 'l' || type === 'h') {
|
|
70
|
+
throw new Error('Archive contains links, which are not allowed');
|
|
71
|
+
}
|
|
72
|
+
if (type !== '-' && type !== 'd') {
|
|
73
|
+
throw new Error(`Archive contains unsupported entry type '${type}'`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 确保插件目录存在 */
|
|
79
|
+
function ensurePluginDir() {
|
|
80
|
+
if (!fs.existsSync(PLUGIN_DIR)) {
|
|
81
|
+
fs.mkdirSync(PLUGIN_DIR, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 从可能含有 PHP Warning 前缀的响应文本中提取 JSON */
|
|
86
|
+
function extractJson(text) {
|
|
87
|
+
// 找到第一个 '{' 的位置,从那里开始解析
|
|
88
|
+
const idx = text.indexOf('{');
|
|
89
|
+
if (idx === -1) return null;
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(text.slice(idx));
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 发起 registry API 请求,返回解析后的 JSON */
|
|
98
|
+
async function apiRequest(action, params = {}) {
|
|
99
|
+
const url = new URL(REGISTRY_BASE);
|
|
100
|
+
url.searchParams.set('action', action);
|
|
101
|
+
for (const [k, v] of Object.entries(params)) {
|
|
102
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, v);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const res = await fetch(url.toString());
|
|
106
|
+
const text = await res.text();
|
|
107
|
+
const body = extractJson(text);
|
|
108
|
+
|
|
109
|
+
if (!res.ok || !body || body.status !== 'success') {
|
|
110
|
+
const msg = body?.error || `HTTP ${res.status}`;
|
|
111
|
+
throw new Error(`Registry error: ${msg}`);
|
|
112
|
+
}
|
|
113
|
+
return body.data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── 核心操作 ──────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/** 列出所有插件(支持 category / tag / author / q / sort / order / page / limit) */
|
|
119
|
+
async function listPlugins(opts = {}) {
|
|
120
|
+
const params = {};
|
|
121
|
+
if (opts.category) params.category = opts.category;
|
|
122
|
+
if (opts.tag) params.tag = opts.tag;
|
|
123
|
+
if (opts.author) params.author = opts.author;
|
|
124
|
+
if (opts.q) params.q = opts.q;
|
|
125
|
+
if (opts.sort) params.sort = opts.sort;
|
|
126
|
+
if (opts.order) params.order = opts.order;
|
|
127
|
+
if (opts.page) params.page = opts.page;
|
|
128
|
+
if (opts.limit) params.limit = opts.limit;
|
|
129
|
+
return apiRequest('plugins', params);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 搜索插件(q 参数必填) */
|
|
133
|
+
async function searchPlugins(query, opts = {}) {
|
|
134
|
+
if (!query) throw new Error('Search query is required');
|
|
135
|
+
const params = { q: query };
|
|
136
|
+
if (opts.category) params.category = opts.category;
|
|
137
|
+
if (opts.tag) params.tag = opts.tag;
|
|
138
|
+
if (opts.sort) params.sort = opts.sort;
|
|
139
|
+
return apiRequest('search', params);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 获取单个插件信息(实际 API 未实现 info action,通过 plugins 列表过滤) */
|
|
143
|
+
async function getPluginInfo(name) {
|
|
144
|
+
const plugins = await listPlugins({ q: name, limit: 100 });
|
|
145
|
+
const plugin = plugins.find(p => p.name === name);
|
|
146
|
+
if (!plugin) throw new Error(`Plugin '${name}' not found in registry`);
|
|
147
|
+
return plugin;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 带重试的 fetch(P1 修复:install 网络间歇性失败 30-40%)
|
|
152
|
+
* 3 次重试 + 指数退避(500ms, 1s, 2s)
|
|
153
|
+
*/
|
|
154
|
+
async function fetchWithRetry(url, retries = 3, baseDelay = 500) {
|
|
155
|
+
for (let i = 0; i < retries; i++) {
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(url.toString());
|
|
158
|
+
if (res.ok) return res;
|
|
159
|
+
// 5xx / 429 可重试
|
|
160
|
+
if ((res.status >= 500 || res.status === 429) && i < retries - 1) {
|
|
161
|
+
const delay = baseDelay * Math.pow(2, i);
|
|
162
|
+
await new Promise(r => setTimeout(r, delay));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
return res;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
if (i < retries - 1) {
|
|
168
|
+
const delay = baseDelay * Math.pow(2, i);
|
|
169
|
+
await new Promise(r => setTimeout(r, delay));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 安装插件:下载 tgz 并解压到 ~/.jsql/plugins/<name>/ */
|
|
178
|
+
async function installPlugin(name) {
|
|
179
|
+
assertValidPluginName(name);
|
|
180
|
+
ensurePluginDir();
|
|
181
|
+
|
|
182
|
+
// 检查是否已安装
|
|
183
|
+
const pluginDir = path.join(PLUGIN_DIR, name);
|
|
184
|
+
const manifestPath = path.join(pluginDir, 'manifest.json');
|
|
185
|
+
if (fs.existsSync(manifestPath)) {
|
|
186
|
+
const existing = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
187
|
+
throw new Error(`Plugin '${name}' is already installed (v${existing.version})`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 下载插件包(带重试)
|
|
191
|
+
const url = new URL(REGISTRY_BASE);
|
|
192
|
+
url.searchParams.set('action', 'download');
|
|
193
|
+
url.searchParams.set('name', name);
|
|
194
|
+
|
|
195
|
+
const res = await fetchWithRetry(url);
|
|
196
|
+
const ct = res.headers.get('content-type') || '';
|
|
197
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
198
|
+
|
|
199
|
+
if (buf.length > MAX_ARCHIVE_BYTES) {
|
|
200
|
+
throw new Error(`Plugin archive too large (${buf.length} > ${MAX_ARCHIVE_BYTES} bytes)`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 检查 Content-Type 或 magic bytes
|
|
204
|
+
if (!ct.includes('gzip') && buf[0] !== 0x1f) {
|
|
205
|
+
// 返回了错误 JSON,不是 tgz
|
|
206
|
+
const text = buf.toString('utf8');
|
|
207
|
+
const body = extractJson(text);
|
|
208
|
+
throw new Error(body?.error || `Download failed (HTTP ${res.status})`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const archivePath = path.join(PLUGIN_DIR, `${name}.tgz`);
|
|
212
|
+
fs.writeFileSync(archivePath, buf, { mode: 0o600 });
|
|
213
|
+
|
|
214
|
+
// 解压 tgz:先校验清单,再用 execFile 避免 shell 注入
|
|
215
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
216
|
+
try {
|
|
217
|
+
assertSafeTarEntries(archivePath, pluginDir);
|
|
218
|
+
execFileSync('tar', ['-xzf', archivePath, '-C', pluginDir], {
|
|
219
|
+
stdio: 'pipe',
|
|
220
|
+
});
|
|
221
|
+
} catch (e) {
|
|
222
|
+
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
223
|
+
fs.rmSync(archivePath, { force: true });
|
|
224
|
+
throw new Error(`Failed to extract plugin archive: ${e.message}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 读取 manifest(兼容两种路径:根级 + plugins/ 子目录)
|
|
228
|
+
let manifestFile = path.join(pluginDir, 'manifest.json');
|
|
229
|
+
if (!fs.existsSync(manifestFile)) {
|
|
230
|
+
manifestFile = path.join(pluginDir, 'plugins', 'manifest.json');
|
|
231
|
+
}
|
|
232
|
+
let manifest;
|
|
233
|
+
if (fs.existsSync(manifestFile)) {
|
|
234
|
+
manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
235
|
+
} else {
|
|
236
|
+
// 创建默认 manifest
|
|
237
|
+
manifest = { name, version: '0.0.0', installedAt: new Date().toISOString() };
|
|
238
|
+
fs.writeFileSync(path.join(pluginDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 清理压缩包
|
|
242
|
+
fs.rmSync(archivePath, { force: true });
|
|
243
|
+
|
|
244
|
+
return manifest;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 卸载插件
|
|
249
|
+
* P0 安全修复:校验 name 不含路径遍历序列,使用 basename 隔离,
|
|
250
|
+
* 防止 `jsql plugin uninstall '../../etc/something'` 删除任意目录。
|
|
251
|
+
*/
|
|
252
|
+
function uninstallPlugin(name) {
|
|
253
|
+
if (typeof name !== 'string' || !name) {
|
|
254
|
+
throw new Error('Plugin name must be a non-empty string');
|
|
255
|
+
}
|
|
256
|
+
// 拒绝包含路径遍历的名称
|
|
257
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
258
|
+
throw new Error(`Invalid plugin name: '${name}'`);
|
|
259
|
+
}
|
|
260
|
+
// 使用 basename 进一步隔离
|
|
261
|
+
const safeName = path.basename(name);
|
|
262
|
+
if (safeName !== name) {
|
|
263
|
+
throw new Error(`Invalid plugin name: '${name}'`);
|
|
264
|
+
}
|
|
265
|
+
const pluginDir = path.join(PLUGIN_DIR, safeName);
|
|
266
|
+
// 二次校验:确保解析后的路径严格在 PLUGIN_DIR 之下
|
|
267
|
+
const resolved = path.resolve(pluginDir);
|
|
268
|
+
const pluginDirResolved = path.resolve(PLUGIN_DIR);
|
|
269
|
+
if (resolved !== pluginDirResolved && !resolved.startsWith(pluginDirResolved + path.sep)) {
|
|
270
|
+
throw new Error(`Plugin name resolves outside plugin directory: '${name}'`);
|
|
271
|
+
}
|
|
272
|
+
if (!fs.existsSync(pluginDir)) {
|
|
273
|
+
throw new Error(`Plugin '${safeName}' is not installed`);
|
|
274
|
+
}
|
|
275
|
+
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
276
|
+
return { name: safeName, uninstalled: true };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 列出已安装的插件(兼容根级 + plugins/ 子目录两种布局) */
|
|
280
|
+
function listInstalled() {
|
|
281
|
+
ensurePluginDir();
|
|
282
|
+
const entries = fs.readdirSync(PLUGIN_DIR, { withFileTypes: true });
|
|
283
|
+
const plugins = [];
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
if (!entry.isDirectory()) continue;
|
|
286
|
+
// 兼容两种路径:根级 manifest.json + plugins/ 子目录 manifest.json
|
|
287
|
+
const manifestPaths = [
|
|
288
|
+
path.join(PLUGIN_DIR, entry.name, 'manifest.json'),
|
|
289
|
+
path.join(PLUGIN_DIR, entry.name, 'plugins', 'manifest.json'),
|
|
290
|
+
];
|
|
291
|
+
for (const manifestPath of manifestPaths) {
|
|
292
|
+
if (fs.existsSync(manifestPath)) {
|
|
293
|
+
try {
|
|
294
|
+
plugins.push(JSON.parse(fs.readFileSync(manifestPath, 'utf8')));
|
|
295
|
+
} catch { /* 跳过损坏的 manifest */ }
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return plugins;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─── 格式化输出 ────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
/** 格式化插件列表为表格 */
|
|
306
|
+
function formatPluginList(plugins) {
|
|
307
|
+
if (!plugins.length) return 'No plugins found.';
|
|
308
|
+
const rows = plugins.map(p =>
|
|
309
|
+
`${p.name.padEnd(24)} ${(p.version || '?').padEnd(10)} ${(p.description || '').slice(0, 50)}`
|
|
310
|
+
);
|
|
311
|
+
return rows.join('\n');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** 格式化插件详情 */
|
|
315
|
+
function formatPluginInfo(p) {
|
|
316
|
+
const lines = [
|
|
317
|
+
`Name: ${p.name}`,
|
|
318
|
+
`Version: ${p.version}`,
|
|
319
|
+
`Author: ${p.author}`,
|
|
320
|
+
`License: ${p.license}`,
|
|
321
|
+
`Category: ${p.category}`,
|
|
322
|
+
`Description: ${p.description}`,
|
|
323
|
+
`Tags: ${(p.tags || []).join(', ')}`,
|
|
324
|
+
`Downloads: ${p.downloads || 0}`,
|
|
325
|
+
];
|
|
326
|
+
if (p.jsql) {
|
|
327
|
+
lines.push(`JSQL: ${p.jsql.minVersion}+${p.jsql.maxVersion ? ' – ' + p.jsql.maxVersion : ''}`);
|
|
328
|
+
}
|
|
329
|
+
return lines.join('\n');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
PLUGIN_DIR,
|
|
334
|
+
ensurePluginDir,
|
|
335
|
+
assertValidPluginName,
|
|
336
|
+
assertSafeTarEntries,
|
|
337
|
+
listPlugins,
|
|
338
|
+
searchPlugins,
|
|
339
|
+
getPluginInfo,
|
|
340
|
+
installPlugin,
|
|
341
|
+
uninstallPlugin,
|
|
342
|
+
listInstalled,
|
|
343
|
+
formatPluginList,
|
|
344
|
+
formatPluginInfo,
|
|
345
|
+
fetchWithRetry,
|
|
346
|
+
};
|
package/test/plugin-xql.test.js
CHANGED
|
@@ -125,10 +125,33 @@ async function testTableName() {
|
|
|
125
125
|
await db.stop();
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
async function testInstalledPluginAutoLoad() {
|
|
129
|
+
console.log('\n===== 注册表插件自动加载(entry 仓库相对路径 + 根级文件)=====');
|
|
130
|
+
const os = require('os');
|
|
131
|
+
const fs = require('fs');
|
|
132
|
+
const home = os.homedir();
|
|
133
|
+
const name = '_xqltest_registry';
|
|
134
|
+
const dir = path.join(home, '.jsql', 'plugins', name);
|
|
135
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
136
|
+
fs.writeFileSync(path.join(dir, 'index.js'),
|
|
137
|
+
"module.exports = { name: '" + name + "', install(engine) { engine.__xqltest = true; } };\n");
|
|
138
|
+
fs.writeFileSync(path.join(dir, 'manifest.json'),
|
|
139
|
+
JSON.stringify({ name, version: '1.0.0', entry: 'plugins/' + name + '/index.js', files: [] }));
|
|
140
|
+
try {
|
|
141
|
+
const db = new Database(':memory:', { modules: true });
|
|
142
|
+
await db.start();
|
|
143
|
+
ok('仓库相对 entry + 根级 index.js 能自动加载', db.__xqltest === true);
|
|
144
|
+
await db.stop();
|
|
145
|
+
} finally {
|
|
146
|
+
try { fs.rmSync(path.join(home, '.jsql', 'plugins', name), { recursive: true, force: true }); } catch { /* ignore */ }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
128
150
|
(async () => {
|
|
129
151
|
await testEngine(Database, 'Database (Pure JS)');
|
|
130
152
|
await testAutoLoadOff();
|
|
131
153
|
await testTableName();
|
|
154
|
+
await testInstalledPluginAutoLoad();
|
|
132
155
|
|
|
133
156
|
// native 引擎(Linux 上可用;Android 等平台无二进制时跳过)
|
|
134
157
|
try {
|