Skip to content

Commit 8828d4f

Browse files
committed
test: cover cli and emp-share custom flows
1 parent 3a613b2 commit 8828d4f

26 files changed

Lines changed: 2272 additions & 45 deletions

.superpowers/plans/2026-07-01-cli-emp-share-custom-capability-tests.md

Lines changed: 417 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"test:apps:single": "node scripts/run-root-test.mjs apps-single",
4949
"test:library-output": "node scripts/run-root-test.mjs library-output",
5050
"test:apps:browser": "node scripts/run-app-browser-tests.mjs",
51+
"test:browser:all": "corepack pnpm exec rstest run --config rstest.config.ts --browser --browser.name chromium",
52+
"test:browser:watch": "pnpm exec rstest watch --browser --browser.headless=false",
5153
"workflow:check": "node scripts/emp-workflow-check.mjs",
5254
"ci:verify": "corepack pnpm workflow:check && corepack pnpm test:toolchain && corepack pnpm test:tsconfig && corepack pnpm test:ts7:packages && corepack pnpm test:tsgo && corepack pnpm test:cli && corepack pnpm test:packages && corepack pnpm test:rules && corepack pnpm release:check && corepack pnpm check:rslib-presets",
5355
"apps:list": "node scripts/apps.mjs list",

packages/cli/src/server/connect/prod.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import https from 'https'
99
import path from 'path'
1010
import serveStatic from 'serve-static'
1111
import {parse} from 'url'
12+
import {logger} from 'src/helper'
1213

1314
//
1415
const app = connect()
@@ -21,14 +22,18 @@ export class ProdServer {
2122
let entry = 'index'
2223
const entryKeys = Object.keys(store.rsConfig.entry)
2324
if (entryKeys.length === 0) {
24-
return store.logger.sysError(`emp serve must include entry!`)
25+
logger.sysError(`emp serve must include entry!`)
26+
process.exitCode = 1
27+
return
2528
}
2629
if (entryKeys.includes(entry)) {
2730
} else {
2831
entry = entryKeys[0]
2932
}
3033
if (!fs.existsSync(store.outDir)) {
31-
return store.logger.sysError(`emp serve must be executed after emp build,${store.outDir} not exist!`)
34+
logger.sysError(`emp serve must be executed after emp build,${store.outDir} not exist!`)
35+
process.exitCode = 1
36+
return
3237
}
3338
const staticRoot = store.resolve(store.rsConfig.output?.path as any)
3439

packages/cli/src/store/empConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,12 @@ export class EmpConfig {
409409
} else {
410410
this.store.empOptions = empOptionsFn || {}
411411
}
412+
if (this.store.cliOptions.envVars) {
413+
this.store.empOptions.define = {
414+
...(this.store.empOptions.define || {}),
415+
...this.store.cliOptions.envVars,
416+
}
417+
}
412418
} finally {
413419
logger.timeEnd(timeTag)
414420
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import path from 'node:path'
2+
import {describe, expect, it} from '@rstest/core'
3+
import {listFiles, createRealProject, findFreePort, runCli, spawnCli, waitForHttp, waitForPortReleased, writeProjectFile} from '../support/real-project'
4+
5+
async function writeRuntimeFixture(root: string, options: {outDir: string; port?: number}) {
6+
await writeProjectFile(
7+
root,
8+
'package.json',
9+
JSON.stringify(
10+
{
11+
name: 'cli-command-runtime-fixture',
12+
private: true,
13+
type: 'module',
14+
},
15+
null,
16+
2,
17+
) + '\n',
18+
)
19+
20+
await writeProjectFile(
21+
root,
22+
'tsconfig.json',
23+
JSON.stringify(
24+
{
25+
compilerOptions: {
26+
target: 'ES2020',
27+
module: 'ESNext',
28+
moduleResolution: 'Bundler',
29+
lib: ['ES2020', 'DOM'],
30+
baseUrl: '.',
31+
strict: true,
32+
skipLibCheck: true,
33+
},
34+
},
35+
null,
36+
2,
37+
) + '\n',
38+
)
39+
40+
const configLines = [
41+
'export default () => ({',
42+
" appSrc: 'src',",
43+
" appEntry: 'main.ts',",
44+
' build: {',
45+
` outDir: ${JSON.stringify(options.outDir)},`,
46+
" assetsDir: 'assets-real',",
47+
" sourcemap: {js: 'source-map', css: true},",
48+
' },',
49+
" html: {mountId: 'runtime-root', title: 'CLI Runtime Fixture'},",
50+
" entries: {main: {}},",
51+
]
52+
if (options.port) {
53+
configLines.push(` server: {host: '127.0.0.1', port: ${options.port}},`)
54+
}
55+
configLines.push('})', '')
56+
57+
await writeProjectFile(root, 'emp.config.ts', configLines.join('\n'))
58+
await writeProjectFile(
59+
root,
60+
'src/main.ts',
61+
[
62+
"import './style.css'",
63+
"document.getElementById('runtime-root')!.textContent = 'runtime'",
64+
'',
65+
].join('\n'),
66+
)
67+
await writeProjectFile(root, 'src/style.css', '.runtime { color: #123456; }\n')
68+
}
69+
70+
describe('cli command runtime', () => {
71+
it('rejects unsupported dts and init commands with the v4 alpha message', async () => {
72+
const repoRoot = path.resolve(import.meta.dirname, '../../..')
73+
const dts = await runCli(['dts'], repoRoot)
74+
const init = await runCli(['init'], repoRoot)
75+
76+
expect(dts.code).toBe(1)
77+
expect(init.code).toBe(1)
78+
expect(`${dts.stdout}${dts.stderr}`).toContain('emp dts 在 @empjs/cli v4 alpha 中尚未实现。')
79+
expect(`${init.stdout}${init.stderr}`).toContain('emp init 在 @empjs/cli v4 alpha 中尚未实现。')
80+
})
81+
82+
it('rejects invalid env-vars input with a key=value message', async () => {
83+
const repoRoot = path.resolve(import.meta.dirname, '../../..')
84+
const result = await runCli(['build', '--env-vars', 'bad'], repoRoot)
85+
86+
expect(result.code).not.toBe(0)
87+
expect(`${result.stdout}${result.stderr}`).toContain('key=value')
88+
})
89+
90+
it('surfaces analyzer output when build --analyze is enabled', async () => {
91+
const project = await createRealProject('cli-analyze')
92+
try {
93+
await writeRuntimeFixture(project.root, {outDir: 'dist-analyze', port: await findFreePort()})
94+
const result = await runCli(['build', '--analyze', '--clearLog=false'], project.root, 180000)
95+
expect(result.code).toBe(0)
96+
expect(result.stderr).not.toContain('Failed to compile')
97+
98+
const files = await listFiles(path.join(project.root, 'dist-analyze'))
99+
const analyzerArtifact = files.find(
100+
file =>
101+
file.endsWith('report.html') ||
102+
file.endsWith('analyzer.html') ||
103+
file.endsWith('bundle-report.html') ||
104+
(file.endsWith('.html') && file !== 'main.html'),
105+
)
106+
const analyzerLog = `${result.stdout}\n${result.stderr}`
107+
108+
expect(Boolean(analyzerArtifact) || /Could't analyze webpack bundle/i.test(analyzerLog)).toBe(true)
109+
} finally {
110+
await project.cleanup()
111+
}
112+
})
113+
114+
it('starts build --watch --serve, serves html, and stops cleanly on SIGTERM', async () => {
115+
const project = await createRealProject('cli-watch-serve')
116+
try {
117+
const port = await findFreePort()
118+
await writeRuntimeFixture(project.root, {outDir: 'dist-watch', port})
119+
const child = await spawnCli(['build', '--watch', '--serve', '--clearLog=false'], project.root)
120+
121+
try {
122+
const response = await waitForHttp(`http://127.0.0.1:${port}/main.html`, 180000)
123+
expect(response.status).toBe(200)
124+
const html = await response.text()
125+
expect(html).toContain('CLI Runtime Fixture')
126+
expect(html).toContain('runtime-root')
127+
const files = await listFiles(path.join(project.root, 'dist-watch'))
128+
const jsFile = files.find(file => file.startsWith('js/') && file.endsWith('.js'))
129+
expect(jsFile).toBeTruthy()
130+
const assetResponse = await waitForHttp(`http://127.0.0.1:${port}/${jsFile}`)
131+
expect(assetResponse.status).toBe(200)
132+
} finally {
133+
await child.stop('SIGTERM')
134+
}
135+
await waitForPortReleased(port)
136+
137+
expect(child.child.exitCode ?? child.child.signalCode).not.toBeNull()
138+
const output = child.output()
139+
expect(`${output.stdout}${output.stderr}`).not.toContain('Failed to compile')
140+
} finally {
141+
await project.cleanup()
142+
}
143+
})
144+
})
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import {readFile, readdir, stat} from 'node:fs/promises'
2+
import path from 'node:path'
3+
import {describe, expect, it} from '@rstest/core'
4+
import {
5+
createRealProject,
6+
pathExists,
7+
runCli,
8+
writeProjectFile,
9+
} from '../support/real-project'
10+
11+
async function listFiles(root: string, relativeDir = ''): Promise<string[]> {
12+
const dir = path.join(root, relativeDir)
13+
const entries = await readdir(dir, {withFileTypes: true})
14+
const files: string[] = []
15+
for (const entry of entries) {
16+
const nextRelative = path.join(relativeDir, entry.name)
17+
const nextPath = path.join(root, nextRelative)
18+
if (entry.isDirectory()) {
19+
files.push(...(await listFiles(root, nextRelative)))
20+
} else if ((await stat(nextPath)).isFile()) {
21+
files.push(nextRelative)
22+
}
23+
}
24+
return files.sort()
25+
}
26+
27+
async function findFirst(root: string, predicate: (file: string) => boolean) {
28+
const files = await listFiles(root)
29+
return files.find(predicate)
30+
}
31+
32+
describe('cli config build', () => {
33+
it('builds a real fixture with env vars, aliases, assets, and sourcemaps', async () => {
34+
const project = await createRealProject('cli-config-build')
35+
try {
36+
await writeProjectFile(
37+
project.root,
38+
'package.json',
39+
JSON.stringify(
40+
{
41+
name: 'cli-config-build-fixture',
42+
private: true,
43+
type: 'module',
44+
},
45+
null,
46+
2,
47+
) + '\n',
48+
)
49+
50+
await writeProjectFile(
51+
project.root,
52+
'tsconfig.json',
53+
JSON.stringify(
54+
{
55+
compilerOptions: {
56+
target: 'ES2020',
57+
module: 'ESNext',
58+
moduleResolution: 'Bundler',
59+
lib: ['ES2020', 'DOM'],
60+
baseUrl: '.',
61+
strict: true,
62+
skipLibCheck: true,
63+
},
64+
},
65+
null,
66+
2,
67+
) + '\n',
68+
)
69+
70+
await writeProjectFile(
71+
project.root,
72+
'emp.config.ts',
73+
[
74+
'export default store => ({',
75+
" base: '/cdn/',",
76+
" appSrc: 'src',",
77+
" appEntry: 'main.ts',",
78+
" build: {",
79+
" outDir: 'dist-real',",
80+
" assetsDir: 'assets-real',",
81+
" sourcemap: {js: 'source-map', css: true},",
82+
' },',
83+
" html: {mountId: 'custom-root', title: 'CLI Real Config'},",
84+
" entries: {main: {}},",
85+
" define: {",
86+
" __EMP_TEST_ENV__: JSON.stringify('from-define'),",
87+
" API_URL: JSON.stringify(store.cliOptions.envVars?.API_URL ?? ''),",
88+
' },',
89+
" defineFix: 'all',",
90+
` resolve: {alias: {'@fixture': ${JSON.stringify(path.join(project.root, 'src', 'fixture'))}}},`,
91+
'})',
92+
'',
93+
].join('\n'),
94+
)
95+
96+
await writeProjectFile(
97+
project.root,
98+
'src/fixture/message.ts',
99+
"export const message = 'alias-hit'\n",
100+
)
101+
102+
await writeProjectFile(
103+
project.root,
104+
'src/style.css',
105+
[
106+
':root {',
107+
" --fixture-color: #123456;",
108+
'}',
109+
'.fixture {',
110+
' color: var(--fixture-color);',
111+
'}',
112+
'',
113+
].join('\n'),
114+
)
115+
116+
await writeProjectFile(
117+
project.root,
118+
'src/main.ts',
119+
[
120+
'declare const process: {env: {API_URL?: string}}',
121+
'declare const __EMP_TEST_ENV__: string',
122+
'',
123+
"import {message} from '@fixture/message'",
124+
"import './style.css'",
125+
'',
126+
'const mount = document.getElementById("custom-root")',
127+
'if (!mount) throw new Error("mount missing")',
128+
'mount.textContent = [',
129+
" 'title:' + document.title,",
130+
" 'env:' + process.env.API_URL,",
131+
" 'define:' + process.env.__EMP_TEST_ENV__,",
132+
' message,',
133+
"].join('|')",
134+
'',
135+
].join('\n'),
136+
)
137+
138+
const result = await runCli(
139+
[
140+
'build',
141+
'--env',
142+
'prod',
143+
'--env-vars',
144+
'API_URL=https://api.example.test',
145+
'--clearLog=false',
146+
],
147+
project.root,
148+
180000,
149+
)
150+
151+
expect(result.code).toBe(0)
152+
expect(result.stderr).not.toContain('Failed to compile')
153+
154+
const htmlPath = path.join(project.root, 'dist-real', 'main.html')
155+
const html = await readFile(htmlPath, 'utf8')
156+
expect(html).toContain('<title>CLI Real Config</title>')
157+
expect(html).toContain('id="custom-root"')
158+
expect(html).toContain('/cdn/')
159+
160+
const files = await listFiles(path.join(project.root, 'dist-real'))
161+
const jsFile = files.find(file => file.startsWith('js/') && file.endsWith('.js'))
162+
const cssFile = files.find(file => file.startsWith('css/') && file.endsWith('.css'))
163+
const jsMapFile = files.find(file => file.startsWith('js/') && file.endsWith('.js.map'))
164+
const cssMapFile = files.find(file => file.startsWith('css/') && file.endsWith('.css.map'))
165+
166+
expect(jsFile).toBeTruthy()
167+
expect(cssFile).toBeTruthy()
168+
expect(jsMapFile).toBeTruthy()
169+
expect(cssMapFile).toBeTruthy()
170+
171+
const js = await readFile(path.join(project.root, 'dist-real', jsFile as string), 'utf8')
172+
const css = await readFile(path.join(project.root, 'dist-real', cssFile as string), 'utf8')
173+
174+
expect(js).toContain('alias-hit')
175+
expect(js).toContain('from-define')
176+
expect(js).toContain('https://api.example.test')
177+
expect(js).not.toContain('process.env.API_URL')
178+
expect(css).toContain('--fixture-color')
179+
180+
const indexMap = await findFirst(path.join(project.root, 'dist-real'), file => file.endsWith('.map'))
181+
expect(indexMap).toBeTruthy()
182+
expect(await pathExists(path.join(project.root, 'dist-real', indexMap as string))).toBe(true)
183+
} finally {
184+
await project.cleanup()
185+
}
186+
})
187+
})

0 commit comments

Comments
 (0)