@onexapis/cli 1.1.52 → 1.1.53
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/dist/cli.js +355 -230
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +353 -228
- package/dist/cli.mjs.map +1 -1
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -7,21 +7,21 @@ import fs8 from 'fs/promises';
|
|
|
7
7
|
import crypto from 'crypto';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
|
+
import http from 'http';
|
|
11
|
+
import fs3 from 'fs';
|
|
12
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
10
13
|
import os from 'os';
|
|
11
14
|
import dotenv from 'dotenv';
|
|
12
15
|
import fs from 'fs-extra';
|
|
13
16
|
import ejs from 'ejs';
|
|
14
17
|
import { execSync, spawn } from 'child_process';
|
|
15
18
|
import { Command } from 'commander';
|
|
16
|
-
import fs3 from 'fs';
|
|
17
19
|
import inquirer from 'inquirer';
|
|
18
20
|
import archiver from 'archiver';
|
|
19
21
|
import FormData from 'form-data';
|
|
20
22
|
import fetch2 from 'node-fetch';
|
|
21
23
|
import AdmZip from 'adm-zip';
|
|
22
24
|
import chokidar from 'chokidar';
|
|
23
|
-
import http from 'http';
|
|
24
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
25
25
|
import semver from 'semver';
|
|
26
26
|
|
|
27
27
|
var __defProp = Object.defineProperty;
|
|
@@ -1337,6 +1337,240 @@ export const {
|
|
|
1337
1337
|
}
|
|
1338
1338
|
});
|
|
1339
1339
|
|
|
1340
|
+
// src/utils/dev-server.ts
|
|
1341
|
+
var dev_server_exports = {};
|
|
1342
|
+
__export(dev_server_exports, {
|
|
1343
|
+
createDevServer: () => createDevServer
|
|
1344
|
+
});
|
|
1345
|
+
function createDevServer(options) {
|
|
1346
|
+
const clients = /* @__PURE__ */ new Set();
|
|
1347
|
+
const themeDataPath = path9.join(options.distDir, "theme-data.json");
|
|
1348
|
+
const server = http.createServer((req, res) => {
|
|
1349
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1350
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
1351
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
1352
|
+
if (req.method === "OPTIONS") {
|
|
1353
|
+
res.writeHead(200);
|
|
1354
|
+
res.end();
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
const url = new URL(req.url || "/", `http://localhost:${options.port}`);
|
|
1358
|
+
const pathname = url.pathname;
|
|
1359
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
1360
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1361
|
+
res.end(
|
|
1362
|
+
generatePreviewHTML(options.themeName, options.port, themeDataPath)
|
|
1363
|
+
);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (pathname === "/preview-runtime.js") {
|
|
1367
|
+
serveFile(res, options.previewRuntimePath);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (pathname.startsWith("/_assets/")) {
|
|
1371
|
+
const parts = pathname.replace(/^\/_assets\//, "").split("/");
|
|
1372
|
+
const assetSubpath = parts.slice(1).join("/");
|
|
1373
|
+
const assetPath = path9.join(options.themePath, "assets", assetSubpath);
|
|
1374
|
+
if (!assetPath.startsWith(path9.join(options.themePath, "assets"))) {
|
|
1375
|
+
res.writeHead(403);
|
|
1376
|
+
res.end("Forbidden");
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
serveFile(res, assetPath);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (pathname.startsWith("/themes/")) {
|
|
1383
|
+
const match = pathname.match(/^\/themes\/[^/]+\/assets\/(.+)/);
|
|
1384
|
+
if (match) {
|
|
1385
|
+
const assetPath = path9.join(options.themePath, "assets", match[1]);
|
|
1386
|
+
if (!assetPath.startsWith(path9.join(options.themePath, "assets"))) {
|
|
1387
|
+
res.writeHead(403);
|
|
1388
|
+
res.end("Forbidden");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
serveFile(res, assetPath);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (pathname.startsWith("/assets/")) {
|
|
1396
|
+
const subpath = pathname.replace(/^\/assets\//, "");
|
|
1397
|
+
const segments = subpath.split("/");
|
|
1398
|
+
const assetsBase = path9.join(options.themePath, "assets");
|
|
1399
|
+
let assetPath;
|
|
1400
|
+
if (segments[0] === options.themeName || segments[0] === options.themeName.replace(/^my-/, "")) {
|
|
1401
|
+
assetPath = path9.join(assetsBase, segments.slice(1).join("/"));
|
|
1402
|
+
} else {
|
|
1403
|
+
assetPath = path9.join(assetsBase, subpath);
|
|
1404
|
+
}
|
|
1405
|
+
if (assetPath.startsWith(assetsBase) && fs3.existsSync(assetPath)) {
|
|
1406
|
+
serveFile(res, assetPath);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
if (segments.length > 1) {
|
|
1410
|
+
const fallbackPath = path9.join(assetsBase, segments.slice(1).join("/"));
|
|
1411
|
+
if (fallbackPath.startsWith(assetsBase) && fs3.existsSync(fallbackPath)) {
|
|
1412
|
+
serveFile(res, fallbackPath);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const filePath = path9.join(options.distDir, pathname);
|
|
1418
|
+
if (!filePath.startsWith(options.distDir)) {
|
|
1419
|
+
res.writeHead(403);
|
|
1420
|
+
res.end("Forbidden");
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (fs3.existsSync(filePath) && fs3.statSync(filePath).isFile()) {
|
|
1424
|
+
serveFile(res, filePath);
|
|
1425
|
+
} else {
|
|
1426
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1427
|
+
res.end(
|
|
1428
|
+
generatePreviewHTML(options.themeName, options.port, themeDataPath)
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
const wss = new WebSocketServer({ server });
|
|
1433
|
+
wss.on("connection", (ws) => {
|
|
1434
|
+
clients.add(ws);
|
|
1435
|
+
ws.on("close", () => clients.delete(ws));
|
|
1436
|
+
});
|
|
1437
|
+
server.listen(options.port);
|
|
1438
|
+
return {
|
|
1439
|
+
broadcast(message) {
|
|
1440
|
+
const data = JSON.stringify(message);
|
|
1441
|
+
for (const client of clients) {
|
|
1442
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1443
|
+
client.send(data);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
close() {
|
|
1448
|
+
wss.close();
|
|
1449
|
+
server.close();
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function serveFile(res, filePath) {
|
|
1454
|
+
try {
|
|
1455
|
+
if (!fs3.existsSync(filePath)) {
|
|
1456
|
+
res.writeHead(404);
|
|
1457
|
+
res.end("Not Found");
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const ext = path9.extname(filePath);
|
|
1461
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
1462
|
+
const content = fs3.readFileSync(filePath);
|
|
1463
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1464
|
+
res.end(content);
|
|
1465
|
+
} catch {
|
|
1466
|
+
res.writeHead(500);
|
|
1467
|
+
res.end("Internal Server Error");
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function generatePreviewHTML(themeName, port, themeDataPath) {
|
|
1471
|
+
let fontLinks = "";
|
|
1472
|
+
let fontVarsCSS = "";
|
|
1473
|
+
if (themeDataPath) {
|
|
1474
|
+
try {
|
|
1475
|
+
const themeData = JSON.parse(fs3.readFileSync(themeDataPath, "utf-8"));
|
|
1476
|
+
const typography = (themeData?.themeConfig || themeData?.theme?.config)?.typography?.fontFamily;
|
|
1477
|
+
if (typography) {
|
|
1478
|
+
const fontFamilies = /* @__PURE__ */ new Set();
|
|
1479
|
+
for (const value of Object.values(typography)) {
|
|
1480
|
+
const primary = value.split(",")[0].trim();
|
|
1481
|
+
if (primary && ![
|
|
1482
|
+
"serif",
|
|
1483
|
+
"sans-serif",
|
|
1484
|
+
"monospace",
|
|
1485
|
+
"system-ui",
|
|
1486
|
+
"Georgia",
|
|
1487
|
+
"Inter",
|
|
1488
|
+
"Consolas"
|
|
1489
|
+
].includes(primary)) {
|
|
1490
|
+
fontFamilies.add(primary);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (fontFamilies.size > 0) {
|
|
1494
|
+
const families = Array.from(fontFamilies).map(
|
|
1495
|
+
(f) => `family=${f.replace(/\s+/g, "+")}:wght@300;400;500;600;700;800;900`
|
|
1496
|
+
).join("&");
|
|
1497
|
+
fontLinks = `<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1498
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1499
|
+
<link href="https://fonts.googleapis.com/css2?${families}&display=swap" rel="stylesheet">`;
|
|
1500
|
+
}
|
|
1501
|
+
const heading = typography.heading || typography.body || "system-ui";
|
|
1502
|
+
const body = typography.body || "system-ui";
|
|
1503
|
+
fontVarsCSS = `
|
|
1504
|
+
:root {
|
|
1505
|
+
--font-heading: ${heading};
|
|
1506
|
+
--font-body: ${body};
|
|
1507
|
+
}
|
|
1508
|
+
body { font-family: var(--font-body); }
|
|
1509
|
+
h1, h2, h3, h4, h5, h6 { font-family: var(--font-heading); }`;
|
|
1510
|
+
}
|
|
1511
|
+
} catch {
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return `<!DOCTYPE html>
|
|
1515
|
+
<html lang="en">
|
|
1516
|
+
<head>
|
|
1517
|
+
<meta charset="UTF-8">
|
|
1518
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1519
|
+
<title>OneX Dev \u2014 ${themeName}</title>
|
|
1520
|
+
${fontLinks}
|
|
1521
|
+
<!-- Tailwind CSS Play CDN \u2014 JIT compilation in browser for dev preview -->
|
|
1522
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
1523
|
+
<script>
|
|
1524
|
+
tailwind.config = {
|
|
1525
|
+
theme: {
|
|
1526
|
+
extend: {
|
|
1527
|
+
aspectRatio: {
|
|
1528
|
+
'2/1': '2 / 1',
|
|
1529
|
+
'3/2': '3 / 2',
|
|
1530
|
+
'4/3': '4 / 3',
|
|
1531
|
+
'3/4': '3 / 4',
|
|
1532
|
+
'5/4': '5 / 4',
|
|
1533
|
+
'16/9': '16 / 9',
|
|
1534
|
+
'21/9': '21 / 9',
|
|
1535
|
+
},
|
|
1536
|
+
fontFamily: {
|
|
1537
|
+
playfair: ['Playfair Display', 'Georgia', 'serif'],
|
|
1538
|
+
roboto: ['Roboto', 'system-ui', 'sans-serif'],
|
|
1539
|
+
},
|
|
1540
|
+
},
|
|
1541
|
+
},
|
|
1542
|
+
}
|
|
1543
|
+
</script>
|
|
1544
|
+
<style>
|
|
1545
|
+
#onex-preview-root { margin-top: 0; }${fontVarsCSS}
|
|
1546
|
+
</style>
|
|
1547
|
+
</head>
|
|
1548
|
+
<body>
|
|
1549
|
+
<div id="onex-preview-root"></div>
|
|
1550
|
+
<script type="module" src="/preview-runtime.js"></script>
|
|
1551
|
+
</body>
|
|
1552
|
+
</html>`;
|
|
1553
|
+
}
|
|
1554
|
+
var MIME_TYPES;
|
|
1555
|
+
var init_dev_server = __esm({
|
|
1556
|
+
"src/utils/dev-server.ts"() {
|
|
1557
|
+
MIME_TYPES = {
|
|
1558
|
+
".js": "application/javascript",
|
|
1559
|
+
".mjs": "application/javascript",
|
|
1560
|
+
".css": "text/css",
|
|
1561
|
+
".json": "application/json",
|
|
1562
|
+
".html": "text/html",
|
|
1563
|
+
".svg": "image/svg+xml",
|
|
1564
|
+
".png": "image/png",
|
|
1565
|
+
".jpg": "image/jpeg",
|
|
1566
|
+
".jpeg": "image/jpeg",
|
|
1567
|
+
".webp": "image/webp",
|
|
1568
|
+
".gif": "image/gif",
|
|
1569
|
+
".map": "application/json"
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1340
1574
|
// src/utils/file-helpers.ts
|
|
1341
1575
|
init_logger();
|
|
1342
1576
|
async function renderTemplate(templatePath, data) {
|
|
@@ -4012,231 +4246,7 @@ async function cloneCommand(themeName, options) {
|
|
|
4012
4246
|
// src/commands/dev.ts
|
|
4013
4247
|
init_logger();
|
|
4014
4248
|
init_compile_theme();
|
|
4015
|
-
|
|
4016
|
-
".js": "application/javascript",
|
|
4017
|
-
".mjs": "application/javascript",
|
|
4018
|
-
".css": "text/css",
|
|
4019
|
-
".json": "application/json",
|
|
4020
|
-
".html": "text/html",
|
|
4021
|
-
".svg": "image/svg+xml",
|
|
4022
|
-
".png": "image/png",
|
|
4023
|
-
".jpg": "image/jpeg",
|
|
4024
|
-
".jpeg": "image/jpeg",
|
|
4025
|
-
".webp": "image/webp",
|
|
4026
|
-
".gif": "image/gif",
|
|
4027
|
-
".map": "application/json"
|
|
4028
|
-
};
|
|
4029
|
-
function createDevServer(options) {
|
|
4030
|
-
const clients = /* @__PURE__ */ new Set();
|
|
4031
|
-
const themeDataPath = path9.join(options.distDir, "theme-data.json");
|
|
4032
|
-
const server = http.createServer((req, res) => {
|
|
4033
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4034
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
4035
|
-
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
4036
|
-
if (req.method === "OPTIONS") {
|
|
4037
|
-
res.writeHead(200);
|
|
4038
|
-
res.end();
|
|
4039
|
-
return;
|
|
4040
|
-
}
|
|
4041
|
-
const url = new URL(req.url || "/", `http://localhost:${options.port}`);
|
|
4042
|
-
const pathname = url.pathname;
|
|
4043
|
-
if (pathname === "/" || pathname === "/index.html") {
|
|
4044
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4045
|
-
res.end(
|
|
4046
|
-
generatePreviewHTML(options.themeName, options.port, themeDataPath)
|
|
4047
|
-
);
|
|
4048
|
-
return;
|
|
4049
|
-
}
|
|
4050
|
-
if (pathname === "/preview-runtime.js") {
|
|
4051
|
-
serveFile(res, options.previewRuntimePath);
|
|
4052
|
-
return;
|
|
4053
|
-
}
|
|
4054
|
-
if (pathname.startsWith("/_assets/")) {
|
|
4055
|
-
const parts = pathname.replace(/^\/_assets\//, "").split("/");
|
|
4056
|
-
const assetSubpath = parts.slice(1).join("/");
|
|
4057
|
-
const assetPath = path9.join(options.themePath, "assets", assetSubpath);
|
|
4058
|
-
if (!assetPath.startsWith(path9.join(options.themePath, "assets"))) {
|
|
4059
|
-
res.writeHead(403);
|
|
4060
|
-
res.end("Forbidden");
|
|
4061
|
-
return;
|
|
4062
|
-
}
|
|
4063
|
-
serveFile(res, assetPath);
|
|
4064
|
-
return;
|
|
4065
|
-
}
|
|
4066
|
-
if (pathname.startsWith("/themes/")) {
|
|
4067
|
-
const match = pathname.match(/^\/themes\/[^/]+\/assets\/(.+)/);
|
|
4068
|
-
if (match) {
|
|
4069
|
-
const assetPath = path9.join(options.themePath, "assets", match[1]);
|
|
4070
|
-
if (!assetPath.startsWith(path9.join(options.themePath, "assets"))) {
|
|
4071
|
-
res.writeHead(403);
|
|
4072
|
-
res.end("Forbidden");
|
|
4073
|
-
return;
|
|
4074
|
-
}
|
|
4075
|
-
serveFile(res, assetPath);
|
|
4076
|
-
return;
|
|
4077
|
-
}
|
|
4078
|
-
}
|
|
4079
|
-
if (pathname.startsWith("/assets/")) {
|
|
4080
|
-
const subpath = pathname.replace(/^\/assets\//, "");
|
|
4081
|
-
const segments = subpath.split("/");
|
|
4082
|
-
const assetsBase = path9.join(options.themePath, "assets");
|
|
4083
|
-
let assetPath;
|
|
4084
|
-
if (segments[0] === options.themeName || segments[0] === options.themeName.replace(/^my-/, "")) {
|
|
4085
|
-
assetPath = path9.join(assetsBase, segments.slice(1).join("/"));
|
|
4086
|
-
} else {
|
|
4087
|
-
assetPath = path9.join(assetsBase, subpath);
|
|
4088
|
-
}
|
|
4089
|
-
if (assetPath.startsWith(assetsBase) && fs3.existsSync(assetPath)) {
|
|
4090
|
-
serveFile(res, assetPath);
|
|
4091
|
-
return;
|
|
4092
|
-
}
|
|
4093
|
-
if (segments.length > 1) {
|
|
4094
|
-
const fallbackPath = path9.join(assetsBase, segments.slice(1).join("/"));
|
|
4095
|
-
if (fallbackPath.startsWith(assetsBase) && fs3.existsSync(fallbackPath)) {
|
|
4096
|
-
serveFile(res, fallbackPath);
|
|
4097
|
-
return;
|
|
4098
|
-
}
|
|
4099
|
-
}
|
|
4100
|
-
}
|
|
4101
|
-
const filePath = path9.join(options.distDir, pathname);
|
|
4102
|
-
if (!filePath.startsWith(options.distDir)) {
|
|
4103
|
-
res.writeHead(403);
|
|
4104
|
-
res.end("Forbidden");
|
|
4105
|
-
return;
|
|
4106
|
-
}
|
|
4107
|
-
if (fs3.existsSync(filePath) && fs3.statSync(filePath).isFile()) {
|
|
4108
|
-
serveFile(res, filePath);
|
|
4109
|
-
} else {
|
|
4110
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4111
|
-
res.end(
|
|
4112
|
-
generatePreviewHTML(options.themeName, options.port, themeDataPath)
|
|
4113
|
-
);
|
|
4114
|
-
}
|
|
4115
|
-
});
|
|
4116
|
-
const wss = new WebSocketServer({ server });
|
|
4117
|
-
wss.on("connection", (ws) => {
|
|
4118
|
-
clients.add(ws);
|
|
4119
|
-
ws.on("close", () => clients.delete(ws));
|
|
4120
|
-
});
|
|
4121
|
-
server.listen(options.port);
|
|
4122
|
-
return {
|
|
4123
|
-
broadcast(message) {
|
|
4124
|
-
const data = JSON.stringify(message);
|
|
4125
|
-
for (const client of clients) {
|
|
4126
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
4127
|
-
client.send(data);
|
|
4128
|
-
}
|
|
4129
|
-
}
|
|
4130
|
-
},
|
|
4131
|
-
close() {
|
|
4132
|
-
wss.close();
|
|
4133
|
-
server.close();
|
|
4134
|
-
}
|
|
4135
|
-
};
|
|
4136
|
-
}
|
|
4137
|
-
function serveFile(res, filePath) {
|
|
4138
|
-
try {
|
|
4139
|
-
if (!fs3.existsSync(filePath)) {
|
|
4140
|
-
res.writeHead(404);
|
|
4141
|
-
res.end("Not Found");
|
|
4142
|
-
return;
|
|
4143
|
-
}
|
|
4144
|
-
const ext = path9.extname(filePath);
|
|
4145
|
-
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
4146
|
-
const content = fs3.readFileSync(filePath);
|
|
4147
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
4148
|
-
res.end(content);
|
|
4149
|
-
} catch {
|
|
4150
|
-
res.writeHead(500);
|
|
4151
|
-
res.end("Internal Server Error");
|
|
4152
|
-
}
|
|
4153
|
-
}
|
|
4154
|
-
function generatePreviewHTML(themeName, port, themeDataPath) {
|
|
4155
|
-
let fontLinks = "";
|
|
4156
|
-
let fontVarsCSS = "";
|
|
4157
|
-
if (themeDataPath) {
|
|
4158
|
-
try {
|
|
4159
|
-
const themeData = JSON.parse(fs3.readFileSync(themeDataPath, "utf-8"));
|
|
4160
|
-
const typography = (themeData?.themeConfig || themeData?.theme?.config)?.typography?.fontFamily;
|
|
4161
|
-
if (typography) {
|
|
4162
|
-
const fontFamilies = /* @__PURE__ */ new Set();
|
|
4163
|
-
for (const value of Object.values(typography)) {
|
|
4164
|
-
const primary = value.split(",")[0].trim();
|
|
4165
|
-
if (primary && ![
|
|
4166
|
-
"serif",
|
|
4167
|
-
"sans-serif",
|
|
4168
|
-
"monospace",
|
|
4169
|
-
"system-ui",
|
|
4170
|
-
"Georgia",
|
|
4171
|
-
"Inter",
|
|
4172
|
-
"Consolas"
|
|
4173
|
-
].includes(primary)) {
|
|
4174
|
-
fontFamilies.add(primary);
|
|
4175
|
-
}
|
|
4176
|
-
}
|
|
4177
|
-
if (fontFamilies.size > 0) {
|
|
4178
|
-
const families = Array.from(fontFamilies).map(
|
|
4179
|
-
(f) => `family=${f.replace(/\s+/g, "+")}:wght@300;400;500;600;700;800;900`
|
|
4180
|
-
).join("&");
|
|
4181
|
-
fontLinks = `<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
4182
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
4183
|
-
<link href="https://fonts.googleapis.com/css2?${families}&display=swap" rel="stylesheet">`;
|
|
4184
|
-
}
|
|
4185
|
-
const heading = typography.heading || typography.body || "system-ui";
|
|
4186
|
-
const body = typography.body || "system-ui";
|
|
4187
|
-
fontVarsCSS = `
|
|
4188
|
-
:root {
|
|
4189
|
-
--font-heading: ${heading};
|
|
4190
|
-
--font-body: ${body};
|
|
4191
|
-
}
|
|
4192
|
-
body { font-family: var(--font-body); }
|
|
4193
|
-
h1, h2, h3, h4, h5, h6 { font-family: var(--font-heading); }`;
|
|
4194
|
-
}
|
|
4195
|
-
} catch {
|
|
4196
|
-
}
|
|
4197
|
-
}
|
|
4198
|
-
return `<!DOCTYPE html>
|
|
4199
|
-
<html lang="en">
|
|
4200
|
-
<head>
|
|
4201
|
-
<meta charset="UTF-8">
|
|
4202
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4203
|
-
<title>OneX Dev \u2014 ${themeName}</title>
|
|
4204
|
-
${fontLinks}
|
|
4205
|
-
<!-- Tailwind CSS Play CDN \u2014 JIT compilation in browser for dev preview -->
|
|
4206
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
4207
|
-
<script>
|
|
4208
|
-
tailwind.config = {
|
|
4209
|
-
theme: {
|
|
4210
|
-
extend: {
|
|
4211
|
-
aspectRatio: {
|
|
4212
|
-
'2/1': '2 / 1',
|
|
4213
|
-
'3/2': '3 / 2',
|
|
4214
|
-
'4/3': '4 / 3',
|
|
4215
|
-
'3/4': '3 / 4',
|
|
4216
|
-
'5/4': '5 / 4',
|
|
4217
|
-
'16/9': '16 / 9',
|
|
4218
|
-
'21/9': '21 / 9',
|
|
4219
|
-
},
|
|
4220
|
-
fontFamily: {
|
|
4221
|
-
playfair: ['Playfair Display', 'Georgia', 'serif'],
|
|
4222
|
-
roboto: ['Roboto', 'system-ui', 'sans-serif'],
|
|
4223
|
-
},
|
|
4224
|
-
},
|
|
4225
|
-
},
|
|
4226
|
-
}
|
|
4227
|
-
</script>
|
|
4228
|
-
<style>
|
|
4229
|
-
#onex-preview-root { margin-top: 0; }${fontVarsCSS}
|
|
4230
|
-
</style>
|
|
4231
|
-
</head>
|
|
4232
|
-
<body>
|
|
4233
|
-
<div id="onex-preview-root"></div>
|
|
4234
|
-
<script type="module" src="/preview-runtime.js"></script>
|
|
4235
|
-
</body>
|
|
4236
|
-
</html>`;
|
|
4237
|
-
}
|
|
4238
|
-
|
|
4239
|
-
// src/commands/dev.ts
|
|
4249
|
+
init_dev_server();
|
|
4240
4250
|
async function devCommand(options) {
|
|
4241
4251
|
logger.header("OneX Dev Server");
|
|
4242
4252
|
let themePath;
|
|
@@ -5071,6 +5081,121 @@ Or use the --bump flag:
|
|
|
5071
5081
|
}
|
|
5072
5082
|
logger.newLine();
|
|
5073
5083
|
logger.success(`\u2713 Theme "${themeId}" v${version2} published!`);
|
|
5084
|
+
await uploadThumbnail(apiUrl, themeId, themePath, distDir);
|
|
5085
|
+
}
|
|
5086
|
+
async function uploadThumbnail(apiUrl, themeId, themePath, distDir) {
|
|
5087
|
+
const THUMBNAIL_CANDIDATES = [
|
|
5088
|
+
{ file: "thumbnail.png", mime: "image/png" },
|
|
5089
|
+
{ file: "thumbnail.jpg", mime: "image/jpeg" },
|
|
5090
|
+
{ file: "thumbnail.jpeg", mime: "image/jpeg" },
|
|
5091
|
+
{ file: "thumbnail.webp", mime: "image/webp" }
|
|
5092
|
+
];
|
|
5093
|
+
let imageBase64 = null;
|
|
5094
|
+
let mimeType = "image/png";
|
|
5095
|
+
for (const { file, mime } of THUMBNAIL_CANDIDATES) {
|
|
5096
|
+
const candidate = path9.join(themePath, file);
|
|
5097
|
+
if (fs.existsSync(candidate)) {
|
|
5098
|
+
const buf = fs.readFileSync(candidate);
|
|
5099
|
+
imageBase64 = `data:${mime};base64,${buf.toString("base64")}`;
|
|
5100
|
+
mimeType = mime;
|
|
5101
|
+
logger.info(`Using local thumbnail: ${file}`);
|
|
5102
|
+
break;
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
if (!imageBase64) {
|
|
5106
|
+
logger.startSpinner("Taking screenshot for thumbnail...");
|
|
5107
|
+
try {
|
|
5108
|
+
const buf = await screenshotHomePage(themePath, distDir);
|
|
5109
|
+
imageBase64 = `data:image/png;base64,${buf.toString("base64")}`;
|
|
5110
|
+
mimeType = "image/png";
|
|
5111
|
+
logger.stopSpinner(true, "Screenshot captured");
|
|
5112
|
+
} catch (err) {
|
|
5113
|
+
logger.stopSpinner(false, "Screenshot failed \u2014 skipping thumbnail");
|
|
5114
|
+
logger.info(
|
|
5115
|
+
"Tip: add thumbnail.png to your theme root to set a custom thumbnail"
|
|
5116
|
+
);
|
|
5117
|
+
return;
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
logger.startSpinner("Uploading thumbnail...");
|
|
5121
|
+
try {
|
|
5122
|
+
const uploadRes = await authenticatedFetch(`${apiUrl}/media/images/upload`, {
|
|
5123
|
+
method: "POST",
|
|
5124
|
+
body: JSON.stringify({
|
|
5125
|
+
prefix: `themes/${themeId}`,
|
|
5126
|
+
image: imageBase64,
|
|
5127
|
+
name: "thumbnail.png"
|
|
5128
|
+
})
|
|
5129
|
+
});
|
|
5130
|
+
const uploadData = await uploadRes.json();
|
|
5131
|
+
const uploadBody = uploadData.statusCode ? uploadData.body : uploadData;
|
|
5132
|
+
if (!uploadRes.ok || !uploadBody.url) {
|
|
5133
|
+
throw new Error(uploadBody.error || "Upload failed");
|
|
5134
|
+
}
|
|
5135
|
+
const patchRes = await authenticatedFetch(
|
|
5136
|
+
`${apiUrl}/website-api/themes/${encodeURIComponent(themeId)}`,
|
|
5137
|
+
{
|
|
5138
|
+
method: "PATCH",
|
|
5139
|
+
body: JSON.stringify({ thumbnail_url: uploadBody.url })
|
|
5140
|
+
}
|
|
5141
|
+
);
|
|
5142
|
+
if (!patchRes.ok) {
|
|
5143
|
+
const patchData = await patchRes.json();
|
|
5144
|
+
const patchBody = patchData.statusCode ? patchData.body : patchData;
|
|
5145
|
+
throw new Error(patchBody.error || "Failed to set thumbnail");
|
|
5146
|
+
}
|
|
5147
|
+
logger.stopSpinner(true, "Thumbnail set");
|
|
5148
|
+
} catch (err) {
|
|
5149
|
+
logger.stopSpinner(false, "Thumbnail upload skipped");
|
|
5150
|
+
logger.info(
|
|
5151
|
+
`Theme published successfully. Thumbnail can be updated later.`
|
|
5152
|
+
);
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
async function screenshotHomePage(themePath, distDir) {
|
|
5156
|
+
const { compilePreviewRuntime: compilePreviewRuntime2 } = await Promise.resolve().then(() => (init_compile_theme(), compile_theme_exports));
|
|
5157
|
+
const { createDevServer: createDevServer2 } = await Promise.resolve().then(() => (init_dev_server(), dev_server_exports));
|
|
5158
|
+
const previewRuntimePath = await compilePreviewRuntime2(themePath);
|
|
5159
|
+
const themeName = path9.basename(themePath);
|
|
5160
|
+
const port = await findFreePort(4500);
|
|
5161
|
+
const server = createDevServer2({
|
|
5162
|
+
port,
|
|
5163
|
+
distDir,
|
|
5164
|
+
previewRuntimePath,
|
|
5165
|
+
themeName,
|
|
5166
|
+
themePath
|
|
5167
|
+
});
|
|
5168
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
5169
|
+
const puppeteer = await import('puppeteer');
|
|
5170
|
+
const browser = await puppeteer.default.launch({
|
|
5171
|
+
headless: true,
|
|
5172
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
5173
|
+
});
|
|
5174
|
+
try {
|
|
5175
|
+
const page = await browser.newPage();
|
|
5176
|
+
await page.setViewport({ width: 1440, height: 900 });
|
|
5177
|
+
await page.goto(`http://localhost:${port}`, {
|
|
5178
|
+
waitUntil: "networkidle0",
|
|
5179
|
+
timeout: 3e4
|
|
5180
|
+
});
|
|
5181
|
+
await page.waitForSelector("#onex-preview-root > *", { timeout: 15e3 });
|
|
5182
|
+
const screenshot = await page.screenshot({ fullPage: true, type: "png" });
|
|
5183
|
+
return Buffer.from(screenshot);
|
|
5184
|
+
} finally {
|
|
5185
|
+
await browser.close();
|
|
5186
|
+
server.close();
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
async function findFreePort(start) {
|
|
5190
|
+
const net = await import('net');
|
|
5191
|
+
return new Promise((resolve) => {
|
|
5192
|
+
const srv = net.createServer();
|
|
5193
|
+
srv.listen(start, () => {
|
|
5194
|
+
const addr = srv.address();
|
|
5195
|
+
srv.close(() => resolve(addr.port));
|
|
5196
|
+
});
|
|
5197
|
+
srv.on("error", () => resolve(findFreePort(start + 1)));
|
|
5198
|
+
});
|
|
5074
5199
|
}
|
|
5075
5200
|
async function uploadVideoMultipart(apiUrl, themeId, video) {
|
|
5076
5201
|
const fileName = path9.basename(video.originalPath);
|