nowebcli 0.2.16 → 0.2.18

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,7 +1,7 @@
1
1
  {
2
2
  "name": "nowebcli",
3
- "version": "0.2.16",
4
- "description": "Create, preview and publish community templates for Noweb",
3
+ "version": "0.2.18",
4
+ "description": "Create, preview and upload community templates for Noweb",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -38,7 +38,6 @@
38
38
  "screenshots": "tsx scripts/generate-template-screenshots.ts",
39
39
  "fonts:update": "node --import tsx scripts/download-template-fonts.mjs",
40
40
  "template": "node scripts/noweb.mjs",
41
- "template:publish": "node --env-file-if-exists=.env --env-file-if-exists=.env.local scripts/noweb.mjs publish",
42
41
  "migrate": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-database.ts",
43
42
  "migrate:project-data": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-project-data.ts",
44
43
  "ci": "npm audit --omit=dev --audit-level=high && npm run typecheck && npm run lint && npm run test && npm run build && npm run test:e2e"
package/scripts/noweb.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn, spawnSync } from "node:child_process";
4
- import { createHash, randomBytes } from "node:crypto";
4
+ import { createHash } from "node:crypto";
5
5
  import { existsSync } from "node:fs";
6
6
  import { createServer } from "node:http";
7
7
  import { access, chmod, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
@@ -17,11 +17,10 @@ const root = process.cwd();
17
17
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
18
18
  const engineRoot = existsSync(join(root, "templates/templateCompiler.ts")) ? root : packageRoot;
19
19
  const repositoryTemplatesRoot = join(root, "templates");
20
- const types = new Set(["portfolio", "app", "business", "other"]);
21
20
  const maxAssetBytes = 20 * 1024 * 1024;
22
21
  const maxAssetsBytes = 50 * 1024 * 1024;
23
22
  const cliArgs = process.argv.slice(2);
24
- const [command = "help", slug] = cliArgs.filter((argument) => !argument.startsWith("--"));
23
+ const [command = "help", selection] = cliArgs.filter((argument) => !argument.startsWith("--"));
25
24
  const shouldInstall = !cliArgs.includes("--no-install");
26
25
  const dryRun = cliArgs.includes("--dry-run");
27
26
  const apiBase = (process.env.NOWEB_URL || "https://noweb.app").replace(/\/$/, "");
@@ -45,22 +44,21 @@ function success(message) {
45
44
  function usage() {
46
45
  console.log(`noweb
47
46
 
48
- create [slug] scaffold the current folder
49
- reset [slug] reset template files and public/; keep dependencies (--yes)
47
+ create scaffold the current folder
48
+ reset reset template files and public/; keep dependencies (--yes)
50
49
  setup install local React and TypeScript dependencies
51
50
  dev preview at http://127.0.0.1:4173
52
51
  check validate and compile this template
53
52
  cover create public/cover.jpg locally (1920×1080; --force to replace)
54
53
  login connect the CLI to your Noweb account
55
54
  list list your uploaded templates
56
- download <slug> restore one into the current folder
55
+ download <id> restore one into the current folder
57
56
  upload save this template without publishing
58
- delete [slug] delete one of your templates
59
- publish make the uploaded template public`);
57
+ delete [id] delete one of your templates`);
60
58
  }
61
59
 
62
60
  async function createTemplate(value, install = shouldInstall) {
63
- const validSlug = requireSlug(value || `template-${randomBytes(6).toString("hex")}`);
61
+ if (value) throw new Error(`Manage template names and slugs on ${apiBase}/account.`);
64
62
  const directory = root;
65
63
 
66
64
  for (const file of ["Template.tsx", "customizations.json", "template.json"]) {
@@ -69,13 +67,13 @@ async function createTemplate(value, install = shouldInstall) {
69
67
 
70
68
  await mkdir(join(directory, "public"), { recursive: true });
71
69
  await Promise.all([
72
- writeFile(join(directory, "template.json"), `${JSON.stringify({ slug: validSlug, type: "other" }, null, 2)}\n`),
70
+ writeFile(join(directory, "template.json"), `${JSON.stringify({}, null, 2)}\n`),
73
71
  writeFile(join(directory, "Template.tsx"), starterTemplate),
74
72
  writeFile(join(directory, "customizations.json"), starterCustomizations),
75
73
  writeFile(join(directory, "public/.gitkeep"), ""),
76
74
  ]);
77
75
  await setupWorkspace(install);
78
- success(`Created ${validSlug} in ${directory}. Add a 16:9 public/cover.jpg (JPEG, up to 5 MB).`);
76
+ success(`Created template in ${directory}. Add a 16:9 public/cover.jpg (JPEG, up to 5 MB).`);
79
77
  }
80
78
 
81
79
  async function resetTemplate(value) {
@@ -83,7 +81,7 @@ async function resetTemplate(value) {
83
81
  if (root === engineRoot || !files.every((file) => existsSync(join(root, file)))) {
84
82
  throw new Error("Run reset inside a template folder containing Template.tsx, customizations.json and template.json.");
85
83
  }
86
- const validSlug = requireSlug(value || `template-${randomBytes(6).toString("hex")}`);
84
+ if (value) throw new Error(`Manage template names and slugs on ${apiBase}/account.`);
87
85
  if (!cliArgs.includes("--yes")) {
88
86
  if (!process.stdin.isTTY) throw new Error("Pass --yes to confirm resetting the template files and public/.");
89
87
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
@@ -97,7 +95,7 @@ async function resetTemplate(value) {
97
95
  }
98
96
  progress.start("Resetting template files…");
99
97
  for (const file of [...files, "public"]) await rm(join(root, file), { recursive: true, force: true });
100
- await createTemplate(validSlug, false);
98
+ await createTemplate(undefined, false);
101
99
  }
102
100
 
103
101
  async function setupWorkspace(install = true) {
@@ -116,37 +114,39 @@ async function setupWorkspace(install = true) {
116
114
  }
117
115
 
118
116
  async function checkRepositoryTemplates(value, quiet = false, assetRoot = "/assets") {
119
- const slugs = value ? [requireSlug(value)] : await findTemplates();
120
- if (!slugs.length) throw new Error("No templates with template.json found.");
117
+ const available = await findTemplates();
118
+ if (value && !available.includes(value)) throw new Error("Template folder not found.");
119
+ const folders = value ? [value] : available;
120
+ if (!folders.length) throw new Error("No templates with template.json found.");
121
121
 
122
122
  const results = [];
123
- for (const currentSlug of slugs) {
124
- const assetBase = assetRoot === "/assets" ? assetRoot : `${assetRoot}/${currentSlug}`;
125
- if (!quiet) progress.start(`Compiling ${currentSlug}…`);
126
- const result = await buildTemplate(join(repositoryTemplatesRoot, currentSlug), currentSlug, assetBase);
123
+ for (const folder of folders) {
124
+ const assetBase = assetRoot === "/assets" ? assetRoot : `${assetRoot}/${folder}`;
125
+ if (!quiet) progress.start(`Compiling ${folder}…`);
126
+ const result = await buildTemplate(join(repositoryTemplatesRoot, folder), assetBase);
127
127
  results.push(result);
128
- if (!quiet) success(`✓ ${currentSlug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
128
+ if (!quiet) success(`✓ ${folder} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
129
129
  }
130
130
  return results;
131
131
  }
132
132
 
133
- async function checkProject(quiet = false) {
133
+ async function checkProject(quiet = false, requireCover = true) {
134
134
  if (!quiet) progress.start("Validating and compiling template…");
135
- const result = await buildTemplate(root);
136
- if (!quiet) success(`✓ ${result.slug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
135
+ const result = await buildTemplate(root, "/assets", requireCover);
136
+ if (!quiet) success(`✓ Template (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
137
137
  return result;
138
138
  }
139
139
 
140
- async function buildTemplate(directory, slugOverride, assetBase = "/assets", requireCover = true) {
140
+ async function buildTemplate(directory, assetBase = "/assets", requireCover = true) {
141
141
  const [metadataSource, source, customizationsSource] = await Promise.all([
142
142
  readFile(join(directory, "template.json"), "utf8"),
143
143
  readFile(join(directory, "Template.tsx"), "utf8"),
144
144
  readFile(join(directory, "customizations.json"), "utf8"),
145
145
  ]);
146
146
  const metadata = JSON.parse(metadataSource);
147
- const currentSlug = requireSlug(slugOverride || metadata?.slug);
148
- if (!metadata || !types.has(metadata.type)) throw new Error(`${currentSlug}: template.json needs a type (${[...types].join(", ")}).`);
149
- if (metadata.id != null && (!Number.isInteger(metadata.id) || metadata.id < 1)) throw new Error(`${currentSlug}: template.json has an invalid id.`);
147
+ const folder = basename(directory);
148
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) throw new Error("template.json must be an object with an optional id.");
149
+ if (metadata.id != null && (!Number.isInteger(metadata.id) || metadata.id < 1)) throw new Error(`${folder}: template.json has an invalid id.`);
150
150
 
151
151
  const [{ compileTemplateAssets }, { validateTemplateCode }, { validateTemplateCustomizations }] = await Promise.all([
152
152
  load("templates/templateCompiler.ts"),
@@ -159,9 +159,9 @@ async function buildTemplate(directory, slugOverride, assetBase = "/assets", req
159
159
  defaultValue: resolveAssetValue(field.defaultValue, assetBase),
160
160
  }));
161
161
  const ids = customizations.map((field) => field.fieldId);
162
- if (new Set(ids).size !== ids.length) throw new Error(`${currentSlug}: customization field IDs must be unique.`);
162
+ if (new Set(ids).size !== ids.length) throw new Error(`${folder}: customization field IDs must be unique.`);
163
163
  for (const match of source.matchAll(/\bcustom\(\s*(\d+)\s*\)/g)) {
164
- if (!ids.includes(Number(match[1]))) throw new Error(`${currentSlug}: custom(${match[1]}) has no customization.`);
164
+ if (!ids.includes(Number(match[1]))) throw new Error(`${folder}: custom(${match[1]}) has no customization.`);
165
165
  }
166
166
  const coverPath = join(directory, "public", "cover.jpg");
167
167
  let cover = null;
@@ -176,7 +176,7 @@ async function buildTemplate(directory, slugOverride, assetBase = "/assets", req
176
176
  for (const file of assetFiles) assetHash.update(file.path).update(file.body);
177
177
  const { js, css } = await compileTemplateAssets(source, customizations, assetBase);
178
178
  const versionedJs = assetFiles.length ? `${js}\n/* template-assets:${assetHash.digest("hex")} */` : js;
179
- return { ...metadata, assetFiles, cover, css, customizations, fields: customizations.length, js: versionedJs, slug: currentSlug, source };
179
+ return { id: metadata.id, assetFiles, cover, css, customizations, fields: customizations.length, js: versionedJs, source };
180
180
  }
181
181
 
182
182
  async function createCover() {
@@ -184,7 +184,7 @@ async function createCover() {
184
184
  const force = cliArgs.includes("--force");
185
185
  if (!force && existsSync(coverPath)) throw new Error("public/cover.jpg already exists. Use `noweb cover --force` to replace it.");
186
186
  progress.start("Compiling cover preview…");
187
- const template = await buildTemplate(root, undefined, "/assets", false);
187
+ const template = await buildTemplate(root, "/assets", false);
188
188
  const server = createServer(async (request, response) => {
189
189
  try {
190
190
  const pathname = new URL(request.url || "/", "http://127.0.0.1").pathname;
@@ -251,10 +251,10 @@ async function dev() {
251
251
  try {
252
252
  const url = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`);
253
253
  if (url.pathname === "/") {
254
- const template = await checkProject(true);
254
+ const template = await checkProject(true, false);
255
255
  response.writeHead(200, { "Cache-Control": "no-store", "Content-Type": "text/html; charset=utf-8" });
256
256
  response.end(previewHtml(template));
257
- success(`✓ rebuilt ${template.slug}`);
257
+ success("✓ Rebuilt template");
258
258
  return;
259
259
  }
260
260
  await servePublic(url.pathname, response);
@@ -275,14 +275,12 @@ async function dev() {
275
275
  async function upload() {
276
276
  const template = await checkProject();
277
277
  if (dryRun) {
278
- success(`Ready to upload ${template.slug}`);
278
+ success("Ready to upload template");
279
279
  return;
280
280
  }
281
281
 
282
282
  const credentials = await requireCredentials();
283
283
  const form = new FormData();
284
- form.set("slug", template.slug);
285
- form.set("type", template.type);
286
284
  if (template.id) form.set("id", String(template.id));
287
285
  form.set("source", template.source);
288
286
  form.set("customizations", JSON.stringify(template.customizations));
@@ -300,47 +298,31 @@ async function upload() {
300
298
  const result = await response.json().catch(() => ({}));
301
299
  if (!response.ok) throw new Error(result.error || `Upload failed (${response.status}).`);
302
300
  if (!Number.isInteger(result.id) || result.id < 1) throw new Error("Upload returned an invalid template ID.");
303
- await writeFile(join(root, "template.json"), `${JSON.stringify({ id: result.id, slug: template.slug, type: template.type }, null, 2)}\n`);
304
- success(`Uploaded ${template.slug} (${result.is_published ? "public" : "unpublished"}): ${new URL(result.url, apiBase)}`);
305
- }
306
-
307
- async function publishUploadedTemplate() {
308
- const template = JSON.parse(await readFile(join(root, "template.json"), "utf8"));
309
- if (!Number.isInteger(template.id) || template.id < 1) throw new Error("Upload this template first: npx nowebcli upload");
310
- const credentials = await requireCredentials();
311
- progress.start("Publishing template…");
312
- const response = await fetch(`${apiBase}/api/templates/visibility`, {
313
- method: "PATCH",
314
- headers: { Authorization: `Bearer ${credentials.token}`, "Content-Type": "application/json" },
315
- body: JSON.stringify({ id: template.id, published: true }),
316
- });
317
- const result = await response.json().catch(() => ({}));
318
- if (!response.ok) throw new Error(result.error || `Publish failed (${response.status}).`);
319
- success(`Published ${template.slug}: ${new URL(result.url, apiBase)}`);
301
+ await writeFile(join(root, "template.json"), `${JSON.stringify({ id: result.id }, null, 2)}\n`);
302
+ success(`Uploaded template #${result.id} (${result.is_published ? "public" : "unpublished"}): ${new URL(result.url, apiBase)}`);
320
303
  }
321
304
 
322
305
  async function deleteTemplate(value) {
323
306
  const metadata = value ? null : JSON.parse(await readFile(join(root, "template.json"), "utf8"));
324
- const selectedSlug = requireSlug(value || metadata?.slug);
325
- const selectedId = Number.isInteger(metadata?.id) && metadata.id > 0 ? metadata.id : null;
307
+ const selectedId = requireTemplateId(value ?? metadata?.id);
326
308
 
327
309
  if (!cliArgs.includes("--yes")) {
328
310
  if (!process.stdin.isTTY) throw new Error("Pass --yes to confirm deletion.");
329
311
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
330
- const answer = await prompt.question(`Delete ${selectedSlug} permanently? [y/N] `);
312
+ const answer = await prompt.question(`Delete template #${selectedId} permanently? [y/N] `);
331
313
  prompt.close();
332
314
  if (!/^y(?:es)?$/i.test(answer.trim())) return;
333
315
  }
334
316
 
335
317
  const credentials = await requireCredentials();
336
- const selector = selectedId ? `id=${selectedId}` : `slug=${encodeURIComponent(selectedSlug)}`;
318
+ const selector = `id=${selectedId}`;
337
319
  const response = await fetch(`${apiBase}/api/templates/download?${selector}`, {
338
320
  method: "DELETE",
339
321
  headers: { Authorization: `Bearer ${credentials.token}` },
340
322
  });
341
323
  const result = await response.json().catch(() => ({}));
342
324
  if (!response.ok) throw new Error(result.error || `Delete failed (${response.status}).`);
343
- success(`Deleted ${selectedSlug}`);
325
+ success(`Deleted template #${selectedId}`);
344
326
  }
345
327
 
346
328
  async function listTemplates() {
@@ -351,43 +333,34 @@ async function listTemplates() {
351
333
  const result = await response.json().catch(() => ({}));
352
334
  if (!response.ok) throw new Error(result.error || `Could not list templates (${response.status}).`);
353
335
  const templates = Array.isArray(result.templates) ? result.templates : [];
354
- const projects = Array.isArray(result.projects) ? result.projects : [];
355
- if (!templates.length && !projects.length) {
356
- console.log("No templates or projects.");
336
+ if (!templates.length) {
337
+ console.log("No templates.");
357
338
  return;
358
339
  }
359
- if (templates.length) {
360
- console.log("Templates");
361
- for (const template of templates) console.log(`${template.slug || template.id}\t${template.is_published ? "public" : "unpublished"}`);
362
- }
363
- if (projects.length) {
364
- if (templates.length) console.log();
365
- console.log("Projects");
366
- for (const project of projects) console.log(`${project.id}\t${project.domain}`);
367
- }
340
+ console.log("Templates");
341
+ for (const template of templates) console.log(`${template.id}\t${template.is_published ? "public" : "unpublished"}`);
368
342
  }
369
343
 
370
344
  async function downloadTemplate(value) {
371
- const selectedSlug = requireSlug(value);
345
+ const selectedId = requireTemplateId(value);
372
346
  for (const file of ["Template.tsx", "customizations.json", "template.json"]) {
373
347
  if (existsSync(join(root, file))) throw new Error(`Template file already exists: ${file}`);
374
348
  }
375
349
 
376
350
  const credentials = await requireCredentials();
377
- progress.start(`Downloading ${selectedSlug}…`);
378
- const response = await fetch(`${apiBase}/api/templates/download?slug=${encodeURIComponent(selectedSlug)}`, {
351
+ progress.start(`Downloading template #${selectedId}…`);
352
+ const response = await fetch(`${apiBase}/api/templates/download?id=${selectedId}`, {
379
353
  headers: { Authorization: `Bearer ${credentials.token}` },
380
354
  });
381
355
  const result = await response.json().catch(() => ({}));
382
356
  if (!response.ok) throw new Error(result.error || `Download failed (${response.status}).`);
383
357
  const template = result.template;
384
358
  if (
385
- !template || typeof template.metadata?.slug !== "string" ||
359
+ !template ||
386
360
  !Number.isInteger(template.metadata?.id) || template.metadata.id < 1 ||
387
- !types.has(template.metadata?.type) || typeof template.source !== "string" ||
361
+ typeof template.source !== "string" ||
388
362
  !Array.isArray(template.customizations) || !Array.isArray(template.assets) || template.assets.length > 200
389
363
  ) throw new Error("The server returned an invalid template.");
390
- const downloadedSlug = requireSlug(template.metadata.slug);
391
364
 
392
365
  const assets = await Promise.all(template.assets.map(async (asset) => {
393
366
  const path = requireAssetPath(asset?.path);
@@ -410,13 +383,13 @@ async function downloadTemplate(value) {
410
383
  await writeFile(destination, asset.body);
411
384
  }));
412
385
  await Promise.all([
413
- writeFile(join(root, "template.json"), `${JSON.stringify(template.metadata, null, 2)}\n`),
386
+ writeFile(join(root, "template.json"), `${JSON.stringify({ id: template.metadata.id }, null, 2)}\n`),
414
387
  writeFile(join(root, "Template.tsx"), template.source),
415
388
  writeFile(join(root, "customizations.json"), `${JSON.stringify(template.customizations, null, 2)}\n`),
416
389
  ...(!assets.length ? [writeFile(join(root, "public/.gitkeep"), "")] : []),
417
390
  ]);
418
391
  await setupWorkspace(shouldInstall);
419
- success(`Downloaded ${downloadedSlug} into ${root}`);
392
+ success(`Downloaded template #${selectedId} into ${root}`);
420
393
  }
421
394
 
422
395
  async function login() {
@@ -512,65 +485,6 @@ function wait(milliseconds) {
512
485
  return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
513
486
  }
514
487
 
515
- async function publish(value) {
516
- if (engineRoot !== root) throw new Error("Publishing is only available from the Noweb repository.");
517
- const databaseUrl = process.env.DATABASE_POOL_URL ?? process.env.DATABASE_URL;
518
- if (!databaseUrl) throw new Error("DATABASE_POOL_URL or DATABASE_URL is required to publish templates.");
519
-
520
- const storageApi = await load("lib/object-storage.ts");
521
- const storage = storageApi.getObjectStorageClientConfig();
522
- const publicUrl = storageApi.getObjectStoragePublicUrl()?.replace(/\/$/, "");
523
- if (!storage || !publicUrl) throw new Error("Object storage configuration is required to publish templates.");
524
- const templates = await checkRepositoryTemplates(value, false, `${publicUrl}/template-assets`);
525
- const { default: postgres } = await import("postgres");
526
- const sql = postgres(databaseUrl, { connect_timeout: 10, max: 1 });
527
-
528
- try {
529
- for (const original of templates) {
530
- const published = await sql.begin(async (transaction) => {
531
- await transaction`SELECT pg_advisory_xact_lock(hashtextextended(${original.slug}, 0))`;
532
- let [reserved] = await transaction`SELECT id FROM templates WHERE slug = ${original.slug} FOR UPDATE`;
533
- if (!reserved) {
534
- [reserved] = await transaction`
535
- INSERT INTO templates (slug, type, component, compiled_js, compiled_css, customizations, author_id, is_published)
536
- VALUES (${original.slug}, ${original.type}, ${original.source}, '', '', ${transaction.json(original.customizations)}, 'github', false) RETURNING id
537
- `;
538
- }
539
- const { rewriteProjectAssetReferences } = await load("lib/project-template-snapshot.ts");
540
- const template = { ...original, ...rewriteProjectAssetReferences(
541
- { js: original.js, css: original.css, source: original.source, customizations: original.customizations },
542
- new Map([[`${publicUrl}/template-assets/${original.slug}`, `${publicUrl}/templates/${reserved.id}`]]),
543
- ) };
544
- await uploadTemplateAssets(template, storageApi.createObjectStorageClient(storage), storage.bucketName, reserved.id);
545
- const [updated] = await transaction`
546
- UPDATE templates SET
547
- type = ${template.type}, component = ${template.source},
548
- compiled_js = ${template.js}, compiled_css = ${template.css},
549
- customizations = ${transaction.json(template.customizations)}, is_published = true
550
- WHERE id = ${reserved.id} AND (
551
- type IS DISTINCT FROM ${template.type}
552
- OR component IS DISTINCT FROM ${template.source}
553
- OR compiled_js IS DISTINCT FROM ${template.js}
554
- OR compiled_css IS DISTINCT FROM ${template.css}
555
- OR customizations::jsonb IS DISTINCT FROM ${transaction.json(template.customizations)}::jsonb
556
- OR is_published IS DISTINCT FROM true
557
- )
558
- RETURNING id
559
- `;
560
- return updated;
561
- });
562
- if (!published) {
563
- console.log(`Unchanged ${original.slug}`);
564
- continue;
565
- }
566
- await (await load("lib/template-screenshot.ts")).generateTemplatePreview(Number(published.id));
567
- success(`Published ${original.slug} as template ${published.id}`);
568
- }
569
- } finally {
570
- await sql.end({ timeout: 5 });
571
- }
572
- }
573
-
574
488
  async function findTemplates() {
575
489
  const entries = await readdir(repositoryTemplatesRoot, { withFileTypes: true });
576
490
  return entries
@@ -651,30 +565,15 @@ function resolveAssetValue(value, assetBase) {
651
565
  return value;
652
566
  }
653
567
 
654
- async function uploadTemplateAssets(template, client, bucketName, templateId) {
655
- const { PutObjectCommand } = await import("@aws-sdk/client-s3");
656
- for (const file of template.assetFiles) {
657
- await client.send(new PutObjectCommand({
658
- Bucket: bucketName,
659
- Key: `templates/${templateId}/${file.path}`,
660
- Body: file.body,
661
- CacheControl: "public, max-age=0, must-revalidate",
662
- ContentType: mimeTypes[extname(file.path).toLowerCase()] || "application/octet-stream",
663
- }));
664
- }
665
- await client.send(new PutObjectCommand({
666
- Bucket: bucketName, Key: `template-previews/${templateId}.jpg`, Body: template.cover,
667
- CacheControl: "public, max-age=0, must-revalidate", ContentType: "image/jpeg",
668
- }));
669
- }
670
-
671
568
  function previewHtml(template) {
672
- return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(template.slug)}</title><style>${escapeClosingTag(template.css, "style")}</style></head><body><div id="root"></div><script>${escapeClosingTag(template.js, "script")}</script><script>window.__nowebRenderTemplate(document.getElementById("root"), {});</script></body></html>`;
569
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Template preview</title><style>${escapeClosingTag(template.css, "style")}</style></head><body><div id="root"></div><script>${escapeClosingTag(template.js, "script")}</script><script>window.__nowebRenderTemplate(document.getElementById("root"), {});</script></body></html>`;
673
570
  }
674
571
 
675
- function requireSlug(value) {
676
- if (!value || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) throw new Error("Use a lowercase template slug, for example: minimal-portfolio");
677
- return value;
572
+ function requireTemplateId(value) {
573
+ if (!/^[1-9]\d*$/.test(String(value)) || !Number.isSafeInteger(Number(value))) {
574
+ throw new Error("Use a template ID from `noweb list`.");
575
+ }
576
+ return Number(value);
678
577
  }
679
578
 
680
579
  function requireAssetPath(value) {
@@ -780,18 +679,18 @@ const workspaceTsconfig = {
780
679
  try {
781
680
  await access(join(engineRoot, "templates/templateCompiler.ts"));
782
681
 
783
- if (command === "create") await createTemplate(slug);
784
- else if (command === "reset") await resetTemplate(slug);
682
+ if (command === "create") await createTemplate(selection);
683
+ else if (command === "reset") await resetTemplate(selection);
785
684
  else if (command === "setup") await setupWorkspace(shouldInstall);
786
- else if (command === "check") await (slug && engineRoot === root ? checkRepositoryTemplates(slug) : checkProject());
685
+ else if (command === "check") await (selection && engineRoot === root ? checkRepositoryTemplates(selection) : checkProject());
787
686
  else if (command === "dev") await dev();
788
687
  else if (command === "cover") await createCover();
789
688
  else if (command === "login") await login();
790
689
  else if (command === "list") await listTemplates();
791
- else if (command === "download") await downloadTemplate(slug);
690
+ else if (command === "download") await downloadTemplate(selection);
792
691
  else if (command === "upload") await upload();
793
- else if (command === "delete") await deleteTemplate(slug);
794
- else if (command === "publish") await (engineRoot === root ? publish(slug) : publishUploadedTemplate());
692
+ else if (command === "delete") await deleteTemplate(selection);
693
+ else if (command === "publish") throw new Error(`Publish templates on the website: ${apiBase}/account`);
795
694
  else if (command === "help" || command === "--help" || command === "-h") usage();
796
695
  else throw new Error(`Unknown command: ${command}`);
797
696
  } catch (error) {
@@ -13,8 +13,8 @@ import { validateTemplateCode } from "./validateTemplateCode.ts";
13
13
  const run = promisify(execFile);
14
14
  const templatesPath = path.dirname(fileURLToPath(import.meta.url));
15
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")));
16
+ const esbuildPath = require.resolve(/* turbopackIgnore: true */ "esbuild/bin/esbuild");
17
+ const modulesPath = path.dirname(path.dirname(require.resolve(/* turbopackIgnore: true */ "react/package.json")));
18
18
 
19
19
  const TEMPLATE_COMPILE_TIMEOUT_MS = 10_000;
20
20
  const TEMPLATE_COMPILE_MAX_BUFFER = 8 * 1024 * 1024;
@@ -166,7 +166,7 @@ function runtimeAliases() {
166
166
  return [
167
167
  // npx and project dependencies must share the renderer's React instance.
168
168
  ...["react", "react-dom"].map((name) =>
169
- `--alias:${name}=${path.dirname(require.resolve(`${name}/package.json`, { paths: [modulesPath] }))}`,
169
+ `--alias:${name}=${path.dirname(require.resolve(/* turbopackIgnore: true */ `${name}/package.json`, { paths: [modulesPath] }))}`,
170
170
  ),
171
171
  ...TEMPLATE_RUNTIME_IMPORTS.map(
172
172
  (moduleName) => `--alias:${moduleName}=${templateRuntimePath()}`,