@shellui/cli 0.0.1 ā 0.0.5
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/README.md +53 -46
- package/bin/shellui.js +0 -1
- package/package.json +24 -17
- package/src/cli.js +5 -118
- package/src/commands/README.md +40 -0
- package/src/commands/build.js +200 -0
- package/src/commands/index.js +9 -0
- package/src/commands/start.js +180 -0
- package/src/utils/__tests__/config-loaders.test.js +211 -0
- package/src/utils/__tests__/config.test.js +146 -0
- package/src/utils/config-loaders.js +142 -0
- package/src/utils/config.js +69 -0
- package/src/utils/index.js +15 -0
- package/src/utils/package-path.js +54 -0
- package/src/utils/service-worker-plugin.js +151 -0
- package/src/utils/vite.js +82 -0
- package/src/app.jsx +0 -31
- package/src/index.html +0 -12
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createServer } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import {
|
|
7
|
+
loadConfig,
|
|
8
|
+
getCoreSrcPath,
|
|
9
|
+
createResolveAlias,
|
|
10
|
+
createPostCSSConfig,
|
|
11
|
+
createViteDefine,
|
|
12
|
+
resolvePackagePath,
|
|
13
|
+
} from '../utils/index.js';
|
|
14
|
+
import { serviceWorkerDevPlugin } from '../utils/service-worker-plugin.js';
|
|
15
|
+
|
|
16
|
+
let currentServer = null;
|
|
17
|
+
let configWatcher = null;
|
|
18
|
+
let restartTimeout = null;
|
|
19
|
+
let isFirstStart = true;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start the Vite server with current configuration
|
|
23
|
+
* @param {string} root - Root directory
|
|
24
|
+
* @param {string} cwd - Current working directory
|
|
25
|
+
* @param {boolean} shouldOpen - Whether to open the browser (only on first start)
|
|
26
|
+
* @returns {Promise<import('vite').ViteDevServer>}
|
|
27
|
+
*/
|
|
28
|
+
async function startServer(root, cwd, shouldOpen = false) {
|
|
29
|
+
// Load configuration
|
|
30
|
+
const config = await loadConfig(root);
|
|
31
|
+
|
|
32
|
+
// Get core package paths
|
|
33
|
+
const corePackagePath = resolvePackagePath('@shellui/core');
|
|
34
|
+
const coreSrcPath = getCoreSrcPath();
|
|
35
|
+
|
|
36
|
+
// Check if static folder exists in project root
|
|
37
|
+
const staticPath = path.resolve(cwd, root, 'static');
|
|
38
|
+
const publicDir = fs.existsSync(staticPath) ? staticPath : false;
|
|
39
|
+
|
|
40
|
+
const server = await createServer({
|
|
41
|
+
root: coreSrcPath,
|
|
42
|
+
plugins: [react(), serviceWorkerDevPlugin(corePackagePath, coreSrcPath)],
|
|
43
|
+
define: createViteDefine(config),
|
|
44
|
+
resolve: {
|
|
45
|
+
alias: createResolveAlias(),
|
|
46
|
+
},
|
|
47
|
+
css: {
|
|
48
|
+
postcss: createPostCSSConfig(),
|
|
49
|
+
},
|
|
50
|
+
publicDir: publicDir || false,
|
|
51
|
+
// Disable source maps in dev mode to avoid errors from missing source map files
|
|
52
|
+
// Source maps are enabled in build mode for production debugging
|
|
53
|
+
esbuild: {
|
|
54
|
+
sourcemap: false,
|
|
55
|
+
},
|
|
56
|
+
server: {
|
|
57
|
+
port: config.port || 3000,
|
|
58
|
+
open: shouldOpen,
|
|
59
|
+
fs: {
|
|
60
|
+
// Allow serving files from core package, SDK package, and user's project
|
|
61
|
+
allow: [corePackagePath, cwd],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await server.listen();
|
|
67
|
+
return server;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Restart the server when config changes
|
|
72
|
+
* @param {string} root - Root directory
|
|
73
|
+
* @param {string} cwd - Current working directory
|
|
74
|
+
*/
|
|
75
|
+
async function restartServer(root, cwd) {
|
|
76
|
+
if (restartTimeout) {
|
|
77
|
+
clearTimeout(restartTimeout);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
restartTimeout = setTimeout(async () => {
|
|
81
|
+
try {
|
|
82
|
+
console.log(pc.yellow('\nš Config file changed, restarting server...\n'));
|
|
83
|
+
|
|
84
|
+
// Close existing server
|
|
85
|
+
if (currentServer) {
|
|
86
|
+
await currentServer.close();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Start new server with updated config (don't open browser on restart)
|
|
90
|
+
currentServer = await startServer(root, cwd, false);
|
|
91
|
+
currentServer.printUrls();
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(pc.red(`Error restarting server: ${e.message}`));
|
|
94
|
+
}
|
|
95
|
+
}, 300); // Debounce: wait 300ms before restarting
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Watch config file for changes
|
|
100
|
+
* @param {string} root - Root directory
|
|
101
|
+
* @param {string} cwd - Current working directory
|
|
102
|
+
*/
|
|
103
|
+
function watchConfig(root, cwd) {
|
|
104
|
+
const configDir = path.resolve(cwd, root);
|
|
105
|
+
const tsConfigPath = path.join(configDir, 'shellui.config.ts');
|
|
106
|
+
const jsonConfigPath = path.join(configDir, 'shellui.config.json');
|
|
107
|
+
|
|
108
|
+
// Determine which config file exists (prefer TypeScript)
|
|
109
|
+
let configPath = null;
|
|
110
|
+
if (fs.existsSync(tsConfigPath)) {
|
|
111
|
+
configPath = tsConfigPath;
|
|
112
|
+
} else if (fs.existsSync(jsonConfigPath)) {
|
|
113
|
+
configPath = jsonConfigPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Only watch if config file exists
|
|
117
|
+
if (!configPath) {
|
|
118
|
+
console.log(
|
|
119
|
+
pc.yellow(`No shellui.config.ts or shellui.config.json found, config watching disabled.`),
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Close existing watcher if any
|
|
125
|
+
if (configWatcher) {
|
|
126
|
+
configWatcher.close();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
configWatcher = fs.watch(configPath, { persistent: true }, async (eventType) => {
|
|
130
|
+
if (eventType === 'change') {
|
|
131
|
+
await restartServer(root, cwd);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log(pc.green(`š Watching config file: ${configPath}`));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Start command - Starts the ShellUI development server
|
|
140
|
+
* @param {string} root - Root directory (default: '.')
|
|
141
|
+
*/
|
|
142
|
+
export async function startCommand(root = '.') {
|
|
143
|
+
const cwd = process.cwd();
|
|
144
|
+
|
|
145
|
+
console.log(pc.blue(`Starting ShellUI...`));
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Start initial server (open browser only on first start)
|
|
149
|
+
currentServer = await startServer(root, cwd, isFirstStart);
|
|
150
|
+
isFirstStart = false;
|
|
151
|
+
currentServer.printUrls();
|
|
152
|
+
|
|
153
|
+
// Watch config file for changes
|
|
154
|
+
watchConfig(root, cwd);
|
|
155
|
+
|
|
156
|
+
// Handle graceful shutdown
|
|
157
|
+
process.on('SIGTERM', async () => {
|
|
158
|
+
if (configWatcher) {
|
|
159
|
+
configWatcher.close();
|
|
160
|
+
}
|
|
161
|
+
if (currentServer) {
|
|
162
|
+
await currentServer.close();
|
|
163
|
+
}
|
|
164
|
+
process.exit(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
process.on('SIGINT', async () => {
|
|
168
|
+
if (configWatcher) {
|
|
169
|
+
configWatcher.close();
|
|
170
|
+
}
|
|
171
|
+
if (currentServer) {
|
|
172
|
+
await currentServer.close();
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
});
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error(pc.red(`Error starting server: ${e.message}`));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach, expect } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
import { loadJsonConfig, loadTypeScriptConfig } from '../config-loaders.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
// Create a temporary test directory
|
|
12
|
+
const testDir = path.join(__dirname, 'test-fixtures-loaders');
|
|
13
|
+
const originalConsoleLog = console.log;
|
|
14
|
+
const originalConsoleError = console.error;
|
|
15
|
+
|
|
16
|
+
describe('Config Loaders', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Create test directory
|
|
19
|
+
if (!fs.existsSync(testDir)) {
|
|
20
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
// Suppress console output during tests
|
|
23
|
+
console.log = () => {};
|
|
24
|
+
console.error = () => {};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
// Restore console first
|
|
29
|
+
console.log = originalConsoleLog;
|
|
30
|
+
console.error = originalConsoleError;
|
|
31
|
+
|
|
32
|
+
// Clean up test directory
|
|
33
|
+
if (fs.existsSync(testDir)) {
|
|
34
|
+
const files = fs.readdirSync(testDir);
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const filePath = path.join(testDir, file);
|
|
37
|
+
try {
|
|
38
|
+
const stat = fs.statSync(filePath);
|
|
39
|
+
if (stat.isDirectory()) {
|
|
40
|
+
fs.rmSync(filePath, { recursive: true, force: true });
|
|
41
|
+
} else {
|
|
42
|
+
fs.unlinkSync(filePath);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// Ignore cleanup errors
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
fs.rmdirSync(testDir);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// Ignore cleanup errors
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('loadJsonConfig', () => {
|
|
57
|
+
test('should load valid JSON config file', () => {
|
|
58
|
+
const configPath = path.join(testDir, 'test-config.json');
|
|
59
|
+
const expectedConfig = {
|
|
60
|
+
name: 'test-app',
|
|
61
|
+
version: '1.0.0',
|
|
62
|
+
routes: ['/home', '/about'],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(configPath, JSON.stringify(expectedConfig, null, 2));
|
|
66
|
+
|
|
67
|
+
const config = loadJsonConfig(configPath);
|
|
68
|
+
|
|
69
|
+
expect(config).toStrictEqual(expectedConfig);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should throw error for invalid JSON', () => {
|
|
73
|
+
const configPath = path.join(testDir, 'invalid-config.json');
|
|
74
|
+
fs.writeFileSync(configPath, '{ invalid json }');
|
|
75
|
+
|
|
76
|
+
expect(() => {
|
|
77
|
+
loadJsonConfig(configPath);
|
|
78
|
+
}).toThrow(/JSON/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should throw error for missing file', () => {
|
|
82
|
+
const configPath = path.join(testDir, 'non-existent.json');
|
|
83
|
+
|
|
84
|
+
expect(() => {
|
|
85
|
+
loadJsonConfig(configPath);
|
|
86
|
+
}).toThrow(/ENOENT/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should handle empty JSON object', () => {
|
|
90
|
+
const configPath = path.join(testDir, 'empty-config.json');
|
|
91
|
+
fs.writeFileSync(configPath, '{}');
|
|
92
|
+
|
|
93
|
+
const config = loadJsonConfig(configPath);
|
|
94
|
+
|
|
95
|
+
expect(config).toStrictEqual({});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should handle complex nested JSON', () => {
|
|
99
|
+
const configPath = path.join(testDir, 'complex-config.json');
|
|
100
|
+
const expectedConfig = {
|
|
101
|
+
app: {
|
|
102
|
+
name: 'test',
|
|
103
|
+
settings: {
|
|
104
|
+
theme: 'dark',
|
|
105
|
+
language: 'en',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
features: ['feature1', 'feature2'],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
fs.writeFileSync(configPath, JSON.stringify(expectedConfig, null, 2));
|
|
112
|
+
|
|
113
|
+
const config = loadJsonConfig(configPath);
|
|
114
|
+
|
|
115
|
+
expect(config).toStrictEqual(expectedConfig);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('loadTypeScriptConfig', () => {
|
|
120
|
+
test('should load valid TypeScript config file with default export', async () => {
|
|
121
|
+
const configPath = path.join(testDir, 'test-config.ts');
|
|
122
|
+
const expectedConfig = {
|
|
123
|
+
name: 'test-app',
|
|
124
|
+
version: '2.0.0',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const tsConfigContent = `export default ${JSON.stringify(expectedConfig)};`;
|
|
128
|
+
fs.writeFileSync(configPath, tsConfigContent);
|
|
129
|
+
|
|
130
|
+
const config = await loadTypeScriptConfig(configPath, testDir);
|
|
131
|
+
|
|
132
|
+
expect(config).toStrictEqual(expectedConfig);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('should load TypeScript config with named export', async () => {
|
|
136
|
+
const configPath = path.join(testDir, 'test-config-named.ts');
|
|
137
|
+
const expectedConfig = {
|
|
138
|
+
name: 'test-app',
|
|
139
|
+
routes: ['/home'],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const tsConfigContent = `export const config = ${JSON.stringify(expectedConfig)};`;
|
|
143
|
+
fs.writeFileSync(configPath, tsConfigContent);
|
|
144
|
+
|
|
145
|
+
const loadedConfig = await loadTypeScriptConfig(configPath, testDir);
|
|
146
|
+
|
|
147
|
+
expect(loadedConfig).toStrictEqual(expectedConfig);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should handle TypeScript config with both default and named exports (prefers default)', async () => {
|
|
151
|
+
const configPath = path.join(testDir, 'test-config-both.ts');
|
|
152
|
+
const defaultConfig = { name: 'default' };
|
|
153
|
+
const namedConfig = { name: 'named' };
|
|
154
|
+
|
|
155
|
+
const tsConfigContent = `export default ${JSON.stringify(defaultConfig)};\nexport const config = ${JSON.stringify(namedConfig)};`;
|
|
156
|
+
fs.writeFileSync(configPath, tsConfigContent);
|
|
157
|
+
|
|
158
|
+
const loadedConfig = await loadTypeScriptConfig(configPath, testDir);
|
|
159
|
+
|
|
160
|
+
expect(loadedConfig).toStrictEqual(defaultConfig);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should throw error for invalid TypeScript file', async () => {
|
|
164
|
+
const configPath = path.join(testDir, 'invalid-config.ts');
|
|
165
|
+
fs.writeFileSync(configPath, 'export const invalid = syntax error;');
|
|
166
|
+
|
|
167
|
+
await expect(loadTypeScriptConfig(configPath, testDir)).rejects.toThrow(
|
|
168
|
+
/Failed to load TypeScript config/,
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('should throw error for missing TypeScript file', async () => {
|
|
173
|
+
const configPath = path.join(testDir, 'non-existent.ts');
|
|
174
|
+
|
|
175
|
+
await expect(loadTypeScriptConfig(configPath, testDir)).rejects.toThrow(
|
|
176
|
+
/Failed to load TypeScript config/,
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('should clean up temporary loader script', async () => {
|
|
181
|
+
const configPath = path.join(testDir, 'cleanup-test.ts');
|
|
182
|
+
const expectedConfig = { name: 'cleanup-test' };
|
|
183
|
+
|
|
184
|
+
const tsConfigContent = `export default ${JSON.stringify(expectedConfig)};`;
|
|
185
|
+
fs.writeFileSync(configPath, tsConfigContent);
|
|
186
|
+
|
|
187
|
+
await loadTypeScriptConfig(configPath, testDir);
|
|
188
|
+
|
|
189
|
+
const tempScriptPath = path.join(testDir, '.shellui-config-loader.mjs');
|
|
190
|
+
expect(fs.existsSync(tempScriptPath)).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('should handle complex TypeScript config with functions and types', async () => {
|
|
194
|
+
const configPath = path.join(testDir, 'complex-config.ts');
|
|
195
|
+
const expectedConfig = {
|
|
196
|
+
name: 'complex-app',
|
|
197
|
+
settings: {
|
|
198
|
+
port: 3000,
|
|
199
|
+
host: 'localhost',
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const tsConfigContent = `interface Config { name: string; settings: { port: number; host: string; }; }\nconst config: Config = ${JSON.stringify(expectedConfig)};\nexport default config;`;
|
|
204
|
+
fs.writeFileSync(configPath, tsConfigContent);
|
|
205
|
+
|
|
206
|
+
const loadedConfig = await loadTypeScriptConfig(configPath, testDir);
|
|
207
|
+
|
|
208
|
+
expect(loadedConfig).toStrictEqual(expectedConfig);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach, expect } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
// Create a temporary test directory
|
|
12
|
+
const testDir = path.join(__dirname, 'test-fixtures-config');
|
|
13
|
+
const originalConsoleLog = console.log;
|
|
14
|
+
const originalConsoleError = console.error;
|
|
15
|
+
const originalCwd = process.cwd();
|
|
16
|
+
|
|
17
|
+
describe('loadConfig', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Create test directory
|
|
20
|
+
if (!fs.existsSync(testDir)) {
|
|
21
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
// Change to test directory
|
|
24
|
+
process.chdir(testDir);
|
|
25
|
+
// Suppress console output during tests
|
|
26
|
+
console.log = () => {};
|
|
27
|
+
console.error = () => {};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
// Restore console first
|
|
32
|
+
console.log = originalConsoleLog;
|
|
33
|
+
console.error = originalConsoleError;
|
|
34
|
+
// Restore original working directory
|
|
35
|
+
process.chdir(originalCwd);
|
|
36
|
+
// Clean up test directory
|
|
37
|
+
if (fs.existsSync(testDir)) {
|
|
38
|
+
const files = fs.readdirSync(testDir);
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const filePath = path.join(testDir, file);
|
|
41
|
+
try {
|
|
42
|
+
const stat = fs.statSync(filePath);
|
|
43
|
+
if (stat.isDirectory()) {
|
|
44
|
+
fs.rmSync(filePath, { recursive: true, force: true });
|
|
45
|
+
} else {
|
|
46
|
+
fs.unlinkSync(filePath);
|
|
47
|
+
}
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Ignore cleanup errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
fs.rmdirSync(testDir);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Ignore cleanup errors
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should prefer TypeScript config over JSON config', async () => {
|
|
61
|
+
const tsConfig = { name: 'ts-config', version: '1.0.0' };
|
|
62
|
+
const jsonConfig = { name: 'json-config', version: '2.0.0' };
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync('shellui.config.ts', `export default ${JSON.stringify(tsConfig)};`);
|
|
65
|
+
fs.writeFileSync('shellui.config.json', JSON.stringify(jsonConfig, null, 2));
|
|
66
|
+
|
|
67
|
+
const config = await loadConfig('.');
|
|
68
|
+
|
|
69
|
+
expect(config).toStrictEqual(tsConfig);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should load JSON config when TypeScript config does not exist', async () => {
|
|
73
|
+
const jsonConfig = { name: 'json-only', version: '1.0.0' };
|
|
74
|
+
|
|
75
|
+
fs.writeFileSync('shellui.config.json', JSON.stringify(jsonConfig, null, 2));
|
|
76
|
+
|
|
77
|
+
const config = await loadConfig('.');
|
|
78
|
+
|
|
79
|
+
expect(config).toStrictEqual(jsonConfig);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should return empty object when no config files exist', async () => {
|
|
83
|
+
const config = await loadConfig('.');
|
|
84
|
+
|
|
85
|
+
expect(config).toStrictEqual({});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should handle custom root directory', async () => {
|
|
89
|
+
const subDir = path.join(testDir, 'subdir');
|
|
90
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
91
|
+
|
|
92
|
+
const jsonConfig = { name: 'subdir-config', version: '1.0.0' };
|
|
93
|
+
fs.writeFileSync(path.join(subDir, 'shellui.config.json'), JSON.stringify(jsonConfig, null, 2));
|
|
94
|
+
|
|
95
|
+
const config = await loadConfig(subDir);
|
|
96
|
+
|
|
97
|
+
expect(config).toStrictEqual(jsonConfig);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('should handle TypeScript config in custom root', async () => {
|
|
101
|
+
const subDir = path.join(testDir, 'subdir');
|
|
102
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
const tsConfig = { name: 'subdir-ts-config', version: '1.0.0' };
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
path.join(subDir, 'shellui.config.ts'),
|
|
107
|
+
`export default ${JSON.stringify(tsConfig)};`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const config = await loadConfig(subDir);
|
|
111
|
+
|
|
112
|
+
expect(config).toStrictEqual(tsConfig);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should handle errors gracefully and return empty object', async () => {
|
|
116
|
+
const invalidJsonPath = path.join(testDir, 'shellui.config.json');
|
|
117
|
+
fs.writeFileSync(invalidJsonPath, '{ invalid json }');
|
|
118
|
+
|
|
119
|
+
const config = await loadConfig('.');
|
|
120
|
+
|
|
121
|
+
// Should return empty object on error
|
|
122
|
+
expect(config).toStrictEqual({});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('should handle invalid TypeScript config gracefully', async () => {
|
|
126
|
+
const invalidTsPath = path.join(testDir, 'shellui.config.ts');
|
|
127
|
+
fs.writeFileSync(invalidTsPath, 'export const invalid = syntax error;');
|
|
128
|
+
|
|
129
|
+
const config = await loadConfig('.');
|
|
130
|
+
|
|
131
|
+
// Should return empty object on error
|
|
132
|
+
expect(config).toStrictEqual({});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('should work with relative path', async () => {
|
|
136
|
+
const jsonConfig = { name: 'relative-config', version: '1.0.0' };
|
|
137
|
+
fs.writeFileSync('shellui.config.json', JSON.stringify(jsonConfig, null, 2));
|
|
138
|
+
|
|
139
|
+
// Change to parent directory
|
|
140
|
+
process.chdir(path.dirname(testDir));
|
|
141
|
+
|
|
142
|
+
const config = await loadConfig(path.basename(testDir));
|
|
143
|
+
|
|
144
|
+
expect(config).toStrictEqual(jsonConfig);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Load TypeScript configuration file using tsx
|
|
9
|
+
* @param {string} configPath - Path to the TypeScript config file
|
|
10
|
+
* @param {string} configDir - Directory containing the config file
|
|
11
|
+
* @returns {Promise<Object>} Configuration object
|
|
12
|
+
*/
|
|
13
|
+
export async function loadTypeScriptConfig(configPath, configDir) {
|
|
14
|
+
// Load TypeScript config using tsx CLI via child process
|
|
15
|
+
// This avoids module cycle issues with dynamic registration
|
|
16
|
+
// Use file URL for the import to ensure proper resolution
|
|
17
|
+
const configFileUrl = pathToFileURL(configPath).href;
|
|
18
|
+
const loaderScript = `
|
|
19
|
+
import * as configModule from ${JSON.stringify(configFileUrl)};
|
|
20
|
+
// Extract the actual config, handling both default export and named exports
|
|
21
|
+
let result = configModule.default || configModule.config || configModule;
|
|
22
|
+
// If result itself has a default property, extract it
|
|
23
|
+
if (result && typeof result === 'object' && result.default && Object.keys(result).length === 1) {
|
|
24
|
+
result = result.default;
|
|
25
|
+
}
|
|
26
|
+
console.log(JSON.stringify(result));
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
// Write temporary script to load the config
|
|
30
|
+
const tempScriptPath = path.join(configDir, '.shellui-config-loader.mjs');
|
|
31
|
+
fs.writeFileSync(tempScriptPath, loaderScript);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Find tsx - try to resolve it from node_modules
|
|
35
|
+
const { createRequire } = await import('module');
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
|
|
38
|
+
let tsxPath;
|
|
39
|
+
try {
|
|
40
|
+
// Try to resolve tsx package
|
|
41
|
+
const tsxPackagePath = require.resolve('tsx/package.json');
|
|
42
|
+
const tsxPackageDir = path.dirname(tsxPackagePath);
|
|
43
|
+
tsxPath = path.join(tsxPackageDir, 'dist/cli.mjs');
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(tsxPath)) {
|
|
46
|
+
throw new Error('tsx CLI not found');
|
|
47
|
+
}
|
|
48
|
+
} catch (resolveError) {
|
|
49
|
+
// Fallback to npx if tsx not found
|
|
50
|
+
tsxPath = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Execute tsx to run the loader script
|
|
54
|
+
const result = await new Promise((resolve, reject) => {
|
|
55
|
+
const useNpx = !tsxPath;
|
|
56
|
+
const command = useNpx ? 'npx' : 'node';
|
|
57
|
+
const args = useNpx ? ['-y', 'tsx', tempScriptPath] : [tsxPath, tempScriptPath];
|
|
58
|
+
|
|
59
|
+
// Pass environment variables to child process, including build detection vars
|
|
60
|
+
const childEnv = {
|
|
61
|
+
...process.env,
|
|
62
|
+
// Ensure build detection variables are passed to the child process
|
|
63
|
+
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
64
|
+
SHELLUI_BUILD: process.env.SHELLUI_BUILD || 'false',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const child = spawn(command, args, {
|
|
68
|
+
cwd: configDir,
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
+
env: childEnv,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let stdout = '';
|
|
74
|
+
let stderr = '';
|
|
75
|
+
|
|
76
|
+
child.stdout.on('data', (data) => {
|
|
77
|
+
stdout += data.toString();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
child.stderr.on('data', (data) => {
|
|
81
|
+
stderr += data.toString();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
child.on('close', (code) => {
|
|
85
|
+
if (code !== 0) {
|
|
86
|
+
reject(
|
|
87
|
+
new Error(
|
|
88
|
+
`Failed to load TypeScript config: ${stderr || `Process exited with code ${code}`}`,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(stdout.trim());
|
|
94
|
+
// If the parsed result has a 'default' key, extract it
|
|
95
|
+
const config = parsed.default || parsed.config || parsed;
|
|
96
|
+
resolve(config);
|
|
97
|
+
} catch (parseError) {
|
|
98
|
+
reject(
|
|
99
|
+
new Error(
|
|
100
|
+
`Failed to parse config output: ${parseError.message}\nOutput: ${stdout}\nStderr: ${stderr}`,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
child.on('error', (error) => {
|
|
108
|
+
reject(new Error(`Failed to execute tsx: ${error.message}`));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Clean up temp script
|
|
113
|
+
if (fs.existsSync(tempScriptPath)) {
|
|
114
|
+
fs.unlinkSync(tempScriptPath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(pc.green(`Loaded TypeScript config from ${configPath}`));
|
|
118
|
+
return result;
|
|
119
|
+
} catch (execError) {
|
|
120
|
+
// Clean up temp script on error
|
|
121
|
+
if (fs.existsSync(tempScriptPath)) {
|
|
122
|
+
try {
|
|
123
|
+
fs.unlinkSync(tempScriptPath);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Ignore cleanup errors
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw execError;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load JSON configuration file
|
|
134
|
+
* @param {string} configPath - Path to the JSON config file
|
|
135
|
+
* @returns {Object} Configuration object
|
|
136
|
+
*/
|
|
137
|
+
export function loadJsonConfig(configPath) {
|
|
138
|
+
const configFile = fs.readFileSync(configPath, 'utf-8');
|
|
139
|
+
const config = JSON.parse(configFile);
|
|
140
|
+
console.log(pc.green(`Loaded JSON config from ${configPath}`));
|
|
141
|
+
return config;
|
|
142
|
+
}
|