jsql-neo 6.5.24 → 6.5.26

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/updater.js ADDED
@@ -0,0 +1,365 @@
1
+ 'use strict';
2
+ /**
3
+ * jsql update —— 检查并安装 jsql-neo 的新版本。
4
+ *
5
+ * 设计取舍:
6
+ * - 版本来源优先 npm registry(https://registry.npmjs.org/jsql-neo/latest),
7
+ * 失败时退回 `npm view jsql-neo version`;两者都失败就优雅报错,不 crash。
8
+ * - 只有「全局安装」才自动执行 `npm install -g jsql-neo@latest`;
9
+ * 本地依赖安装绝不自动改用户的 package.json,只打印清晰指令。
10
+ * - `--check` 只看不装;`--dry-run` 打印将要执行的命令;`--force` 即使已是最新也重装。
11
+ *
12
+ * 不引入任何第三方依赖(与项目其它部分保持一致)。
13
+ */
14
+
15
+ const path = require('path');
16
+ const https = require('https');
17
+ const { execFileSync, spawnSync } = require('child_process');
18
+
19
+ const PKG_NAME = 'jsql-neo';
20
+ const REGISTRY_LATEST = `https://registry.npmjs.org/${PKG_NAME}/latest`;
21
+ const FETCH_TIMEOUT_MS = 8000;
22
+ const MAX_BODY_BYTES = 512 * 1024;
23
+
24
+ function packageDir() {
25
+ return path.resolve(__dirname, '..');
26
+ }
27
+
28
+ function currentVersion() {
29
+ return require('../package.json').version;
30
+ }
31
+
32
+ // ─── 版本比较 ────────────────────────────────────────────────
33
+
34
+ /** 解析 semver(宽松):v1.2.3 / 1.2 / 1.2.3-beta.1 → { nums, pre } */
35
+ function parseVersion(v) {
36
+ let s = String(v == null ? '' : v).trim();
37
+ if (s[0] === 'v' || s[0] === 'V') s = s.slice(1);
38
+ const plus = s.indexOf('+');
39
+ if (plus !== -1) s = s.slice(0, plus);
40
+ const dash = s.indexOf('-');
41
+ const core = dash === -1 ? s : s.slice(0, dash);
42
+ const pre = dash === -1 ? '' : s.slice(dash + 1);
43
+ const nums = core.split('.').map((n) => {
44
+ const x = parseInt(n, 10);
45
+ return Number.isFinite(x) ? x : 0;
46
+ });
47
+ while (nums.length < 3) nums.push(0);
48
+ return { nums, pre };
49
+ }
50
+
51
+ /** semver-ish 比较:a < b → -1,相等 → 0,a > b → 1(prerelease 视为更低) */
52
+ function compareVersions(a, b) {
53
+ const A = parseVersion(a);
54
+ const B = parseVersion(b);
55
+ for (let i = 0; i < 3; i++) {
56
+ if (A.nums[i] !== B.nums[i]) return A.nums[i] > B.nums[i] ? 1 : -1;
57
+ }
58
+ if (A.pre === B.pre) return 0;
59
+ if (!A.pre) return 1;
60
+ if (!B.pre) return -1;
61
+ return A.pre > B.pre ? 1 : -1;
62
+ }
63
+
64
+ function isNewer(latest, current) {
65
+ return compareVersions(latest, current) > 0;
66
+ }
67
+
68
+ // ─── 网络 ───────────────────────────────────────────────────
69
+
70
+ function httpGetText(url, redirects = 0) {
71
+ return new Promise((resolve, reject) => {
72
+ if (redirects > 5) {
73
+ reject(new Error(`too many redirects for ${url}`));
74
+ return;
75
+ }
76
+ let req;
77
+ try {
78
+ req = https.get(url, {
79
+ headers: {
80
+ 'User-Agent': `jsql-neo/${currentVersion()} (+https://github.com/vexify-org/JSQL-neo)`,
81
+ Accept: 'application/vnd.npm.install-v1+json, application/json;q=0.9, */*;q=0.1',
82
+ },
83
+ }, (res) => {
84
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
85
+ res.resume();
86
+ const next = new URL(res.headers.location, url).toString();
87
+ if (!/^https?:/i.test(next)) {
88
+ reject(new Error(`invalid redirect target for ${url}`));
89
+ return;
90
+ }
91
+ httpGetText(next, redirects + 1).then(resolve, reject);
92
+ return;
93
+ }
94
+ if (res.statusCode !== 200) {
95
+ res.resume();
96
+ reject(new Error(`HTTP ${res.statusCode} for ${url}`));
97
+ return;
98
+ }
99
+ const chunks = [];
100
+ let received = 0;
101
+ res.on('data', (chunk) => {
102
+ received += chunk.length;
103
+ if (received > MAX_BODY_BYTES) {
104
+ res.destroy();
105
+ reject(new Error(`response too large for ${url}`));
106
+ return;
107
+ }
108
+ chunks.push(chunk);
109
+ });
110
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
111
+ res.on('error', reject);
112
+ });
113
+ } catch (e) {
114
+ reject(e);
115
+ return;
116
+ }
117
+ req.on('error', reject);
118
+ req.setTimeout(FETCH_TIMEOUT_MS, () => {
119
+ try { req.destroy(); } catch (_) { /* ignore */ }
120
+ reject(new Error(`request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`));
121
+ });
122
+ });
123
+ }
124
+
125
+ /**
126
+ * 查询 npm 上的最新版本号。
127
+ * @param {{ fetchImpl?: Function, npmView?: boolean }} [opts] 测试可注入
128
+ * @returns {Promise<{version: string, source: string}>}
129
+ */
130
+ async function fetchLatestVersion(opts = {}) {
131
+ const fetchImpl = opts.fetchImpl || httpGetText;
132
+ const errors = [];
133
+
134
+ // 1) registry HTTP(快,且不需要 npm 进程)
135
+ try {
136
+ const body = await fetchImpl(REGISTRY_LATEST);
137
+ const data = JSON.parse(body);
138
+ if (data && typeof data.version === 'string' && data.version.trim()) {
139
+ return { version: data.version.trim(), source: 'registry' };
140
+ }
141
+ throw new Error('registry response has no version field');
142
+ } catch (e) {
143
+ errors.push(`registry: ${e.message}`);
144
+ }
145
+
146
+ // 2) npm CLI 兜底(有些环境走私有 registry 配置,curl 直连反而不通)
147
+ if (opts.npmView !== false) {
148
+ try {
149
+ const out = execFileSync(npmCommand(), ['view', PKG_NAME, 'version'], {
150
+ encoding: 'utf8',
151
+ timeout: 15000,
152
+ stdio: ['ignore', 'pipe', 'pipe'],
153
+ }).trim();
154
+ // `npm view` 在旧版本里可能输出多行/带引号
155
+ const m = out.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
156
+ if (m) return { version: m[0], source: 'npm-view' };
157
+ throw new Error(`unexpected npm view output: ${out.slice(0, 120)}`);
158
+ } catch (e) {
159
+ errors.push(`npm view: ${e.message}`);
160
+ }
161
+ }
162
+
163
+ const err = new Error(`could not determine the latest version (${errors.join('; ')})`);
164
+ err.causes = errors;
165
+ throw err;
166
+ }
167
+
168
+ // ─── 安装形态判断 ───────────────────────────────────────────
169
+
170
+ function npmCommand() {
171
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
172
+ }
173
+
174
+ function isInside(child, parent) {
175
+ const rel = path.relative(path.resolve(parent), path.resolve(child));
176
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
177
+ }
178
+
179
+ /** 收集可能的全局 node_modules 根目录 */
180
+ function globalRoots() {
181
+ const roots = [];
182
+ try {
183
+ const out = execFileSync(npmCommand(), ['root', '-g'], {
184
+ encoding: 'utf8',
185
+ timeout: 15000,
186
+ stdio: ['ignore', 'pipe', 'ignore'],
187
+ }).trim();
188
+ if (out) roots.push(out);
189
+ } catch (_) { /* npm 不可用就靠下面的启发式 */ }
190
+
191
+ if (process.env.npm_config_prefix) {
192
+ roots.push(path.join(process.env.npm_config_prefix, 'lib', 'node_modules'));
193
+ if (process.platform === 'win32') {
194
+ roots.push(path.join(process.env.npm_config_prefix, 'node_modules'));
195
+ }
196
+ }
197
+ if (process.platform === 'win32' && process.env.APPDATA) {
198
+ roots.push(path.join(process.env.APPDATA, 'npm', 'node_modules'));
199
+ }
200
+ return Array.from(new Set(roots.filter(Boolean)));
201
+ }
202
+
203
+ /**
204
+ * 判断当前这份 jsql-neo 是否来自全局安装。
205
+ * @param {string} [dir] 包目录,默认是 lib/..
206
+ */
207
+ function isGlobalInstall(dir) {
208
+ const pkgDir = path.resolve(dir || packageDir());
209
+ return globalRoots().some((root) => isInside(pkgDir, root));
210
+ }
211
+
212
+ /** 当前包是否位于某个项目的 node_modules 下(本地依赖安装) */
213
+ function isLocalDependency(dir) {
214
+ const pkgDir = path.resolve(dir || packageDir());
215
+ return path.basename(path.dirname(pkgDir)) === 'node_modules';
216
+ }
217
+
218
+ // ─── 更新流程 ───────────────────────────────────────────────
219
+
220
+ function installCommand(version) {
221
+ return `${npmCommand()} install -g ${PKG_NAME}@${version}`;
222
+ }
223
+
224
+ /**
225
+ * 执行更新检查 / 安装。
226
+ * @param {{checkOnly?: boolean, force?: boolean, dryRun?: boolean}} [opts]
227
+ */
228
+ async function runUpdate(opts = {}) {
229
+ const current = currentVersion();
230
+ let latestInfo;
231
+ try {
232
+ latestInfo = await fetchLatestVersion({ fetchImpl: opts.fetchImpl, npmView: opts.npmView });
233
+ } catch (e) {
234
+ return {
235
+ ok: false,
236
+ current,
237
+ latest: null,
238
+ error: e.message,
239
+ hints: [
240
+ '检查网络/registry 配置,或手动执行: ' + installCommand('latest'),
241
+ ],
242
+ };
243
+ }
244
+
245
+ const latest = latestInfo.version;
246
+ const updateAvailable = isNewer(latest, current);
247
+ const global = typeof opts.global === 'boolean' ? opts.global : isGlobalInstall();
248
+ const localDep = typeof opts.localDependency === 'boolean' ? opts.localDependency : isLocalDependency();
249
+ const result = {
250
+ ok: true,
251
+ current,
252
+ latest,
253
+ latestSource: latestInfo.source,
254
+ updateAvailable,
255
+ installType: global ? 'global' : (localDep ? 'local-dependency' : 'unknown'),
256
+ action: 'none',
257
+ command: null,
258
+ installed: false,
259
+ warnings: [],
260
+ };
261
+
262
+ if (!updateAvailable && !opts.force) {
263
+ result.action = opts.checkOnly ? 'checked' : 'up-to-date';
264
+ return result;
265
+ }
266
+
267
+ if (opts.checkOnly) {
268
+ result.action = 'checked';
269
+ result.command = global ? installCommand(latest) : installCommand(latest);
270
+ return result;
271
+ }
272
+
273
+ if (opts.dryRun) {
274
+ result.action = 'would-install';
275
+ result.command = installCommand(latest);
276
+ return result;
277
+ }
278
+
279
+ // 非全局安装:不自动改动用户项目依赖,只给清晰指令
280
+ if (!global) {
281
+ result.action = 'instructions';
282
+ result.command = installCommand(latest);
283
+ result.localHint = `npm install ${PKG_NAME}@${latest}`;
284
+ result.warnings.push(
285
+ 'jsql-neo 不是全局安装(当前位于项目 node_modules 或开发目录),未自动安装。'
286
+ );
287
+ return result;
288
+ }
289
+
290
+ const args = ['install', '-g', `${PKG_NAME}@${latest}`];
291
+ const r = spawnSync(npmCommand(), args, {
292
+ stdio: 'inherit',
293
+ timeout: 10 * 60 * 1000,
294
+ });
295
+ result.command = `${npmCommand()} ${args.join(' ')}`;
296
+ if (r.error) {
297
+ result.ok = false;
298
+ result.error = r.error.message;
299
+ result.action = 'install-failed';
300
+ result.warnings.push('可尝试手动执行并检查权限: ' + result.command);
301
+ return result;
302
+ }
303
+ if (r.status !== 0) {
304
+ result.ok = false;
305
+ result.error = `npm install exited with code ${r.status}`;
306
+ result.action = 'install-failed';
307
+ result.warnings.push(
308
+ '如果是权限问题,尝试: sudo ' + result.command,
309
+ );
310
+ return result;
311
+ }
312
+
313
+ result.installed = true;
314
+ result.action = 'installed';
315
+ result.after = latest;
316
+ return result;
317
+ }
318
+
319
+ /** 把 runUpdate 的结果渲染成人读文本 */
320
+ function formatResult(r) {
321
+ const lines = [];
322
+ if (!r.ok) {
323
+ lines.push(`更新失败: ${r.error}`);
324
+ for (const h of r.hints || []) lines.push(` ${h}`);
325
+ return lines.join('\n');
326
+ }
327
+ lines.push(`当前版本: ${r.current}`);
328
+ lines.push(`最新版本: ${r.latest}${r.latestSource === 'npm-view' ? ' (via npm view)' : ''}`);
329
+
330
+ if (r.action === 'up-to-date') {
331
+ lines.push('已是最新版本。');
332
+ } else if (r.action === 'checked') {
333
+ lines.push(r.updateAvailable ? '有新版本可用。' : '已是最新版本。');
334
+ if (r.updateAvailable) lines.push(`运行 \`jsql update\` 安装,或手动执行: ${r.command}`);
335
+ } else if (r.action === 'would-install') {
336
+ lines.push(`将执行: ${r.command}`);
337
+ lines.push('(--dry-run,未做任何修改)');
338
+ } else if (r.action === 'installed') {
339
+ lines.push(`已更新到 ${r.latest}。重新打开终端后 \`jsql version\` 生效。`);
340
+ } else if (r.action === 'instructions') {
341
+ lines.push('检测到非全局安装,未自动更新(避免改动你的项目依赖)。');
342
+ lines.push(`全局安装: ${r.command}`);
343
+ if (r.localHint) lines.push(`项目内升级: ${r.localHint}`);
344
+ } else if (r.action === 'install-failed') {
345
+ lines.push(`安装失败: ${r.error}`);
346
+ }
347
+ for (const w of r.warnings || []) lines.push(`注意: ${w}`);
348
+ return lines.join('\n');
349
+ }
350
+
351
+ module.exports = {
352
+ PKG_NAME,
353
+ REGISTRY_LATEST,
354
+ parseVersion,
355
+ compareVersions,
356
+ isNewer,
357
+ currentVersion,
358
+ fetchLatestVersion,
359
+ isGlobalInstall,
360
+ isLocalDependency,
361
+ globalRoots,
362
+ runUpdate,
363
+ formatResult,
364
+ installCommand,
365
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.5.24",
3
+ "version": "6.5.26",
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",
@@ -50,12 +50,12 @@
50
50
  "scripts": {
51
51
  "postinstall": "node postinstall.js",
52
52
  "build:native": "node scripts/build-native.js",
53
- "test": "node test/smoke.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js && node test/plugin-xql.test.js && node test/bug-regression-2.test.js && node test/issue-34-35-36.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
53
+ "test": "node test/smoke.js && node test/result-raw.test.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js && node test/plugin-xql.test.js && node test/bug-regression-2.test.js && node test/issue-34-35-36.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
54
54
  "test:parser": "node test/sql-parser.test.js",
55
55
  "test:btree": "node test/btree.test.js",
56
56
  "test:native": "node test/native.test.js",
57
57
  "test:regress": "node test/regress-5.1.0.js",
58
- "test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/issue-34-35-36.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
58
+ "test:all": "node test/smoke.js && node test/result-raw.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/issue-34-35-36.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
59
59
  "test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
60
60
  "test:orms": "node examples/orms/run-all.js",
61
61
  "test:dml": "node test/dml-constraints.test.js",