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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nowebcli",
3
- "version": "0.2.13",
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
- console.log(`Created ${validSlug} in ${directory}. Add a 16:9 public/cover.jpg (JPEG, up to 5 MB).`);
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) console.log(`✓ ${currentSlug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
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) console.log(`✓ ${result.slug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
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
- console.log("Created public/cover.jpg (1920×1080, 16:9). Check the image before uploading.");
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
- console.log(`✓ rebuilt ${template.slug}`);
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
- console.log(`Ready to upload ${template.slug}`);
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
- console.log(`Uploaded ${template.slug} (unpublished): ${new URL(result.url, apiBase)}`);
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
- console.log(`Published ${template.slug}: ${new URL(result.url, apiBase)}`);
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
- console.log(`Deleted ${selectedSlug}`);
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
- console.log(`Downloaded ${downloadedSlug} into ${root}`);
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
- console.log("✓ Logged in to Noweb");
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
- console.log(`Published ${original.slug} as template ${published.id}`);
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
- console.error(`\n${error instanceof Error ? error.message : error}`);
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(path.join(nodePath, ".bin", "esbuild"), [
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(path.join(nodePath, ".bin", "esbuild"), [
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
  }