@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.
@@ -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.4",
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
- "ora": "^8.0.1"
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
  },