@nexical/cli 0.11.7 → 0.11.9
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/{chunk-LZ3YQWAR.js → chunk-OUGA4CB4.js} +15 -11
- package/dist/chunk-OUGA4CB4.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.js +1 -1
- package/dist/src/commands/module/add.js +51 -20
- 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 +55 -46
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.js +38 -13
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.js +16 -4
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.js +19 -2
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/commands/setup.js +1 -1
- package/package.json +1 -1
- 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
- package/dist/chunk-LZ3YQWAR.js.map +0 -1
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { BaseCommand
|
|
1
|
+
import { BaseCommand } from '@nexical/cli-core';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
|
+
interface ModuleInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
description: string;
|
|
10
|
+
type: 'backend' | 'frontend' | 'legacy';
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export default class ModuleListCommand extends BaseCommand {
|
|
7
14
|
static usage = 'module list';
|
|
8
15
|
static description = 'List installed modules.';
|
|
@@ -10,71 +17,87 @@ export default class ModuleListCommand extends BaseCommand {
|
|
|
10
17
|
|
|
11
18
|
async run() {
|
|
12
19
|
const projectRoot = this.projectRoot as string;
|
|
13
|
-
const modulesDir = path.resolve(projectRoot, 'modules');
|
|
14
|
-
logger.debug(`Scanning for modules in: ${modulesDir}`);
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
// Define locations to scan
|
|
22
|
+
const builtInLocations = [
|
|
23
|
+
{ type: 'backend', path: path.join(projectRoot, 'apps/backend/modules') },
|
|
24
|
+
{ type: 'frontend', path: path.join(projectRoot, 'apps/frontend/modules') },
|
|
25
|
+
// Check legacy `modules` folder just in case?
|
|
26
|
+
{ type: 'legacy', path: path.join(projectRoot, 'modules') },
|
|
27
|
+
];
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
const modules = await fs.readdir(modulesDir);
|
|
23
|
-
const validModules: { name: string; version: string; description: string }[] = [];
|
|
24
|
-
|
|
25
|
-
for (const moduleName of modules) {
|
|
26
|
-
const modulePath = path.join(modulesDir, moduleName);
|
|
27
|
-
if ((await fs.stat(modulePath)).isDirectory()) {
|
|
28
|
-
let version = 'unknown';
|
|
29
|
-
let description = '';
|
|
30
|
-
|
|
31
|
-
const pkgJsonPath = path.join(modulePath, 'package.json');
|
|
32
|
-
const moduleYamlPath = path.join(modulePath, 'module.yaml');
|
|
33
|
-
const moduleYmlPath = path.join(modulePath, 'module.yml');
|
|
34
|
-
|
|
35
|
-
let pkg: Record<string, unknown> = {};
|
|
36
|
-
let modConfig: Record<string, unknown> = {};
|
|
37
|
-
|
|
38
|
-
if (await fs.pathExists(pkgJsonPath)) {
|
|
39
|
-
try {
|
|
40
|
-
pkg = await fs.readJson(pkgJsonPath);
|
|
41
|
-
} catch {
|
|
42
|
-
/* ignore */
|
|
43
|
-
}
|
|
44
|
-
}
|
|
29
|
+
const allModules: ModuleInfo[] = [];
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? moduleYamlPath
|
|
50
|
-
: moduleYmlPath;
|
|
51
|
-
const content = await fs.readFile(configPath, 'utf8');
|
|
52
|
-
modConfig = YAML.parse(content) || {};
|
|
53
|
-
} catch {
|
|
54
|
-
/* ignore */
|
|
55
|
-
}
|
|
56
|
-
}
|
|
31
|
+
for (const loc of builtInLocations) {
|
|
32
|
+
if (await fs.pathExists(loc.path)) {
|
|
33
|
+
const modules = await fs.readdir(loc.path);
|
|
57
34
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
35
|
+
for (const moduleName of modules) {
|
|
36
|
+
const modulePath = path.join(loc.path, moduleName);
|
|
37
|
+
if ((await fs.stat(modulePath)).isDirectory()) {
|
|
38
|
+
const info = await this.getModuleInfo(
|
|
39
|
+
modulePath,
|
|
40
|
+
moduleName,
|
|
41
|
+
loc.type as 'backend' | 'frontend' | 'legacy',
|
|
42
|
+
);
|
|
43
|
+
allModules.push(info);
|
|
44
|
+
}
|
|
63
45
|
}
|
|
64
46
|
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (allModules.length === 0) {
|
|
50
|
+
this.info('No modules installed.');
|
|
51
|
+
} else {
|
|
52
|
+
// Sort by type then name
|
|
53
|
+
allModules.sort((a, b) => {
|
|
54
|
+
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
55
|
+
return a.name.localeCompare(b.name);
|
|
56
|
+
});
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.table(allModules);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async getModuleInfo(
|
|
63
|
+
modulePath: string,
|
|
64
|
+
dirName: string,
|
|
65
|
+
type: 'backend' | 'frontend' | 'legacy',
|
|
66
|
+
): Promise<ModuleInfo> {
|
|
67
|
+
let version = 'unknown';
|
|
68
|
+
let description = '';
|
|
69
|
+
|
|
70
|
+
const pkgJsonPath = path.join(modulePath, 'package.json');
|
|
71
|
+
const moduleYamlPath = path.join(modulePath, 'module.yaml');
|
|
72
|
+
const moduleYmlPath = path.join(modulePath, 'module.yml');
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
let pkg: Record<string, unknown> = {};
|
|
75
|
+
let modConfig: Record<string, unknown> = {};
|
|
76
|
+
|
|
77
|
+
if (await fs.pathExists(pkgJsonPath)) {
|
|
78
|
+
try {
|
|
79
|
+
pkg = (await fs.readJson(pkgJsonPath)) || {};
|
|
80
|
+
} catch {
|
|
81
|
+
/* ignore */
|
|
71
82
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if ((await fs.pathExists(moduleYamlPath)) || (await fs.pathExists(moduleYmlPath))) {
|
|
86
|
+
try {
|
|
87
|
+
const configPath = (await fs.pathExists(moduleYamlPath)) ? moduleYamlPath : moduleYmlPath;
|
|
88
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
89
|
+
modConfig = YAML.parse(content) || {};
|
|
90
|
+
} catch {
|
|
91
|
+
/* ignore */
|
|
77
92
|
}
|
|
78
93
|
}
|
|
94
|
+
|
|
95
|
+
version = (pkg.version as string) || (modConfig.version as string) || 'unknown';
|
|
96
|
+
description = (pkg.description as string) || (modConfig.description as string) || '';
|
|
97
|
+
|
|
98
|
+
// Use config name if available, else dirName
|
|
99
|
+
const name = (modConfig.name as string) || dirName;
|
|
100
|
+
|
|
101
|
+
return { name, version, description, type };
|
|
79
102
|
}
|
|
80
103
|
}
|
|
@@ -16,27 +16,44 @@ export default class ModuleRemoveCommand extends BaseCommand {
|
|
|
16
16
|
const projectRoot = this.projectRoot as string;
|
|
17
17
|
const { name } = options;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Check locations
|
|
20
|
+
const locations = [
|
|
21
|
+
{ type: 'backend', path: `apps/backend/modules/${name}` },
|
|
22
|
+
{ type: 'frontend', path: `apps/frontend/modules/${name}` },
|
|
23
|
+
{ type: 'legacy', path: `modules/${name}` },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
let targetLoc: { type: string; path: string } | null = null;
|
|
27
|
+
let fullPath = '';
|
|
28
|
+
|
|
29
|
+
for (const loc of locations) {
|
|
30
|
+
const absPath = path.resolve(projectRoot, loc.path);
|
|
31
|
+
if (await fs.pathExists(absPath)) {
|
|
32
|
+
targetLoc = loc;
|
|
33
|
+
fullPath = absPath;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
23
37
|
|
|
24
|
-
if (!
|
|
25
|
-
this.error(`Module ${name} not found
|
|
38
|
+
if (!targetLoc) {
|
|
39
|
+
this.error(`Module ${name} not found in any standard location.`);
|
|
26
40
|
return;
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
const relativePath = targetLoc.path;
|
|
44
|
+
|
|
45
|
+
logger.debug('Removing module at:', fullPath);
|
|
46
|
+
this.info(`Removing module ${name} (${targetLoc.type})...`);
|
|
30
47
|
|
|
31
48
|
try {
|
|
32
49
|
await runCommand(`git submodule deinit -f ${relativePath}`, projectRoot);
|
|
33
50
|
await runCommand(`git rm -f ${relativePath}`, projectRoot);
|
|
34
51
|
|
|
35
|
-
// Clean up .git/modules
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
// Clean up .git/modules if needed (git rm often handles this but sometimes leaves stale dirs in .git/modules)
|
|
53
|
+
// The path in .git/modules depends on how it was added.
|
|
54
|
+
// Usually .git/modules/apps/backend/modules/name
|
|
55
|
+
// We'll leave strict git cleanup to git, manually removing can be risky if path structure varies.
|
|
56
|
+
// But we can check for the directory itself just in case.
|
|
40
57
|
|
|
41
58
|
this.info('Syncing workspace dependencies...');
|
|
42
59
|
await runCommand('npm install', projectRoot);
|
|
@@ -63,8 +80,27 @@ export default class ModuleRemoveCommand extends BaseCommand {
|
|
|
63
80
|
const content = await fs.readFile(configPath, 'utf8');
|
|
64
81
|
const config = YAML.parse(content) || {};
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
let changed = false;
|
|
84
|
+
|
|
85
|
+
if (config.modules) {
|
|
86
|
+
// Check if object
|
|
87
|
+
if (!Array.isArray(config.modules)) {
|
|
88
|
+
for (const key of Object.keys(config.modules)) {
|
|
89
|
+
if (Array.isArray(config.modules[key]) && config.modules[key].includes(moduleName)) {
|
|
90
|
+
config.modules[key] = config.modules[key].filter((m: string) => m !== moduleName);
|
|
91
|
+
changed = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// Legacy array
|
|
96
|
+
if (config.modules.includes(moduleName)) {
|
|
97
|
+
config.modules = config.modules.filter((m: string) => m !== moduleName);
|
|
98
|
+
changed = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (changed) {
|
|
68
104
|
await fs.writeFile(configPath, YAML.stringify(config));
|
|
69
105
|
logger.debug(`Removed ${moduleName} from nexical.yaml modules list.`);
|
|
70
106
|
}
|
|
@@ -20,17 +20,31 @@ export default class ModuleUpdateCommand extends BaseCommand {
|
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
22
|
if (name) {
|
|
23
|
-
|
|
24
|
-
const
|
|
23
|
+
// Check locations
|
|
24
|
+
const locations = [
|
|
25
|
+
{ type: 'backend', path: `apps/backend/modules/${name}` },
|
|
26
|
+
{ type: 'frontend', path: `apps/frontend/modules/${name}` },
|
|
27
|
+
{ type: 'legacy', path: `modules/${name}` },
|
|
28
|
+
];
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
let targetLoc: { type: string; path: string } | null = null;
|
|
31
|
+
|
|
32
|
+
for (const loc of locations) {
|
|
33
|
+
const absPath = path.resolve(projectRoot, loc.path);
|
|
34
|
+
if (await fs.pathExists(absPath)) {
|
|
35
|
+
targetLoc = loc;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!targetLoc) {
|
|
27
41
|
this.error(`Module ${name} not found.`);
|
|
28
42
|
return;
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
const relativePath = targetLoc.path;
|
|
46
|
+
|
|
31
47
|
// Update specific module
|
|
32
|
-
// We enter the directory and pull? Or generic submodule update?
|
|
33
|
-
// Generic submodule update --remote src/modules/name
|
|
34
48
|
await runCommand(`git submodule update --remote --merge ${relativePath}`, projectRoot);
|
|
35
49
|
} else {
|
|
36
50
|
// Update all
|
package/src/commands/run.ts
CHANGED
|
@@ -38,9 +38,29 @@ export default class RunCommand extends BaseCommand {
|
|
|
38
38
|
// Handle module:script syntax
|
|
39
39
|
if (script.includes(':')) {
|
|
40
40
|
const [moduleName, name] = script.split(':');
|
|
41
|
-
execPath = path.resolve(projectRoot, 'modules', moduleName);
|
|
42
41
|
scriptName = name;
|
|
43
42
|
|
|
43
|
+
const locations = [
|
|
44
|
+
{ type: 'backend', path: `apps/backend/modules/${moduleName}` },
|
|
45
|
+
{ type: 'frontend', path: `apps/frontend/modules/${moduleName}` },
|
|
46
|
+
{ type: 'legacy', path: `modules/${moduleName}` },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
let found = false;
|
|
50
|
+
for (const loc of locations) {
|
|
51
|
+
const absPath = path.resolve(projectRoot, loc.path);
|
|
52
|
+
if (await fs.pathExists(absPath)) {
|
|
53
|
+
execPath = absPath;
|
|
54
|
+
found = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!found) {
|
|
60
|
+
this.error(`Module ${moduleName} not found.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
logger.debug(`Resolving module script: ${moduleName}:${scriptName} at ${execPath}`);
|
|
45
65
|
} else {
|
|
46
66
|
logger.debug(`Resolving core script: ${scriptName} at ${execPath}`);
|
|
@@ -122,7 +122,7 @@ if (args[0] === 'build') {
|
|
|
122
122
|
'module',
|
|
123
123
|
'add',
|
|
124
124
|
moduleDir,
|
|
125
|
-
|
|
125
|
+
// Name is inferred from module.yaml
|
|
126
126
|
],
|
|
127
127
|
projectDir,
|
|
128
128
|
{ env },
|
|
@@ -132,7 +132,8 @@ if (args[0] === 'build') {
|
|
|
132
132
|
console.error('Module Add Failed:', modResult.stderr || modResult.stdout);
|
|
133
133
|
}
|
|
134
134
|
expect(modResult.exitCode).toBe(0);
|
|
135
|
-
|
|
135
|
+
// Defaults to backend module
|
|
136
|
+
expect(fs.existsSync(path.join(projectDir, 'apps/backend/modules/my-test-module'))).toBe(true);
|
|
136
137
|
|
|
137
138
|
// --- STEP 3: BUILD ---
|
|
138
139
|
// Run: nexical run build
|
|
@@ -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
|
}
|