fix(update): verify debug Metro project identity
这个提交包含在:
父节点
3a80ad5dd5
当前提交
4a00fc39f0
@ -484,3 +484,13 @@ pnpm --dir packages/update test
|
||||
- 工作区内 18 个 XuqmGroup 子仓库的本地 `origin` 也已改为 SSH,审计结果不再存在
|
||||
`https://user:token@host` 形式。旧访问令牌曾出现在命令审计输出,必须在 Gitea
|
||||
轮换;本文不记录其值。
|
||||
|
||||
### 2026-07-27 / Debug Metro 工程身份约束
|
||||
|
||||
- `withXuqmModuleConfig()` 新增仅供本机开发链路读取的
|
||||
`/xuqm/metro-info` 工程标识;它不进入 Release bundle,也不承载业务或凭据信息。
|
||||
- `xuqm-rn run android/ios` 在复用已占用端口前校验工程绝对路径。其他工程的
|
||||
Metro 或不支持该协议的旧 Metro 会被明确拒绝,避免 React Native CLI 仅凭
|
||||
“packager running” 静默加载错误工程或陈旧代码。
|
||||
- 当前工程的标准 Metro 继续直接提供 startup/common/app/buz 源码与 Fast Refresh;
|
||||
Debug 不执行插件构建、解压或安装。单元测试覆盖工程标识响应与 CLI 参数转发。
|
||||
|
||||
@ -53,6 +53,11 @@ module.exports = withXuqmModuleConfig(getDefaultConfig(__dirname))
|
||||
- `pnpm release:android -- --apk`:生成包含同一批插件的 Release APK。
|
||||
- `pnpm publish:android`:上传插件产物;SDK npm 制品发布只能通过 Jenkins。
|
||||
|
||||
`withXuqmModuleConfig()` 会让开发服务器公开当前工程标识;`xuqm-rn run` 在复用
|
||||
已有 Metro 前必须校验该标识。若端口属于其他工程或不支持标识校验的旧服务,命令
|
||||
会明确失败并要求重启 Metro,禁止静默连接后运行错误 bundle。确认属于当前工程后
|
||||
仍完全使用标准 Metro 与 Fast Refresh,不生成或读取 Debug 插件包。
|
||||
|
||||
发布使用的 `XUQM_API_TOKEN` 只能由 Jenkins Credentials 注入环境变量,不属于
|
||||
`xuqm.modules.json` 或 `config.xuqmconfig`。CLI 不读取项目文件中的 Token,避免可用凭据
|
||||
进入源码、插件包或构建日志。
|
||||
|
||||
@ -80,10 +80,35 @@ function createModuleSerializer(environment = process.env) {
|
||||
function withXuqmModuleConfig(metroConfig) {
|
||||
const configured = withXuqmConfig(metroConfig)
|
||||
const moduleSerializer = createModuleSerializer()
|
||||
if (!moduleSerializer) return configured
|
||||
const configuredFilter = configured.serializer?.processModuleFilter
|
||||
return {
|
||||
const configuredEnhanceMiddleware = configured.server?.enhanceMiddleware
|
||||
const result = {
|
||||
...configured,
|
||||
server: {
|
||||
...configured.server,
|
||||
enhanceMiddleware(middleware, metroServer) {
|
||||
const nextMiddleware = configuredEnhanceMiddleware
|
||||
? configuredEnhanceMiddleware(middleware, metroServer)
|
||||
: middleware
|
||||
return (request, response, next) => {
|
||||
if (request.url === '/xuqm/metro-info') {
|
||||
response.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
projectRoot: path.resolve(process.cwd()),
|
||||
protocolVersion: 1,
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
nextMiddleware(request, response, next)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
if (!moduleSerializer) return result
|
||||
return {
|
||||
...result,
|
||||
serializer: {
|
||||
...configured.serializer,
|
||||
...moduleSerializer,
|
||||
|
||||
@ -6,7 +6,7 @@ import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const { createModuleSerializer } = require('../metro')
|
||||
const { createModuleSerializer, withXuqmModuleConfig } = require('../metro')
|
||||
|
||||
function context(cacheFile, moduleId, moduleType, moduleIndex, reset = false) {
|
||||
return {
|
||||
@ -46,3 +46,32 @@ test('Metro module ranges share startup/common and isolate every app or buz', ()
|
||||
rmSync(directory, { force: true, recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('Metro exposes the current project identity for safe debug reuse', () => {
|
||||
const configured = withXuqmModuleConfig({})
|
||||
const middleware = configured.server.enhanceMiddleware((_request, _response, next) => next(), {})
|
||||
let contentType = ''
|
||||
let responseBody = ''
|
||||
let delegated = false
|
||||
middleware(
|
||||
{ url: '/xuqm/metro-info' },
|
||||
{
|
||||
setHeader(name, value) {
|
||||
if (name === 'Content-Type') contentType = value
|
||||
},
|
||||
end(value) {
|
||||
responseBody = value
|
||||
},
|
||||
},
|
||||
() => {
|
||||
delegated = true
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(delegated, false)
|
||||
assert.equal(contentType, 'application/json; charset=utf-8')
|
||||
assert.deepEqual(JSON.parse(responseBody), {
|
||||
projectRoot: path.resolve(process.cwd()),
|
||||
protocolVersion: 1,
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { strToU8, zipSync } from 'fflate'
|
||||
@ -663,8 +664,75 @@ function startMetro(optionArgs) {
|
||||
runReactNative(['start', ...optionArgs])
|
||||
}
|
||||
|
||||
function runApp(platform, optionArgs) {
|
||||
function optionValue(optionArgs, name) {
|
||||
const index = optionArgs.indexOf(name)
|
||||
return index >= 0 ? optionArgs[index + 1] : undefined
|
||||
}
|
||||
|
||||
function readMetroIdentity(port) {
|
||||
return new Promise(resolve => {
|
||||
const request = http.get(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
path: '/xuqm/metro-info',
|
||||
port,
|
||||
timeout: 800,
|
||||
},
|
||||
response => {
|
||||
let body = ''
|
||||
response.setEncoding('utf8')
|
||||
response.on('data', chunk => {
|
||||
body += chunk
|
||||
})
|
||||
response.on('end', () => {
|
||||
if (response.statusCode !== 200) {
|
||||
resolve({ kind: 'unmanaged' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const value = JSON.parse(body)
|
||||
resolve(
|
||||
typeof value?.projectRoot === 'string'
|
||||
? { kind: 'managed', projectRoot: path.resolve(value.projectRoot) }
|
||||
: { kind: 'unmanaged' },
|
||||
)
|
||||
} catch {
|
||||
resolve({ kind: 'unmanaged' })
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
request.on('timeout', () => request.destroy())
|
||||
request.on('error', error => {
|
||||
resolve(
|
||||
error?.code === 'ECONNREFUSED' || error?.code === 'ECONNRESET'
|
||||
? { kind: 'absent' }
|
||||
: { kind: 'unmanaged' },
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function assertMetroBelongsToCurrentProject(optionArgs) {
|
||||
if (optionArgs.includes('--no-packager')) return
|
||||
const parsedPort = Number.parseInt(optionValue(optionArgs, '--port') ?? '8081', 10)
|
||||
const port = Number.isInteger(parsedPort) && parsedPort > 0 ? parsedPort : 8081
|
||||
const identity = await readMetroIdentity(port)
|
||||
if (identity.kind === 'absent') return
|
||||
if (identity.kind === 'managed' && identity.projectRoot === path.resolve(root)) return
|
||||
if (identity.kind === 'managed') {
|
||||
fail(
|
||||
`Metro port ${port} belongs to ${identity.projectRoot}; stop it or choose another --port before running this project`,
|
||||
)
|
||||
}
|
||||
fail(
|
||||
`Metro port ${port} is occupied by an unidentified or outdated server; restart it with \`xuqm-rn start --reset-cache\``,
|
||||
)
|
||||
}
|
||||
|
||||
async function runApp(platform, optionArgs) {
|
||||
assertPlatform(platform)
|
||||
await assertMetroBelongsToCurrentProject(optionArgs)
|
||||
runReactNative([platform === 'android' ? 'run-android' : 'run-ios', ...optionArgs])
|
||||
}
|
||||
|
||||
@ -705,7 +773,7 @@ try {
|
||||
startMetro(args)
|
||||
break
|
||||
case 'run':
|
||||
runApp(args.shift() ?? 'android', args)
|
||||
await runApp(args.shift() ?? 'android', args)
|
||||
break
|
||||
case 'build':
|
||||
build(args.shift() ?? 'android', args)
|
||||
|
||||
@ -552,15 +552,26 @@ test('start and run forward arguments through the host-local React Native CLI',
|
||||
env: environment,
|
||||
stdio: 'pipe',
|
||||
})
|
||||
execFileSync(process.execPath, [cli, 'run', 'android', '--device', 'emulator-5554'], {
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[cli, 'run', 'android', '--device', 'emulator-5554', '--port', '45999', '--no-packager'],
|
||||
{
|
||||
cwd: root,
|
||||
env: environment,
|
||||
stdio: 'pipe',
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const calls = JSON.parse(readFileSync(path.join(root, 'calls.json'), 'utf8'))
|
||||
assert.deepEqual(calls[0].args, ['start', '--reset-cache'])
|
||||
assert.deepEqual(calls[1].args, ['run-android', '--device', 'emulator-5554'])
|
||||
assert.deepEqual(calls[1].args, [
|
||||
'run-android',
|
||||
'--device',
|
||||
'emulator-5554',
|
||||
'--port',
|
||||
'45999',
|
||||
'--no-packager',
|
||||
])
|
||||
assert.equal(calls[0].forceColor, '0')
|
||||
assert.equal(calls[1].forceColor, '0')
|
||||
assert.equal(calls[0].noColor, null)
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户