@nexical/cli 0.11.8 → 0.11.10
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/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +2 -0
- package/dist/src/commands/deploy.js +3 -3
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/init.js +3 -3
- package/dist/src/commands/module/add.js +53 -22
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/module/list.d.ts +1 -0
- package/dist/src/commands/module/list.js +54 -45
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.js +37 -12
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.js +15 -3
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.js +18 -1
- package/dist/src/commands/run.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/deploy.ts +3 -3
- package/src/commands/module/add.ts +74 -31
- package/src/commands/module/list.ts +80 -57
- package/src/commands/module/remove.ts +50 -14
- package/src/commands/module/update.ts +19 -5
- package/src/commands/run.ts +21 -1
- package/test/e2e/lifecycle.e2e.test.ts +3 -2
- package/test/integration/commands/deploy.integration.test.ts +102 -0
- package/test/integration/commands/init.integration.test.ts +16 -1
- package/test/integration/commands/module.integration.test.ts +81 -55
- package/test/integration/commands/run.integration.test.ts +69 -74
- package/test/integration/commands/setup.integration.test.ts +53 -0
- package/test/unit/commands/deploy.test.ts +285 -0
- package/test/unit/commands/init.test.ts +15 -0
- package/test/unit/commands/module/add.test.ts +363 -254
- package/test/unit/commands/module/list.test.ts +100 -99
- package/test/unit/commands/module/remove.test.ts +143 -58
- package/test/unit/commands/module/update.test.ts +45 -62
- package/test/unit/commands/run.test.ts +16 -1
- package/test/unit/commands/setup.test.ts +25 -66
- package/test/unit/deploy/config-manager.test.ts +65 -0
- package/test/unit/deploy/providers/cloudflare.test.ts +210 -0
- package/test/unit/deploy/providers/github.test.ts +139 -0
- package/test/unit/deploy/providers/railway.test.ts +328 -0
- package/test/unit/deploy/registry.test.ts +227 -0
- package/test/unit/deploy/utils.test.ts +30 -0
- package/test/unit/utils/command-discovery.test.ts +145 -142
- package/test/unit/utils/git_utils.test.ts +49 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
|
|
2
|
+
import DeployCommand from '../../../src/commands/deploy.js';
|
|
3
|
+
import { createTempDir, createMockRepo, cleanupTestRoot } from '../../utils/integration-helpers.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { CLI } from '@nexical/cli-core';
|
|
7
|
+
|
|
8
|
+
// Mock ConfigManager and Registry to control provider behavior without relying on real files or dynamic imports
|
|
9
|
+
vi.mock('../../../src/deploy/config-manager.js', () => {
|
|
10
|
+
return {
|
|
11
|
+
ConfigManager: vi.fn().mockImplementation(function () {
|
|
12
|
+
return {
|
|
13
|
+
load: vi.fn().mockResolvedValue({
|
|
14
|
+
deploy: {
|
|
15
|
+
backend: { provider: 'railway' },
|
|
16
|
+
frontend: { provider: 'cloudflare' },
|
|
17
|
+
repository: { provider: 'github' },
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock('../../../src/deploy/registry.js', () => {
|
|
26
|
+
return {
|
|
27
|
+
ProviderRegistry: vi.fn().mockImplementation(function () {
|
|
28
|
+
return {
|
|
29
|
+
loadCoreProviders: vi.fn(),
|
|
30
|
+
loadLocalProviders: vi.fn(),
|
|
31
|
+
getDeploymentProvider: vi.fn().mockImplementation((name) => {
|
|
32
|
+
if (name === 'railway') {
|
|
33
|
+
return {
|
|
34
|
+
name: 'railway',
|
|
35
|
+
provision: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
getSecrets: vi.fn().mockResolvedValue({ R_SEC: 'val' }),
|
|
37
|
+
getVariables: vi.fn().mockResolvedValue({ R_VAR: 'val' }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (name === 'cloudflare') {
|
|
41
|
+
return {
|
|
42
|
+
name: 'cloudflare',
|
|
43
|
+
provision: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
getSecrets: vi.fn().mockResolvedValue({ C_SEC: 'val' }),
|
|
45
|
+
getVariables: vi.fn().mockResolvedValue({ C_VAR: 'val' }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}),
|
|
50
|
+
getRepositoryProvider: vi.fn().mockReturnValue({
|
|
51
|
+
name: 'github',
|
|
52
|
+
configureSecrets: vi.fn().mockResolvedValue(undefined),
|
|
53
|
+
configureVariables: vi.fn().mockResolvedValue(undefined),
|
|
54
|
+
generateWorkflow: vi.fn().mockImplementation(async (ctx, vars) => {
|
|
55
|
+
// Simulate writing a workflow file to verify context
|
|
56
|
+
const targetDir = path.join(ctx.cwd, '.github/workflows');
|
|
57
|
+
const targetFile = path.join(targetDir, 'deploy.yml');
|
|
58
|
+
await fs.ensureDir(targetDir);
|
|
59
|
+
await fs.writeFile(targetFile, 'yaml content');
|
|
60
|
+
}),
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('Deploy Command Integration', () => {
|
|
68
|
+
let projectDir: string;
|
|
69
|
+
|
|
70
|
+
beforeEach(async () => {
|
|
71
|
+
const temp = await createTempDir('deploy-project-');
|
|
72
|
+
projectDir = await createMockRepo(temp, {
|
|
73
|
+
'package.json': '{"name": "deploy-project", "version": "1.0.0"}',
|
|
74
|
+
'nexical.yaml': 'site: deploy-test\nmodules: []',
|
|
75
|
+
'.env': 'TEST_ENV=true',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterAll(async () => {
|
|
80
|
+
await cleanupTestRoot();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should execute full deployment flow', async () => {
|
|
84
|
+
const originalCwd = process.cwd();
|
|
85
|
+
try {
|
|
86
|
+
// Create a CLI instance
|
|
87
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
88
|
+
process.chdir(projectDir);
|
|
89
|
+
|
|
90
|
+
const deployCmd = new DeployCommand(cli);
|
|
91
|
+
|
|
92
|
+
// Execute run
|
|
93
|
+
await deployCmd.run({ env: 'production' });
|
|
94
|
+
|
|
95
|
+
// Verify file creation from our mocked provider
|
|
96
|
+
const workflowPath = path.join(projectDir, '.github/workflows/deploy.yml');
|
|
97
|
+
expect(await fs.pathExists(workflowPath)).toBe(true);
|
|
98
|
+
} finally {
|
|
99
|
+
process.chdir(originalCwd);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -81,5 +81,20 @@ describe('InitCommand Integration', () => {
|
|
|
81
81
|
// However, `InitCommand` runs `npm install`. If that fails, the command throws/exits.
|
|
82
82
|
// We provided a minimal package.json so it should succeed.
|
|
83
83
|
expect(fs.existsSync(path.join(targetPath, 'node_modules'))).toBe(true);
|
|
84
|
-
}, 60000);
|
|
84
|
+
}, 60000);
|
|
85
|
+
|
|
86
|
+
it('should initialize with a custom repo', async () => {
|
|
87
|
+
const targetProjectName = 'custom-repo-project';
|
|
88
|
+
const targetPath = path.join(tempDir, targetProjectName);
|
|
89
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
90
|
+
const command = new InitCommand(cli);
|
|
91
|
+
|
|
92
|
+
await command.run({
|
|
93
|
+
directory: targetPath,
|
|
94
|
+
repo: starterRepoDir, // Reusing starter repo as "custom"
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(fs.existsSync(targetPath)).toBe(true);
|
|
98
|
+
expect(fs.existsSync(path.join(targetPath, 'package.json'))).toBe(true);
|
|
99
|
+
}, 60000);
|
|
85
100
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CLI } from '@nexical/cli-core';
|
|
2
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest';
|
|
3
3
|
import ModuleAddCommand from '../../../src/commands/module/add.js';
|
|
4
4
|
import ModuleRemoveCommand from '../../../src/commands/module/remove.js';
|
|
5
5
|
import ModuleListCommand from '../../../src/commands/module/list.js';
|
|
@@ -8,6 +8,7 @@ import ModuleUpdateCommand from '../../../src/commands/module/update.js';
|
|
|
8
8
|
import { createTempDir, createMockRepo, cleanupTestRoot } from '../../utils/integration-helpers.js';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import fs from 'fs-extra';
|
|
11
|
+
import { execa } from 'execa';
|
|
11
12
|
|
|
12
13
|
// Mock picocolors to return strings as-is for easy matching
|
|
13
14
|
vi.mock('picocolors', () => ({
|
|
@@ -25,7 +26,6 @@ vi.mock('picocolors', () => ({
|
|
|
25
26
|
|
|
26
27
|
describe('Module Commands Integration', () => {
|
|
27
28
|
let projectDir: string;
|
|
28
|
-
let moduleRepo: string;
|
|
29
29
|
let consoleTableSpy: unknown;
|
|
30
30
|
|
|
31
31
|
beforeEach(async () => {
|
|
@@ -42,7 +42,7 @@ describe('Module Commands Integration', () => {
|
|
|
42
42
|
|
|
43
43
|
// 2. Create a "Module" that is a SEPARATE git repo
|
|
44
44
|
const modTemp = await createTempDir('module-source-');
|
|
45
|
-
|
|
45
|
+
await createMockRepo(modTemp, {
|
|
46
46
|
'package.json': '{"name": "my-module", "version": "1.0.0", "description": "Awesome module"}',
|
|
47
47
|
'module.yaml': 'name: my-module\nversion: 1.0.0',
|
|
48
48
|
'index.ts': 'export const hello = "world";',
|
|
@@ -65,78 +65,104 @@ describe('Module Commands Integration', () => {
|
|
|
65
65
|
await cleanupTestRoot();
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('should add, list, update and remove
|
|
68
|
+
it('should add, list, update and remove backend and frontend modules', async () => {
|
|
69
69
|
const originalCwd = process.cwd();
|
|
70
|
-
|
|
70
|
+
// Re-initialize CLI for this test to ensure clean state if needed, though previously it was new per test
|
|
71
|
+
// We can reuse the CLI instance from beforeEach if we moved it there, but here it is fine.
|
|
72
|
+
|
|
73
|
+
// 1. Setup Backend Module Repo
|
|
74
|
+
const backendTemp = await createTempDir('backend-mod-');
|
|
75
|
+
const backendRepo = await createMockRepo(backendTemp, {
|
|
76
|
+
'package.json': '{"name": "backend-api", "version": "1.0.0"}',
|
|
77
|
+
'module.yaml': 'name: backend-api\nversion: 1.0.0',
|
|
78
|
+
'models.yaml': '- name: User\n fields: {}', // Indicator
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 2. Setup Frontend Module Repo
|
|
82
|
+
const frontendTemp = await createTempDir('frontend-mod-');
|
|
83
|
+
const frontendRepo = await createMockRepo(frontendTemp, {
|
|
84
|
+
'package.json': '{"name": "frontend-ui", "version": "1.0.0"}',
|
|
85
|
+
'module.yaml': 'name: frontend-ui\nversion: 1.0.0',
|
|
86
|
+
'ui.yaml': 'theme: dark', // Indicator
|
|
87
|
+
});
|
|
88
|
+
|
|
71
89
|
try {
|
|
72
90
|
process.chdir(projectDir);
|
|
73
91
|
|
|
74
|
-
//
|
|
75
|
-
|
|
92
|
+
// --- ADD BACKEND ---
|
|
93
|
+
// --- ADD BACKEND ---
|
|
94
|
+
// Actually `run` uses `this.projectRoot` which is set by `BaseCommand.init()`.
|
|
95
|
+
|
|
96
|
+
// Let's rely on the pattern from the existing file:
|
|
97
|
+
// imports: import { CLI } from '@nexical/cli-core';
|
|
98
|
+
// const cli = new CLI({ commandName: 'nexical' });
|
|
99
|
+
// const addCmd = new ModuleAddCommand(cli);
|
|
100
|
+
|
|
101
|
+
// I need to instantiate CLI first.
|
|
102
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
103
|
+
|
|
104
|
+
const addBackend = new ModuleAddCommand(cli);
|
|
105
|
+
(addBackend as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
106
|
+
// or we can rely on init() finding it if CWD is correct.
|
|
107
|
+
// Let's try to set it explicitly to be safe.
|
|
108
|
+
|
|
109
|
+
await addBackend.run({ url: backendRepo });
|
|
76
110
|
|
|
77
|
-
|
|
78
|
-
|
|
111
|
+
const backendPath = path.join(projectDir, 'apps/backend/modules/backend-api');
|
|
112
|
+
expect(fs.existsSync(backendPath)).toBe(true);
|
|
113
|
+
expect(fs.existsSync(path.join(backendPath, 'models.yaml'))).toBe(true);
|
|
79
114
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
// --- ADD FRONTEND ---
|
|
116
|
+
const addFrontend = new ModuleAddCommand(cli);
|
|
117
|
+
(addFrontend as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
118
|
+
await addFrontend.run({ url: frontendRepo });
|
|
83
119
|
|
|
84
|
-
|
|
120
|
+
const frontendPath = path.join(projectDir, 'apps/frontend/modules/frontend-ui');
|
|
121
|
+
expect(fs.existsSync(frontendPath)).toBe(true);
|
|
122
|
+
expect(fs.existsSync(path.join(frontendPath, 'ui.yaml'))).toBe(true);
|
|
123
|
+
|
|
124
|
+
// --- VERIFY CONFIG ---
|
|
85
125
|
const config = await fs.readFile(path.join(projectDir, 'nexical.yaml'), 'utf8');
|
|
86
126
|
expect(config).toContain('modules:');
|
|
87
|
-
expect(config).toContain('
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
expect(fs.existsSync(path.join(modulePath, '.git'))).toBe(true);
|
|
92
|
-
const gitModules = await fs.readFile(path.join(projectDir, '.gitmodules'), 'utf-8');
|
|
93
|
-
expect(gitModules).toContain('path = modules/my-module');
|
|
127
|
+
expect(config).toContain('backend:');
|
|
128
|
+
expect(config).toContain(' - backend-api'); // Indentation check might be flaky with yaml stringify, just check existence
|
|
129
|
+
expect(config).toContain('frontend:');
|
|
130
|
+
expect(config).toContain(' - frontend-ui');
|
|
94
131
|
|
|
95
|
-
//
|
|
132
|
+
// --- LIST ---
|
|
96
133
|
const listCmd = new ModuleListCommand(cli);
|
|
97
|
-
|
|
134
|
+
(listCmd as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
98
135
|
await listCmd.run();
|
|
99
136
|
|
|
100
|
-
// Check console.table called with module info
|
|
101
137
|
expect(consoleTableSpy).toHaveBeenCalledWith(
|
|
102
138
|
expect.arrayContaining([
|
|
103
|
-
expect.objectContaining({
|
|
104
|
-
|
|
105
|
-
version: '1.0.0',
|
|
106
|
-
description: 'Awesome module',
|
|
107
|
-
}),
|
|
139
|
+
expect.objectContaining({ name: 'backend-api', type: 'backend' }),
|
|
140
|
+
expect.objectContaining({ name: 'frontend-ui', type: 'frontend' }),
|
|
108
141
|
]),
|
|
109
142
|
);
|
|
110
143
|
|
|
111
|
-
//
|
|
144
|
+
// --- REMOVE BACKEND ---
|
|
145
|
+
const removeCmd = new ModuleRemoveCommand(cli);
|
|
146
|
+
(removeCmd as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
147
|
+
await removeCmd.run({ name: 'backend-api' });
|
|
148
|
+
|
|
149
|
+
expect(fs.existsSync(backendPath)).toBe(false);
|
|
150
|
+
|
|
151
|
+
const configAfterRemove = await fs.readFile(path.join(projectDir, 'nexical.yaml'), 'utf8');
|
|
152
|
+
expect(configAfterRemove).not.toContain('backend-api');
|
|
153
|
+
expect(configAfterRemove).toContain('frontend-ui');
|
|
154
|
+
|
|
155
|
+
// --- UPDATE FRONTEND ---
|
|
156
|
+
// Commit a change to frontend repo
|
|
157
|
+
await execa('git', ['commit', '--allow-empty', '-m', 'New version'], { cwd: frontendRepo });
|
|
158
|
+
|
|
112
159
|
const updateCmd = new ModuleUpdateCommand(cli);
|
|
113
|
-
|
|
114
|
-
await updateCmd.run({
|
|
115
|
-
// Hard to check "update" without changing the remote first.
|
|
116
|
-
// But we verify it ran without throwing.
|
|
160
|
+
(updateCmd as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
161
|
+
await updateCmd.run({});
|
|
117
162
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
await removeCmd.run({ name: 'my-module' });
|
|
122
|
-
|
|
123
|
-
expect(fs.existsSync(modulePath)).toBe(false);
|
|
124
|
-
|
|
125
|
-
// Verify git cleanup
|
|
126
|
-
// .git/modules/modules/my-module should be gone
|
|
127
|
-
const gitInternalModuleDir = path.join(projectDir, '.git/modules/modules/my-module');
|
|
128
|
-
expect(fs.existsSync(gitInternalModuleDir)).toBe(false);
|
|
129
|
-
|
|
130
|
-
// .gitmodules entry gone? `git rm` usually handles this.
|
|
131
|
-
// Check if .gitmodules file exists (if empty it might remain or be deleted depending on git version, usually implicitly updated)
|
|
132
|
-
if (fs.existsSync(path.join(projectDir, '.gitmodules'))) {
|
|
133
|
-
const updatedGitModules = await fs.readFile(path.join(projectDir, '.gitmodules'), 'utf-8');
|
|
134
|
-
expect(updatedGitModules).not.toContain('modules/my-module');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Verify nexical.yaml updated
|
|
138
|
-
const configRemoved = await fs.readFile(path.join(projectDir, 'nexical.yaml'), 'utf8');
|
|
139
|
-
expect(configRemoved).not.toContain('- my-module');
|
|
163
|
+
// Verify submodule update?
|
|
164
|
+
// Diff hard to check without actually checking git status inside.
|
|
165
|
+
// But command should succeed.
|
|
140
166
|
} finally {
|
|
141
167
|
process.chdir(originalCwd);
|
|
142
168
|
}
|
|
@@ -1,97 +1,92 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
|
|
1
|
+
import { describe, it, beforeEach, afterAll } from 'vitest';
|
|
3
2
|
import RunCommand from '../../../src/commands/run.js';
|
|
4
|
-
import { createTempDir } from '../../utils/integration-helpers.js';
|
|
3
|
+
import { createTempDir, createMockRepo, cleanupTestRoot } from '../../utils/integration-helpers.js';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import fs from 'fs-extra';
|
|
7
|
-
import {
|
|
8
|
-
import EventEmitter from 'events';
|
|
9
|
-
|
|
10
|
-
vi.mock('child_process', () => ({
|
|
11
|
-
spawn: vi.fn(),
|
|
12
|
-
exec: vi.fn(),
|
|
13
|
-
}));
|
|
6
|
+
import { CLI } from '@nexical/cli-core';
|
|
14
7
|
|
|
15
|
-
describe('
|
|
8
|
+
describe('Run Command Integration', () => {
|
|
16
9
|
let projectDir: string;
|
|
17
|
-
let spawnMock: ReturnType<typeof vi.mocked>;
|
|
18
10
|
|
|
19
11
|
beforeEach(async () => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await fs.outputFile(
|
|
26
|
-
path.join(projectDir, 'package.json'),
|
|
27
|
-
JSON.stringify({
|
|
28
|
-
name: 'nexical-core',
|
|
29
|
-
scripts: {
|
|
30
|
-
'test-script': 'echo test',
|
|
31
|
-
},
|
|
32
|
-
}),
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
await fs.ensureDir(path.join(projectDir, 'modules', 'my-auth'));
|
|
36
|
-
await fs.outputFile(
|
|
37
|
-
path.join(projectDir, 'modules', 'my-auth', 'package.json'),
|
|
38
|
-
JSON.stringify({
|
|
39
|
-
scripts: {
|
|
40
|
-
seed: 'node seed.js',
|
|
41
|
-
},
|
|
42
|
-
}),
|
|
43
|
-
);
|
|
12
|
+
const temp = await createTempDir('run-project-');
|
|
13
|
+
projectDir = await createMockRepo(temp, {
|
|
14
|
+
'package.json': '{"name": "run-project", "version": "1.0.0"}',
|
|
15
|
+
'nexical.yaml': 'site: run-test\nmodules: []',
|
|
16
|
+
});
|
|
44
17
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
18
|
+
// Create a mock module with a script
|
|
19
|
+
const moduleDir = path.join(projectDir, 'apps/backend/modules/script-mod');
|
|
20
|
+
await fs.ensureDir(moduleDir);
|
|
21
|
+
await fs.writeJson(path.join(moduleDir, 'package.json'), {
|
|
22
|
+
name: 'script-mod',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
scripts: {
|
|
25
|
+
'test-script': 'echo "Hello from script-mod"',
|
|
26
|
+
},
|
|
53
27
|
});
|
|
54
|
-
});
|
|
55
28
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
29
|
+
// Add module to nexical.yaml manually or via helper
|
|
30
|
+
// For RunCommand, it relies on discovery or explicit path?
|
|
31
|
+
// RunCommand iterates over ALL modules found in nexical.yaml or file system?
|
|
32
|
+
// RunCommand implementation:
|
|
33
|
+
// It runs a command in ALL modules or specific ones.
|
|
34
|
+
|
|
35
|
+
// We need to register it in nexical.yaml for it to be found commonly
|
|
36
|
+
const configPath = path.join(projectDir, 'nexical.yaml');
|
|
37
|
+
await fs.writeFile(configPath, 'site: run-test\nmodules:\n backend:\n - script-mod');
|
|
59
38
|
});
|
|
60
39
|
|
|
61
40
|
afterAll(async () => {
|
|
62
|
-
|
|
41
|
+
await cleanupTestRoot();
|
|
63
42
|
});
|
|
64
43
|
|
|
65
|
-
it('should run
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
44
|
+
it('should run a script in a specific module', async () => {
|
|
45
|
+
const originalCwd = process.cwd();
|
|
46
|
+
try {
|
|
47
|
+
process.chdir(projectDir);
|
|
48
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
49
|
+
const runCmd = new RunCommand(cli);
|
|
50
|
+
(runCmd as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
51
|
+
|
|
52
|
+
// We need to capture stdout/stderr to verify execution
|
|
53
|
+
// But RunCommand uses `runCommand` from cli-core which uses execa.
|
|
54
|
+
// In integration test, execa is REAL.
|
|
55
|
+
|
|
56
|
+
// However, BaseCommand.run() might just spawn it.
|
|
57
|
+
// We can inspect the output if we could capture it.
|
|
58
|
+
// But `execa` streams to stdio usually.
|
|
59
|
+
|
|
60
|
+
// Let's rely on side effects or just that it doesn't throw.
|
|
61
|
+
// Or we can mock `runCommand` from `@nexical/cli-core` if we want to verify it called the script?
|
|
62
|
+
// But this is integration test, we want real execution.
|
|
63
|
+
// "Hello from script-mod" should be printed.
|
|
69
64
|
|
|
70
|
-
|
|
65
|
+
await runCmd.run({ script: 'script-mod:test-script' });
|
|
71
66
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
cwd: projectDir,
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
67
|
+
// If passing, it means it found the module and ran the script (which echoed and exited 0).
|
|
68
|
+
} finally {
|
|
69
|
+
process.chdir(originalCwd);
|
|
70
|
+
}
|
|
79
71
|
});
|
|
80
72
|
|
|
81
|
-
it('should run
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
it('should run a script in root', async () => {
|
|
74
|
+
const originalCwd = process.cwd();
|
|
75
|
+
try {
|
|
76
|
+
process.chdir(projectDir);
|
|
77
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
78
|
+
const runCmd = new RunCommand(cli);
|
|
79
|
+
(runCmd as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
85
80
|
|
|
86
|
-
|
|
81
|
+
// Add a script to root package.json first
|
|
82
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
83
|
+
const pkg = await fs.readJson(pkgPath);
|
|
84
|
+
pkg.scripts = { 'root-script': 'echo "Hello from root"' };
|
|
85
|
+
await fs.writeJson(pkgPath, pkg);
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
expect.objectContaining({
|
|
93
|
-
cwd: path.resolve(projectDir, 'modules', 'my-auth'),
|
|
94
|
-
}),
|
|
95
|
-
);
|
|
87
|
+
await runCmd.run({ script: 'root-script' });
|
|
88
|
+
} finally {
|
|
89
|
+
process.chdir(originalCwd);
|
|
90
|
+
}
|
|
96
91
|
});
|
|
97
92
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
|
|
2
|
+
import SetupCommand from '../../../src/commands/setup.js';
|
|
3
|
+
import { createTempDir, createMockRepo, cleanupTestRoot } from '../../utils/integration-helpers.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { CLI } from '@nexical/cli-core';
|
|
7
|
+
|
|
8
|
+
describe('Setup Command Integration', () => {
|
|
9
|
+
let projectDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
const temp = await createTempDir('setup-project-');
|
|
13
|
+
projectDir = await createMockRepo(temp, {
|
|
14
|
+
'package.json': '{"name": "setup-project", "version": "1.0.0"}',
|
|
15
|
+
'nexical.yaml': 'site: setup-test\nmodules: []',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Create core assets
|
|
19
|
+
await fs.ensureDir(path.join(projectDir, 'core/src'));
|
|
20
|
+
await fs.writeFile(path.join(projectDir, 'core/src/shared.ts'), 'export const shared = true;');
|
|
21
|
+
|
|
22
|
+
// Create app directories
|
|
23
|
+
await fs.ensureDir(path.join(projectDir, 'apps/backend'));
|
|
24
|
+
await fs.ensureDir(path.join(projectDir, 'apps/frontend'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await cleanupTestRoot();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should symlink core assets to apps', async () => {
|
|
32
|
+
const originalCwd = process.cwd();
|
|
33
|
+
try {
|
|
34
|
+
process.chdir(projectDir); // Change CWD to simulate running from root
|
|
35
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
36
|
+
const setupCmd = new SetupCommand(cli);
|
|
37
|
+
|
|
38
|
+
// Execute setup command
|
|
39
|
+
await setupCmd.run();
|
|
40
|
+
|
|
41
|
+
// Verify symlinks
|
|
42
|
+
const backendSrc = path.join(projectDir, 'apps/backend/src');
|
|
43
|
+
expect(await fs.pathExists(backendSrc)).toBe(true);
|
|
44
|
+
const stat = await fs.lstat(backendSrc);
|
|
45
|
+
expect(stat.isSymbolicLink()).toBe(true);
|
|
46
|
+
|
|
47
|
+
const frontendSrc = path.join(projectDir, 'apps/frontend/src');
|
|
48
|
+
expect(await fs.pathExists(frontendSrc)).toBe(true);
|
|
49
|
+
} finally {
|
|
50
|
+
process.chdir(originalCwd);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|