@objectstack/cli 1.0.11 → 1.0.12
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/.turbo/turbo-build.log +14 -9
- package/CHANGELOG.md +13 -0
- package/README.md +132 -13
- package/dist/bin.js +1488 -178
- package/dist/index.d.ts +97 -1
- package/dist/index.js +1938 -5
- package/package.json +9 -9
- package/src/bin.ts +53 -6
- package/src/commands/compile.ts +66 -39
- package/src/commands/create.ts +15 -11
- package/src/commands/dev.ts +12 -16
- package/src/commands/doctor.ts +13 -9
- package/src/commands/generate.ts +297 -0
- package/src/commands/info.ts +111 -0
- package/src/commands/init.ts +313 -0
- package/src/commands/serve.ts +134 -48
- package/src/commands/studio.ts +40 -0
- package/src/commands/test.ts +2 -2
- package/src/commands/validate.ts +130 -0
- package/src/index.ts +9 -0
- package/src/utils/config.ts +78 -0
- package/src/utils/console.ts +319 -0
- package/src/utils/format.ts +261 -0
- package/test/commands.test.ts +26 -1
- package/tsup.config.ts +18 -9
- package/dist/bin.d.ts +0 -2
- package/dist/chunk-2YXVEYO7.js +0 -64
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { ZodError } from 'zod';
|
|
4
|
+
import { ObjectStackDefinitionSchema } from '@objectstack/spec';
|
|
5
|
+
import { loadConfig } from '../utils/config.js';
|
|
6
|
+
import {
|
|
7
|
+
printHeader,
|
|
8
|
+
printKV,
|
|
9
|
+
printSuccess,
|
|
10
|
+
printError,
|
|
11
|
+
printStep,
|
|
12
|
+
createTimer,
|
|
13
|
+
formatZodErrors,
|
|
14
|
+
collectMetadataStats,
|
|
15
|
+
printMetadataStats,
|
|
16
|
+
} from '../utils/format.js';
|
|
17
|
+
|
|
18
|
+
export const validateCommand = new Command('validate')
|
|
19
|
+
.description('Validate ObjectStack configuration against the protocol schema')
|
|
20
|
+
.argument('[config]', 'Configuration file path')
|
|
21
|
+
.option('--strict', 'Treat warnings as errors')
|
|
22
|
+
.option('--json', 'Output results as JSON')
|
|
23
|
+
.action(async (configPath, options) => {
|
|
24
|
+
const timer = createTimer();
|
|
25
|
+
|
|
26
|
+
if (!options.json) {
|
|
27
|
+
printHeader('Validate');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// 1. Load configuration
|
|
32
|
+
if (!options.json) printStep('Loading configuration...');
|
|
33
|
+
const { config, absolutePath, duration } = await loadConfig(configPath);
|
|
34
|
+
|
|
35
|
+
if (!options.json) {
|
|
36
|
+
printKV('Config', absolutePath);
|
|
37
|
+
printKV('Load time', `${duration}ms`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Validate against schema
|
|
41
|
+
if (!options.json) printStep('Validating against ObjectStack Protocol...');
|
|
42
|
+
const result = ObjectStackDefinitionSchema.safeParse(config);
|
|
43
|
+
|
|
44
|
+
if (!result.success) {
|
|
45
|
+
if (options.json) {
|
|
46
|
+
console.log(JSON.stringify({
|
|
47
|
+
valid: false,
|
|
48
|
+
errors: (result.error as unknown as ZodError).issues,
|
|
49
|
+
duration: timer.elapsed(),
|
|
50
|
+
}, null, 2));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('');
|
|
55
|
+
printError('Validation failed');
|
|
56
|
+
formatZodErrors(result.error as unknown as ZodError);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Collect and display stats
|
|
61
|
+
const stats = collectMetadataStats(config);
|
|
62
|
+
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.log(JSON.stringify({
|
|
65
|
+
valid: true,
|
|
66
|
+
manifest: config.manifest,
|
|
67
|
+
stats,
|
|
68
|
+
duration: timer.elapsed(),
|
|
69
|
+
}, null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Warnings (non-blocking)
|
|
74
|
+
const warnings: string[] = [];
|
|
75
|
+
|
|
76
|
+
if (stats.objects === 0) {
|
|
77
|
+
warnings.push('No objects defined — this stack has no data model');
|
|
78
|
+
}
|
|
79
|
+
if (stats.apps === 0 && stats.plugins === 0) {
|
|
80
|
+
warnings.push('No apps or plugins defined — this stack may not do much');
|
|
81
|
+
}
|
|
82
|
+
if (!config.manifest?.id) {
|
|
83
|
+
warnings.push('Missing manifest.id — required for deployment');
|
|
84
|
+
}
|
|
85
|
+
if (!config.manifest?.namespace) {
|
|
86
|
+
warnings.push('Missing manifest.namespace — required for multi-app hosting');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 5. Display results
|
|
90
|
+
console.log('');
|
|
91
|
+
printSuccess(`Validation passed ${chalk.dim(`(${timer.display()})`)}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
if (config.manifest) {
|
|
95
|
+
console.log(` ${chalk.bold(config.manifest.name || config.manifest.id || 'Unnamed')} ${chalk.dim(`v${config.manifest.version || '0.0.0'}`)}`);
|
|
96
|
+
if (config.manifest.description) {
|
|
97
|
+
console.log(chalk.dim(` ${config.manifest.description}`));
|
|
98
|
+
}
|
|
99
|
+
console.log('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
printMetadataStats(stats);
|
|
103
|
+
|
|
104
|
+
if (warnings.length > 0) {
|
|
105
|
+
console.log('');
|
|
106
|
+
for (const w of warnings) {
|
|
107
|
+
console.log(chalk.yellow(` ⚠ ${w}`));
|
|
108
|
+
}
|
|
109
|
+
if (options.strict) {
|
|
110
|
+
console.log('');
|
|
111
|
+
printError('Strict mode: warnings treated as errors');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log('');
|
|
117
|
+
} catch (error: any) {
|
|
118
|
+
if (options.json) {
|
|
119
|
+
console.log(JSON.stringify({
|
|
120
|
+
valid: false,
|
|
121
|
+
error: error.message,
|
|
122
|
+
duration: timer.elapsed(),
|
|
123
|
+
}, null, 2));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
127
|
+
printError(error.message || String(error));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
1
|
export * from './commands/compile.js';
|
|
2
|
+
export * from './commands/validate.js';
|
|
3
|
+
export * from './commands/info.js';
|
|
4
|
+
export * from './commands/init.js';
|
|
5
|
+
export * from './commands/generate.js';
|
|
6
|
+
export * from './commands/create.js';
|
|
7
|
+
export * from './commands/dev.js';
|
|
8
|
+
export * from './commands/serve.js';
|
|
9
|
+
export * from './commands/test.js';
|
|
10
|
+
export * from './commands/doctor.js';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { bundleRequire } from 'bundle-require';
|
|
5
|
+
import { printError } from './format.js';
|
|
6
|
+
|
|
7
|
+
export interface LoadedConfig {
|
|
8
|
+
config: any;
|
|
9
|
+
absolutePath: string;
|
|
10
|
+
duration: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the config file path. Supports:
|
|
15
|
+
* - explicit path (objectstack.config.ts)
|
|
16
|
+
* - auto-detection (searches for objectstack.config.{ts,js,mjs})
|
|
17
|
+
*/
|
|
18
|
+
export function resolveConfigPath(source?: string): string {
|
|
19
|
+
if (source) {
|
|
20
|
+
const abs = path.resolve(process.cwd(), source);
|
|
21
|
+
if (!fs.existsSync(abs)) {
|
|
22
|
+
printError(`Config file not found: ${chalk.white(abs)}`);
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(chalk.dim(' Hint: Run this command from a directory with objectstack.config.ts'));
|
|
25
|
+
console.log(chalk.dim(' Or specify the path: objectstack <command> path/to/config.ts'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
return abs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Auto-detect
|
|
32
|
+
const candidates = [
|
|
33
|
+
'objectstack.config.ts',
|
|
34
|
+
'objectstack.config.js',
|
|
35
|
+
'objectstack.config.mjs',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
const abs = path.resolve(process.cwd(), candidate);
|
|
40
|
+
if (fs.existsSync(abs)) return abs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
printError('No objectstack.config.{ts,js,mjs} found in current directory');
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(chalk.dim(' Hint: Run `objectstack init` to create a new project'));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load and bundle a config file using bundle-require.
|
|
51
|
+
* Returns the resolved config object and load time.
|
|
52
|
+
*/
|
|
53
|
+
export async function loadConfig(source?: string): Promise<LoadedConfig> {
|
|
54
|
+
const absolutePath = resolveConfigPath(source);
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
|
|
57
|
+
const { mod } = await bundleRequire({
|
|
58
|
+
filepath: absolutePath,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const config = mod.default || mod;
|
|
62
|
+
if (!config) {
|
|
63
|
+
throw new Error(`No default export found in ${path.basename(absolutePath)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
config,
|
|
68
|
+
absolutePath,
|
|
69
|
+
duration: Date.now() - start,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check whether a file exists at the given path (relative to cwd).
|
|
75
|
+
*/
|
|
76
|
+
export function configExists(name: string = 'objectstack.config.ts'): boolean {
|
|
77
|
+
return fs.existsSync(path.resolve(process.cwd(), name));
|
|
78
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console UI Integration Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles resolving, spawning, and proxying the @objectstack/console
|
|
5
|
+
* frontend when the CLI is started with --ui or via the `studio` command.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import net from 'net';
|
|
10
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
|
|
13
|
+
// ─── Constants ──────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** URL mount path for the Console UI inside the ObjectStack server */
|
|
16
|
+
export const STUDIO_PATH = '/_studio';
|
|
17
|
+
|
|
18
|
+
/** Internal port range start for the Vite dev server */
|
|
19
|
+
const VITE_PORT_START = 24678;
|
|
20
|
+
|
|
21
|
+
// ─── Path Resolution ────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the filesystem path to the @objectstack/console package.
|
|
25
|
+
* Searches workspace locations first, then falls back to node_modules.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveConsolePath(): string | null {
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
|
|
30
|
+
// Workspace candidates (monorepo layouts)
|
|
31
|
+
const candidates = [
|
|
32
|
+
path.resolve(cwd, 'apps/console'),
|
|
33
|
+
path.resolve(cwd, '../../apps/console'),
|
|
34
|
+
path.resolve(cwd, '../apps/console'),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
const pkgPath = path.join(candidate, 'package.json');
|
|
39
|
+
if (fs.existsSync(pkgPath)) {
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
42
|
+
if (pkg.name === '@objectstack/console') return candidate;
|
|
43
|
+
} catch {
|
|
44
|
+
// Skip invalid package.json
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback: resolve from node_modules
|
|
50
|
+
try {
|
|
51
|
+
const { createRequire } = require('module');
|
|
52
|
+
const req = createRequire(import.meta.url);
|
|
53
|
+
const resolved = req.resolve('@objectstack/console/package.json');
|
|
54
|
+
return path.dirname(resolved);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check whether the Console has a pre-built `dist/` directory.
|
|
62
|
+
*/
|
|
63
|
+
export function hasConsoleDist(consolePath: string): boolean {
|
|
64
|
+
return fs.existsSync(path.join(consolePath, 'dist', 'index.html'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Port Utilities ─────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find the next available TCP port starting from `start`.
|
|
71
|
+
*/
|
|
72
|
+
export function findAvailablePort(start: number = VITE_PORT_START): Promise<number> {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const server = net.createServer();
|
|
75
|
+
server.once('error', () => {
|
|
76
|
+
// Port in use — try next
|
|
77
|
+
findAvailablePort(start + 1).then(resolve, reject);
|
|
78
|
+
});
|
|
79
|
+
server.once('listening', () => {
|
|
80
|
+
server.close(() => resolve(start));
|
|
81
|
+
});
|
|
82
|
+
server.listen(start);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Vite Dev Server ────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export interface ViteDevResult {
|
|
89
|
+
/** Port the Vite dev server is listening on */
|
|
90
|
+
port: number;
|
|
91
|
+
/** Child process handle */
|
|
92
|
+
process: ChildProcess;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Spawn a Vite dev server for the Console application.
|
|
97
|
+
*
|
|
98
|
+
* Sets environment variables so the Console runs in server mode and
|
|
99
|
+
* connects to the ObjectStack API on the same origin.
|
|
100
|
+
*
|
|
101
|
+
* @param consolePath - Absolute path to the @objectstack/console package
|
|
102
|
+
* @param options.serverPort - The main ObjectStack server port (for display only)
|
|
103
|
+
*/
|
|
104
|
+
export async function spawnViteDevServer(
|
|
105
|
+
consolePath: string,
|
|
106
|
+
options: { serverPort?: number } = {},
|
|
107
|
+
): Promise<ViteDevResult> {
|
|
108
|
+
const vitePort = await findAvailablePort(VITE_PORT_START);
|
|
109
|
+
|
|
110
|
+
// Resolve the Vite binary from the Console's own dependencies
|
|
111
|
+
const viteBinCandidates = [
|
|
112
|
+
path.join(consolePath, 'node_modules', '.bin', 'vite'),
|
|
113
|
+
path.join(consolePath, '..', '..', 'node_modules', '.bin', 'vite'),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
let viteBin: string | null = null;
|
|
117
|
+
for (const candidate of viteBinCandidates) {
|
|
118
|
+
if (fs.existsSync(candidate)) {
|
|
119
|
+
viteBin = candidate;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const command = viteBin || 'npx';
|
|
125
|
+
const args = viteBin
|
|
126
|
+
? ['--port', String(vitePort), '--strictPort']
|
|
127
|
+
: ['vite', '--port', String(vitePort), '--strictPort'];
|
|
128
|
+
|
|
129
|
+
const child = spawn(command, args, {
|
|
130
|
+
cwd: consolePath,
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
VITE_BASE: `${STUDIO_PATH}/`,
|
|
134
|
+
VITE_PORT: String(vitePort),
|
|
135
|
+
VITE_HMR_PORT: String(vitePort),
|
|
136
|
+
VITE_RUNTIME_MODE: 'server',
|
|
137
|
+
VITE_SERVER_URL: '', // Same-origin API
|
|
138
|
+
NODE_ENV: 'development',
|
|
139
|
+
},
|
|
140
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Accumulate stderr for error reporting
|
|
144
|
+
let stderr = '';
|
|
145
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
146
|
+
stderr += data.toString();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Wait for Vite to signal readiness
|
|
150
|
+
await new Promise<void>((resolve, reject) => {
|
|
151
|
+
const timeout = setTimeout(() => {
|
|
152
|
+
child.kill();
|
|
153
|
+
reject(new Error(`Vite dev server timed out after 30 s.\n${stderr}`));
|
|
154
|
+
}, 30_000);
|
|
155
|
+
|
|
156
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
157
|
+
const output = data.toString();
|
|
158
|
+
// Vite prints "ready in Xms" or "Local: http://..." when ready
|
|
159
|
+
if (output.includes('Local:') || output.includes('ready in')) {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
resolve();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
child.on('error', (err) => {
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
reject(err);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
child.on('exit', (code) => {
|
|
171
|
+
if (code !== 0 && code !== null) {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
reject(new Error(`Vite exited with code ${code}.\n${stderr}`));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return { port: vitePort, process: child };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Console Plugin Factories ───────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a lightweight kernel plugin that proxies `/_studio/*` requests
|
|
185
|
+
* to the Vite dev server. Used in development mode.
|
|
186
|
+
*/
|
|
187
|
+
export function createConsoleProxyPlugin(vitePort: number) {
|
|
188
|
+
return {
|
|
189
|
+
name: 'com.objectstack.console-proxy',
|
|
190
|
+
|
|
191
|
+
init: async () => {},
|
|
192
|
+
|
|
193
|
+
start: async (ctx: any) => {
|
|
194
|
+
const httpServer = ctx.getService?.('http.server');
|
|
195
|
+
if (!httpServer?.getRawApp) {
|
|
196
|
+
ctx.logger?.warn?.('Console proxy: http.server service not found — skipping');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const app = httpServer.getRawApp();
|
|
201
|
+
|
|
202
|
+
// Redirect bare path to trailing-slash (SPA convention)
|
|
203
|
+
app.get(STUDIO_PATH, (c: any) => c.redirect(`${STUDIO_PATH}/`));
|
|
204
|
+
|
|
205
|
+
// Proxy all /_studio/* requests to the Vite dev server
|
|
206
|
+
app.all(`${STUDIO_PATH}/*`, async (c: any) => {
|
|
207
|
+
const targetUrl = `http://localhost:${vitePort}${c.req.path}`;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const headers = new Headers(c.req.raw.headers);
|
|
211
|
+
headers.delete('host');
|
|
212
|
+
|
|
213
|
+
const isBodyAllowed = !['GET', 'HEAD'].includes(c.req.method);
|
|
214
|
+
|
|
215
|
+
const resp = await fetch(targetUrl, {
|
|
216
|
+
method: c.req.method,
|
|
217
|
+
headers,
|
|
218
|
+
body: isBodyAllowed ? c.req.raw.body : undefined,
|
|
219
|
+
// @ts-expect-error — duplex required for streaming request body
|
|
220
|
+
duplex: isBodyAllowed ? 'half' : undefined,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Forward the full response (status, headers, body)
|
|
224
|
+
return new Response(resp.body, {
|
|
225
|
+
status: resp.status,
|
|
226
|
+
headers: resp.headers,
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
return c.text('Console dev server is starting…', 502);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create a lightweight kernel plugin that serves the pre-built Console
|
|
238
|
+
* static files at `/_studio/*`. Used in production mode.
|
|
239
|
+
*
|
|
240
|
+
* Uses Node.js built-in fs for static file serving to avoid external
|
|
241
|
+
* bundling dependencies.
|
|
242
|
+
*/
|
|
243
|
+
export function createConsoleStaticPlugin(distPath: string) {
|
|
244
|
+
return {
|
|
245
|
+
name: 'com.objectstack.console-static',
|
|
246
|
+
|
|
247
|
+
init: async () => {},
|
|
248
|
+
|
|
249
|
+
start: async (ctx: any) => {
|
|
250
|
+
const httpServer = ctx.getService?.('http.server');
|
|
251
|
+
if (!httpServer?.getRawApp) {
|
|
252
|
+
ctx.logger?.warn?.('Console static: http.server service not found — skipping');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const app = httpServer.getRawApp();
|
|
257
|
+
const absoluteDist = path.resolve(distPath);
|
|
258
|
+
|
|
259
|
+
const indexPath = path.join(absoluteDist, 'index.html');
|
|
260
|
+
if (!fs.existsSync(indexPath)) {
|
|
261
|
+
ctx.logger?.warn?.(`Console static: dist not found at ${absoluteDist}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Redirect bare path
|
|
266
|
+
app.get(STUDIO_PATH, (c: any) => c.redirect(`${STUDIO_PATH}/`));
|
|
267
|
+
|
|
268
|
+
// Serve static files with SPA fallback
|
|
269
|
+
app.get(`${STUDIO_PATH}/*`, async (c: any) => {
|
|
270
|
+
const reqPath = c.req.path.substring(STUDIO_PATH.length) || '/';
|
|
271
|
+
const filePath = path.join(absoluteDist, reqPath);
|
|
272
|
+
|
|
273
|
+
// Security: prevent path traversal
|
|
274
|
+
if (!filePath.startsWith(absoluteDist)) {
|
|
275
|
+
return c.text('Forbidden', 403);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Try serving the exact file
|
|
279
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
280
|
+
const content = fs.readFileSync(filePath);
|
|
281
|
+
return new Response(content, {
|
|
282
|
+
headers: { 'content-type': mimeType(filePath) },
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// SPA fallback: serve index.html for non-file routes
|
|
287
|
+
const html = fs.readFileSync(indexPath);
|
|
288
|
+
return new Response(html, {
|
|
289
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
const MIME_TYPES: Record<string, string> = {
|
|
299
|
+
'.html': 'text/html; charset=utf-8',
|
|
300
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
301
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
302
|
+
'.css': 'text/css; charset=utf-8',
|
|
303
|
+
'.json': 'application/json; charset=utf-8',
|
|
304
|
+
'.svg': 'image/svg+xml',
|
|
305
|
+
'.png': 'image/png',
|
|
306
|
+
'.jpg': 'image/jpeg',
|
|
307
|
+
'.jpeg': 'image/jpeg',
|
|
308
|
+
'.gif': 'image/gif',
|
|
309
|
+
'.ico': 'image/x-icon',
|
|
310
|
+
'.woff': 'font/woff',
|
|
311
|
+
'.woff2': 'font/woff2',
|
|
312
|
+
'.ttf': 'font/ttf',
|
|
313
|
+
'.map': 'application/json',
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
function mimeType(filePath: string): string {
|
|
317
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
318
|
+
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
319
|
+
}
|