@onexapis/cli 1.1.51 → 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 +468 -233
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +481 -246
- 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,246 +4246,22 @@ async function cloneCommand(themeName, options) {
|
|
|
4012
4246
|
// src/commands/dev.ts
|
|
4013
4247
|
init_logger();
|
|
4014
4248
|
init_compile_theme();
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
|
|
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
|
|
4240
|
-
async function devCommand(options) {
|
|
4241
|
-
logger.header("OneX Dev Server");
|
|
4242
|
-
let themePath;
|
|
4243
|
-
let themeName;
|
|
4244
|
-
if (options.theme) {
|
|
4245
|
-
themeName = options.theme;
|
|
4246
|
-
try {
|
|
4247
|
-
const workspaceThemePath = path9.join(getThemesDir(), themeName);
|
|
4248
|
-
if (fs.existsSync(workspaceThemePath)) {
|
|
4249
|
-
themePath = workspaceThemePath;
|
|
4250
|
-
} else {
|
|
4251
|
-
themePath = path9.join(process.cwd(), themeName);
|
|
4252
|
-
}
|
|
4253
|
-
} catch {
|
|
4254
|
-
themePath = path9.join(process.cwd(), themeName);
|
|
4249
|
+
init_dev_server();
|
|
4250
|
+
async function devCommand(options) {
|
|
4251
|
+
logger.header("OneX Dev Server");
|
|
4252
|
+
let themePath;
|
|
4253
|
+
let themeName;
|
|
4254
|
+
if (options.theme) {
|
|
4255
|
+
themeName = options.theme;
|
|
4256
|
+
try {
|
|
4257
|
+
const workspaceThemePath = path9.join(getThemesDir(), themeName);
|
|
4258
|
+
if (fs.existsSync(workspaceThemePath)) {
|
|
4259
|
+
themePath = workspaceThemePath;
|
|
4260
|
+
} else {
|
|
4261
|
+
themePath = path9.join(process.cwd(), themeName);
|
|
4262
|
+
}
|
|
4263
|
+
} catch {
|
|
4264
|
+
themePath = path9.join(process.cwd(), themeName);
|
|
4255
4265
|
}
|
|
4256
4266
|
if (!fs.existsSync(themePath)) {
|
|
4257
4267
|
logger.error(`Theme "${themeName}" not found.`);
|
|
@@ -4625,6 +4635,18 @@ var MIME_MAP = {
|
|
|
4625
4635
|
".json": "application/json"
|
|
4626
4636
|
};
|
|
4627
4637
|
var HASH_LEN = 8;
|
|
4638
|
+
var VIDEO_EXTENSIONS = [
|
|
4639
|
+
".mp4",
|
|
4640
|
+
".webm",
|
|
4641
|
+
".ogg",
|
|
4642
|
+
".mov",
|
|
4643
|
+
".avi",
|
|
4644
|
+
".mkv"
|
|
4645
|
+
];
|
|
4646
|
+
function isVideoAsset(filePath) {
|
|
4647
|
+
const lower = filePath.toLowerCase();
|
|
4648
|
+
return VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
4649
|
+
}
|
|
4628
4650
|
function mimeFor(filename) {
|
|
4629
4651
|
const ext = path9.extname(filename).toLowerCase();
|
|
4630
4652
|
return MIME_MAP[ext] || "application/octet-stream";
|
|
@@ -4826,8 +4848,30 @@ Or use the --bump flag:
|
|
|
4826
4848
|
);
|
|
4827
4849
|
process.exit(1);
|
|
4828
4850
|
}
|
|
4851
|
+
const videoAssets = assetEntries.filter((a) => isVideoAsset(a.originalPath));
|
|
4852
|
+
const regularAssets = assetEntries.filter(
|
|
4853
|
+
(a) => !isVideoAsset(a.originalPath)
|
|
4854
|
+
);
|
|
4855
|
+
const videoUrls = {};
|
|
4856
|
+
if (videoAssets.length > 0) {
|
|
4857
|
+
logger.startSpinner(`Uploading ${videoAssets.length} video(s)...`);
|
|
4858
|
+
try {
|
|
4859
|
+
for (const video of videoAssets) {
|
|
4860
|
+
const url = await uploadVideoMultipart(apiUrl, themeId, video);
|
|
4861
|
+
videoUrls[video.originalPath] = url;
|
|
4862
|
+
}
|
|
4863
|
+
logger.stopSpinner(true, `Uploaded ${videoAssets.length} video(s)`);
|
|
4864
|
+
} catch (error) {
|
|
4865
|
+
logger.stopSpinner(false, "Video upload failed");
|
|
4866
|
+
logger.error(error instanceof Error ? error.message : "Upload error");
|
|
4867
|
+
process.exit(1);
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4829
4870
|
try {
|
|
4830
|
-
const assetMap = buildAssetMap(
|
|
4871
|
+
const assetMap = buildAssetMap(regularAssets);
|
|
4872
|
+
for (const [originalPath, url] of Object.entries(videoUrls)) {
|
|
4873
|
+
assetMap[originalPath] = url;
|
|
4874
|
+
}
|
|
4831
4875
|
const assetMapPath = path9.join(distDir, "asset-map.json");
|
|
4832
4876
|
await fs.writeFile(assetMapPath, JSON.stringify(assetMap, null, 2));
|
|
4833
4877
|
} catch (error) {
|
|
@@ -4848,7 +4892,7 @@ Or use the --bump flag:
|
|
|
4848
4892
|
method: "POST",
|
|
4849
4893
|
body: JSON.stringify({
|
|
4850
4894
|
version: version2,
|
|
4851
|
-
assets:
|
|
4895
|
+
assets: regularAssets.map((a) => ({
|
|
4852
4896
|
path: a.hashedPath,
|
|
4853
4897
|
hash: a.hash,
|
|
4854
4898
|
size: a.size,
|
|
@@ -4882,7 +4926,7 @@ Or use the --bump flag:
|
|
|
4882
4926
|
if (assetUploads.length > 0) {
|
|
4883
4927
|
logger.startSpinner(`Uploading ${assetUploads.length} asset(s) to S3...`);
|
|
4884
4928
|
const CONCURRENCY = 8;
|
|
4885
|
-
const byHashedPath = new Map(
|
|
4929
|
+
const byHashedPath = new Map(regularAssets.map((a) => [a.hashedPath, a]));
|
|
4886
4930
|
const queue = [...assetUploads];
|
|
4887
4931
|
let uploaded = 0;
|
|
4888
4932
|
let failed = 0;
|
|
@@ -5037,6 +5081,197 @@ Or use the --bump flag:
|
|
|
5037
5081
|
}
|
|
5038
5082
|
logger.newLine();
|
|
5039
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
|
+
});
|
|
5199
|
+
}
|
|
5200
|
+
async function uploadVideoMultipart(apiUrl, themeId, video) {
|
|
5201
|
+
const fileName = path9.basename(video.originalPath);
|
|
5202
|
+
const initRes = await authenticatedFetch(
|
|
5203
|
+
`${apiUrl}/media/videos/multipart/init`,
|
|
5204
|
+
{
|
|
5205
|
+
method: "POST",
|
|
5206
|
+
body: JSON.stringify({
|
|
5207
|
+
file_name: fileName,
|
|
5208
|
+
content_type: video.contentType,
|
|
5209
|
+
file_size: video.size,
|
|
5210
|
+
prefix: `themes/${themeId}/assets`
|
|
5211
|
+
})
|
|
5212
|
+
}
|
|
5213
|
+
);
|
|
5214
|
+
const initData = await initRes.json();
|
|
5215
|
+
const initBody = initData.statusCode ? initData.body : initData;
|
|
5216
|
+
if (!initRes.ok || !initBody.upload_id) {
|
|
5217
|
+
throw new Error(
|
|
5218
|
+
`Init multipart failed for ${fileName}: ${initBody.error || initRes.status}`
|
|
5219
|
+
);
|
|
5220
|
+
}
|
|
5221
|
+
const { upload_id, file_key, chunk_size, chunk_urls } = initBody;
|
|
5222
|
+
const fileBuffer = await fs.promises.readFile(video.absPath);
|
|
5223
|
+
const CHUNK_CONCURRENCY = 4;
|
|
5224
|
+
const queue = [...chunk_urls];
|
|
5225
|
+
const parts = [];
|
|
5226
|
+
async function chunkWorker() {
|
|
5227
|
+
while (queue.length > 0) {
|
|
5228
|
+
const chunk = queue.shift();
|
|
5229
|
+
if (!chunk) break;
|
|
5230
|
+
const start = (chunk.part_number - 1) * chunk_size;
|
|
5231
|
+
const end = Math.min(start + chunk_size, fileBuffer.length);
|
|
5232
|
+
const body = fileBuffer.subarray(start, end);
|
|
5233
|
+
const res = await fetch(chunk.upload_url, { method: "PUT", body });
|
|
5234
|
+
if (!res.ok) {
|
|
5235
|
+
throw new Error(
|
|
5236
|
+
`Chunk ${chunk.part_number} upload failed: HTTP ${res.status}`
|
|
5237
|
+
);
|
|
5238
|
+
}
|
|
5239
|
+
const etag = res.headers.get("etag") || res.headers.get("ETag");
|
|
5240
|
+
if (!etag) {
|
|
5241
|
+
throw new Error(`Chunk ${chunk.part_number}: missing ETag header`);
|
|
5242
|
+
}
|
|
5243
|
+
parts.push({ part_number: chunk.part_number, etag });
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
await Promise.all(
|
|
5247
|
+
Array.from(
|
|
5248
|
+
{ length: Math.min(CHUNK_CONCURRENCY, chunk_urls.length) },
|
|
5249
|
+
() => chunkWorker()
|
|
5250
|
+
)
|
|
5251
|
+
);
|
|
5252
|
+
parts.sort((a, b) => a.part_number - b.part_number);
|
|
5253
|
+
const completeRes = await authenticatedFetch(
|
|
5254
|
+
`${apiUrl}/media/videos/multipart/complete`,
|
|
5255
|
+
{
|
|
5256
|
+
method: "POST",
|
|
5257
|
+
body: JSON.stringify({ upload_id, file_key, parts })
|
|
5258
|
+
}
|
|
5259
|
+
);
|
|
5260
|
+
const completeData = await completeRes.json();
|
|
5261
|
+
const completeBody = completeData.statusCode ? completeData.body : completeData;
|
|
5262
|
+
if (!completeRes.ok || !completeBody.url) {
|
|
5263
|
+
try {
|
|
5264
|
+
await authenticatedFetch(`${apiUrl}/media/videos/multipart/abort`, {
|
|
5265
|
+
method: "POST",
|
|
5266
|
+
body: JSON.stringify({ upload_id, file_key })
|
|
5267
|
+
});
|
|
5268
|
+
} catch {
|
|
5269
|
+
}
|
|
5270
|
+
throw new Error(
|
|
5271
|
+
`Complete multipart failed for ${fileName}: ${completeBody.error || completeRes.status}`
|
|
5272
|
+
);
|
|
5273
|
+
}
|
|
5274
|
+
return completeBody.url;
|
|
5040
5275
|
}
|
|
5041
5276
|
async function createZip(sourceDir, outputPath, exclude) {
|
|
5042
5277
|
const archiver2 = (await import('archiver')).default;
|