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 +2 -3
- package/scripts/noweb.mjs +63 -164
- package/templates/templateCompiler.ts +3 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nowebcli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Create, preview and
|
|
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
|
|
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",
|
|
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
|
|
49
|
-
reset
|
|
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 <
|
|
55
|
+
download <id> restore one into the current folder
|
|
57
56
|
upload save this template without publishing
|
|
58
|
-
delete [
|
|
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
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
120
|
-
if (!
|
|
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
|
|
124
|
-
const assetBase = assetRoot === "/assets" ? assetRoot : `${assetRoot}/${
|
|
125
|
-
if (!quiet) progress.start(`Compiling ${
|
|
126
|
-
const result = await buildTemplate(join(repositoryTemplatesRoot,
|
|
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(`✓ ${
|
|
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(`✓
|
|
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,
|
|
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
|
|
148
|
-
if (!metadata ||
|
|
149
|
-
if (metadata.id != null && (!Number.isInteger(metadata.id) || metadata.id < 1)) throw new Error(`${
|
|
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(`${
|
|
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(`${
|
|
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 {
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
|
304
|
-
success(`Uploaded
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
console.log("No templates or projects.");
|
|
336
|
+
if (!templates.length) {
|
|
337
|
+
console.log("No templates.");
|
|
357
338
|
return;
|
|
358
339
|
}
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
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
|
|
378
|
-
const response = await fetch(`${apiBase}/api/templates/download?
|
|
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 ||
|
|
359
|
+
!template ||
|
|
386
360
|
!Number.isInteger(template.metadata?.id) || template.metadata.id < 1 ||
|
|
387
|
-
|
|
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
|
|
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
|
|
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
|
|
676
|
-
if (
|
|
677
|
-
|
|
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(
|
|
784
|
-
else if (command === "reset") await resetTemplate(
|
|
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 (
|
|
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(
|
|
690
|
+
else if (command === "download") await downloadTemplate(selection);
|
|
792
691
|
else if (command === "upload") await upload();
|
|
793
|
-
else if (command === "delete") await deleteTemplate(
|
|
794
|
-
else if (command === "publish")
|
|
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()}`,
|