@onexapis/cli 1.1.1 → 1.1.3

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.
@@ -3,6 +3,7 @@
3
3
  import React, { useState, useEffect, useRef, useCallback } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
5
  import * as jsxRuntime from "react/jsx-runtime";
6
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
7
 
7
8
  // Core subpath imports — MUST match what setup-theme-globals.ts sets
8
9
  // Only import browser-safe subpaths (NOT the root @onexapis/core which pulls server code)
@@ -11,6 +12,8 @@ import * as coreUtils from "@onexapis/core/utils";
11
12
  import * as coreContexts from "@onexapis/core/contexts";
12
13
  import * as coreComponents from "@onexapis/core/components";
13
14
  import * as coreRegistry from "@onexapis/core/registry";
15
+ import * as coreCommerce from "@onexapis/core/commerce";
16
+ import * as coreCommerceHooks from "@onexapis/core/commerce/hooks";
14
17
 
15
18
  // Set React globals
16
19
  (globalThis as any).__ONEX_REACT__ = React;
@@ -26,8 +29,40 @@ import * as coreRegistry from "@onexapis/core/registry";
26
29
  contexts: coreContexts,
27
30
  components: coreComponents,
28
31
  registry: coreRegistry,
32
+ commerce: coreCommerce,
33
+ "commerce/hooks": coreCommerceHooks,
29
34
  };
30
35
 
36
+ // Initialize CommerceClient for preview
37
+ // If .env has NEXT_PUBLIC_API_URL + NEXT_PUBLIC_COMPANY_ID, use real API data.
38
+ // Otherwise, use a stub that returns empty data.
39
+ try {
40
+ const { CommerceClient, setCommerceClient } = coreCommerce as any;
41
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL;
42
+ const companyId = process.env.NEXT_PUBLIC_COMPANY_ID;
43
+
44
+ if (apiUrl && companyId && CommerceClient && setCommerceClient) {
45
+ // Real API connection — preview will show actual products/blogs
46
+ setCommerceClient(new CommerceClient({ apiUrl, companyId }));
47
+ } else if (setCommerceClient) {
48
+ // No API config — use stub that returns empty data
49
+ const emptyPagination = { total: 0, page: 1, limit: 10, totalPages: 0, hasNext: false, hasPrev: false };
50
+ const emptyList = { data: [], pagination: emptyPagination };
51
+ setCommerceClient({
52
+ getProducts: () => Promise.resolve(emptyList),
53
+ getProductBySlug: () => Promise.resolve(null),
54
+ getProductById: () => Promise.resolve(null),
55
+ getProductCategories: () => Promise.resolve([]),
56
+ getBlogs: () => Promise.resolve(emptyList),
57
+ getBlogBySlug: () => Promise.resolve(null),
58
+ getBlogById: () => Promise.resolve(null),
59
+ getBlogCategories: () => Promise.resolve([]),
60
+ getSettings: () => Promise.resolve({}),
61
+ });
62
+ }
63
+ } catch {
64
+ // CommerceClient not available — safe to ignore
65
+ }
31
66
 
32
67
  // ===== 2. THEME LOADING =====
33
68
 
@@ -39,11 +74,12 @@ interface ThemeExports {
39
74
 
40
75
  async function loadThemeBundle(timestamp?: number): Promise<ThemeExports> {
41
76
  const cacheBust = timestamp ? `?t=${timestamp}` : `?t=${Date.now()}`;
42
- const module = await import(/* @vite-ignore */ `/bundle-entry.js${cacheBust}`);
77
+ const module = await import(
78
+ /* @vite-ignore */ `/bundle-entry.js${cacheBust}`
79
+ );
43
80
  return module;
44
81
  }
45
82
 
46
-
47
83
  // ===== 3. SECTION RESOLUTION (adapted from storefront section-renderer.tsx) =====
48
84
 
49
85
  function getSectionComponent(
@@ -62,12 +98,18 @@ function getSectionComponent(
62
98
  const componentKey = toPascalCase(baseName) + toPascalCase(template);
63
99
  const schemaKey = toCamelCase(baseName) + "Schema";
64
100
 
65
- const Component = themeExports[componentKey] as React.ComponentType<any> | undefined;
101
+ const Component = themeExports[componentKey] as
102
+ | React.ComponentType<any>
103
+ | undefined;
66
104
  const schema = themeExports[schemaKey] as any | undefined;
67
105
 
68
106
  if (!Component) return null;
69
107
 
70
- return { Component, schema, template: { id: template, name: capitalize(template) } };
108
+ return {
109
+ Component,
110
+ schema,
111
+ template: { id: template, name: capitalize(template) },
112
+ };
71
113
  }
72
114
 
73
115
  function capitalize(str: string): string {
@@ -79,10 +121,12 @@ function toCamelCase(str: string): string {
79
121
  }
80
122
 
81
123
  function toPascalCase(str: string): string {
82
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
124
+ return str
125
+ .split("-")
126
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
127
+ .join("");
83
128
  }
84
129
 
85
-
86
130
  // ===== 4. SECTION DATA ENRICHMENT =====
87
131
 
88
132
  /**
@@ -111,7 +155,6 @@ function enrichSectionWithDefaults(section: any, schema: any): any {
111
155
  };
112
156
  }
113
157
 
114
-
115
158
  // ===== 5. PAGE CONFIG DISCOVERY =====
116
159
 
117
160
  function discoverPageConfigs(
@@ -144,12 +187,31 @@ function discoverPageConfigs(
144
187
  return pages;
145
188
  }
146
189
 
147
-
148
190
  // ===== 6. PREVIEW APP COMPONENT =====
149
191
 
192
+ /**
193
+ * Match URL pathname to a page index.
194
+ * "/" or "/home" → home page, "/showcase" → showcase page, etc.
195
+ */
196
+ function getInitialPageFromURL(
197
+ pages: Array<{ key: string; label: string; config: any }>
198
+ ): number {
199
+ const pathname = window.location.pathname
200
+ .replace(/^\/+|\/+$/g, "")
201
+ .toLowerCase();
202
+ if (!pathname || pathname === "home") return 0; // home is always first after sorting
203
+
204
+ const idx = pages.findIndex((p) => {
205
+ const pageSlug = p.label.toLowerCase().replace(/\s+/g, "-");
206
+ const pageKey = p.key.replace("PageConfig", "").toLowerCase();
207
+ return pageSlug === pathname || pageKey === pathname;
208
+ });
209
+ return idx >= 0 ? idx : -2; // -2 = page not found
210
+ }
211
+
150
212
  function PreviewApp() {
151
213
  const [themeExports, setThemeExports] = useState<ThemeExports | null>(null);
152
- const [selectedPage, setSelectedPage] = useState(0);
214
+ const [selectedPage, setSelectedPage] = useState(-1); // -1 = not yet resolved from URL
153
215
  const [wsStatus, setWsStatus] = useState<
154
216
  "connected" | "disconnected" | "rebuilding"
155
217
  >("disconnected");
@@ -173,6 +235,16 @@ function PreviewApp() {
173
235
  loadTheme();
174
236
  }, [loadTheme]);
175
237
 
238
+ // Handle browser back/forward navigation
239
+ useEffect(() => {
240
+ const handlePopState = () => {
241
+ // Reset to -1 to re-resolve from URL
242
+ setSelectedPage(-1);
243
+ };
244
+ window.addEventListener("popstate", handlePopState);
245
+ return () => window.removeEventListener("popstate", handlePopState);
246
+ }, []);
247
+
176
248
  // WebSocket connection for hot reload
177
249
  useEffect(() => {
178
250
  function connect() {
@@ -232,7 +304,9 @@ function PreviewApp() {
232
304
 
233
305
  if (!themeExports) {
234
306
  return (
235
- <div style={{ padding: 40, textAlign: "center", fontFamily: "system-ui" }}>
307
+ <div
308
+ style={{ padding: 40, textAlign: "center", fontFamily: "system-ui" }}
309
+ >
236
310
  <p>Loading theme...</p>
237
311
  </div>
238
312
  );
@@ -242,14 +316,81 @@ function PreviewApp() {
242
316
  const pages = discoverPageConfigs(themeExports);
243
317
  const themeId = themeExports.themeConfig?.id || "";
244
318
  const themePrefix = themeId ? `${themeId}-` : "";
245
- const currentPage = pages[selectedPage] || pages[0];
319
+
320
+ // Resolve initial page from URL path (once pages are discovered)
321
+ const resolvedPage =
322
+ selectedPage === -1 ? getInitialPageFromURL(pages) : selectedPage;
323
+
324
+ // Page not found (-2)
325
+ if (resolvedPage === -2) {
326
+ const pathname = window.location.pathname;
327
+ return (
328
+ <div
329
+ style={{
330
+ padding: "80px 40px",
331
+ textAlign: "center",
332
+ fontFamily: "system-ui",
333
+ }}
334
+ >
335
+ <h1 style={{ fontSize: "4rem", margin: 0, color: "#E11D48" }}>404</h1>
336
+ <p style={{ fontSize: "1.25rem", color: "#333", marginTop: 12 }}>
337
+ Page not found:{" "}
338
+ <code
339
+ style={{
340
+ background: "#f1f5f9",
341
+ padding: "2px 8px",
342
+ borderRadius: 4,
343
+ }}
344
+ >
345
+ {pathname}
346
+ </code>
347
+ </p>
348
+ <p style={{ color: "#888", marginTop: 8 }}>Available pages:</p>
349
+ <div
350
+ style={{
351
+ display: "flex",
352
+ gap: 8,
353
+ justifyContent: "center",
354
+ marginTop: 12,
355
+ }}
356
+ >
357
+ {pages.map((page) => {
358
+ const pageSlug = page.key.replace("PageConfig", "").toLowerCase();
359
+ const pagePath = pageSlug === "home" ? "/" : `/${pageSlug}`;
360
+ return (
361
+ <a
362
+ key={page.key}
363
+ href={pagePath}
364
+ style={{
365
+ padding: "6px 16px",
366
+ borderRadius: 6,
367
+ background: "#E11D48",
368
+ color: "white",
369
+ textDecoration: "none",
370
+ fontSize: 14,
371
+ fontWeight: 500,
372
+ }}
373
+ >
374
+ {page.label}
375
+ </a>
376
+ );
377
+ })}
378
+ </div>
379
+ </div>
380
+ );
381
+ }
382
+
383
+ const currentPage = pages[resolvedPage] || pages[0];
246
384
 
247
385
  if (!currentPage) {
248
386
  return (
249
- <div style={{ padding: 40, textAlign: "center", fontFamily: "system-ui" }}>
387
+ <div
388
+ style={{ padding: 40, textAlign: "center", fontFamily: "system-ui" }}
389
+ >
250
390
  <p>No page configs found in theme exports.</p>
251
391
  <p style={{ color: "#888", marginTop: 8 }}>
252
- Ensure your bundle-entry.ts exports page configs (e.g., homePageConfig).
392
+ Ensure your bundle-entry.ts exports page configs (e.g.,
393
+ homePageConfig).
253
394
  </p>
254
395
  </div>
255
396
  );
@@ -290,28 +431,33 @@ function PreviewApp() {
290
431
  gap: 4,
291
432
  }}
292
433
  >
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
- ))}
434
+ {pages.map((page, i) => {
435
+ const pageSlug = page.key.replace("PageConfig", "").toLowerCase();
436
+ const pagePath = pageSlug === "home" ? "/" : `/${pageSlug}`;
437
+ return (
438
+ <button
439
+ key={page.key}
440
+ onClick={() => {
441
+ setSelectedPage(i);
442
+ window.history.pushState(null, "", pagePath);
443
+ const el = document.getElementById("page-indicator");
444
+ if (el) el.textContent = page.label;
445
+ }}
446
+ style={{
447
+ padding: "4px 12px",
448
+ border: "none",
449
+ borderRadius: 4,
450
+ cursor: "pointer",
451
+ background: i === resolvedPage ? "#E11D48" : "transparent",
452
+ color: i === resolvedPage ? "white" : "#333",
453
+ fontSize: 12,
454
+ fontWeight: 500,
455
+ }}
456
+ >
457
+ {page.label}
458
+ </button>
459
+ );
460
+ })}
315
461
  </div>
316
462
  )}
317
463
 
@@ -361,8 +507,20 @@ function updateToolbar(status: string, label: string) {
361
507
  }
362
508
  }
363
509
 
364
-
365
510
  // ===== 7. MOUNT =====
366
511
 
512
+ const queryClient = new QueryClient({
513
+ defaultOptions: {
514
+ queries: {
515
+ retry: false,
516
+ refetchOnWindowFocus: false,
517
+ },
518
+ },
519
+ });
520
+
367
521
  const root = createRoot(document.getElementById("onex-preview-root")!);
368
- root.render(<PreviewApp />);
522
+ root.render(
523
+ <QueryClientProvider client={queryClient}>
524
+ <PreviewApp />
525
+ </QueryClientProvider>
526
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onexapis/cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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",
@@ -48,20 +48,23 @@
48
48
  "registry": "https://registry.npmjs.org/"
49
49
  },
50
50
  "dependencies": {
51
+ "@onexapis/core": "workspace:*",
52
+ "@tanstack/react-query": "^5.90.16",
51
53
  "@aws-sdk/client-s3": "^3.470.0",
52
54
  "adm-zip": "^0.5.16",
53
55
  "archiver": "^7.0.1",
54
56
  "chalk": "^5.3.0",
57
+ "chokidar": "^4.0.0",
55
58
  "commander": "^12.1.0",
56
59
  "dotenv": "^17.3.1",
57
60
  "ejs": "^3.1.10",
61
+ "esbuild": "^0.25.0",
58
62
  "form-data": "^4.0.5",
59
63
  "fs-extra": "^11.2.0",
60
64
  "glob": "^10.3.10",
61
65
  "inquirer": "^9.2.12",
66
+ "jiti": "^2.6.1",
62
67
  "node-fetch": "^3.3.2",
63
- "chokidar": "^4.0.0",
64
- "esbuild": "^0.25.0",
65
68
  "open": "^10.1.0",
66
69
  "ora": "^8.0.1",
67
70
  "ws": "^8.18.0"
@@ -0,0 +1,4 @@
1
+ # API Configuration (optional — enables real data in preview)
2
+ # Get your Company ID from the OneX dashboard
3
+ NEXT_PUBLIC_API_URL=https://api-dev.onexeos.com
4
+ NEXT_PUBLIC_COMPANY_ID=
@@ -14,7 +14,8 @@
14
14
  "author": "<%= author %>",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@onexapis/core": "^1.0.0"
17
+ "@onexapis/core": "^1.0.2",
18
+ "@tanstack/react-query": "^5.90.16"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@types/react": "^19",