cabloy 5.1.202 → 5.1.203
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/.cabloy-version +1 -1
- package/CHANGELOG.md +11 -0
- package/CONTRIBUTING.md +18 -17
- package/README.md +2 -2
- package/package.json +2 -3
- package/repo-agent-governance/managed-assets.json +12 -12
- package/repo-agent-governance/policies/repository.md +1 -0
- package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -2
- package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +1 -1
- package/repo-agent-governance/skills/cabloy-worktree-environment/SKILL.md +5 -5
- package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
- package/repo-docs/assets/img/cabloy-admin-tabs-cover-en.png +0 -0
- package/repo-docs/assets/img/cabloy-ai-crud-cover-en.png +0 -0
- package/repo-docs/demo.md +14 -0
- package/repo-docs/frontend/scripts.md +10 -18
- package/repo-docs/fullstack/parallel-worktree-environment.md +2 -2
- package/repo-docs/index.md +2 -2
- package/repo-docs/reference/repo-scripts.md +15 -31
- package/repo-e2e/config/playwright.config.ts +14 -19
- package/repo-e2e/scripts/e2e.test.ts +17 -0
- package/repo-e2e/scripts/e2e.ts +5 -1
- package/repo-e2e/scripts/runE2e.ts +3 -15
- package/repo-e2e/scripts/runE2eArgs.test.ts +21 -17
- package/repo-e2e/scripts/runE2eArgs.ts +16 -29
- package/repo-e2e/scripts/startE2eVona.ts +20 -9
- package/repo-e2e/specs/a-commerce.spec.ts +232 -31
- package/repo-e2e/specs/cabloy-basic.spec.ts +39 -12
- package/repo-e2e/specs/home-user-account.spec.ts +9 -2
- package/scripts/upgrade.test.ts +147 -0
- package/scripts/upgrade.ts +49 -27
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +51 -5
- package/vona/packages-cli/cli-set-api/src/lib/command/bin.dev.ts +6 -1
- package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +41 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/e2eFixture.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/e2eFixture.ts +224 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/e2eFixture.ts +694 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/e2eFixture.test.ts +434 -0
|
@@ -40,11 +40,23 @@ function collectConsoleErrors(page: Page) {
|
|
|
40
40
|
|
|
41
41
|
async function loginAsAdmin(page: Page) {
|
|
42
42
|
await page.goto('/admin/', { waitUntil: 'load' });
|
|
43
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
43
44
|
if (page.url().includes('/admin/login')) {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const usernameInput = page.getByPlaceholder('Your Username');
|
|
46
|
+
const passwordInput = page.getByPlaceholder('Your Password');
|
|
47
|
+
await usernameInput.fill('admin');
|
|
48
|
+
await passwordInput.fill('123456');
|
|
49
|
+
await expect(usernameInput).toHaveValue('admin');
|
|
50
|
+
await expect(passwordInput).toHaveValue('123456');
|
|
46
51
|
await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
52
|
+
const loginResponse = page.waitForResponse(response => {
|
|
53
|
+
const url = new URL(response.url());
|
|
54
|
+
return (
|
|
55
|
+
response.request().method() === 'POST' && url.pathname === '/api/home/user/passport/login'
|
|
56
|
+
);
|
|
57
|
+
});
|
|
47
58
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
59
|
+
expect((await loginResponse).ok()).toBeTruthy();
|
|
48
60
|
await expect(page).not.toHaveURL(/\/admin\/login(?:\?|$)/);
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -158,7 +170,7 @@ test(
|
|
|
158
170
|
async ({ page, request }) => {
|
|
159
171
|
const response = await request.get('/');
|
|
160
172
|
expect(response.ok()).toBeTruthy();
|
|
161
|
-
expect(response.headers()['cache-control']).toBe('
|
|
173
|
+
expect(response.headers()['cache-control']).toBe('public, max-age=600');
|
|
162
174
|
const html = await response.text();
|
|
163
175
|
expect(html.toLowerCase()).not.toContain('data-zova-hydrated');
|
|
164
176
|
expect(html).not.toContain('ssr-body-ready-observer');
|
|
@@ -166,9 +178,7 @@ test(
|
|
|
166
178
|
|
|
167
179
|
const routeOverrideResponse = await request.get('/demo/basic/routeQueryB');
|
|
168
180
|
expect(routeOverrideResponse.ok()).toBeTruthy();
|
|
169
|
-
expect(routeOverrideResponse.headers()['cache-control']).toBe(
|
|
170
|
-
'no-cache, no-store, must-revalidate',
|
|
171
|
-
);
|
|
181
|
+
expect(routeOverrideResponse.headers()['cache-control']).toBe('public, max-age=300');
|
|
172
182
|
|
|
173
183
|
const unlocalizedResponse = await request.get('/demo/basic/state');
|
|
174
184
|
expect(unlocalizedResponse.ok()).toBeTruthy();
|
|
@@ -192,7 +202,7 @@ test(
|
|
|
192
202
|
);
|
|
193
203
|
|
|
194
204
|
test(
|
|
195
|
-
'ATP-BASIC-SSR-03: concurrent public routes
|
|
205
|
+
'ATP-BASIC-SSR-03: concurrent public routes preserve their cache policies in managed test mode',
|
|
196
206
|
{ tag: ['@web', '@smoke'] },
|
|
197
207
|
async ({ request }) => {
|
|
198
208
|
const responses = await Promise.all(
|
|
@@ -205,9 +215,13 @@ test(
|
|
|
205
215
|
|
|
206
216
|
for (const [index, response] of responses.entries()) {
|
|
207
217
|
expect(response.ok(), `response ${index}`).toBeTruthy();
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
const cacheControl =
|
|
219
|
+
index % 3 === 0
|
|
220
|
+
? 'public, max-age=600'
|
|
221
|
+
: index % 3 === 1
|
|
222
|
+
? 'public, max-age=300'
|
|
223
|
+
: 'no-cache, no-store, must-revalidate';
|
|
224
|
+
expect(response.headers()['cache-control'], `response ${index}`).toBe(cacheControl);
|
|
211
225
|
}
|
|
212
226
|
},
|
|
213
227
|
);
|
|
@@ -338,6 +352,7 @@ test(
|
|
|
338
352
|
const consoleErrors = collectConsoleErrors(page);
|
|
339
353
|
try {
|
|
340
354
|
await page.goto('/admin/login', { waitUntil: 'load' });
|
|
355
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
341
356
|
await page.evaluate(() => {
|
|
342
357
|
indexedDB.deleteDatabase('localforage');
|
|
343
358
|
localStorage.clear();
|
|
@@ -346,13 +361,25 @@ test(
|
|
|
346
361
|
await page.goto('/admin/login?returnTo=/rest/resource/training-student%253Astudent', {
|
|
347
362
|
waitUntil: 'load',
|
|
348
363
|
});
|
|
349
|
-
await page.
|
|
350
|
-
|
|
364
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
365
|
+
const usernameInput = page.getByPlaceholder('Your Username');
|
|
366
|
+
const passwordInput = page.getByPlaceholder('Your Password');
|
|
367
|
+
await usernameInput.fill('admin');
|
|
368
|
+
await passwordInput.fill('123456');
|
|
369
|
+
await expect(usernameInput).toHaveValue('admin');
|
|
370
|
+
await expect(passwordInput).toHaveValue('123456');
|
|
351
371
|
await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
352
372
|
|
|
353
373
|
const initialSelect = waitForStudentSelect(page);
|
|
374
|
+
const loginResponse = page.waitForResponse(response => {
|
|
375
|
+
const url = new URL(response.url());
|
|
376
|
+
return (
|
|
377
|
+
response.request().method() === 'POST' && url.pathname === '/api/home/user/passport/login'
|
|
378
|
+
);
|
|
379
|
+
});
|
|
354
380
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
355
381
|
try {
|
|
382
|
+
expect((await loginResponse).ok()).toBeTruthy();
|
|
356
383
|
await expect(page).toHaveURL(studentResourceUrl);
|
|
357
384
|
await initialSelect;
|
|
358
385
|
} finally {
|
|
@@ -591,11 +591,18 @@ test(
|
|
|
591
591
|
{ tag: ['@account', '@admin'] },
|
|
592
592
|
async ({ page }) => {
|
|
593
593
|
await page.goto('/admin/', { waitUntil: 'load' });
|
|
594
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
594
595
|
if (page.url().includes('/admin/login')) {
|
|
595
|
-
|
|
596
|
-
|
|
596
|
+
const usernameInput = page.getByPlaceholder('Your Username');
|
|
597
|
+
const passwordInput = page.getByPlaceholder('Your Password');
|
|
598
|
+
await usernameInput.fill('admin');
|
|
599
|
+
await passwordInput.fill('123456');
|
|
600
|
+
await expect(usernameInput).toHaveValue('admin');
|
|
601
|
+
await expect(passwordInput).toHaveValue('123456');
|
|
597
602
|
await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
603
|
+
const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
|
|
598
604
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
605
|
+
expect((await loginResponse).ok()).toBeTruthy();
|
|
599
606
|
await expect(page).not.toHaveURL(/\/admin\/login(?:\?|$)/);
|
|
600
607
|
}
|
|
601
608
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { mergeFrameworkE2eAssets, reconcileFrameworkE2ePackageJson } from './upgrade.ts';
|
|
8
|
+
|
|
9
|
+
const frameworkFiles = [
|
|
10
|
+
'repo-e2e/config/playwright.config.ts',
|
|
11
|
+
'repo-e2e/scripts/runE2e.ts',
|
|
12
|
+
'repo-e2e/specs/cabloy-basic.spec.ts',
|
|
13
|
+
'repo-e2e/specs/home-user-account.spec.ts',
|
|
14
|
+
'repo-e2e/specs/a-commerce.spec.ts',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function writeFile(root: string, path: string, content: string): void {
|
|
18
|
+
const file = resolve(root, path);
|
|
19
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
20
|
+
writeFileSync(file, content);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readPackage(root: string): {
|
|
24
|
+
scripts: Record<string, string>;
|
|
25
|
+
dependencies?: Record<string, string>;
|
|
26
|
+
devDependencies?: Record<string, string>;
|
|
27
|
+
} {
|
|
28
|
+
return JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf-8'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fixture(): { root: string; project: string; source: string; dispose(): void } {
|
|
32
|
+
const root = mkdtempSync(resolve(tmpdir(), 'cabloy-upgrade-e2e-'));
|
|
33
|
+
const project = resolve(root, 'project');
|
|
34
|
+
const source = resolve(root, 'source');
|
|
35
|
+
writeFile(project, '__CABLOY_BASIC__', '');
|
|
36
|
+
writeFile(
|
|
37
|
+
project,
|
|
38
|
+
'package.json',
|
|
39
|
+
`${JSON.stringify(
|
|
40
|
+
{
|
|
41
|
+
scripts: {
|
|
42
|
+
'test:e2e': 'node repo-e2e/scripts/runE2e.ts --clean',
|
|
43
|
+
'test:e2e:fast': 'node repo-e2e/scripts/runE2e.ts --fast',
|
|
44
|
+
'test:project': 'node project-tests.ts',
|
|
45
|
+
},
|
|
46
|
+
dependencies: { '@playwright/test': '^1.0.0', 'project-owned': '^2.0.0' },
|
|
47
|
+
devDependencies: { 'another-tool': '^3.0.0' },
|
|
48
|
+
},
|
|
49
|
+
null,
|
|
50
|
+
2,
|
|
51
|
+
)}\n`,
|
|
52
|
+
);
|
|
53
|
+
writeFile(
|
|
54
|
+
source,
|
|
55
|
+
'package.json',
|
|
56
|
+
`${JSON.stringify({ scripts: { 'test:e2e': 'node repo-e2e/scripts/runE2e.ts' }, devDependencies: { '@playwright/test': '^1.62.1' } })}\n`,
|
|
57
|
+
);
|
|
58
|
+
for (const path of frameworkFiles) {
|
|
59
|
+
writeFile(project, path, `old ${path}`);
|
|
60
|
+
writeFile(source, path, `new ${path}`);
|
|
61
|
+
}
|
|
62
|
+
writeFile(project, 'repo-e2e/specs/project-owned.spec.ts', 'project browser test');
|
|
63
|
+
writeFile(project, 'repo-e2e/config/playwright.basic.config.ts', 'legacy config');
|
|
64
|
+
return { root, project, source, dispose: () => rmSync(root, { recursive: true, force: true }) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test('reconciles one managed E2E script and removes only the known retired fast script', () => {
|
|
68
|
+
const { project, source, dispose } = fixture();
|
|
69
|
+
try {
|
|
70
|
+
reconcileFrameworkE2ePackageJson(false, project, source);
|
|
71
|
+
const first = readFileSync(resolve(project, 'package.json'), 'utf-8');
|
|
72
|
+
const pkg = readPackage(project);
|
|
73
|
+
assert.equal(pkg.scripts['test:e2e'], 'node repo-e2e/scripts/runE2e.ts');
|
|
74
|
+
assert.equal(Object.hasOwn(pkg.scripts, 'test:e2e:fast'), false);
|
|
75
|
+
assert.equal(pkg.scripts['test:project'], 'node project-tests.ts');
|
|
76
|
+
assert.deepEqual(pkg.dependencies, { 'project-owned': '^2.0.0' });
|
|
77
|
+
assert.deepEqual(pkg.devDependencies, {
|
|
78
|
+
'another-tool': '^3.0.0',
|
|
79
|
+
'@playwright/test': '^1.62.1',
|
|
80
|
+
});
|
|
81
|
+
reconcileFrameworkE2ePackageJson(false, project, source);
|
|
82
|
+
assert.equal(readFileSync(resolve(project, 'package.json'), 'utf-8'), first);
|
|
83
|
+
} finally {
|
|
84
|
+
dispose();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('preserves a customized fast alias and project-owned browser specs', () => {
|
|
89
|
+
const { project, source, dispose } = fixture();
|
|
90
|
+
try {
|
|
91
|
+
const pkg = readPackage(project);
|
|
92
|
+
pkg.scripts['test:e2e:fast'] = 'node project-tests.ts --quick';
|
|
93
|
+
writeFile(project, 'package.json', `${JSON.stringify(pkg, null, 2)}\n`);
|
|
94
|
+
reconcileFrameworkE2ePackageJson(false, project, source);
|
|
95
|
+
mergeFrameworkE2eAssets(false, project, source);
|
|
96
|
+
assert.equal(readPackage(project).scripts['test:e2e:fast'], 'node project-tests.ts --quick');
|
|
97
|
+
for (const path of frameworkFiles) {
|
|
98
|
+
assert.equal(readFileSync(resolve(project, path), 'utf-8'), `new ${path}`);
|
|
99
|
+
}
|
|
100
|
+
assert.equal(
|
|
101
|
+
readFileSync(resolve(project, 'repo-e2e/specs/project-owned.spec.ts'), 'utf-8'),
|
|
102
|
+
'project browser test',
|
|
103
|
+
);
|
|
104
|
+
assert.equal(existsSync(resolve(project, 'repo-e2e/config/playwright.basic.config.ts')), false);
|
|
105
|
+
} finally {
|
|
106
|
+
dispose();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('isolated dry-run reports changes without writing files or contacting npm', () => {
|
|
111
|
+
const { project, source, dispose } = fixture();
|
|
112
|
+
const before = readFileSync(resolve(project, 'package.json'), 'utf-8');
|
|
113
|
+
try {
|
|
114
|
+
reconcileFrameworkE2ePackageJson(true, project, source);
|
|
115
|
+
mergeFrameworkE2eAssets(true, project, source);
|
|
116
|
+
assert.equal(readFileSync(resolve(project, 'package.json'), 'utf-8'), before);
|
|
117
|
+
assert.equal(
|
|
118
|
+
readFileSync(resolve(project, 'repo-e2e/scripts/runE2e.ts'), 'utf-8'),
|
|
119
|
+
'old repo-e2e/scripts/runE2e.ts',
|
|
120
|
+
);
|
|
121
|
+
assert.equal(existsSync(resolve(project, 'repo-e2e/config/playwright.basic.config.ts')), true);
|
|
122
|
+
assert.equal(
|
|
123
|
+
readFileSync(resolve(project, 'repo-e2e/specs/project-owned.spec.ts'), 'utf-8'),
|
|
124
|
+
'project browser test',
|
|
125
|
+
);
|
|
126
|
+
} finally {
|
|
127
|
+
dispose();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('Start edition is not reconciled by the Basic framework E2E upgrader', () => {
|
|
132
|
+
const { project, source, dispose } = fixture();
|
|
133
|
+
try {
|
|
134
|
+
rmSync(resolve(project, '__CABLOY_BASIC__'));
|
|
135
|
+
writeFile(project, '__CABLOY_START__', '');
|
|
136
|
+
const before = readFileSync(resolve(project, 'package.json'), 'utf-8');
|
|
137
|
+
reconcileFrameworkE2ePackageJson(false, project, source);
|
|
138
|
+
mergeFrameworkE2eAssets(false, project, source);
|
|
139
|
+
assert.equal(readFileSync(resolve(project, 'package.json'), 'utf-8'), before);
|
|
140
|
+
assert.equal(
|
|
141
|
+
readFileSync(resolve(project, 'repo-e2e/scripts/runE2e.ts'), 'utf-8'),
|
|
142
|
+
'old repo-e2e/scripts/runE2e.ts',
|
|
143
|
+
);
|
|
144
|
+
} finally {
|
|
145
|
+
dispose();
|
|
146
|
+
}
|
|
147
|
+
});
|
package/scripts/upgrade.ts
CHANGED
|
@@ -68,7 +68,8 @@ const FRAMEWORK_E2E_FILES_CABLOY_BASIC: string[] = [
|
|
|
68
68
|
'repo-e2e/specs/a-commerce.spec.ts',
|
|
69
69
|
];
|
|
70
70
|
|
|
71
|
-
const
|
|
71
|
+
const FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC = 'test:e2e';
|
|
72
|
+
const LEGACY_FRAMEWORK_E2E_FAST_SCRIPT_CABLOY_BASIC = 'node repo-e2e/scripts/runE2e.ts --fast';
|
|
72
73
|
|
|
73
74
|
const FRAMEWORK_E2E_DEV_DEPENDENCY_CABLOY_BASIC = '@playwright/test';
|
|
74
75
|
|
|
@@ -147,9 +148,9 @@ function copyDirectory(src: string, dest: string): void {
|
|
|
147
148
|
cpSync(src, dest, { recursive: true, filter: shouldCopyPath });
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
function resolveEdition(): 'basic' | 'start' {
|
|
151
|
-
const hasBasic = existsSync(resolve(
|
|
152
|
-
const hasStart = existsSync(resolve(
|
|
151
|
+
function resolveEdition(rootDir = ROOT_DIR): 'basic' | 'start' {
|
|
152
|
+
const hasBasic = existsSync(resolve(rootDir, '__CABLOY_BASIC__'));
|
|
153
|
+
const hasStart = existsSync(resolve(rootDir, '__CABLOY_START__'));
|
|
153
154
|
if (hasBasic === hasStart) {
|
|
154
155
|
throw new Error(
|
|
155
156
|
hasBasic
|
|
@@ -226,12 +227,16 @@ async function extractTarball(tarballPath: string, targetDir: string): Promise<v
|
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
function mergeFrameworkE2eAssets(
|
|
230
|
-
|
|
230
|
+
export function mergeFrameworkE2eAssets(
|
|
231
|
+
dryRun?: boolean,
|
|
232
|
+
rootDir = ROOT_DIR,
|
|
233
|
+
tempDir = TEMP_DIR,
|
|
234
|
+
): void {
|
|
235
|
+
if (resolveEdition(rootDir) !== 'basic') return;
|
|
231
236
|
|
|
232
237
|
for (const dir of FRAMEWORK_E2E_DIRS_CABLOY_BASIC) {
|
|
233
|
-
const src = resolve(
|
|
234
|
-
const dest = resolve(
|
|
238
|
+
const src = resolve(tempDir, dir);
|
|
239
|
+
const dest = resolve(rootDir, dir);
|
|
235
240
|
if (!existsSync(src)) {
|
|
236
241
|
throw new Error(`Expected framework E2E directory in package: ${dir}`);
|
|
237
242
|
}
|
|
@@ -243,8 +248,8 @@ function mergeFrameworkE2eAssets(dryRun?: boolean): void {
|
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
for (const file of FRAMEWORK_E2E_FILES_CABLOY_BASIC) {
|
|
246
|
-
const src = resolve(
|
|
247
|
-
const dest = resolve(
|
|
251
|
+
const src = resolve(tempDir, file);
|
|
252
|
+
const dest = resolve(rootDir, file);
|
|
248
253
|
if (!existsSync(src)) {
|
|
249
254
|
throw new Error(`Expected framework E2E file in package: ${file}`);
|
|
250
255
|
}
|
|
@@ -265,7 +270,7 @@ function mergeFrameworkE2eAssets(dryRun?: boolean): void {
|
|
|
265
270
|
'repo-e2e/specs/a-commerce/commerce.spec.ts',
|
|
266
271
|
];
|
|
267
272
|
for (const file of legacyFrameworkFiles) {
|
|
268
|
-
const dest = resolve(
|
|
273
|
+
const dest = resolve(rootDir, file);
|
|
269
274
|
if (!existsSync(dest)) continue;
|
|
270
275
|
if (dryRun) {
|
|
271
276
|
log(` [dry-run] Remove legacy framework E2E file: ${file}`);
|
|
@@ -334,11 +339,15 @@ function reconcileGovernanceAssets(dryRun?: boolean): void {
|
|
|
334
339
|
}
|
|
335
340
|
}
|
|
336
341
|
|
|
337
|
-
function reconcileFrameworkE2ePackageJson(
|
|
338
|
-
|
|
342
|
+
export function reconcileFrameworkE2ePackageJson(
|
|
343
|
+
dryRun?: boolean,
|
|
344
|
+
rootDir = ROOT_DIR,
|
|
345
|
+
tempDir = TEMP_DIR,
|
|
346
|
+
): void {
|
|
347
|
+
if (resolveEdition(rootDir) !== 'basic') return;
|
|
339
348
|
|
|
340
|
-
const projectPackagePath = resolve(
|
|
341
|
-
const sourcePackagePath = resolve(
|
|
349
|
+
const projectPackagePath = resolve(rootDir, 'package.json');
|
|
350
|
+
const sourcePackagePath = resolve(tempDir, 'package.json');
|
|
342
351
|
const projectPackage = readPackageJson(projectPackagePath);
|
|
343
352
|
const sourcePackage = readPackageJson(sourcePackagePath);
|
|
344
353
|
const sourcePlaywrightVersion =
|
|
@@ -349,19 +358,30 @@ function reconcileFrameworkE2ePackageJson(dryRun?: boolean): void {
|
|
|
349
358
|
);
|
|
350
359
|
}
|
|
351
360
|
|
|
361
|
+
const sourceScript = sourcePackage.scripts?.[FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC];
|
|
362
|
+
if (!sourceScript) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Expected framework E2E script in package.json: ${FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
352
368
|
let changed = false;
|
|
353
|
-
|
|
354
|
-
const sourceValue = sourcePackage.scripts?.[name];
|
|
355
|
-
if (!sourceValue) {
|
|
356
|
-
throw new Error(`Expected framework E2E script in package.json: ${name}`);
|
|
357
|
-
}
|
|
358
|
-
if (projectPackage.scripts?.[name] === sourceValue) continue;
|
|
369
|
+
if (projectPackage.scripts?.[FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC] !== sourceScript) {
|
|
359
370
|
changed = true;
|
|
360
371
|
if (dryRun) {
|
|
361
|
-
log(` [dry-run] Set package.json scripts.${
|
|
372
|
+
log(` [dry-run] Set package.json scripts.${FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC}`);
|
|
362
373
|
} else {
|
|
363
374
|
projectPackage.scripts ??= {};
|
|
364
|
-
projectPackage.scripts[
|
|
375
|
+
projectPackage.scripts[FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC] = sourceScript;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (projectPackage.scripts?.['test:e2e:fast'] === LEGACY_FRAMEWORK_E2E_FAST_SCRIPT_CABLOY_BASIC) {
|
|
380
|
+
changed = true;
|
|
381
|
+
if (dryRun) {
|
|
382
|
+
log(' [dry-run] Remove package.json scripts.test:e2e:fast (retired framework value)');
|
|
383
|
+
} else {
|
|
384
|
+
delete projectPackage.scripts['test:e2e:fast'];
|
|
365
385
|
}
|
|
366
386
|
}
|
|
367
387
|
|
|
@@ -635,7 +655,9 @@ async function main(): Promise<void> {
|
|
|
635
655
|
}
|
|
636
656
|
}
|
|
637
657
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
658
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
659
|
+
main().catch(err => {
|
|
660
|
+
console.error(`\nUpgrade failed: ${err.message}`);
|
|
661
|
+
process.exit(1);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { glob } from '@cabloy/module-glob';
|
|
|
2
2
|
import type { VonaConfigMeta, VonaMetaFlavor, VonaMetaMode } from '@cabloy/module-info';
|
|
3
3
|
|
|
4
4
|
import { BeanCliBase } from '@cabloy/cli';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import nodemon from 'nodemon';
|
|
7
8
|
import { rimraf } from 'rimraf';
|
|
@@ -11,10 +12,23 @@ import type { VonaBinConfigOptions } from './toolsBin/types.ts';
|
|
|
11
12
|
import { getImportEsm } from '../utils.ts';
|
|
12
13
|
import { generateVonaMeta } from './toolsBin/generateVonaMeta.ts';
|
|
13
14
|
|
|
15
|
+
export function resolveDevMode(mode: unknown): 'dev' | 'test' {
|
|
16
|
+
if (mode === undefined || mode === 'dev') return 'dev';
|
|
17
|
+
if (mode === 'test') return 'test';
|
|
18
|
+
throw new Error('Invalid --mode: expected dev or test');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveTestWorkers(mode: 'dev' | 'test', workers: unknown): number | undefined {
|
|
22
|
+
if (mode !== 'test') return workers as number | undefined;
|
|
23
|
+
if (workers === undefined || workers === 1) return 1;
|
|
24
|
+
throw new Error('--mode=test requires --workers=1');
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
declare module '@cabloy/cli' {
|
|
15
28
|
interface ICommandArgv {
|
|
16
29
|
workers?: number;
|
|
17
30
|
flavor?: VonaMetaFlavor;
|
|
31
|
+
mode?: VonaMetaMode;
|
|
18
32
|
}
|
|
19
33
|
}
|
|
20
34
|
|
|
@@ -30,17 +44,49 @@ export class CliBinDev extends BeanCliBase {
|
|
|
30
44
|
|
|
31
45
|
async _dev(projectPath: string) {
|
|
32
46
|
const { argv } = this.context;
|
|
33
|
-
const mode
|
|
47
|
+
const mode = resolveDevMode(argv.mode);
|
|
48
|
+
const workers = resolveTestWorkers(mode, argv.workers);
|
|
34
49
|
const flavor: VonaMetaFlavor = argv.flavor || 'normal';
|
|
35
50
|
const configMeta: VonaConfigMeta = { flavor, mode };
|
|
36
51
|
const configOptions: VonaBinConfigOptions = {
|
|
37
52
|
appDir: projectPath,
|
|
38
53
|
runtimeDir: '.vona',
|
|
39
|
-
workers
|
|
54
|
+
workers,
|
|
40
55
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
try {
|
|
57
|
+
const { modulesMeta } = await generateVonaMeta(configMeta, configOptions);
|
|
58
|
+
if (mode === 'test') {
|
|
59
|
+
await this._runTest(projectPath);
|
|
60
|
+
} else {
|
|
61
|
+
await this._run(projectPath, modulesMeta);
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
await rimraf(path.join(projectPath, '.vona'));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async _runTest(projectPath: string) {
|
|
69
|
+
const child = spawn(process.execPath, [getImportEsm(), '.vona/bootstrap.ts'], {
|
|
70
|
+
cwd: projectPath,
|
|
71
|
+
stdio: 'inherit',
|
|
72
|
+
});
|
|
73
|
+
const forwardSignal = (signal: NodeJS.Signals) => child.kill(signal);
|
|
74
|
+
const onInterrupt = () => forwardSignal('SIGINT');
|
|
75
|
+
const onTerminate = () => forwardSignal('SIGTERM');
|
|
76
|
+
process.on('SIGINT', onInterrupt);
|
|
77
|
+
process.on('SIGTERM', onTerminate);
|
|
78
|
+
try {
|
|
79
|
+
await new Promise<void>((resolve, reject) => {
|
|
80
|
+
child.once('error', reject);
|
|
81
|
+
child.once('exit', (code, signal) => {
|
|
82
|
+
if (code === 0) resolve();
|
|
83
|
+
else reject(new Error(`Test-mode dev server exited: ${signal ?? code}`));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
} finally {
|
|
87
|
+
process.off('SIGINT', onInterrupt);
|
|
88
|
+
process.off('SIGTERM', onTerminate);
|
|
89
|
+
}
|
|
44
90
|
}
|
|
45
91
|
|
|
46
92
|
async _run(projectPath: string, _modulesMeta: Awaited<ReturnType<typeof glob>>) {
|
|
@@ -3,7 +3,7 @@ export default {
|
|
|
3
3
|
info: {
|
|
4
4
|
version: '5.0.0',
|
|
5
5
|
title: 'Cli: Bin: Dev',
|
|
6
|
-
usage: 'npm run vona :bin:dev -- [--workers=] [--flavor=]',
|
|
6
|
+
usage: 'npm run vona :bin:dev -- [--workers=] [--flavor=] [--mode=dev|test]',
|
|
7
7
|
},
|
|
8
8
|
options: {
|
|
9
9
|
workers: {
|
|
@@ -14,5 +14,10 @@ export default {
|
|
|
14
14
|
description: 'flavor',
|
|
15
15
|
type: 'string',
|
|
16
16
|
},
|
|
17
|
+
mode: {
|
|
18
|
+
description: 'dev or test',
|
|
19
|
+
type: 'string',
|
|
20
|
+
choices: ['dev', 'test'],
|
|
21
|
+
},
|
|
17
22
|
},
|
|
18
23
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { CliBinDev, resolveDevMode, resolveTestWorkers } from '../src/lib/bean/cli.bin.dev.ts';
|
|
8
|
+
import command from '../src/lib/command/bin.dev.ts';
|
|
9
|
+
|
|
10
|
+
describe('bin.dev mode', () => {
|
|
11
|
+
it('defaults to dev and accepts test explicitly', () => {
|
|
12
|
+
assert.equal(resolveDevMode(undefined), 'dev');
|
|
13
|
+
assert.equal(resolveDevMode('dev'), 'dev');
|
|
14
|
+
assert.equal(resolveDevMode('test'), 'test');
|
|
15
|
+
assert.deepEqual(command.options.mode.choices, ['dev', 'test']);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('rejects invalid modes before runtime generation', () => {
|
|
19
|
+
for (const value of ['', 'prod', 'unknown', true, ['dev', 'test'], null]) {
|
|
20
|
+
assert.throws(() => resolveDevMode(value), /Invalid --mode: expected dev or test/);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('forces one worker in test mode without changing dev workers', () => {
|
|
25
|
+
assert.equal(resolveTestWorkers('test', undefined), 1);
|
|
26
|
+
assert.equal(resolveTestWorkers('test', 1), 1);
|
|
27
|
+
assert.equal(resolveTestWorkers('dev', undefined), undefined);
|
|
28
|
+
assert.equal(resolveTestWorkers('dev', 3), 3);
|
|
29
|
+
for (const workers of [0, 2, '1', null, [1]]) {
|
|
30
|
+
assert.throws(() => resolveTestWorkers('test', workers), /requires --workers=1/);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('supervises test-mode bootstrap exit without starting a server', async () => {
|
|
35
|
+
const projectPath = mkdtempSync(join(tmpdir(), 'vona-bin-dev-mode-'));
|
|
36
|
+
try {
|
|
37
|
+
mkdirSync(join(projectPath, '.vona'));
|
|
38
|
+
writeFileSync(join(projectPath, '.vona/register.js'), '');
|
|
39
|
+
const cli = Object.create(CliBinDev.prototype) as CliBinDev;
|
|
40
|
+
for (const code of [0, 7]) {
|
|
41
|
+
writeFileSync(join(projectPath, '.vona/bootstrap.ts'), `process.exit(${code});`);
|
|
42
|
+
if (code === 0) {
|
|
43
|
+
await cli._runTest(projectPath);
|
|
44
|
+
} else {
|
|
45
|
+
await assert.rejects(cli._runTest(projectPath), /Test-mode dev server exited: 7/);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
rmSync(projectPath, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|