@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,69 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { loadTypeScriptConfig, loadJsonConfig } from './config-loaders.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merge Sentry config from env into the loaded config. Only adds sentry when
|
|
8
|
+
* SENTRY_DSN is set and Sentry is not disabled via SENTRY_ENABLED=false|0.
|
|
9
|
+
* @param {Object} config - Loaded config object
|
|
10
|
+
* @returns {Object} Config with sentry merged from env when enabled
|
|
11
|
+
*/
|
|
12
|
+
function mergeSentryFromEnv(config) {
|
|
13
|
+
const dsn = process.env.SENTRY_DSN;
|
|
14
|
+
const enabled = process.env.SENTRY_ENABLED;
|
|
15
|
+
if (!dsn || enabled === 'false' || enabled === '0') {
|
|
16
|
+
return config;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
...config,
|
|
20
|
+
sentry: {
|
|
21
|
+
dsn,
|
|
22
|
+
...(process.env.SENTRY_ENVIRONMENT && { environment: process.env.SENTRY_ENVIRONMENT }),
|
|
23
|
+
...(process.env.SENTRY_RELEASE && { release: process.env.SENTRY_RELEASE }),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load configuration from shellui.config.ts or shellui.config.json file
|
|
30
|
+
* Prefers TypeScript config over JSON config. Sentry is merged from env on load
|
|
31
|
+
* (SENTRY_DSN, SENTRY_ENABLED, SENTRY_ENVIRONMENT, SENTRY_RELEASE).
|
|
32
|
+
* @param {string} root - Root directory to search for config (default: current working directory)
|
|
33
|
+
* @returns {Promise<Object>} Configuration object
|
|
34
|
+
*/
|
|
35
|
+
export async function loadConfig(root = '.') {
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
const configDir = path.resolve(cwd, root);
|
|
38
|
+
const tsConfigPath = path.join(configDir, 'shellui.config.ts');
|
|
39
|
+
const jsonConfigPath = path.join(configDir, 'shellui.config.json');
|
|
40
|
+
|
|
41
|
+
let config = {};
|
|
42
|
+
let activeConfigPath = null;
|
|
43
|
+
|
|
44
|
+
// Prefer TypeScript config over JSON
|
|
45
|
+
if (fs.existsSync(tsConfigPath)) {
|
|
46
|
+
activeConfigPath = tsConfigPath;
|
|
47
|
+
} else if (fs.existsSync(jsonConfigPath)) {
|
|
48
|
+
activeConfigPath = jsonConfigPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (activeConfigPath) {
|
|
52
|
+
try {
|
|
53
|
+
if (activeConfigPath === tsConfigPath) {
|
|
54
|
+
config = await loadTypeScriptConfig(activeConfigPath, configDir);
|
|
55
|
+
} else {
|
|
56
|
+
config = loadJsonConfig(activeConfigPath);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(pc.red(`Failed to load config from ${activeConfigPath}: ${e.message}`));
|
|
60
|
+
if (e.stack) {
|
|
61
|
+
console.error(pc.red(e.stack));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
console.log(pc.yellow(`No shellui.config.ts or shellui.config.json found, using defaults.`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return mergeSentryFromEnv(config);
|
|
69
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities index - Export all utility functions
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for all utility functions.
|
|
5
|
+
* Import from './utils/index.js' to get all utilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { resolvePackagePath, resolveSdkEntry } from './package-path.js';
|
|
9
|
+
export { loadConfig } from './config.js';
|
|
10
|
+
export {
|
|
11
|
+
getCoreSrcPath,
|
|
12
|
+
createResolveAlias,
|
|
13
|
+
createPostCSSConfig,
|
|
14
|
+
createViteDefine,
|
|
15
|
+
} from './vite.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the path to a package in the monorepo or node_modules
|
|
11
|
+
* @param {string} packageName - The name of the package
|
|
12
|
+
* @returns {string} The absolute path to the package
|
|
13
|
+
*/
|
|
14
|
+
export function resolvePackagePath(packageName) {
|
|
15
|
+
try {
|
|
16
|
+
// Try to resolve the package using require.resolve
|
|
17
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
18
|
+
return path.dirname(packageJsonPath);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// Fallback: assume workspace structure or pnpm symlinked node_modules
|
|
21
|
+
// Go up from cli/src/utils/package-path.js -> cli/src/utils -> cli/src -> cli -> packages -> packageName
|
|
22
|
+
const packagesDir = path.resolve(__dirname, '../../../');
|
|
23
|
+
const resolved = path.join(packagesDir, packageName.replace('@shellui/', ''));
|
|
24
|
+
// Resolve symlinks to get the canonical path — pnpm uses symlinks that
|
|
25
|
+
// point to different .pnpm/ directories; Vite resolves real paths so we
|
|
26
|
+
// need to be consistent to avoid mismatched root vs input paths.
|
|
27
|
+
try {
|
|
28
|
+
return fs.realpathSync(resolved);
|
|
29
|
+
} catch {
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the @shellui/sdk entry point for Vite alias.
|
|
37
|
+
* In workspace/monorepo mode, returns the source path (src/index.ts) so Vite
|
|
38
|
+
* can process TypeScript directly without a pre-build step.
|
|
39
|
+
* When installed from npm (no source), returns null so Vite uses normal
|
|
40
|
+
* node_modules resolution (dist/index.js via package exports).
|
|
41
|
+
* @returns {string|null} Absolute path to SDK source entry, or null to use normal resolution
|
|
42
|
+
*/
|
|
43
|
+
export function resolveSdkEntry() {
|
|
44
|
+
const sdkPackagePath = resolvePackagePath('@shellui/sdk');
|
|
45
|
+
const srcEntry = path.join(sdkPackagePath, 'src', 'index.ts');
|
|
46
|
+
|
|
47
|
+
// In workspace mode, source is available — alias to it for a no-build dev flow
|
|
48
|
+
if (fs.existsSync(srcEntry)) {
|
|
49
|
+
return srcEntry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// When installed from npm only dist/ exists — let Vite resolve through package exports
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { build } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { createResolveAlias } from './vite.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vite plugin to build and serve service worker in dev mode
|
|
9
|
+
*/
|
|
10
|
+
export function serviceWorkerDevPlugin(corePackagePath, coreSrcPath) {
|
|
11
|
+
let swCode = null;
|
|
12
|
+
let isBuilding = false;
|
|
13
|
+
let buildError = null;
|
|
14
|
+
|
|
15
|
+
const swPath = path.join(corePackagePath, 'src', 'service-worker', 'sw-dev.ts');
|
|
16
|
+
|
|
17
|
+
async function buildServiceWorker() {
|
|
18
|
+
if (isBuilding) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(swPath)) {
|
|
23
|
+
buildError = `Service worker source not found at: ${swPath}`;
|
|
24
|
+
console.warn(buildError);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
isBuilding = true;
|
|
29
|
+
buildError = null;
|
|
30
|
+
try {
|
|
31
|
+
// Use Vite's build API to properly resolve imports and bundle dependencies
|
|
32
|
+
// Use same root and resolve config as the main Vite server for consistent resolution
|
|
33
|
+
const result = await build({
|
|
34
|
+
root: coreSrcPath,
|
|
35
|
+
plugins: [react()],
|
|
36
|
+
resolve: {
|
|
37
|
+
alias: createResolveAlias(),
|
|
38
|
+
},
|
|
39
|
+
build: {
|
|
40
|
+
write: false,
|
|
41
|
+
sourcemap: false, // Disable source maps for service worker in dev mode
|
|
42
|
+
rollupOptions: {
|
|
43
|
+
input: swPath,
|
|
44
|
+
output: {
|
|
45
|
+
format: 'es',
|
|
46
|
+
entryFileNames: 'sw.js',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Extract the built code from the output
|
|
53
|
+
// Vite build with write: false returns a RollupOutput object with output array
|
|
54
|
+
let outputChunk = null;
|
|
55
|
+
|
|
56
|
+
if (result && Array.isArray(result) && result.length > 0) {
|
|
57
|
+
// Handle array format (multiple outputs)
|
|
58
|
+
const buildOutput = result[0];
|
|
59
|
+
if (buildOutput && buildOutput.output && Array.isArray(buildOutput.output)) {
|
|
60
|
+
outputChunk = buildOutput.output.find((o) => o.type === 'chunk');
|
|
61
|
+
}
|
|
62
|
+
} else if (result && result.output && Array.isArray(result.output)) {
|
|
63
|
+
// Handle single output format
|
|
64
|
+
outputChunk = result.output.find((o) => o.type === 'chunk');
|
|
65
|
+
} else if (result && result.code) {
|
|
66
|
+
// Handle direct code format (unlikely but possible)
|
|
67
|
+
outputChunk = { code: result.code };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (outputChunk && outputChunk.code) {
|
|
71
|
+
// Strip any source map references from the code
|
|
72
|
+
// This prevents browser devtools from trying to load non-existent source maps
|
|
73
|
+
swCode = outputChunk.code.replace(/\/\/# sourceMappingURL=.*$/gm, '');
|
|
74
|
+
console.log('✓ Dev service worker built successfully');
|
|
75
|
+
} else {
|
|
76
|
+
buildError = `Service worker build completed but no output chunk found. Result type: ${typeof result}, isArray: ${Array.isArray(result)}`;
|
|
77
|
+
console.warn(buildError);
|
|
78
|
+
console.warn('Build result structure:', JSON.stringify(Object.keys(result || {}), null, 2));
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
buildError = error.message;
|
|
82
|
+
console.error('Failed to build dev service worker:', error.message);
|
|
83
|
+
if (error.stack) {
|
|
84
|
+
console.error(error.stack);
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
isBuilding = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
name: 'shellui-service-worker-dev',
|
|
93
|
+
async buildStart() {
|
|
94
|
+
// Try to build immediately when plugin loads
|
|
95
|
+
await buildServiceWorker();
|
|
96
|
+
},
|
|
97
|
+
configureServer(server) {
|
|
98
|
+
// Also build when server is ready (in case buildStart was too early)
|
|
99
|
+
server.httpServer?.once('listening', async () => {
|
|
100
|
+
if (!swCode && !buildError) {
|
|
101
|
+
await buildServiceWorker();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Handle source map requests first - return empty source map instead of 404
|
|
106
|
+
// This prevents browser devtools from showing errors when source maps don't exist
|
|
107
|
+
// (e.g., from React DevTools or other browser extensions)
|
|
108
|
+
server.middlewares.use((req, res, next) => {
|
|
109
|
+
if (req.url && req.url.endsWith('.map')) {
|
|
110
|
+
// Return a valid but empty source map to satisfy devtools
|
|
111
|
+
// This prevents errors while still disabling source maps in dev mode
|
|
112
|
+
res.statusCode = 200;
|
|
113
|
+
res.setHeader('Content-Type', 'application/json');
|
|
114
|
+
res.end(
|
|
115
|
+
JSON.stringify({
|
|
116
|
+
version: 3,
|
|
117
|
+
sources: [],
|
|
118
|
+
names: [],
|
|
119
|
+
mappings: '',
|
|
120
|
+
file: req.url.replace('.map', ''),
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
next();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Serve the service worker at /sw.js
|
|
129
|
+
server.middlewares.use(async (req, res, next) => {
|
|
130
|
+
if (req.url === '/sw.js' || req.url.startsWith('/sw.js?')) {
|
|
131
|
+
// Ensure it's built (try one more time if not ready)
|
|
132
|
+
if (!swCode && !buildError) {
|
|
133
|
+
await buildServiceWorker();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (swCode) {
|
|
137
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
138
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
139
|
+
res.end(swCode);
|
|
140
|
+
} else {
|
|
141
|
+
res.statusCode = 404;
|
|
142
|
+
const errorMsg = buildError || 'Service worker not available';
|
|
143
|
+
res.end(`// ${errorMsg}\n// Service worker build failed or not ready`);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
next();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import tailwindcssPlugin from '@tailwindcss/postcss';
|
|
3
|
+
import autoprefixerPlugin from 'autoprefixer';
|
|
4
|
+
import { resolvePackagePath, resolveSdkEntry } from './index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the path to the core package source directory
|
|
8
|
+
* @returns {string} Absolute path to core package src directory
|
|
9
|
+
*/
|
|
10
|
+
export function getCoreSrcPath() {
|
|
11
|
+
const corePackagePath = resolvePackagePath('@shellui/core');
|
|
12
|
+
return path.join(corePackagePath, 'src');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create Vite resolve.alias configuration.
|
|
17
|
+
* Always sets '@' to core/src. Sets '@shellui/sdk' to source entry when in
|
|
18
|
+
* workspace mode; omits the alias when installed from npm so Vite resolves
|
|
19
|
+
* through the package's exports field (dist/index.js).
|
|
20
|
+
* @returns {Object} Vite resolve.alias object
|
|
21
|
+
*/
|
|
22
|
+
export function createResolveAlias() {
|
|
23
|
+
const corePackagePath = resolvePackagePath('@shellui/core');
|
|
24
|
+
const sdkEntry = resolveSdkEntry();
|
|
25
|
+
|
|
26
|
+
const alias = {
|
|
27
|
+
'@': path.join(corePackagePath, 'src'),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (sdkEntry) {
|
|
31
|
+
alias['@shellui/sdk'] = sdkEntry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return alias;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create PostCSS configuration for Vite.
|
|
39
|
+
* Provides Tailwind CSS v4 and autoprefixer plugins programmatically so the
|
|
40
|
+
* CLI owns all CSS build dependencies — core doesn't need to ship postcss.config
|
|
41
|
+
* or have CSS tooling in its own dependencies.
|
|
42
|
+
* @returns {Object} PostCSS configuration for Vite's css.postcss option
|
|
43
|
+
*/
|
|
44
|
+
export function createPostCSSConfig() {
|
|
45
|
+
return {
|
|
46
|
+
plugins: [tailwindcssPlugin(), autoprefixerPlugin()],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create Vite define configuration for ShellUI config injection
|
|
52
|
+
* App config is in __SHELLUI_CONFIG__; Sentry is in three separate globals
|
|
53
|
+
* (__SHELLUI_SENTRY_DSN__, __SHELLUI_SENTRY_ENVIRONMENT__, __SHELLUI_SENTRY_RELEASE__).
|
|
54
|
+
* @param {Object} config - Configuration object
|
|
55
|
+
* @returns {Object} Vite define configuration
|
|
56
|
+
*/
|
|
57
|
+
export function createViteDefine(config) {
|
|
58
|
+
// Ensure config is serializable; omit sentry so it is only in the three Sentry globals
|
|
59
|
+
const serializableConfig = JSON.parse(JSON.stringify(config));
|
|
60
|
+
delete serializableConfig.sentry;
|
|
61
|
+
|
|
62
|
+
// Verify navigation is preserved after serialization
|
|
63
|
+
if (config.navigation && !serializableConfig.navigation) {
|
|
64
|
+
console.warn('Warning: Navigation was lost during serialization. This should not happen.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sentry = config?.sentry;
|
|
68
|
+
// Double-stringify: Vite's define inserts the value as-is into the code
|
|
69
|
+
// If we pass '{"title":"shellui"}', Vite inserts it as: const x = {"title":"shellui"}; (invalid - object literal)
|
|
70
|
+
// If we pass '"{\"title\":\"shellui\"}"', Vite inserts it as: const x = "{\"title\":\"shellui\"}"; (valid - string literal)
|
|
71
|
+
// So we need to double-stringify to ensure it's inserted as a string
|
|
72
|
+
const configString = JSON.stringify(JSON.stringify(serializableConfig));
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
__SHELLUI_CONFIG__: configString,
|
|
76
|
+
__SHELLUI_SENTRY_DSN__: sentry?.dsn ? JSON.stringify(sentry.dsn) : 'undefined',
|
|
77
|
+
__SHELLUI_SENTRY_ENVIRONMENT__: sentry?.environment
|
|
78
|
+
? JSON.stringify(sentry.environment)
|
|
79
|
+
: 'undefined',
|
|
80
|
+
__SHELLUI_SENTRY_RELEASE__: sentry?.release ? JSON.stringify(sentry.release) : 'undefined',
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/app.jsx
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import ReactDOM from 'react-dom/client';
|
|
3
|
-
|
|
4
|
-
const App = () => {
|
|
5
|
-
// __SHELLUI_CONFIG__ is replaced by Vite at build time
|
|
6
|
-
const config = typeof __SHELLUI_CONFIG__ !== 'undefined' ? __SHELLUI_CONFIG__ : {};
|
|
7
|
-
|
|
8
|
-
return (
|
|
9
|
-
<div style={{ fontFamily: 'system-ui, sans-serif', padding: '2rem' }}>
|
|
10
|
-
<h1>ShellUI</h1>
|
|
11
|
-
<p>Welcome to ShellUI</p>
|
|
12
|
-
|
|
13
|
-
<div style={{
|
|
14
|
-
marginTop: '2rem',
|
|
15
|
-
padding: '1rem',
|
|
16
|
-
background: '#f5f5f5',
|
|
17
|
-
borderRadius: '8px'
|
|
18
|
-
}}>
|
|
19
|
-
<h2>Configuration</h2>
|
|
20
|
-
<pre>{JSON.stringify(config, null, 2)}</pre>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
27
|
-
<React.StrictMode>
|
|
28
|
-
<App />
|
|
29
|
-
</React.StrictMode>
|
|
30
|
-
);
|
|
31
|
-
|
package/src/index.html
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>ShellUI</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
<div id="root"></div>
|
|
10
|
-
<script type="module" src="/app.jsx"></script>
|
|
11
|
-
</body>
|
|
12
|
-
</html>
|