@nexical/cli 0.11.0 → 0.11.1
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/.github/workflows/deploy.yml +1 -1
- package/.husky/pre-commit +1 -0
- package/.prettierignore +8 -0
- package/.prettierrc +7 -0
- package/GEMINI.md +36 -30
- package/README.md +85 -56
- package/dist/chunk-AC4B3HPJ.js +93 -0
- package/dist/chunk-AC4B3HPJ.js.map +1 -0
- package/dist/{chunk-JYASTIIW.js → chunk-PJIOCW2A.js} +1 -1
- package/dist/chunk-PJIOCW2A.js.map +1 -0
- package/dist/{chunk-WKERTCM6.js → chunk-Q7YLW5HJ.js} +5 -2
- package/dist/chunk-Q7YLW5HJ.js.map +1 -0
- package/dist/index.js +41 -12
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.d.ts +4 -1
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/module/add.d.ts +3 -1
- package/dist/src/commands/module/add.js +24 -13
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/module/list.js +9 -5
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.d.ts +3 -1
- package/dist/src/commands/module/remove.js +13 -7
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.d.ts +3 -1
- package/dist/src/commands/module/update.js +7 -5
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.d.ts +4 -1
- package/dist/src/commands/run.js +10 -2
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/commands/setup.js +17 -4
- package/dist/src/commands/setup.js.map +1 -1
- package/dist/src/utils/discovery.js +1 -1
- package/dist/src/utils/git.js +1 -1
- package/dist/src/utils/url-resolver.js +1 -1
- package/eslint.config.mjs +67 -0
- package/index.ts +34 -20
- package/package.json +56 -32
- package/src/commands/init.ts +79 -76
- package/src/commands/module/add.ts +158 -148
- package/src/commands/module/list.ts +61 -50
- package/src/commands/module/remove.ts +59 -54
- package/src/commands/module/update.ts +44 -42
- package/src/commands/run.ts +89 -81
- package/src/commands/setup.ts +78 -60
- package/src/utils/discovery.ts +98 -113
- package/src/utils/git.ts +35 -28
- package/src/utils/url-resolver.ts +50 -45
- package/test/e2e/lifecycle.e2e.test.ts +139 -131
- package/test/integration/commands/init.integration.test.ts +64 -64
- package/test/integration/commands/module.integration.test.ts +122 -122
- package/test/integration/commands/run.integration.test.ts +70 -63
- package/test/integration/utils/command-loading.integration.test.ts +40 -53
- package/test/unit/commands/init.test.ts +163 -128
- package/test/unit/commands/module/add.test.ts +312 -245
- package/test/unit/commands/module/list.test.ts +108 -91
- package/test/unit/commands/module/remove.test.ts +74 -67
- package/test/unit/commands/module/update.test.ts +74 -70
- package/test/unit/commands/run.test.ts +253 -201
- package/test/unit/commands/setup.test.ts +146 -128
- package/test/unit/utils/command-discovery.test.ts +138 -125
- package/test/unit/utils/git.test.ts +135 -117
- package/test/unit/utils/integration-helpers.test.ts +59 -49
- package/test/unit/utils/url-resolver.test.ts +46 -34
- package/test/utils/integration-helpers.ts +36 -29
- package/tsconfig.json +15 -25
- package/tsup.config.ts +14 -14
- package/vitest.config.ts +10 -10
- package/vitest.e2e.config.ts +6 -6
- package/vitest.integration.config.ts +17 -17
- package/dist/chunk-JYASTIIW.js.map +0 -1
- package/dist/chunk-OKXOCNXP.js +0 -105
- package/dist/chunk-OKXOCNXP.js.map +0 -1
- package/dist/chunk-WKERTCM6.js.map +0 -1
|
@@ -5,28 +5,33 @@ import fs from 'fs-extra';
|
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
|
|
7
7
|
describe('CLI Lifecycle E2E', () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
8
|
+
let testRoot: string;
|
|
9
|
+
let starterDir: string;
|
|
10
|
+
let moduleDir: string;
|
|
11
|
+
let mockAstroDir: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
testRoot = await createTempDir('e2e-life-');
|
|
15
|
+
|
|
16
|
+
// 1. Setup Mock "Astro" package
|
|
17
|
+
// We create a local package that impersonates 'astro'.
|
|
18
|
+
// This avoids npm install downloading the internet.
|
|
19
|
+
mockAstroDir = path.join(testRoot, 'mock-astro');
|
|
20
|
+
await fs.ensureDir(mockAstroDir);
|
|
21
|
+
await fs.outputFile(
|
|
22
|
+
path.join(mockAstroDir, 'package.json'),
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
name: 'astro',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
bin: {
|
|
27
|
+
astro: './bin.js',
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
// The mock binary acts as "npx astro"
|
|
32
|
+
await fs.outputFile(
|
|
33
|
+
path.join(mockAstroDir, 'bin.js'),
|
|
34
|
+
`#!/usr/bin/env node
|
|
30
35
|
const fs = require('fs');
|
|
31
36
|
const path = require('path');
|
|
32
37
|
const args = process.argv.slice(2);
|
|
@@ -37,117 +42,120 @@ if (args[0] === 'build') {
|
|
|
37
42
|
if (!fs.existsSync(dist)) fs.mkdirSync(dist, { recursive: true });
|
|
38
43
|
fs.writeFileSync(path.join(dist, 'index.html'), '<html></html>');
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// 3. Setup Mock Module Repo
|
|
75
|
-
moduleDir = path.join(testRoot, 'module-repo');
|
|
76
|
-
await createMockRepo(moduleDir, {
|
|
77
|
-
'package.json': JSON.stringify({ name: 'my-module', version: '0.1.0' }),
|
|
78
|
-
'module.yaml': 'name: my-test-module\nversion: 0.1.0', // Name matches E2E expectation
|
|
79
|
-
'index.ts': 'console.log("module")'
|
|
80
|
-
});
|
|
45
|
+
`,
|
|
46
|
+
);
|
|
47
|
+
await fs.chmod(path.join(mockAstroDir, 'bin.js'), '755');
|
|
48
|
+
|
|
49
|
+
// 2. Setup Mock Starter Repo
|
|
50
|
+
starterDir = path.join(testRoot, 'starter-repo');
|
|
51
|
+
// We need a package.json that points 'astro' to our mock
|
|
52
|
+
await createMockRepo(starterDir, {
|
|
53
|
+
'package.json': JSON.stringify({
|
|
54
|
+
name: 'e2e-project',
|
|
55
|
+
version: '0.0.0',
|
|
56
|
+
dependencies: {
|
|
57
|
+
// Use file: protocol to point to local mock
|
|
58
|
+
astro: `file:${mockAstroDir}`,
|
|
59
|
+
},
|
|
60
|
+
scripts: {
|
|
61
|
+
build: 'astro build',
|
|
62
|
+
dev: 'astro dev',
|
|
63
|
+
preview: 'astro preview',
|
|
64
|
+
setup: 'echo setup',
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
'README.md': '# E2E Starter',
|
|
68
|
+
'nexical.yml': 'name: e2e-test\nversion: 0.0.1', // ESSENTIAL for CLI to recognize project
|
|
69
|
+
'src/pages/index.astro': '--- ---',
|
|
70
|
+
'src/core/index.ts': '// core',
|
|
71
|
+
'src/core/package.json': JSON.stringify({
|
|
72
|
+
scripts: {
|
|
73
|
+
build: 'astro build',
|
|
74
|
+
dev: 'astro dev',
|
|
75
|
+
preview: 'astro preview',
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
81
78
|
});
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
// 3. Setup Mock Module Repo
|
|
81
|
+
moduleDir = path.join(testRoot, 'module-repo');
|
|
82
|
+
await createMockRepo(moduleDir, {
|
|
83
|
+
'package.json': JSON.stringify({ name: 'my-module', version: '0.1.0' }),
|
|
84
|
+
'module.yaml': 'name: my-test-module\nversion: 0.1.0', // Name matches E2E expectation
|
|
85
|
+
'index.ts': 'console.log("module")',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterAll(async () => {
|
|
90
|
+
if (testRoot) await fs.remove(testRoot);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should complete a full project lifecycle', async () => {
|
|
94
|
+
const projectDir = path.join(testRoot, 'my-project');
|
|
95
|
+
|
|
96
|
+
const env = {
|
|
97
|
+
// Essential for git inside init/module commands
|
|
98
|
+
GIT_AUTHOR_NAME: 'Test User',
|
|
99
|
+
GIT_AUTHOR_EMAIL: 'test@example.com',
|
|
100
|
+
GIT_COMMITTER_NAME: 'Test User',
|
|
101
|
+
GIT_COMMITTER_EMAIL: 'test@example.com',
|
|
102
|
+
GIT_ALLOW_PROTOCOL: 'file',
|
|
103
|
+
// DEBUG: 'true' - Removed to reduce noise
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// --- STEP 1: INIT ---
|
|
107
|
+
// Run: nexical init my-project --repo <starter>
|
|
108
|
+
const _initResult = await runCLI(['init', 'my-project', '--repo', starterDir], testRoot, {
|
|
109
|
+
env,
|
|
85
110
|
});
|
|
86
111
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
expect(buildResult.exitCode).toBe(0);
|
|
139
|
-
expect(buildResult.stdout).toContain('MOCK_ASTRO_EXECUTED build');
|
|
140
|
-
|
|
141
|
-
// --- STEP 4: PREVIEW ---
|
|
142
|
-
// Run: nexical run preview
|
|
143
|
-
const previewResult = await runCLI(['run', 'preview'], projectDir, { env });
|
|
144
|
-
|
|
145
|
-
expect(previewResult.exitCode).toBe(0);
|
|
146
|
-
expect(previewResult.stdout).toContain('MOCK_ASTRO_EXECUTED preview');
|
|
147
|
-
|
|
148
|
-
// --- STEP 5: CLEAN ---
|
|
149
|
-
// Clean is now handled by manual file operations or external scripts,
|
|
150
|
-
// it's no longer a top-level command.
|
|
151
|
-
|
|
152
|
-
}, 120000); // Long timeout for full chain
|
|
112
|
+
// 4. Check git initialization (preserved history)
|
|
113
|
+
const { stdout: log } = await execa('git', ['log', '--oneline'], { cwd: projectDir });
|
|
114
|
+
const lines = log.split('\n').filter(Boolean);
|
|
115
|
+
expect(lines.length).toBeGreaterThanOrEqual(2);
|
|
116
|
+
expect(lines[0]).toContain('Initial site commit');
|
|
117
|
+
|
|
118
|
+
// --- STEP 2: MODULE ADD ---
|
|
119
|
+
// Run: nexical module add <module>
|
|
120
|
+
const modResult = await runCLI(
|
|
121
|
+
[
|
|
122
|
+
'module',
|
|
123
|
+
'add',
|
|
124
|
+
moduleDir,
|
|
125
|
+
'my-test-module', // Explicit name
|
|
126
|
+
],
|
|
127
|
+
projectDir,
|
|
128
|
+
{ env },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (modResult.exitCode !== 0) {
|
|
132
|
+
console.error('Module Add Failed:', modResult.stderr || modResult.stdout);
|
|
133
|
+
}
|
|
134
|
+
expect(modResult.exitCode).toBe(0);
|
|
135
|
+
expect(fs.existsSync(path.join(projectDir, 'modules/my-test-module'))).toBe(true);
|
|
136
|
+
|
|
137
|
+
// --- STEP 3: BUILD ---
|
|
138
|
+
// Run: nexical run build
|
|
139
|
+
// Should trigger our mock astro binary
|
|
140
|
+
const buildResult = await runCLI(['run', 'build'], projectDir, { env });
|
|
141
|
+
|
|
142
|
+
if (buildResult.exitCode !== 0) {
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.log(buildResult.stderr || buildResult.stdout);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expect(buildResult.exitCode).toBe(0);
|
|
148
|
+
expect(buildResult.stdout).toContain('MOCK_ASTRO_EXECUTED build');
|
|
149
|
+
|
|
150
|
+
// --- STEP 4: PREVIEW ---
|
|
151
|
+
// Run: nexical run preview
|
|
152
|
+
const previewResult = await runCLI(['run', 'preview'], projectDir, { env });
|
|
153
|
+
|
|
154
|
+
expect(previewResult.exitCode).toBe(0);
|
|
155
|
+
expect(previewResult.stdout).toContain('MOCK_ASTRO_EXECUTED preview');
|
|
156
|
+
|
|
157
|
+
// --- STEP 5: CLEAN ---
|
|
158
|
+
// Clean is now handled by manual file operations or external scripts,
|
|
159
|
+
// it's no longer a top-level command.
|
|
160
|
+
}, 120000); // Long timeout for full chain
|
|
153
161
|
});
|
|
@@ -1,85 +1,85 @@
|
|
|
1
1
|
import { CLI } from '@nexical/cli-core';
|
|
2
|
-
import { describe, it, expect, beforeEach,
|
|
2
|
+
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
|
|
3
3
|
import InitCommand from '../../../src/commands/init.js';
|
|
4
|
-
import { createTempDir, createMockRepo
|
|
4
|
+
import { createTempDir, createMockRepo } from '../../utils/integration-helpers.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
7
|
import { execa } from 'execa';
|
|
8
8
|
|
|
9
9
|
describe('InitCommand Integration', () => {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
let starterRepoDir: string;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
tempDir = await createTempDir('init-integration-');
|
|
15
|
+
const starterDir = await createTempDir('starter-repo-');
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Set Git Identity for the test process so InitCommand's commit works in CI
|
|
33
|
-
process.env.GIT_AUTHOR_NAME = 'Test User';
|
|
34
|
-
process.env.GIT_AUTHOR_EMAIL = 'test@example.com';
|
|
35
|
-
process.env.GIT_COMMITTER_NAME = 'Test User';
|
|
36
|
-
process.env.GIT_COMMITTER_EMAIL = 'test@example.com';
|
|
37
|
-
// Allow file protocol for local cloning in CI
|
|
38
|
-
process.env.GIT_ALLOW_PROTOCOL = 'file';
|
|
17
|
+
// precise setup of a starter repo
|
|
18
|
+
starterRepoDir = await createMockRepo(starterDir, {
|
|
19
|
+
'package.json': JSON.stringify({
|
|
20
|
+
name: 'nexical-starter',
|
|
21
|
+
version: '0.0.0',
|
|
22
|
+
scripts: {
|
|
23
|
+
setup: 'echo setup',
|
|
24
|
+
},
|
|
25
|
+
dependencies: {
|
|
26
|
+
'is-odd': '3.0.1',
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
'README.md': '# Starter Template',
|
|
39
30
|
});
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
// Set Git Identity for the test process so InitCommand's commit works in CI
|
|
33
|
+
process.env.GIT_AUTHOR_NAME = 'Test User';
|
|
34
|
+
process.env.GIT_AUTHOR_EMAIL = 'test@example.com';
|
|
35
|
+
process.env.GIT_COMMITTER_NAME = 'Test User';
|
|
36
|
+
process.env.GIT_COMMITTER_EMAIL = 'test@example.com';
|
|
37
|
+
// Allow file protocol for local cloning in CI
|
|
38
|
+
process.env.GIT_ALLOW_PROTOCOL = 'file';
|
|
39
|
+
});
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
afterAll(async () => {
|
|
42
|
+
// await cleanupTestRoot(); // conflicting with parallel tests
|
|
43
|
+
if (tempDir) await fs.remove(tempDir);
|
|
44
|
+
});
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
it('should initialize a new project from a local git repo', async () => {
|
|
47
|
+
const targetProjectName = 'my-new-project';
|
|
48
|
+
const targetPath = path.join(tempDir, targetProjectName);
|
|
49
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
// For integration, we care about the FS side effects.
|
|
51
|
+
const command = new InitCommand(cli);
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
// Capture stdout/stderr? InitCommand uses consola.
|
|
54
|
+
// For integration, we care about the FS side effects.
|
|
55
|
+
|
|
56
|
+
await command.run({
|
|
57
|
+
directory: targetPath,
|
|
58
|
+
repo: starterRepoDir, // Passing local path as repo URL
|
|
59
|
+
});
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// 1. Check directory exists
|
|
62
|
+
expect(fs.existsSync(targetPath)).toBe(true);
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// 2. Check files cloned
|
|
65
|
+
expect(fs.existsSync(path.join(targetPath, 'package.json'))).toBe(true);
|
|
66
|
+
expect(fs.existsSync(path.join(targetPath, 'README.md'))).toBe(true);
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// 3. Check git initialization
|
|
69
|
+
expect(fs.existsSync(path.join(targetPath, '.git'))).toBe(true);
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// 4. Check git initialization (history should be preserved + one new commit)
|
|
72
|
+
const { stdout: log } = await execa('git', ['log', '--oneline'], { cwd: targetPath });
|
|
73
|
+
const lines = log.split('\n').filter(Boolean);
|
|
74
|
+
expect(lines.length).toBe(2); // Should have "Initial site commit" and "Initial commit"
|
|
75
|
+
expect(lines[0]).toContain('Initial site commit');
|
|
76
|
+
expect(lines[1]).toContain('Initial commit');
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
// 5. Check dependencies (optional, but command tries to install them)
|
|
79
|
+
// Since we are mocking the repo, it doesn't have a real lockfile or valid deps,
|
|
80
|
+
// so `npm install` might have failed or done nothing.
|
|
81
|
+
// However, `InitCommand` runs `npm install`. If that fails, the command throws/exits.
|
|
82
|
+
// We provided a minimal package.json so it should succeed.
|
|
83
|
+
expect(fs.existsSync(path.join(targetPath, 'node_modules'))).toBe(true);
|
|
84
|
+
}, 60000); // Increase timeout for real git/npm ops
|
|
85
85
|
});
|