@qmuse/qmuse-cli 1.0.3-dev.0 → 1.0.3-dev.2
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 +4 -10
- package/dist/dev/context.js +2 -2
- package/dist/dev/index.js +8 -6
- package/dist/dev/plugin.js +16 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,17 +95,11 @@ qmuse dev . --port 5173
|
|
|
95
95
|
|
|
96
96
|
CLI 获取服务端解析的公开云配置,复用 Vite 的 `MUSE_RUNTIME_CONFIG_PATH` 注入机制。配置放在系统临时目录,退出时删除。Token 按需获取且不落盘,CLI 登录凭据不交给页面。数据库、文件和函数请求由浏览器通过用户 Session 直接访问云端,不使用管理 API Key 或 Dev Key。
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
fetch: import.meta.env.DEV
|
|
102
|
-
? (window as Window & {
|
|
103
|
-
__QMUSE_LOCAL_DEV__?: { fetch: typeof globalThis.fetch };
|
|
104
|
-
}).__QMUSE_LOCAL_DEV__?.fetch
|
|
105
|
-
: undefined,
|
|
106
|
-
```
|
|
98
|
+
CLI 将本地 Runtime 的 `__MUSE__.domain` 设置为实际监听地址(如 `http://127.0.0.1:5173`),保留真实的 `appwrite.endpoint`。SDK 使用默认 fetch,将配置、Token 和操作上报发往本地三个固定接口,由 CLI 处理或转发;数据库、文件、函数仍直连云端。接口仅接受同源 JSON POST,并校验 Host、Fetch Metadata 和应用绑定。
|
|
99
|
+
|
|
100
|
+
不解析或改写业务源码,不注入自定义 fetch,不要求固定服务文件路径,也无需修改或发布 qmuse-cloud、Runtime SDK。已下载的模板使用原版云服务资产即可。普通开发和生产构建使用原有配置。其他自行基于 `__MUSE__.domain` 拼接的平台 API 不会被通用转发,需要单独确认接入方式。
|
|
107
101
|
|
|
108
|
-
|
|
102
|
+
服务端必须先部署 `/api/appwrite/local-dev/context` 和 `/api/appwrite/local-dev/mutation-events`,再发布并安装新版 CLI;缺少接口时不会降级为模拟云服务。
|
|
109
103
|
|
|
110
104
|
页面改动热更新,Schema/函数改动继续执行 `qmuse import .`。导入更新本地绑定后,连接会重新获取配置并刷新页面。切换登录账号或应用绑定需重启连接;退出 CLI 不会自动撤销浏览器已经建立的 AppBase Session。SDK 的匿名降级行为保持不变,验收必须检查实际 Session 用户,不能只看页面是否显示。
|
|
111
105
|
|
package/dist/dev/context.js
CHANGED
|
@@ -53,14 +53,14 @@ export async function loadDevContext(root, cfg, environment) {
|
|
|
53
53
|
throw new Error(`获取开发连接失败:${result.errorMessage || result.errorCode || `HTTP ${result.status}`}`);
|
|
54
54
|
return parseDevContext(result.data, project.target.appId);
|
|
55
55
|
}
|
|
56
|
-
export function browserRuntime(context) {
|
|
56
|
+
export function browserRuntime(context, localOrigin) {
|
|
57
57
|
return {
|
|
58
58
|
__MUSE__: {
|
|
59
59
|
appId: context.appId,
|
|
60
60
|
yfdAppId: context.appId,
|
|
61
61
|
appVersionId: context.appVersionId,
|
|
62
62
|
env: context.env,
|
|
63
|
-
domain:
|
|
63
|
+
domain: localOrigin,
|
|
64
64
|
appwrite: context.appwrite,
|
|
65
65
|
},
|
|
66
66
|
__TERN__: {
|
package/dist/dev/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { watchFile, unwatchFile } from 'node:fs';
|
|
3
|
-
import { mkdtemp,
|
|
3
|
+
import { mkdtemp, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
@@ -22,10 +22,6 @@ export async function startDevelopment(options) {
|
|
|
22
22
|
const project = await loadManagedProject(root, options.cfg, options.environment);
|
|
23
23
|
if (!project?.target)
|
|
24
24
|
throw new Error('请先执行 qmuse import . 并等待云服务 READY,再启动本地联调');
|
|
25
|
-
const serviceFile = await readFile(path.join(root, 'src/services/appwrite.ts'), 'utf8').catch(() => '');
|
|
26
|
-
if (!serviceFile.includes('__QMUSE_LOCAL_DEV__')) {
|
|
27
|
-
throw new Error('项目尚未接入本地适配:请按最新版云服务资产在 src/services/appwrite.ts 的 Runtime options 中传入本地 fetch(见 qmuse-cli README)');
|
|
28
|
-
}
|
|
29
25
|
const context = await loadDevContext(root, options.cfg, options.environment);
|
|
30
26
|
const directory = await mkdtemp(path.join(tmpdir(), 'qmuse-local-dev-'));
|
|
31
27
|
const runtimePath = path.join(directory, 'runtime.json');
|
|
@@ -35,8 +31,9 @@ export async function startDevelopment(options) {
|
|
|
35
31
|
const manifestPath = path.join(root, '.qmuse/project.json');
|
|
36
32
|
let refresh;
|
|
37
33
|
let refreshRequested = false;
|
|
34
|
+
let localOrigin = 'http://127.0.0.1:0';
|
|
38
35
|
const writeRuntime = async () => {
|
|
39
|
-
await writeFile(`${runtimePath}.next`, JSON.stringify(browserRuntime(context)), { mode: 0o600 });
|
|
36
|
+
await writeFile(`${runtimePath}.next`, JSON.stringify(browserRuntime(context, localOrigin)), { mode: 0o600 });
|
|
40
37
|
await rename(`${runtimePath}.next`, runtimePath);
|
|
41
38
|
};
|
|
42
39
|
const currentConfig = () => {
|
|
@@ -95,6 +92,11 @@ export async function startDevelopment(options) {
|
|
|
95
92
|
throw new Error('项目需要支持 runtime config 注入的 @qmuse/vite-config');
|
|
96
93
|
}
|
|
97
94
|
await server.listen();
|
|
95
|
+
const address = server.httpServer?.address();
|
|
96
|
+
if (!address || typeof address === 'string')
|
|
97
|
+
throw new Error('无法获取本地开发服务端口');
|
|
98
|
+
localOrigin = `http://127.0.0.1:${address.port}`;
|
|
99
|
+
await writeRuntime();
|
|
98
100
|
watchFile(manifestPath, { interval: 1000 }, (current, previous) => {
|
|
99
101
|
if (closed || current.mtimeMs === previous.mtimeMs)
|
|
100
102
|
return;
|
package/dist/dev/plugin.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
-
const PREFIX = '/__qmuse/local-dev/';
|
|
3
|
-
const TOKEN_HEADER = 'x-qmuse-local-connection';
|
|
4
1
|
const BODY_LIMIT = 64 * 1024;
|
|
5
2
|
const ROUTES = {
|
|
6
3
|
'/runtime/appwrite/config': 'config',
|
|
@@ -26,35 +23,7 @@ async function readBody(req) {
|
|
|
26
23
|
throw new Error('Invalid JSON object');
|
|
27
24
|
return value;
|
|
28
25
|
}
|
|
29
|
-
export function bridgeSource(context, connection) {
|
|
30
|
-
const settings = JSON.stringify({ domain: context.domain, routes: ROUTES, prefix: PREFIX, connection, header: TOKEN_HEADER })
|
|
31
|
-
.replace(/</gu, '\\u003c');
|
|
32
|
-
return `(() => {
|
|
33
|
-
const settings = ${settings};
|
|
34
|
-
const nativeFetch = window.fetch.bind(window);
|
|
35
|
-
window.__QMUSE_LOCAL_DEV__ = {
|
|
36
|
-
protocolVersion: 1,
|
|
37
|
-
async fetch(input, init) {
|
|
38
|
-
const url = new URL(input instanceof Request ? input.url : String(input), location.href);
|
|
39
|
-
const base = new URL(settings.domain);
|
|
40
|
-
const relativePath = url.pathname.slice(base.pathname.replace(/\\/$/, '').length);
|
|
41
|
-
const route = url.origin === base.origin && url.pathname.startsWith(base.pathname.replace(/\\/$/, '') + '/')
|
|
42
|
-
? settings.routes[relativePath] : undefined;
|
|
43
|
-
if (!route) return nativeFetch(input, init);
|
|
44
|
-
const request = new Request(input, init);
|
|
45
|
-
const headers = new Headers(request.headers);
|
|
46
|
-
headers.set(settings.header, settings.connection);
|
|
47
|
-
return nativeFetch(settings.prefix + route, {
|
|
48
|
-
method: request.method, headers,
|
|
49
|
-
body: request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.text(),
|
|
50
|
-
credentials: 'same-origin', signal: request.signal, keepalive: request.keepalive
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
})();`;
|
|
55
|
-
}
|
|
56
26
|
export function localDevPlugin(options) {
|
|
57
|
-
const connection = randomBytes(32).toString('hex');
|
|
58
27
|
let server;
|
|
59
28
|
return {
|
|
60
29
|
name: 'qmuse-local-dev',
|
|
@@ -73,37 +42,35 @@ export function localDevPlugin(options) {
|
|
|
73
42
|
return;
|
|
74
43
|
}
|
|
75
44
|
const url = new URL(req.url || '/', origin);
|
|
76
|
-
if (!url.pathname
|
|
77
|
-
|
|
45
|
+
if (!Object.hasOwn(ROUTES, url.pathname)) {
|
|
46
|
+
if (url.pathname.startsWith('/runtime/') || url.pathname.startsWith('/api/runtime/')) {
|
|
47
|
+
respond(res, 404, { success: false, message: 'Unknown local operation' });
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
next();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Browser requests must be same-origin JSON; CLI credentials stay on the server.
|
|
54
|
+
if (req.headers.origin !== origin) {
|
|
55
|
+
respond(res, 403, { success: false, message: 'Invalid local development origin' });
|
|
78
56
|
return;
|
|
79
57
|
}
|
|
80
|
-
if (
|
|
81
|
-
res
|
|
82
|
-
res.end(bridgeSource(options.context, connection));
|
|
58
|
+
if (req.method !== 'POST') {
|
|
59
|
+
respond(res, 405, { success: false, message: 'POST required' });
|
|
83
60
|
return;
|
|
84
61
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
!timingSafeEqual(Buffer.from(provided), Buffer.from(connection))) {
|
|
88
|
-
respond(res, 403, { success: false, message: 'Invalid local connection' });
|
|
62
|
+
if (req.headers['content-type']?.split(';')[0].trim().toLowerCase() !== 'application/json') {
|
|
63
|
+
respond(res, 415, { success: false, message: 'JSON required' });
|
|
89
64
|
return;
|
|
90
65
|
}
|
|
91
|
-
void handle(url.pathname
|
|
66
|
+
void handle(ROUTES[url.pathname], req, res).catch(() => {
|
|
92
67
|
if (!res.headersSent)
|
|
93
68
|
respond(res, 400, { success: false, message: 'Invalid local development request' });
|
|
94
69
|
});
|
|
95
70
|
});
|
|
96
71
|
},
|
|
97
|
-
transformIndexHtml: {
|
|
98
|
-
order: 'pre',
|
|
99
|
-
handler: () => [{ tag: 'script', attrs: { src: `${PREFIX}bootstrap.js` }, injectTo: 'head-prepend' }],
|
|
100
|
-
},
|
|
101
72
|
};
|
|
102
73
|
async function handle(route, req, res) {
|
|
103
|
-
if (!['config', 'token', 'mutation-events'].includes(route)) {
|
|
104
|
-
respond(res, 404, { success: false, message: 'Unknown local operation' });
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
74
|
const body = await readBody(req);
|
|
108
75
|
const context = options.context;
|
|
109
76
|
if (body.appId !== context.appId || (body.env !== undefined && body.env !== 'dev') ||
|