nowebcli 0.2.13 → 0.2.15
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/package.json +2 -1
- package/scripts/noweb.mjs +47 -14
- package/templates/templateCompiler.ts +8 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nowebcli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Create, preview and publish community templates for Noweb",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"motion": "^12.40.0",
|
|
69
69
|
"next": "^16.2.10",
|
|
70
70
|
"ogl": "^1.0.11",
|
|
71
|
+
"ora": "^9.4.1",
|
|
71
72
|
"pg": "^8.22.0",
|
|
72
73
|
"playwright": "^1.61.1",
|
|
73
74
|
"postcss": "^8.5.6",
|
package/scripts/noweb.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { basename, dirname, extname, join, resolve, sep } from "node:path";
|
|
|
10
10
|
import { createInterface } from "node:readline/promises";
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
12
|
import { register } from "tsx/esm/api";
|
|
13
|
+
import { styleText } from "node:util";
|
|
14
|
+
import ora from "ora";
|
|
13
15
|
|
|
14
16
|
const root = process.cwd();
|
|
15
17
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -26,6 +28,20 @@ const apiBase = (process.env.NOWEB_URL || "https://noweb.app").replace(/\/$/, ""
|
|
|
26
28
|
const clientId = "noweb-cli";
|
|
27
29
|
register({ tsconfig: join(engineRoot, "tsconfig.json") });
|
|
28
30
|
|
|
31
|
+
const terminalColors = process.env.NO_COLOR === undefined && process.env.NODE_DISABLE_COLORS !== "1";
|
|
32
|
+
const progress = ora({
|
|
33
|
+
spinner: "dots",
|
|
34
|
+
color: terminalColors ? "cyan" : false,
|
|
35
|
+
prefixText: styleText("magenta", "noweb"),
|
|
36
|
+
isSilent: !process.stderr.isTTY || Boolean(process.env.CI),
|
|
37
|
+
discardStdin: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function success(message) {
|
|
41
|
+
progress.stop();
|
|
42
|
+
console.log(styleText("green", message));
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
function usage() {
|
|
30
46
|
console.log(`noweb
|
|
31
47
|
|
|
@@ -58,7 +74,7 @@ async function createTemplate(value) {
|
|
|
58
74
|
writeFile(join(directory, "public/.gitkeep"), ""),
|
|
59
75
|
]);
|
|
60
76
|
await setupWorkspace(shouldInstall);
|
|
61
|
-
|
|
77
|
+
success(`Created ${validSlug} in ${directory}. Add a 16:9 public/cover.jpg (JPEG, up to 5 MB).`);
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
async function setupWorkspace(install = true) {
|
|
@@ -71,6 +87,7 @@ async function setupWorkspace(install = true) {
|
|
|
71
87
|
]);
|
|
72
88
|
}
|
|
73
89
|
if (!install) return;
|
|
90
|
+
progress.stop();
|
|
74
91
|
const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install"], { cwd: root, stdio: "inherit" });
|
|
75
92
|
if (result.status !== 0) throw new Error("Could not install the template workspace dependencies.");
|
|
76
93
|
}
|
|
@@ -82,16 +99,18 @@ async function checkRepositoryTemplates(value, quiet = false, assetRoot = "/asse
|
|
|
82
99
|
const results = [];
|
|
83
100
|
for (const currentSlug of slugs) {
|
|
84
101
|
const assetBase = assetRoot === "/assets" ? assetRoot : `${assetRoot}/${currentSlug}`;
|
|
102
|
+
if (!quiet) progress.start(`Compiling ${currentSlug}…`);
|
|
85
103
|
const result = await buildTemplate(join(repositoryTemplatesRoot, currentSlug), currentSlug, assetBase);
|
|
86
104
|
results.push(result);
|
|
87
|
-
if (!quiet)
|
|
105
|
+
if (!quiet) success(`✓ ${currentSlug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
|
|
88
106
|
}
|
|
89
107
|
return results;
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
async function checkProject(quiet = false) {
|
|
111
|
+
if (!quiet) progress.start("Validating and compiling template…");
|
|
93
112
|
const result = await buildTemplate(root);
|
|
94
|
-
if (!quiet)
|
|
113
|
+
if (!quiet) success(`✓ ${result.slug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
|
|
95
114
|
return result;
|
|
96
115
|
}
|
|
97
116
|
|
|
@@ -141,6 +160,7 @@ async function createCover() {
|
|
|
141
160
|
const coverPath = join(root, "public", "cover.jpg");
|
|
142
161
|
const force = cliArgs.includes("--force");
|
|
143
162
|
if (!force && existsSync(coverPath)) throw new Error("public/cover.jpg already exists. Use `noweb cover --force` to replace it.");
|
|
163
|
+
progress.start("Compiling cover preview…");
|
|
144
164
|
const template = await buildTemplate(root, undefined, "/assets", false);
|
|
145
165
|
const server = createServer(async (request, response) => {
|
|
146
166
|
try {
|
|
@@ -162,6 +182,8 @@ async function createCover() {
|
|
|
162
182
|
server.once("error", reject);
|
|
163
183
|
server.listen(0, "127.0.0.1", resolve);
|
|
164
184
|
});
|
|
185
|
+
progress.color = terminalColors ? "magenta" : false;
|
|
186
|
+
progress.text = "Starting Chromium…";
|
|
165
187
|
browser = await chromium.launch({ headless: true, args: ["--enable-unsafe-swiftshader", "--enable-unsafe-webgpu", "--use-angle=swiftshader"] });
|
|
166
188
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 }, deviceScaleFactor: 1 });
|
|
167
189
|
const errors = [];
|
|
@@ -180,13 +202,17 @@ async function createCover() {
|
|
|
180
202
|
new Promise((resolve) => setTimeout(resolve, 10_000)),
|
|
181
203
|
]);
|
|
182
204
|
});
|
|
205
|
+
progress.color = terminalColors ? "yellow" : false;
|
|
206
|
+
progress.text = "Rendering shaders — waiting 5 seconds…";
|
|
183
207
|
await page.waitForTimeout(5000);
|
|
208
|
+
progress.color = terminalColors ? "cyan" : false;
|
|
209
|
+
progress.text = "Saving 1920×1080 cover…";
|
|
184
210
|
const bytes = await page.screenshot({ type: "jpeg", quality: 90, fullPage: false });
|
|
185
211
|
const { validateTemplateCover } = await load("lib/template-cover.ts");
|
|
186
212
|
validateTemplateCover(bytes);
|
|
187
213
|
await mkdir(join(root, "public"), { recursive: true });
|
|
188
214
|
await writeFile(coverPath, bytes, { flag: force ? "w" : "wx" });
|
|
189
|
-
|
|
215
|
+
success("Created public/cover.jpg (1920×1080, 16:9). Check the image before uploading.");
|
|
190
216
|
} finally {
|
|
191
217
|
await browser?.close();
|
|
192
218
|
server.closeAllConnections();
|
|
@@ -205,7 +231,7 @@ async function dev() {
|
|
|
205
231
|
const template = await checkProject(true);
|
|
206
232
|
response.writeHead(200, { "Cache-Control": "no-store", "Content-Type": "text/html; charset=utf-8" });
|
|
207
233
|
response.end(previewHtml(template));
|
|
208
|
-
|
|
234
|
+
success(`✓ rebuilt ${template.slug}`);
|
|
209
235
|
return;
|
|
210
236
|
}
|
|
211
237
|
await servePublic(url.pathname, response);
|
|
@@ -219,14 +245,14 @@ async function dev() {
|
|
|
219
245
|
process.exitCode = 1;
|
|
220
246
|
});
|
|
221
247
|
server.listen(port, "127.0.0.1", () => {
|
|
222
|
-
console.log(`Preview: http://127.0.0.1:${port}\nRefresh the page to rebuild.`);
|
|
248
|
+
console.log(styleText("cyan", `Preview: http://127.0.0.1:${port}\nRefresh the page to rebuild.`));
|
|
223
249
|
});
|
|
224
250
|
}
|
|
225
251
|
|
|
226
252
|
async function upload() {
|
|
227
253
|
const template = await checkProject();
|
|
228
254
|
if (dryRun) {
|
|
229
|
-
|
|
255
|
+
success(`Ready to upload ${template.slug}`);
|
|
230
256
|
return;
|
|
231
257
|
}
|
|
232
258
|
|
|
@@ -242,6 +268,7 @@ async function upload() {
|
|
|
242
268
|
for (const file of template.assetFiles) {
|
|
243
269
|
form.append("assets", new Blob([file.body]), basename(file.path));
|
|
244
270
|
}
|
|
271
|
+
progress.start("Uploading template, assets and cover…");
|
|
245
272
|
const response = await fetch(`${apiBase}/api/templates/upload`, {
|
|
246
273
|
method: "POST",
|
|
247
274
|
headers: { Authorization: `Bearer ${credentials.token}` },
|
|
@@ -251,13 +278,14 @@ async function upload() {
|
|
|
251
278
|
if (!response.ok) throw new Error(result.error || `Upload failed (${response.status}).`);
|
|
252
279
|
if (!Number.isInteger(result.id) || result.id < 1) throw new Error("Upload returned an invalid template ID.");
|
|
253
280
|
await writeFile(join(root, "template.json"), `${JSON.stringify({ id: result.id, slug: template.slug, type: template.type }, null, 2)}\n`);
|
|
254
|
-
|
|
281
|
+
success(`Uploaded ${template.slug} (${result.is_published ? "public" : "unpublished"}): ${new URL(result.url, apiBase)}`);
|
|
255
282
|
}
|
|
256
283
|
|
|
257
284
|
async function publishUploadedTemplate() {
|
|
258
285
|
const template = JSON.parse(await readFile(join(root, "template.json"), "utf8"));
|
|
259
286
|
if (!Number.isInteger(template.id) || template.id < 1) throw new Error("Upload this template first: npx nowebcli upload");
|
|
260
287
|
const credentials = await requireCredentials();
|
|
288
|
+
progress.start("Publishing template…");
|
|
261
289
|
const response = await fetch(`${apiBase}/api/templates/visibility`, {
|
|
262
290
|
method: "PATCH",
|
|
263
291
|
headers: { Authorization: `Bearer ${credentials.token}`, "Content-Type": "application/json" },
|
|
@@ -265,7 +293,7 @@ async function publishUploadedTemplate() {
|
|
|
265
293
|
});
|
|
266
294
|
const result = await response.json().catch(() => ({}));
|
|
267
295
|
if (!response.ok) throw new Error(result.error || `Publish failed (${response.status}).`);
|
|
268
|
-
|
|
296
|
+
success(`Published ${template.slug}: ${new URL(result.url, apiBase)}`);
|
|
269
297
|
}
|
|
270
298
|
|
|
271
299
|
async function deleteTemplate(value) {
|
|
@@ -289,7 +317,7 @@ async function deleteTemplate(value) {
|
|
|
289
317
|
});
|
|
290
318
|
const result = await response.json().catch(() => ({}));
|
|
291
319
|
if (!response.ok) throw new Error(result.error || `Delete failed (${response.status}).`);
|
|
292
|
-
|
|
320
|
+
success(`Deleted ${selectedSlug}`);
|
|
293
321
|
}
|
|
294
322
|
|
|
295
323
|
async function listTemplates() {
|
|
@@ -323,6 +351,7 @@ async function downloadTemplate(value) {
|
|
|
323
351
|
}
|
|
324
352
|
|
|
325
353
|
const credentials = await requireCredentials();
|
|
354
|
+
progress.start(`Downloading ${selectedSlug}…`);
|
|
326
355
|
const response = await fetch(`${apiBase}/api/templates/download?slug=${encodeURIComponent(selectedSlug)}`, {
|
|
327
356
|
headers: { Authorization: `Bearer ${credentials.token}` },
|
|
328
357
|
});
|
|
@@ -364,7 +393,7 @@ async function downloadTemplate(value) {
|
|
|
364
393
|
...(!assets.length ? [writeFile(join(root, "public/.gitkeep"), "")] : []),
|
|
365
394
|
]);
|
|
366
395
|
await setupWorkspace(shouldInstall);
|
|
367
|
-
|
|
396
|
+
success(`Downloaded ${downloadedSlug} into ${root}`);
|
|
368
397
|
}
|
|
369
398
|
|
|
370
399
|
async function login() {
|
|
@@ -378,6 +407,7 @@ async function login() {
|
|
|
378
407
|
|
|
379
408
|
console.log(`Open ${device.verification_uri}\nCode: ${device.user_code}`);
|
|
380
409
|
openBrowser(device.verification_uri_complete);
|
|
410
|
+
progress.start("Waiting for browser login…");
|
|
381
411
|
let interval = Math.max(1, Number(device.interval) || 5);
|
|
382
412
|
const expiresAt = Date.now() + Number(device.expires_in) * 1000;
|
|
383
413
|
while (Date.now() < expiresAt) {
|
|
@@ -398,7 +428,7 @@ async function login() {
|
|
|
398
428
|
token: token.access_token,
|
|
399
429
|
expiresAt: Date.now() + Number(token.expires_in) * 1000,
|
|
400
430
|
});
|
|
401
|
-
|
|
431
|
+
success("✓ Logged in to Noweb");
|
|
402
432
|
return;
|
|
403
433
|
}
|
|
404
434
|
if (token.error === "authorization_pending") continue;
|
|
@@ -511,7 +541,7 @@ async function publish(value) {
|
|
|
511
541
|
continue;
|
|
512
542
|
}
|
|
513
543
|
await (await load("lib/template-screenshot.ts")).generateTemplatePreview(Number(published.id));
|
|
514
|
-
|
|
544
|
+
success(`Published ${original.slug} as template ${published.id}`);
|
|
515
545
|
}
|
|
516
546
|
} finally {
|
|
517
547
|
await sql.end({ timeout: 5 });
|
|
@@ -741,6 +771,9 @@ try {
|
|
|
741
771
|
else if (command === "help" || command === "--help" || command === "-h") usage();
|
|
742
772
|
else throw new Error(`Unknown command: ${command}`);
|
|
743
773
|
} catch (error) {
|
|
744
|
-
|
|
774
|
+
progress.stop();
|
|
775
|
+
console.error(styleText("red", `\n${error instanceof Error ? error.message : error}`));
|
|
745
776
|
process.exitCode = 1;
|
|
777
|
+
} finally {
|
|
778
|
+
progress.stop();
|
|
746
779
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
@@ -13,12 +12,9 @@ import { validateTemplateCode } from "./validateTemplateCode.ts";
|
|
|
13
12
|
|
|
14
13
|
const run = promisify(execFile);
|
|
15
14
|
const templatesPath = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const packageModulesPath = path.join(path.dirname(templatesPath), "node_modules");
|
|
17
|
-
const modulesPath = existsSync(path.join(packageModulesPath, ".bin", "esbuild"))
|
|
18
|
-
? packageModulesPath
|
|
19
|
-
: path.join(process.cwd(), "node_modules");
|
|
20
|
-
|
|
21
15
|
const require = createRequire(import.meta.url);
|
|
16
|
+
const esbuildPath = require.resolve("esbuild/bin/esbuild");
|
|
17
|
+
const modulesPath = path.dirname(path.dirname(require.resolve("react/package.json")));
|
|
22
18
|
|
|
23
19
|
const TEMPLATE_COMPILE_TIMEOUT_MS = 10_000;
|
|
24
20
|
const TEMPLATE_COMPILE_MAX_BUFFER = 8 * 1024 * 1024;
|
|
@@ -46,7 +42,7 @@ export async function compileTemplate(
|
|
|
46
42
|
await writeFile(input, withRuntimeImports(code));
|
|
47
43
|
const nodePath = modulesPath;
|
|
48
44
|
|
|
49
|
-
await runTemplateCommand(
|
|
45
|
+
await runTemplateCommand(esbuildPath, [
|
|
50
46
|
input,
|
|
51
47
|
"--bundle",
|
|
52
48
|
"--format=esm",
|
|
@@ -121,7 +117,7 @@ window.__nowebRenderTemplate = (element, props) => {
|
|
|
121
117
|
|
|
122
118
|
const nodePath = modulesPath;
|
|
123
119
|
|
|
124
|
-
await runTemplateCommand(
|
|
120
|
+
await runTemplateCommand(esbuildPath, [
|
|
125
121
|
entry,
|
|
126
122
|
"--bundle",
|
|
127
123
|
"--format=iife",
|
|
@@ -203,5 +199,9 @@ async function runTemplateCommand(
|
|
|
203
199
|
},
|
|
204
200
|
maxBuffer: TEMPLATE_COMPILE_MAX_BUFFER,
|
|
205
201
|
timeout: TEMPLATE_COMPILE_TIMEOUT_MS,
|
|
202
|
+
}).catch((error) => {
|
|
203
|
+
throw new Error(error.killed
|
|
204
|
+
? "Template compilation timed out after 10 seconds."
|
|
205
|
+
: error.stderr?.trim() || "Template compilation failed.");
|
|
206
206
|
});
|
|
207
207
|
}
|