@onexapis/cli 1.0.4 → 1.1.0
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 +65 -63
- package/dist/cli.js +1091 -105
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1066 -103
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +959 -237
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +793 -90
- package/dist/index.mjs.map +1 -1
- package/dist/preview/preview-app.tsx +368 -0
- package/package.json +7 -2
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// ===== 1. SET UP GLOBALS (must happen before theme bundle loads) =====
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import * as jsxRuntime from "react/jsx-runtime";
|
|
6
|
+
|
|
7
|
+
// Core subpath imports — MUST match what setup-theme-globals.ts sets
|
|
8
|
+
// Only import browser-safe subpaths (NOT the root @onexapis/core which pulls server code)
|
|
9
|
+
import * as coreRenderers from "@onexapis/core/renderers";
|
|
10
|
+
import * as coreUtils from "@onexapis/core/utils";
|
|
11
|
+
import * as coreContexts from "@onexapis/core/contexts";
|
|
12
|
+
import * as coreComponents from "@onexapis/core/components";
|
|
13
|
+
import * as coreRegistry from "@onexapis/core/registry";
|
|
14
|
+
|
|
15
|
+
// Set React globals
|
|
16
|
+
(globalThis as any).__ONEX_REACT__ = React;
|
|
17
|
+
(globalThis as any).__ONEX_REACT_DOM__ = { createRoot };
|
|
18
|
+
(globalThis as any).__ONEX_JSX_RUNTIME__ = jsxRuntime;
|
|
19
|
+
|
|
20
|
+
// Set core globals with subpath modules
|
|
21
|
+
// Theme bundle accesses: globalThis.__ONEX_CORE__["renderers"], globalThis.__ONEX_CORE__["utils"]
|
|
22
|
+
// This matches the pattern in: apps/storefront/lib/setup-theme-globals.ts
|
|
23
|
+
(globalThis as any).__ONEX_CORE__ = {
|
|
24
|
+
renderers: coreRenderers,
|
|
25
|
+
utils: coreUtils,
|
|
26
|
+
contexts: coreContexts,
|
|
27
|
+
components: coreComponents,
|
|
28
|
+
registry: coreRegistry,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// ===== 2. THEME LOADING =====
|
|
33
|
+
|
|
34
|
+
interface ThemeExports {
|
|
35
|
+
themeConfig?: any;
|
|
36
|
+
layoutConfig?: any;
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function loadThemeBundle(timestamp?: number): Promise<ThemeExports> {
|
|
41
|
+
const cacheBust = timestamp ? `?t=${timestamp}` : `?t=${Date.now()}`;
|
|
42
|
+
const module = await import(/* @vite-ignore */ `/bundle-entry.js${cacheBust}`);
|
|
43
|
+
return module;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// ===== 3. SECTION RESOLUTION (adapted from storefront section-renderer.tsx) =====
|
|
48
|
+
|
|
49
|
+
function getSectionComponent(
|
|
50
|
+
themeExports: ThemeExports,
|
|
51
|
+
themePrefix: string,
|
|
52
|
+
sectionType: string,
|
|
53
|
+
template: string = "default"
|
|
54
|
+
) {
|
|
55
|
+
// Strip theme prefix: "cool-store-hero" -> "hero"
|
|
56
|
+
let baseName = sectionType;
|
|
57
|
+
if (themePrefix && baseName.startsWith(themePrefix)) {
|
|
58
|
+
baseName = baseName.substring(themePrefix.length);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Derive export names: "hero" + "default" -> "HeroDefault" + "heroSchema"
|
|
62
|
+
const componentKey = toPascalCase(baseName) + toPascalCase(template);
|
|
63
|
+
const schemaKey = toCamelCase(baseName) + "Schema";
|
|
64
|
+
|
|
65
|
+
const Component = themeExports[componentKey] as React.ComponentType<any> | undefined;
|
|
66
|
+
const schema = themeExports[schemaKey] as any | undefined;
|
|
67
|
+
|
|
68
|
+
if (!Component) return null;
|
|
69
|
+
|
|
70
|
+
return { Component, schema, template: { id: template, name: capitalize(template) } };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function capitalize(str: string): string {
|
|
74
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toCamelCase(str: string): string {
|
|
78
|
+
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toPascalCase(str: string): string {
|
|
82
|
+
return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// ===== 4. SECTION DATA ENRICHMENT =====
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Merge schema defaults into section instances from page config.
|
|
90
|
+
* Page configs have components: [] and blocks: [] — sections would render
|
|
91
|
+
* with settings-only fallbacks. By merging schema.defaults, we get the full
|
|
92
|
+
* component/block structure for a richer preview.
|
|
93
|
+
*/
|
|
94
|
+
function enrichSectionWithDefaults(section: any, schema: any): any {
|
|
95
|
+
if (!schema?.defaults) return section;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...section,
|
|
99
|
+
components:
|
|
100
|
+
section.components?.length > 0
|
|
101
|
+
? section.components
|
|
102
|
+
: schema.defaults.components || [],
|
|
103
|
+
blocks:
|
|
104
|
+
section.blocks?.length > 0
|
|
105
|
+
? section.blocks
|
|
106
|
+
: schema.defaults.blocks || [],
|
|
107
|
+
settings: {
|
|
108
|
+
...(schema.defaults.settings || {}),
|
|
109
|
+
...section.settings,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// ===== 5. PAGE CONFIG DISCOVERY =====
|
|
116
|
+
|
|
117
|
+
function discoverPageConfigs(
|
|
118
|
+
themeExports: ThemeExports
|
|
119
|
+
): Array<{ key: string; label: string; config: any }> {
|
|
120
|
+
const pages: Array<{ key: string; label: string; config: any }> = [];
|
|
121
|
+
|
|
122
|
+
for (const [key, value] of Object.entries(themeExports)) {
|
|
123
|
+
if (
|
|
124
|
+
key.endsWith("PageConfig") &&
|
|
125
|
+
value &&
|
|
126
|
+
typeof value === "object" &&
|
|
127
|
+
"sections" in value
|
|
128
|
+
) {
|
|
129
|
+
const label = key.replace("PageConfig", "");
|
|
130
|
+
const displayLabel =
|
|
131
|
+
label.charAt(0).toUpperCase() +
|
|
132
|
+
label.slice(1).replace(/([A-Z])/g, " $1");
|
|
133
|
+
pages.push({ key, label: displayLabel, config: value });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sort: "home" page first
|
|
138
|
+
pages.sort((a, b) => {
|
|
139
|
+
if (a.config.type === "home") return -1;
|
|
140
|
+
if (b.config.type === "home") return 1;
|
|
141
|
+
return a.label.localeCompare(b.label);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return pages;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
// ===== 6. PREVIEW APP COMPONENT =====
|
|
149
|
+
|
|
150
|
+
function PreviewApp() {
|
|
151
|
+
const [themeExports, setThemeExports] = useState<ThemeExports | null>(null);
|
|
152
|
+
const [selectedPage, setSelectedPage] = useState(0);
|
|
153
|
+
const [wsStatus, setWsStatus] = useState<
|
|
154
|
+
"connected" | "disconnected" | "rebuilding"
|
|
155
|
+
>("disconnected");
|
|
156
|
+
const [error, setError] = useState<string | null>(null);
|
|
157
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
158
|
+
|
|
159
|
+
// Load theme bundle
|
|
160
|
+
const loadTheme = useCallback(async (timestamp?: number) => {
|
|
161
|
+
try {
|
|
162
|
+
setError(null);
|
|
163
|
+
const exports = await loadThemeBundle(timestamp);
|
|
164
|
+
setThemeExports(exports);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
setError(`Failed to load theme: ${err}`);
|
|
167
|
+
console.error("[OneX Dev] Theme load error:", err);
|
|
168
|
+
}
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
// Initial load
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
loadTheme();
|
|
174
|
+
}, [loadTheme]);
|
|
175
|
+
|
|
176
|
+
// WebSocket connection for hot reload
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
function connect() {
|
|
179
|
+
const ws = new WebSocket(`ws://${window.location.host}`);
|
|
180
|
+
wsRef.current = ws;
|
|
181
|
+
|
|
182
|
+
ws.onopen = () => {
|
|
183
|
+
setWsStatus("connected");
|
|
184
|
+
updateToolbar("connected", "Connected");
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
ws.onmessage = (event) => {
|
|
188
|
+
const msg = JSON.parse(event.data);
|
|
189
|
+
if (msg.type === "reload") {
|
|
190
|
+
setWsStatus("rebuilding");
|
|
191
|
+
updateToolbar("rebuilding", "Reloading...");
|
|
192
|
+
loadTheme(msg.timestamp).then(() => {
|
|
193
|
+
setWsStatus("connected");
|
|
194
|
+
updateToolbar("connected", "Connected");
|
|
195
|
+
});
|
|
196
|
+
} else if (msg.type === "error") {
|
|
197
|
+
setError(msg.message);
|
|
198
|
+
updateToolbar("disconnected", "Build Error");
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
ws.onclose = () => {
|
|
203
|
+
setWsStatus("disconnected");
|
|
204
|
+
updateToolbar("disconnected", "Disconnected");
|
|
205
|
+
// Auto-reconnect after 2s
|
|
206
|
+
setTimeout(connect, 2000);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
connect();
|
|
211
|
+
return () => {
|
|
212
|
+
wsRef.current?.close();
|
|
213
|
+
};
|
|
214
|
+
}, [loadTheme]);
|
|
215
|
+
|
|
216
|
+
if (error) {
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
style={{
|
|
220
|
+
padding: 40,
|
|
221
|
+
fontFamily: "monospace",
|
|
222
|
+
color: "#ff4444",
|
|
223
|
+
background: "#1a1a2e",
|
|
224
|
+
minHeight: "100vh",
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<h2>Build Error</h2>
|
|
228
|
+
<pre style={{ whiteSpace: "pre-wrap", marginTop: 16 }}>{error}</pre>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!themeExports) {
|
|
234
|
+
return (
|
|
235
|
+
<div style={{ padding: 40, textAlign: "center", fontFamily: "system-ui" }}>
|
|
236
|
+
<p>Loading theme...</p>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Discover pages and get themeId
|
|
242
|
+
const pages = discoverPageConfigs(themeExports);
|
|
243
|
+
const themeId = themeExports.themeConfig?.id || "";
|
|
244
|
+
const themePrefix = themeId ? `${themeId}-` : "";
|
|
245
|
+
const currentPage = pages[selectedPage] || pages[0];
|
|
246
|
+
|
|
247
|
+
if (!currentPage) {
|
|
248
|
+
return (
|
|
249
|
+
<div style={{ padding: 40, textAlign: "center", fontFamily: "system-ui" }}>
|
|
250
|
+
<p>No page configs found in theme exports.</p>
|
|
251
|
+
<p style={{ color: "#888", marginTop: 8 }}>
|
|
252
|
+
Ensure your bundle-entry.ts exports page configs (e.g., homePageConfig).
|
|
253
|
+
</p>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Get sections for current page, enriched with schema defaults
|
|
259
|
+
const sections = (currentPage.config.sections || [])
|
|
260
|
+
.filter((s: any) => s.enabled !== false)
|
|
261
|
+
.sort((a: any, b: any) => a.order - b.order)
|
|
262
|
+
.map((section: any) => {
|
|
263
|
+
const reg = getSectionComponent(
|
|
264
|
+
themeExports,
|
|
265
|
+
themePrefix,
|
|
266
|
+
section.type,
|
|
267
|
+
section.template || "default"
|
|
268
|
+
);
|
|
269
|
+
const enriched = reg?.schema
|
|
270
|
+
? enrichSectionWithDefaults(section, reg.schema)
|
|
271
|
+
: section;
|
|
272
|
+
return { section: enriched, registration: reg };
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<>
|
|
277
|
+
{/* Page selector (if multiple pages) */}
|
|
278
|
+
{pages.length > 1 && (
|
|
279
|
+
<div
|
|
280
|
+
style={{
|
|
281
|
+
position: "fixed",
|
|
282
|
+
top: 40,
|
|
283
|
+
right: 16,
|
|
284
|
+
zIndex: 9998,
|
|
285
|
+
background: "white",
|
|
286
|
+
borderRadius: 8,
|
|
287
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
|
288
|
+
padding: "4px 8px",
|
|
289
|
+
display: "flex",
|
|
290
|
+
gap: 4,
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
{pages.map((page, i) => (
|
|
294
|
+
<button
|
|
295
|
+
key={page.key}
|
|
296
|
+
onClick={() => {
|
|
297
|
+
setSelectedPage(i);
|
|
298
|
+
const el = document.getElementById("page-indicator");
|
|
299
|
+
if (el) el.textContent = page.label;
|
|
300
|
+
}}
|
|
301
|
+
style={{
|
|
302
|
+
padding: "4px 12px",
|
|
303
|
+
border: "none",
|
|
304
|
+
borderRadius: 4,
|
|
305
|
+
cursor: "pointer",
|
|
306
|
+
background: i === selectedPage ? "#E11D48" : "transparent",
|
|
307
|
+
color: i === selectedPage ? "white" : "#333",
|
|
308
|
+
fontSize: 12,
|
|
309
|
+
fontWeight: 500,
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
{page.label}
|
|
313
|
+
</button>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{/* Render sections */}
|
|
319
|
+
{sections.map(({ section, registration }: any) => {
|
|
320
|
+
if (!registration) {
|
|
321
|
+
return (
|
|
322
|
+
<div
|
|
323
|
+
key={section.id}
|
|
324
|
+
style={{
|
|
325
|
+
padding: "2rem",
|
|
326
|
+
border: "2px dashed #f59e0b",
|
|
327
|
+
margin: "1rem 0",
|
|
328
|
+
background: "#fffbeb",
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
<h3 style={{ margin: 0, fontSize: "1.2rem", color: "#d97706" }}>
|
|
332
|
+
Section Not Found: {section.type}
|
|
333
|
+
</h3>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const { Component, schema, template } = registration;
|
|
338
|
+
return (
|
|
339
|
+
<Component
|
|
340
|
+
key={section.id}
|
|
341
|
+
section={section}
|
|
342
|
+
schema={schema}
|
|
343
|
+
template={template}
|
|
344
|
+
isEditing={false}
|
|
345
|
+
data={{}}
|
|
346
|
+
/>
|
|
347
|
+
);
|
|
348
|
+
})}
|
|
349
|
+
</>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function updateToolbar(status: string, label: string) {
|
|
354
|
+
const statusEl = document.getElementById("ws-status");
|
|
355
|
+
const labelEl = document.getElementById("ws-label");
|
|
356
|
+
if (statusEl) {
|
|
357
|
+
statusEl.className = `status ${status}`;
|
|
358
|
+
}
|
|
359
|
+
if (labelEl) {
|
|
360
|
+
labelEl.textContent = label;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
// ===== 7. MOUNT =====
|
|
366
|
+
|
|
367
|
+
const root = createRoot(document.getElementById("onex-preview-root")!);
|
|
368
|
+
root.render(<PreviewApp />);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onexapis/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI tool for OneX theme development - scaffolds themes using @onexapis/core",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -60,7 +60,11 @@
|
|
|
60
60
|
"glob": "^10.3.10",
|
|
61
61
|
"inquirer": "^9.2.12",
|
|
62
62
|
"node-fetch": "^3.3.2",
|
|
63
|
-
"
|
|
63
|
+
"chokidar": "^4.0.0",
|
|
64
|
+
"esbuild": "^0.25.0",
|
|
65
|
+
"open": "^10.1.0",
|
|
66
|
+
"ora": "^8.0.1",
|
|
67
|
+
"ws": "^8.18.0"
|
|
64
68
|
},
|
|
65
69
|
"devDependencies": {
|
|
66
70
|
"@types/adm-zip": "^0.5.7",
|
|
@@ -69,6 +73,7 @@
|
|
|
69
73
|
"@types/fs-extra": "^11.0.4",
|
|
70
74
|
"@types/inquirer": "^9.0.7",
|
|
71
75
|
"@types/node-fetch": "^2.6.13",
|
|
76
|
+
"@types/ws": "^8.5.0",
|
|
72
77
|
"tsup": "^8.5.1",
|
|
73
78
|
"typescript": "^5.9.3"
|
|
74
79
|
},
|