dsh-ris-yds 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +141 -2
- package/assets/yds-chrome-resource-override.crx +0 -0
- package/bin/check-listing.mjs +247 -0
- package/bin/yds.js +19 -0
- package/cordis.patch.yml +13 -0
- package/lib/browser.js +428 -0
- package/lib/build.js +115 -0
- package/lib/cli.js +1003 -0
- package/lib/confcenter.js +377 -0
- package/lib/config.js +552 -0
- package/lib/fe-dev.js +206 -0
- package/lib/ide-proxy.js +682 -0
- package/lib/ide-watch.js +188 -0
- package/lib/ide.js +702 -0
- package/lib/index.js +267 -0
- package/lib/socks.js +260 -0
- package/lib/token.js +36 -0
- package/lib/yms.js +302 -0
- package/lib/yoncode.js +249 -0
- package/native/yds-rust.darwin-arm64.node +0 -0
- package/native/yds-rust.darwin-x64.node +0 -0
- package/native/yds-rust.linux-arm64.node +0 -0
- package/native/yds-rust.linux-x64.node +0 -0
- package/native/yds-rust.win32-arm64.node +0 -0
- package/native/yds-rust.win32-x64.node +0 -0
- package/package.json +58 -4
- package/skills/dsh-ris-yds/SKILL.md +134 -0
- package/test/browser.test.mjs +442 -0
- package/test/cli.test.mjs +246 -0
- package/test/config.test.mjs +106 -0
- package/test/fe-dev.test.mjs +93 -0
- package/test/ide-config.test.mjs +151 -0
- package/test/ide-proxy.test.mjs +194 -0
- package/test/ide-watch.test.mjs +79 -0
- package/test/launch-proxy.test.mjs +83 -0
- package/test/mode-separation.test.mjs +147 -0
- package/test/proxy-standalone.test.mjs +118 -0
- package/test/yoncode.test.mjs +159 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
browserCookieHint,
|
|
9
|
+
expectedRdbga,
|
|
10
|
+
healBrowserCookie,
|
|
11
|
+
noDevtoolsHint,
|
|
12
|
+
run,
|
|
13
|
+
} from '../lib/cli.js'
|
|
14
|
+
import { rdbgaValue } from '../lib/yms.js'
|
|
15
|
+
|
|
16
|
+
// The suite runs in its own process but inherits the developer's shell: strip every
|
|
17
|
+
// YDS_* override so each case sees only the fixture workspace below.
|
|
18
|
+
for (const key of Object.keys(process.env)) {
|
|
19
|
+
if (key.startsWith('YDS_')) delete process.env[key]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const WORKBENCH = 'https://srmdev.hanslaser.com'
|
|
23
|
+
const WORKBENCH_HOST = 'srmdev.hanslaser.com'
|
|
24
|
+
const CFG = {
|
|
25
|
+
module: 'c-ass-kk-hans',
|
|
26
|
+
context: '/',
|
|
27
|
+
serverBindAddress: '172.20.63.131',
|
|
28
|
+
serverPort: 31010,
|
|
29
|
+
workbenchUrl: WORKBENCH,
|
|
30
|
+
}
|
|
31
|
+
const EXPECTED = rdbgaValue(CFG)
|
|
32
|
+
const TARGETS = [{ type: 'page', url: `${WORKBENCH}/`, webSocketDebuggerUrl: 'ws://a' }]
|
|
33
|
+
const NO_PORT = { port: 0, targets: [], hosts: [], error: '没有开着工作台页签的 DevTools 端口(浏览器未启动或未打开工作台)' }
|
|
34
|
+
const FOUND = { port: 9222, targets: TARGETS, hosts: [WORKBENCH_HOST], error: '' }
|
|
35
|
+
const NO_LIVE = async () => ({ ok: false, values: [], error: 'no powershell' })
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A throwaway workspace with no project config. A remembered yms port keeps config
|
|
39
|
+
* resolution from spawning a child to find a free one.
|
|
40
|
+
*/
|
|
41
|
+
function emptyWorkspace() {
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), 'yds-cli-'))
|
|
43
|
+
mkdirSync(join(dir, '.dsh-ris-yds'), { recursive: true })
|
|
44
|
+
writeFileSync(join(dir, '.dsh-ris-yds', 'yms-port'), '65000')
|
|
45
|
+
return dir
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runInEmptyWorkspace(argv) {
|
|
49
|
+
const dir = emptyWorkspace()
|
|
50
|
+
try {
|
|
51
|
+
return await run(argv, { cwd: dir })
|
|
52
|
+
} finally {
|
|
53
|
+
rmSync(dir, { recursive: true, force: true })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('an unknown browser action is rejected before any port is probed', async () => {
|
|
58
|
+
const result = await runInEmptyWorkspace(['browser', 'frobnicate'])
|
|
59
|
+
assert.equal(result.exitCode, 1)
|
|
60
|
+
assert.match(result.text, /未知动作: frobnicate/)
|
|
61
|
+
assert.match(result.text, /用法: yds browser/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('extra positional arguments are reported, not silently ignored', async () => {
|
|
65
|
+
const result = await runInEmptyWorkspace(['browser', 'status', 'extra'])
|
|
66
|
+
assert.equal(result.exitCode, 1)
|
|
67
|
+
assert.match(result.text, /多余参数: extra/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('an unknown option is reported, not silently ignored', async () => {
|
|
71
|
+
const result = await runInEmptyWorkspace(['browser', 'status', '--bogus'])
|
|
72
|
+
assert.equal(result.exitCode, 1)
|
|
73
|
+
assert.match(result.text, /未知选项: --bogus/)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('an empty --rdbga is rejected instead of silently falling back', async () => {
|
|
77
|
+
const result = await runInEmptyWorkspace(['browser', 'status', '--rdbga='])
|
|
78
|
+
assert.equal(result.exitCode, 1)
|
|
79
|
+
assert.match(result.text, /--rdbga 需要一个 base64 值/)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('--dry-run is refused for the read-only action', async () => {
|
|
83
|
+
const result = await runInEmptyWorkspace(['browser', 'status', '--dry-run'])
|
|
84
|
+
assert.equal(result.exitCode, 1)
|
|
85
|
+
assert.match(result.text, /--dry-run 只用于 fix/)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('an unknown command exits non-zero and prints the usage', async () => {
|
|
89
|
+
const result = await runInEmptyWorkspace(['frobnicate'])
|
|
90
|
+
assert.equal(result.exitCode, 1)
|
|
91
|
+
assert.match(result.text, /用法: node yds\.js <command>/)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('unknown actions of the other commands are rejected as well', async () => {
|
|
95
|
+
for (const argv of [['fe', 'bogus'], ['yoncode', 'bogus'], ['proxy', 'bogus'], ['ide', 'bogus']]) {
|
|
96
|
+
const result = await runInEmptyWorkspace(argv)
|
|
97
|
+
assert.equal(result.exitCode, 1, argv.join(' '))
|
|
98
|
+
assert.match(result.text, /未知动作/)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('a missing workbench URL is named, and no sentence gets a double space', async () => {
|
|
103
|
+
const result = await runInEmptyWorkspace(['browser', 'status'])
|
|
104
|
+
assert.equal(result.exitCode, 1)
|
|
105
|
+
assert.match(result.text, /workbenchUrl 未配置/)
|
|
106
|
+
assert.match(result.text, /无法从 workbenchUrl 推断工作台域名/)
|
|
107
|
+
assert.doesNotMatch(result.text, /在\s{2,}域/)
|
|
108
|
+
assert.doesNotMatch(result.text, /手工在 {2,}/)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('cookie hints degrade gracefully without a workbench URL', () => {
|
|
112
|
+
assert.match(browserCookieHint({ workbenchUrl: '' }, EXPECTED), /workbenchUrl 未配置/)
|
|
113
|
+
assert.ok(browserCookieHint({ workbenchUrl: '' }, EXPECTED).includes(`rdbga=${EXPECTED}`))
|
|
114
|
+
assert.ok(browserCookieHint({ workbenchUrl: WORKBENCH }, EXPECTED).includes(`在 ${WORKBENCH} 域下种 cookie`))
|
|
115
|
+
assert.doesNotMatch(browserCookieHint({}, EXPECTED), /在 {2,}域/)
|
|
116
|
+
|
|
117
|
+
assert.match(noDevtoolsHint({ workbenchUrl: '' }), /workbenchUrl 未配置/)
|
|
118
|
+
assert.doesNotMatch(noDevtoolsHint({}), /在\s{2,}域/)
|
|
119
|
+
assert.ok(noDevtoolsHint({ workbenchUrl: WORKBENCH }).includes(`在 ${WORKBENCH} 域下`))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('the expected value comes only from what can be verified', async () => {
|
|
123
|
+
const own = rdbgaValue(CFG)
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
await expectedRdbga(CFG, '', { liveQuery: NO_LIVE }),
|
|
126
|
+
{ ok: true, value: own, source: '本机配置', hint: 'no powershell' },
|
|
127
|
+
)
|
|
128
|
+
const agree = await expectedRdbga(CFG, '', { liveQuery: async () => ({ ok: true, values: [own], error: '' }) })
|
|
129
|
+
assert.equal(agree.source, '运行中的 JVM')
|
|
130
|
+
assert.equal(agree.value, own)
|
|
131
|
+
|
|
132
|
+
const other = Buffer.from('other,/@1.2.3.4:1').toString('base64')
|
|
133
|
+
const disagree = await expectedRdbga(CFG, '', {
|
|
134
|
+
liveQuery: async () => ({ ok: true, values: [other, 'Zm9v'], error: '' }),
|
|
135
|
+
})
|
|
136
|
+
assert.equal(disagree.ok, false)
|
|
137
|
+
// The rule must be explainable: how many JVMs were seen, which values, and that none
|
|
138
|
+
// matches the configured one.
|
|
139
|
+
assert.match(disagree.reason, /2 个实例/)
|
|
140
|
+
assert.ok(disagree.reason.includes(`${other.slice(0, 10)}…`))
|
|
141
|
+
assert.match(disagree.reason, /都不一致/)
|
|
142
|
+
assert.match(disagree.reason, /--rdbga/)
|
|
143
|
+
|
|
144
|
+
const override = await expectedRdbga(CFG, own, { liveQuery: async () => ({ ok: true, values: ['x'], error: '' }) })
|
|
145
|
+
assert.deepEqual({ ok: override.ok, value: override.value, source: override.source }, { ok: true, value: own, source: '命令行指定' })
|
|
146
|
+
|
|
147
|
+
const bad = await expectedRdbga(CFG, 'not base64 !!', { liveQuery: NO_LIVE })
|
|
148
|
+
assert.equal(bad.ok, false)
|
|
149
|
+
assert.match(bad.reason, /base64|解码/)
|
|
150
|
+
|
|
151
|
+
const noModule = await expectedRdbga({ ...CFG, module: null }, '', { liveQuery: NO_LIVE })
|
|
152
|
+
assert.match(noModule.reason, /模块名/)
|
|
153
|
+
const badBind = await expectedRdbga({ ...CFG, serverBindAddress: 'localhost' }, '', { liveQuery: NO_LIVE })
|
|
154
|
+
assert.match(badBind.reason, /serverBindAddress/)
|
|
155
|
+
const badPort = await expectedRdbga({ ...CFG, serverPort: 70000 }, '', { liveQuery: NO_LIVE })
|
|
156
|
+
assert.match(badPort.reason, /serverPort/)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('a dry run plans the repair but never reads back or writes', async () => {
|
|
160
|
+
const logged = []
|
|
161
|
+
let reads = 0
|
|
162
|
+
const result = await healBrowserCookie(CFG, {
|
|
163
|
+
log: (line) => logged.push(line),
|
|
164
|
+
dryRun: true,
|
|
165
|
+
findPort: async () => FOUND,
|
|
166
|
+
liveQuery: NO_LIVE,
|
|
167
|
+
repair: async ({ dryRun }) => ({ ok: true, tabs: 1, removed: 2, wrote: 1, set: EXPECTED, dryRun }),
|
|
168
|
+
read: async () => { reads += 1; return { ok: true, cookies: [] } },
|
|
169
|
+
})
|
|
170
|
+
assert.deepEqual(
|
|
171
|
+
{ ok: result.ok, dryRun: result.dryRun, removed: result.removed, wrote: result.wrote, source: result.source },
|
|
172
|
+
{ ok: true, dryRun: true, removed: 2, wrote: 1, source: '本机配置' },
|
|
173
|
+
)
|
|
174
|
+
assert.equal(reads, 0)
|
|
175
|
+
assert.match(logged.join('\n'), /预览/)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('the port is probed before the PowerShell query: no port, no cost', async () => {
|
|
179
|
+
const order = []
|
|
180
|
+
const noPort = await healBrowserCookie(CFG, {
|
|
181
|
+
findPort: async () => { order.push('find'); return NO_PORT },
|
|
182
|
+
liveQuery: async () => { order.push('live'); return { ok: false, values: [], error: '' } },
|
|
183
|
+
})
|
|
184
|
+
assert.equal(noPort.ok, false)
|
|
185
|
+
assert.match(noPort.reason, /没有开着工作台页签/)
|
|
186
|
+
assert.deepEqual(order, ['find'])
|
|
187
|
+
|
|
188
|
+
order.length = 0
|
|
189
|
+
const healed = await healBrowserCookie(CFG, {
|
|
190
|
+
findPort: async () => { order.push('find'); return FOUND },
|
|
191
|
+
liveQuery: async () => { order.push('live'); return { ok: false, values: [], error: 'no powershell' } },
|
|
192
|
+
repair: async () => ({ ok: true, tabs: 1, removed: 0, wrote: 0, set: EXPECTED }),
|
|
193
|
+
read: async () => ({
|
|
194
|
+
ok: true,
|
|
195
|
+
cookies: [{ name: 'rdbga', value: EXPECTED, domain: `.${WORKBENCH_HOST}`, path: '/' }],
|
|
196
|
+
}),
|
|
197
|
+
})
|
|
198
|
+
assert.deepEqual(order, ['find', 'live'])
|
|
199
|
+
assert.deepEqual({ ok: healed.ok, port: healed.port, source: healed.source }, { ok: true, port: 9222, source: '本机配置' })
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('a repair that leaves a wrong cookie behind is a failure, not a success', async () => {
|
|
203
|
+
const result = await healBrowserCookie(CFG, {
|
|
204
|
+
findPort: async () => FOUND,
|
|
205
|
+
liveQuery: NO_LIVE,
|
|
206
|
+
repair: async () => ({ ok: true, tabs: 1, removed: 1, wrote: 1, set: EXPECTED }),
|
|
207
|
+
read: async () => ({
|
|
208
|
+
ok: true,
|
|
209
|
+
cookies: [
|
|
210
|
+
{ name: 'rdbga', value: EXPECTED, domain: `.${WORKBENCH_HOST}`, path: '/' },
|
|
211
|
+
{ name: 'rdbga', value: '', domain: WORKBENCH_HOST, path: '/' },
|
|
212
|
+
],
|
|
213
|
+
}),
|
|
214
|
+
})
|
|
215
|
+
assert.equal(result.ok, false)
|
|
216
|
+
assert.match(result.reason, /写入后仍有多个/)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('an unreadable cookie store after the repair is a failure, not a fake success', async () => {
|
|
220
|
+
const result = await healBrowserCookie(CFG, {
|
|
221
|
+
findPort: async () => FOUND,
|
|
222
|
+
liveQuery: NO_LIVE,
|
|
223
|
+
repair: async () => ({ ok: true, tabs: 1, removed: 1, wrote: 1, set: EXPECTED }),
|
|
224
|
+
read: async () => ({ ok: false, cookies: [], error: 'CDP 断开' }),
|
|
225
|
+
})
|
|
226
|
+
assert.equal(result.ok, false)
|
|
227
|
+
assert.match(result.reason, /回读 cookie 失败: CDP 断开/)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('a failed repair is reported with the browser error text', async () => {
|
|
231
|
+
const result = await healBrowserCookie(CFG, {
|
|
232
|
+
findPort: async () => FOUND,
|
|
233
|
+
liveQuery: NO_LIVE,
|
|
234
|
+
repair: async () => ({ ok: false, error: '所有页签都失败' }),
|
|
235
|
+
})
|
|
236
|
+
assert.equal(result.ok, false)
|
|
237
|
+
assert.match(result.reason, /所有页签都失败/)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('the budget cuts off a browser that keeps the repair hanging', async () => {
|
|
241
|
+
const started = Date.now()
|
|
242
|
+
const result = await healBrowserCookie(CFG, { budgetMs: 30, findPort: () => new Promise(() => {}) })
|
|
243
|
+
assert.equal(result.ok, false)
|
|
244
|
+
assert.match(result.reason, /超时/)
|
|
245
|
+
assert.ok(Date.now() - started < 1000)
|
|
246
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Config resolution without any .env: the local listen port is discovered, the
|
|
2
|
+
// front-end port comes from the project (yds.webConfig, else the front end's own
|
|
3
|
+
// override file), and nothing machine-specific has to be configured by hand.
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import test from 'node:test'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_FE_PORT,
|
|
12
|
+
discoverFrontend,
|
|
13
|
+
findFreePort,
|
|
14
|
+
isPortFree,
|
|
15
|
+
pickFrontend,
|
|
16
|
+
portOfUrl,
|
|
17
|
+
readRememberedPort,
|
|
18
|
+
} from '../lib/config.js'
|
|
19
|
+
|
|
20
|
+
/** A throwaway project directory; callers remove it in a finally block. */
|
|
21
|
+
function tempProject() {
|
|
22
|
+
return mkdtempSync(join(tmpdir(), 'yds-config-'))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('yds.webConfig picks the entry whose domainKey is the module', () => {
|
|
26
|
+
const webConfig = {
|
|
27
|
+
'some-other-fe': { 'yds.domainKey': 'other', 'yds.localDevUrl': 'http://127.0.0.1:9999' },
|
|
28
|
+
'c-ass-kk-hans-fe': { 'yds.domainKey': 'c-ass-kk-hans', 'yds.localDevUrl': 'http://127.0.0.1:3004', 'yds.debugScript': 'debug' },
|
|
29
|
+
}
|
|
30
|
+
const frontend = pickFrontend(webConfig, 'c-ass-kk-hans')
|
|
31
|
+
assert.equal(frontend.code, 'c-ass-kk-hans-fe')
|
|
32
|
+
assert.equal(frontend.port, 3004)
|
|
33
|
+
assert.equal(frontend.debugScript, 'debug')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('a single entry wins even when the domainKey differs', () => {
|
|
37
|
+
const frontend = pickFrontend({ 'x-fe': { 'yds.localDevUrl': 'http://127.0.0.1:4100' } }, 'must-not-matter')
|
|
38
|
+
assert.equal(frontend.port, 4100)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('no webConfig at all still yields the historical default', () => {
|
|
42
|
+
const frontend = pickFrontend(undefined, 'c-ass-kk-hans')
|
|
43
|
+
assert.equal(frontend.port, DEFAULT_FE_PORT)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('discoverFrontend reads the port from the front end own override file', () => {
|
|
47
|
+
const dir = tempProject()
|
|
48
|
+
try {
|
|
49
|
+
mkdirSync(join(dir, 'c-ass-kk-hans-fe'))
|
|
50
|
+
writeFileSync(join(dir, 'c-ass-kk-hans-fe', 'resource_override_rules.json'), JSON.stringify({
|
|
51
|
+
v: 2,
|
|
52
|
+
ruleGroups: [{ rules: [{ replace: 'https://127.0.0.1:4567/static/javascripts/index.js' }] }],
|
|
53
|
+
}))
|
|
54
|
+
const frontend = discoverFrontend(dir, 'c-ass-kk-hans')
|
|
55
|
+
assert.equal(frontend.port, 4567)
|
|
56
|
+
assert.equal(frontend.devUrl, 'http://127.0.0.1:4567')
|
|
57
|
+
} finally {
|
|
58
|
+
rmSync(dir, { recursive: true, force: true })
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('discoverFrontend falls back when the project has no front end', () => {
|
|
63
|
+
const dir = tempProject()
|
|
64
|
+
try {
|
|
65
|
+
assert.equal(discoverFrontend(dir, 'mod').port, DEFAULT_FE_PORT)
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(dir, { recursive: true, force: true })
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('portOfUrl understands dev URLs and gives up quietly', () => {
|
|
72
|
+
assert.equal(portOfUrl('http://127.0.0.1:3004'), 3004)
|
|
73
|
+
assert.equal(portOfUrl('https://127.0.0.1:3004/some/path'), 3004)
|
|
74
|
+
assert.equal(portOfUrl('http://127.0.0.1'), 0)
|
|
75
|
+
assert.equal(portOfUrl(''), 0)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('findFreePort skips a port that is already bound', async () => {
|
|
79
|
+
const { createServer } = await import('node:net')
|
|
80
|
+
const busy = await new Promise((resolve) => {
|
|
81
|
+
const server = createServer()
|
|
82
|
+
server.listen(0, '127.0.0.1', () => resolve(server))
|
|
83
|
+
})
|
|
84
|
+
const busyPort = busy.address().port
|
|
85
|
+
try {
|
|
86
|
+
assert.equal(isPortFree(busyPort), false)
|
|
87
|
+
const picked = findFreePort(busyPort)
|
|
88
|
+
assert.equal(picked > busyPort, true, `expected a port above ${busyPort}, got ${picked}`)
|
|
89
|
+
assert.equal(isPortFree(picked), true)
|
|
90
|
+
} finally {
|
|
91
|
+
await new Promise((resolve) => busy.close(resolve))
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('the remembered port survives a restart and absent state is null', () => {
|
|
96
|
+
const dir = tempProject()
|
|
97
|
+
try {
|
|
98
|
+
assert.equal(readRememberedPort(dir), null)
|
|
99
|
+
writeFileSync(join(dir, 'yms-port'), '31012\n')
|
|
100
|
+
assert.equal(readRememberedPort(dir), 31012)
|
|
101
|
+
writeFileSync(join(dir, 'yms-port'), 'not-a-port\n')
|
|
102
|
+
assert.equal(readRememberedPort(dir), null)
|
|
103
|
+
} finally {
|
|
104
|
+
rmSync(dir, { recursive: true, force: true })
|
|
105
|
+
}
|
|
106
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Front-end dev-server wiring: the override rules are generated from the project
|
|
2
|
+
// config, and the same rules are exposed as regex+substitution pairs so an agent
|
|
3
|
+
// can redirect requests through CDP instead of installing the extension.
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import test from 'node:test'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_FE_PORT,
|
|
12
|
+
globToDnrRegex,
|
|
13
|
+
overrideRules,
|
|
14
|
+
pageUrlWithScriptUrl,
|
|
15
|
+
requestRedirects,
|
|
16
|
+
writeOverrideRules,
|
|
17
|
+
} from '../lib/fe-dev.js'
|
|
18
|
+
|
|
19
|
+
const cfg = {
|
|
20
|
+
workspace: '',
|
|
21
|
+
module: 'c-ass-kk-hans',
|
|
22
|
+
workbenchUrl: 'https://srmdev.hanslaser.com',
|
|
23
|
+
fePort: 3004,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('the rules follow the project, not the copy that was hand-edited', () => {
|
|
27
|
+
const rules = overrideRules(cfg)
|
|
28
|
+
const list = rules.ruleGroups[0].rules
|
|
29
|
+
assert.equal(rules.v, 2)
|
|
30
|
+
assert.equal(list.length, 7)
|
|
31
|
+
// The three dynamic values: module, workbench host, dev port.
|
|
32
|
+
assert.match(list[0].match, /c-ass-kk-hans/)
|
|
33
|
+
assert.match(list[6].match, /^https:\/\/srmdev\.hanslaser\.com\/c-ass-kk-hans-festatic\/\*$/)
|
|
34
|
+
assert.match(list[0].replace, /^http:\/\/127\.0\.0\.1:3004\/static\/javascripts\/extend\.js$/)
|
|
35
|
+
// A different project/port produces different rules with no other edits.
|
|
36
|
+
const other = overrideRules({ module: 'st-storein', workbenchUrl: 'https://tj.example.com', fePort: 4321 })
|
|
37
|
+
assert.match(other.ruleGroups[0].rules[0].match, /st-storein/)
|
|
38
|
+
assert.match(other.ruleGroups[0].rules[0].replace, /127\.0\.0\.1:4321/)
|
|
39
|
+
assert.match(other.ruleGroups[0].rules[6].match, /tj\.example\.com/)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('glob → regex keeps the extension’s capture semantics', () => {
|
|
43
|
+
// Last star becomes a capture group (used by \1), the second-to-last becomes
|
|
44
|
+
// [^/]+ so a trailing segment is not swallowed, the rest are .*
|
|
45
|
+
const withSuffix = globToDnrRegex('https://*/mod/*/js/extend*.min.js*', 'http://127.0.0.1:3004/static/js/extend.js')
|
|
46
|
+
assert.equal(withSuffix.regex, '^https://.*/mod/.*/js/extend[^/]+\\.min\\.js(.*)$')
|
|
47
|
+
const filePrefix = globToDnrRegex('https://host/mod-festatic/*', 'http://127.0.0.1:3004/static/*')
|
|
48
|
+
assert.equal(filePrefix.regex, '^https://host/mod-festatic/(.*)$')
|
|
49
|
+
assert.equal(filePrefix.substitution, 'http://127.0.0.1:3004/static/\\1')
|
|
50
|
+
// Anchored, because the browser's DNR match is anchored too.
|
|
51
|
+
assert.ok(filePrefix.regex.startsWith('^') && filePrefix.regex.endsWith('$'))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('the plan is the same seven rules as redirects an agent can apply', () => {
|
|
55
|
+
const redirects = requestRedirects(cfg)
|
|
56
|
+
assert.equal(redirects.length, 7)
|
|
57
|
+
for (const item of redirects) {
|
|
58
|
+
assert.equal(typeof item.regex, 'string')
|
|
59
|
+
assert.ok(item.regex.length > 2)
|
|
60
|
+
assert.equal(typeof item.substitution, 'string')
|
|
61
|
+
// The regex must actually match the glob's own URL shape.
|
|
62
|
+
const sample = item.match.replace('*', 'srmdev.hanslaser.com')
|
|
63
|
+
const probe = sample.replace(/\*/g, 'x')
|
|
64
|
+
assert.match(probe, new RegExp(item.regex), item.regex)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('the rule file is written where the extension can import it', () => {
|
|
69
|
+
const workspace = mkdtempSync(join(tmpdir(), 'yds-fe-'))
|
|
70
|
+
try {
|
|
71
|
+
const written = writeOverrideRules({ ...cfg, workspace })
|
|
72
|
+
assert.equal(written.path, join(workspace, '.dsh-ris-yds', 'resource-override-rules.json'))
|
|
73
|
+
assert.equal(written.rules, 7)
|
|
74
|
+
assert.equal(written.module, 'c-ass-kk-hans')
|
|
75
|
+
assert.equal(written.fePort, DEFAULT_FE_PORT)
|
|
76
|
+
assert.ok(existsSync(written.path))
|
|
77
|
+
const parsed = JSON.parse(readFileSync(written.path, 'utf8'))
|
|
78
|
+
assert.equal(parsed.ruleGroups[0].rules.length, 7)
|
|
79
|
+
assert.equal(parsed.ruleGroups[0].rules[0].type, 'normalOverride')
|
|
80
|
+
} finally {
|
|
81
|
+
rmSync(workspace, { recursive: true, force: true })
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('scriptUrl is appended to a page without any extension', () => {
|
|
86
|
+
const url = pageUrlWithScriptUrl(cfg, '/mdf-node/meta/voucherlist/st_purchaseorderlist?domainKey=upu')
|
|
87
|
+
const parsed = new URL(url)
|
|
88
|
+
assert.equal(parsed.host, 'srmdev.hanslaser.com')
|
|
89
|
+
assert.equal(parsed.searchParams.get('scriptUrl'), 'http://127.0.0.1:3004/static')
|
|
90
|
+
// The page's own parameters survive.
|
|
91
|
+
assert.equal(parsed.searchParams.get('domainKey'), 'upu')
|
|
92
|
+
assert.equal(pageUrlWithScriptUrl(cfg, ''), '')
|
|
93
|
+
})
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join, relative } from 'node:path'
|
|
6
|
+
import {
|
|
7
|
+
applyIdeConfig, belongsToYdsProject, emptyIdeConfigDocument, frontendProjectName, initializeIdeConfig,
|
|
8
|
+
isOurs, javaProjectName, removeIdeConfig, repairFrontendNames, repairYdsRows,
|
|
9
|
+
} from '../lib/ide.js'
|
|
10
|
+
|
|
11
|
+
/** A yds project root: aggregator pom + bootstrap pom with a parent block. */
|
|
12
|
+
function makeProject() {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), 'yds-ide-'))
|
|
14
|
+
mkdirSync(join(root, 'c-ass-kk-hans-be', 'dev-c-ass-kk-hans-bootstrap'), { recursive: true })
|
|
15
|
+
writeFileSync(join(root, 'c-ass-kk-hans-be', 'pom.xml'), '<project><artifactId>c-ass-kk-hans</artifactId></project>')
|
|
16
|
+
writeFileSync(join(root, 'c-ass-kk-hans-be', 'dev-c-ass-kk-hans-bootstrap', 'pom.xml'), `<project>
|
|
17
|
+
<parent>
|
|
18
|
+
<artifactId>c-ass-kk-hans</artifactId>
|
|
19
|
+
<groupId>com.yonyou.ucf</groupId>
|
|
20
|
+
</parent>
|
|
21
|
+
<artifactId>c-ass-kk-hans-bootstrap</artifactId>
|
|
22
|
+
</project>`)
|
|
23
|
+
// The yds marker: an ancestor .vscode/settings.json carrying `yds.` settings.
|
|
24
|
+
mkdirSync(join(root, '.vscode'), { recursive: true })
|
|
25
|
+
writeFileSync(join(root, '.vscode', 'settings.json'),
|
|
26
|
+
'{\n // yds project\n "yds.accessKey": "k",\n "yds.webConfig": {}\n}')
|
|
27
|
+
mkdirSync(join(root, 'c-ass-kk-hans-fe'))
|
|
28
|
+
writeFileSync(join(root, 'c-ass-kk-hans-fe', 'package.json'), '{"name":"fe"}')
|
|
29
|
+
writeFileSync(join(root, 'c-ass-kk-hans-fe', 'module.xml'), '<module name="c-ass-kk-hans-fe" description="前端"/>')
|
|
30
|
+
return root
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('names come from the project files, not our own bookkeeping', () => {
|
|
34
|
+
const root = makeProject()
|
|
35
|
+
const cfg = { projectDir: root, module: 'c-ass-kk-hans' }
|
|
36
|
+
// java = bootstrap pom's parent.artifactId
|
|
37
|
+
assert.equal(javaProjectName(cfg), 'c-ass-kk-hans')
|
|
38
|
+
// frontend = module.xml beside package.json
|
|
39
|
+
assert.equal(frontendProjectName(cfg), 'c-ass-kk-hans-fe')
|
|
40
|
+
// no files: fall back to the YMS module name
|
|
41
|
+
assert.equal(javaProjectName({ projectDir: '/nonexistent', module: 'm1' }), 'm1')
|
|
42
|
+
rmSync(root, { recursive: true, force: true })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('apply initialises the config file before writing our row', () => {
|
|
46
|
+
const root = makeProject()
|
|
47
|
+
const cfg = { workspace: root, projectDir: root, module: 'c-ass-kk-hans' }
|
|
48
|
+
const first = applyIdeConfig(cfg)
|
|
49
|
+
assert.equal(first.action, 'created')
|
|
50
|
+
assert.equal(first.initialized, true)
|
|
51
|
+
const doc = JSON.parse(readFileSync(first.path, 'utf8').replace(/^\/\/.*$/gm, ''))
|
|
52
|
+
// Same shape ris-ide itself generates, plus our row named after the project.
|
|
53
|
+
assert.deepEqual(doc.defaults, emptyIdeConfigDocument().defaults)
|
|
54
|
+
assert.equal(doc.version, 1)
|
|
55
|
+
assert.equal(doc.programs.length, 1)
|
|
56
|
+
assert.equal(doc.programs[0].name, 'c-ass-kk-hans')
|
|
57
|
+
// Idempotent second pass.
|
|
58
|
+
const second = applyIdeConfig(cfg)
|
|
59
|
+
assert.equal(second.action, 'unchanged')
|
|
60
|
+
assert.equal(second.initialized, false)
|
|
61
|
+
// Removal finds it by identity, not by a hardcoded name.
|
|
62
|
+
assert.equal(removeIdeConfig(cfg).action, 'removed')
|
|
63
|
+
rmSync(root, { recursive: true, force: true })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('our rows are recognised across renames', () => {
|
|
67
|
+
const cfg = { module: 'c-ass-kk-hans', projectDir: '/nonexistent' }
|
|
68
|
+
assert.equal(isOurs({ name: 'yds-yms-c-ass-kk-hans' }, cfg), true)
|
|
69
|
+
assert.equal(isOurs({ name: 'other', startCommand: 'node "/x/bin/yds.js" start' }, cfg), true)
|
|
70
|
+
assert.equal(isOurs({ name: 'other', startCommand: 'npm run dev' }, cfg), false)
|
|
71
|
+
// A frontend row with the same name is NOT ours — deleting it would be data loss.
|
|
72
|
+
assert.equal(isOurs({ name: 'c-ass-kk-hans', kind: 'frontend' }, cfg), false)
|
|
73
|
+
assert.equal(isOurs({ name: 'c-ass-kk-hans', kind: 'spring' }, cfg), true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('initialise keeps an existing file untouched', () => {
|
|
77
|
+
const root = mkdtempSync(join(tmpdir(), 'yds-ide2-'))
|
|
78
|
+
mkdirSync(join(root, '.dsh-ris-ide'), { recursive: true })
|
|
79
|
+
const path = join(root, '.dsh-ris-ide', 'config.json')
|
|
80
|
+
writeFileSync(path, '{ "programs": [{ "name": "user-row" }] }\n')
|
|
81
|
+
assert.deepEqual(initializeIdeConfig(root), { created: false, path })
|
|
82
|
+
assert.match(readFileSync(path, 'utf8'), /user-row/)
|
|
83
|
+
rmSync(root, { recursive: true, force: true })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('a frontend row of this project is renamed after its module.xml', () => {
|
|
87
|
+
const root = makeProject()
|
|
88
|
+
const cfg = { workspace: root, projectDir: root, module: 'c-ass-kk-hans' }
|
|
89
|
+
const programs = [
|
|
90
|
+
// generated before the rule existed: package.json's name == the java project name
|
|
91
|
+
{ id: 'frontend:c-ass-kk-hans-fe#start', kind: 'frontend', name: 'c-ass-kk-hans', dir: 'c-ass-kk-hans-fe' },
|
|
92
|
+
// outside the project: never touched
|
|
93
|
+
{ id: 'frontend:other/ui#dev', kind: 'frontend', name: 'other-ui', dir: '/tmp/other/ui' },
|
|
94
|
+
]
|
|
95
|
+
const result = repairFrontendNames(cfg, programs)
|
|
96
|
+
assert.equal(result.programs[0].name, 'c-ass-kk-hans-fe')
|
|
97
|
+
assert.deepEqual(result.renamed, [{ from: 'c-ass-kk-hans', to: 'c-ass-kk-hans-fe' }])
|
|
98
|
+
assert.equal(result.programs[1].name, 'other-ui')
|
|
99
|
+
// Membership is decided by `dir` (walking up to .vscode/settings.json), never by name or kind.
|
|
100
|
+
assert.equal(belongsToYdsProject(cfg, { name: 'x', kind: 'spring', dir: 'c-ass-kk-hans-fe' }), true)
|
|
101
|
+
assert.equal(belongsToYdsProject(cfg, { name: 'x', dir: '../outside' }), false)
|
|
102
|
+
// A row whose directory has no yds ancestor is left alone even if it has a module.xml.
|
|
103
|
+
const outside = join(root, '..', 'plain-npm')
|
|
104
|
+
mkdirSync(outside, { recursive: true })
|
|
105
|
+
writeFileSync(join(outside, 'module.xml'), '<module name="renamed-by-accident"/>')
|
|
106
|
+
const guarded = repairFrontendNames(cfg, [{ name: 'plain', dir: relative(cfg.workspace, outside) }])
|
|
107
|
+
assert.deepEqual(guarded.renamed, [])
|
|
108
|
+
rmSync(outside, { recursive: true, force: true })
|
|
109
|
+
rmSync(root, { recursive: true, force: true })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('a java row is renamed to its parent artifactId unless the name is taken', () => {
|
|
113
|
+
const root = makeProject()
|
|
114
|
+
const cfg = { workspace: root, projectDir: root, module: 'c-ass-kk-hans' }
|
|
115
|
+
const programs = [
|
|
116
|
+
// ours: keeps the java project name
|
|
117
|
+
{ name: 'c-ass-kk-hans', kind: 'spring', dir: '.', startCommand: 'node x/bin/yds.js start' },
|
|
118
|
+
// discovered bootstrap module: would now want the same name -> skipped, not merged
|
|
119
|
+
{ name: 'dev-c-ass-kk-hans-bootstrap', kind: 'spring', dir: 'c-ass-kk-hans-be/dev-c-ass-kk-hans-bootstrap' },
|
|
120
|
+
]
|
|
121
|
+
const result = repairFrontendNames(cfg, programs)
|
|
122
|
+
assert.equal(result.programs[0].name, 'c-ass-kk-hans')
|
|
123
|
+
assert.equal(result.programs[1].name, 'dev-c-ass-kk-hans-bootstrap')
|
|
124
|
+
assert.deepEqual(result.renamed, [])
|
|
125
|
+
assert.deepEqual(result.skipped.map((item) => item.to), ['c-ass-kk-hans'])
|
|
126
|
+
rmSync(root, { recursive: true, force: true })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('the discovered backend row is disabled and a hijacked startCommand is removed', () => {
|
|
130
|
+
const root = makeProject()
|
|
131
|
+
const cfg = { workspace: root, projectDir: root, module: 'c-ass-kk-hans' }
|
|
132
|
+
const ours = { id: `yds:c-ass-kk-hans`, name: 'c-ass-kk-hans', kind: 'spring', dir: '.',
|
|
133
|
+
startCommand: 'node x/bin/yds.js start --foreground' }
|
|
134
|
+
const programs = [
|
|
135
|
+
ours,
|
|
136
|
+
// the row the panel generates for the same backend — must not compete with ours
|
|
137
|
+
{ id: 'spring:be/bootstrap#Main', name: 'dev-c-ass-kk-hans-bootstrap', kind: 'spring', dir: 'c-ass-kk-hans-be/dev-c-ass-kk-hans-bootstrap' },
|
|
138
|
+
// a frontend row an older update copied our command onto
|
|
139
|
+
{ id: 'frontend:c-ass-kk-hans-fe#start', name: 'c-ass-kk-hans', kind: 'frontend', dir: 'c-ass-kk-hans-fe',
|
|
140
|
+
startCommand: 'node x/bin/yds.js start --foreground' },
|
|
141
|
+
]
|
|
142
|
+
const result = repairYdsRows(cfg, programs)
|
|
143
|
+
assert.deepEqual(result.renamed.map((item) => item.to), ['c-ass-kk-hans-fe'])
|
|
144
|
+
assert.deepEqual(result.disabled.map((item) => item.name), ['dev-c-ass-kk-hans-bootstrap'])
|
|
145
|
+
assert.deepEqual(result.cleaned.map((item) => item.name), ['c-ass-kk-hans-fe'])
|
|
146
|
+
assert.equal(result.programs[0].disabled, undefined, 'our row stays enabled')
|
|
147
|
+
assert.equal(result.programs[1].disabled, true)
|
|
148
|
+
assert.equal(result.programs[2].startCommand, undefined)
|
|
149
|
+
assert.equal(result.programs[2].name, 'c-ass-kk-hans-fe')
|
|
150
|
+
rmSync(root, { recursive: true, force: true })
|
|
151
|
+
})
|