nowebcli 0.2.1
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/README.md +188 -0
- package/lib/customizations.ts +252 -0
- package/package.json +103 -0
- package/scripts/noweb.mjs +625 -0
- package/templates/compileTailwind.ts +67 -0
- package/templates/shadersRuntime.tsx +10 -0
- package/templates/templateCompiler.ts +200 -0
- package/templates/templateRuntime.tsx +280 -0
- package/templates/validateTemplateCode.ts +121 -0
- package/tsconfig.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Noweb
|
|
2
|
+
|
|
3
|
+
Noweb ist eine Next.js-App fuer Website-Templates, Projekte, Auth, Checkout und
|
|
4
|
+
Deployment. Nutzer koennen Templates auswaehlen, Inhalte im Editor anpassen,
|
|
5
|
+
Bilder hochladen und Projekte mit eigener Domain veroeffentlichen.
|
|
6
|
+
|
|
7
|
+
## Schnellstart
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Die App laeuft lokal unter `http://localhost:3000`.
|
|
15
|
+
|
|
16
|
+
Vor einem Pull Request oder Deploy:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run ci
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Community-Templates
|
|
23
|
+
|
|
24
|
+
Die vollständige Anleitung für Erstellung, Assets, lokale Vorschau und
|
|
25
|
+
automatische Veröffentlichung steht in [`templates/README.md`](templates/README.md).
|
|
26
|
+
|
|
27
|
+
## Wichtige Befehle
|
|
28
|
+
|
|
29
|
+
- `npm run dev` startet Next lokal mit Webpack.
|
|
30
|
+
- `npm run build` baut die Produktionsversion.
|
|
31
|
+
- `npm run start` startet den gebauten Next-Server.
|
|
32
|
+
- `npm run lint` prueft den Code mit ESLint und erlaubt keine Warnings.
|
|
33
|
+
- `npm run typecheck` prueft TypeScript ohne Emit.
|
|
34
|
+
- `npm run test` fuehrt Unit-Tests aus.
|
|
35
|
+
- `npm run test:e2e` fuehrt den Playwright-Smoke gegen `npm run start` aus.
|
|
36
|
+
- `npx nowebcli create` erstellt ein Community-Template; danach laufen die Befehle lokal ueber `npx noweb`.
|
|
37
|
+
- `npm run ci` fuehrt Audit, Typecheck, Lint, Tests, Build und E2E aus.
|
|
38
|
+
|
|
39
|
+
## Struktur
|
|
40
|
+
|
|
41
|
+
- `app/` enthaelt Seiten, Routen und Server Actions.
|
|
42
|
+
- `app/page.tsx`, `app/HomeSections.tsx`, `app/Footer.tsx` und `app/menu.tsx`
|
|
43
|
+
bilden die Startseite und Navigation.
|
|
44
|
+
- `app/docs/` enthaelt die oeffentliche Doku im Produkt.
|
|
45
|
+
- `lib/auth/` konfiguriert Better Auth.
|
|
46
|
+
- `lib/db.ts` stellt die Postgres-Verbindung bereit.
|
|
47
|
+
- `lib/object-storage.ts` kapselt RustFS/R2-kompatiblen Object Storage.
|
|
48
|
+
- `lib/env.ts` validiert serverseitige Runtime-Konfiguration.
|
|
49
|
+
- `lib/domain.ts` normalisiert und validiert Domains.
|
|
50
|
+
- `lib/templates/` enthaelt Template-Typen, Runtime-Helfer und lokale Templates.
|
|
51
|
+
- `templates/index.ts` ist generiert und soll nicht per Hand editiert werden.
|
|
52
|
+
- `db.txt` dokumentiert das aktuelle Datenbankschema.
|
|
53
|
+
- `scripts/deploy-worker.mjs` veroeffentlicht Projekte als statische Sites.
|
|
54
|
+
- `.github/workflows/deploy.yaml` deployed `main` per SSH auf den Server.
|
|
55
|
+
|
|
56
|
+
## Umgebung
|
|
57
|
+
|
|
58
|
+
Mindestens benoetigt:
|
|
59
|
+
|
|
60
|
+
```env
|
|
61
|
+
DATABASE_URL=postgresql://...
|
|
62
|
+
BETTER_AUTH_SECRET=...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Je nach Feature zusaetzlich:
|
|
66
|
+
|
|
67
|
+
```env
|
|
68
|
+
RESEND_API_KEY=...
|
|
69
|
+
EMAIL_FROM=Noweb <mail@example.com>
|
|
70
|
+
|
|
71
|
+
GOOGLE_CLIENT_ID=...
|
|
72
|
+
GOOGLE_CLIENT_SECRET=...
|
|
73
|
+
GITHUB_CLIENT_ID=...
|
|
74
|
+
GITHUB_CLIENT_SECRET=...
|
|
75
|
+
|
|
76
|
+
POLAR_ACCESS_TOKEN=...
|
|
77
|
+
POLAR_PRODUCT_ID=...
|
|
78
|
+
POLAR_SERVER=sandbox
|
|
79
|
+
|
|
80
|
+
RUSTFS_ACCESS_KEY=...
|
|
81
|
+
RUSTFS_SECRET_KEY=...
|
|
82
|
+
RUSTFS_BUCKET_NAME=...
|
|
83
|
+
RUSTFS_ENDPOINT=...
|
|
84
|
+
RUSTFS_PUBLIC_URL=...
|
|
85
|
+
|
|
86
|
+
SENTRY_DSN=...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Object Storage kann alternativ ueber die `R2_*` Variablen konfiguriert werden.
|
|
90
|
+
Details stehen in [docs/object-storage.md](docs/object-storage.md).
|
|
91
|
+
|
|
92
|
+
## Wie die Hauptteile zusammenhaengen
|
|
93
|
+
|
|
94
|
+
Auth laeuft ueber Better Auth in `lib/auth/server.ts`. Die Daten liegen in
|
|
95
|
+
Postgres, E-Mails werden ueber Resend verschickt, OAuth ist optional ueber
|
|
96
|
+
Google und GitHub aktiv.
|
|
97
|
+
|
|
98
|
+
Projekte und Templates werden in Postgres gespeichert. Das aktuelle Schema
|
|
99
|
+
steht in `db.txt`; Schema-Aenderungen werden direkt in Postgres ausgefuehrt und
|
|
100
|
+
danach dort dokumentiert.
|
|
101
|
+
|
|
102
|
+
Checkout laeuft ueber Polar. Der kanonische Einstieg ist
|
|
103
|
+
`app/checkout/route.ts`, nicht direkt der Better-Auth-Checkout. Details stehen
|
|
104
|
+
in [docs/checkout.md](docs/checkout.md).
|
|
105
|
+
|
|
106
|
+
Bild-Uploads gehen ueber `app/api/uploads/r2/route.ts` in einen S3-kompatiblen
|
|
107
|
+
Storage. Die oeffentliche URL kommt aus `RUSTFS_PUBLIC_URL`, `R2_PUBLIC_URL`
|
|
108
|
+
oder `PUBLIC_DEV_URL`.
|
|
109
|
+
|
|
110
|
+
Template-Previews laufen in einer sandboxed `iframe/srcDoc` Runtime. Oeffentlich
|
|
111
|
+
sichtbar werden Community-Templates nach erfolgreicher automatischer Validierung.
|
|
112
|
+
|
|
113
|
+
Der Deploy Worker nimmt unveroeffentlichte Projekte aus der Datenbank, liest die
|
|
114
|
+
kompilierten Template-Dateien, validiert Domains und schreibt statische Sites
|
|
115
|
+
nach `SITES_DIR` Standard: `/etc/caddy/sites`.
|
|
116
|
+
|
|
117
|
+
## Deployment
|
|
118
|
+
|
|
119
|
+
Bei jedem Push auf `main` fuehrt GitHub Actions `.github/workflows/deploy.yaml`
|
|
120
|
+
aus:
|
|
121
|
+
|
|
122
|
+
1. Per SSH auf den Server verbinden und `origin/main` auschecken.
|
|
123
|
+
2. `npm ci` ausfuehren.
|
|
124
|
+
3. Repository-Templates in die Datenbank und den Preview-Storage publizieren.
|
|
125
|
+
4. `npm run build` ausfuehren.
|
|
126
|
+
5. Den `noweb`-Service neu starten.
|
|
127
|
+
|
|
128
|
+
Die GitHub-Secrets muessen gesetzt sein:
|
|
129
|
+
|
|
130
|
+
- `SERVER_HOST`
|
|
131
|
+
- `SERVER_USER`
|
|
132
|
+
- `SSH_PRIVATE_KEY`
|
|
133
|
+
|
|
134
|
+
Auf dem Server muessen ausserdem Node, npm, die Runtime-Env-Variablen und der
|
|
135
|
+
`noweb` systemd-Service vorhanden sein.
|
|
136
|
+
|
|
137
|
+
## Aenderungs-Checkliste
|
|
138
|
+
|
|
139
|
+
Wenn du etwas aenderst, pruefe die abhaengigen Stellen mit:
|
|
140
|
+
|
|
141
|
+
- Startseite, Preise oder Marketingtext:
|
|
142
|
+
`app/page.tsx`, `app/HomeSections.tsx`, `app/Footer.tsx`, `app/menu.tsx`,
|
|
143
|
+
`docs/checkout.md` bei Preis-/Checkout-Aenderungen.
|
|
144
|
+
- Polar-Produkte oder Checkout:
|
|
145
|
+
`app/checkout/route.ts`, `lib/auth/server.ts`, `lib/polar.ts`,
|
|
146
|
+
`docs/checkout.md`, Env-Variablen auf Server/GitHub.
|
|
147
|
+
- Auth, Login, Verifizierung oder OAuth:
|
|
148
|
+
`lib/auth/server.ts`, `app/auth/**`, Datenbanktabellen und Resend/OAuth-Env.
|
|
149
|
+
- Datenbank-Schema oder Query-Format:
|
|
150
|
+
`lib/db.ts`, Server Actions in `app/**/actions.ts`, `scripts/deploy-worker.mjs`,
|
|
151
|
+
`db.txt`.
|
|
152
|
+
- Template-Felder oder Customizations:
|
|
153
|
+
Template-Definition, Editor-Komponenten in `lib/editor/` und
|
|
154
|
+
`app/my-templates/[id]/`, Deploy Worker, `templates/index.ts` nur ueber den
|
|
155
|
+
Sync-Prozess aktualisieren.
|
|
156
|
+
- Template-Doku:
|
|
157
|
+
passende Seite in `app/docs/` aktualisieren und den Eintrag in
|
|
158
|
+
`app/docs/guides.ts` pruefen.
|
|
159
|
+
- Bild-Uploads oder Storage:
|
|
160
|
+
`lib/object-storage.ts`, `app/api/uploads/r2/route.ts`,
|
|
161
|
+
`docs/object-storage.md`, Storage-Env auf dem Server.
|
|
162
|
+
- Projekt-Deployment oder Domains:
|
|
163
|
+
`scripts/deploy-worker.mjs`, Caddy/systemd-Konfiguration, `docs/deploy-worker.md`.
|
|
164
|
+
- GitHub Deploy:
|
|
165
|
+
`.github/workflows/deploy.yaml`, GitHub-Secrets, Serverpfad `~/noweb`,
|
|
166
|
+
systemd-Service `noweb`.
|
|
167
|
+
- Neue oder geaenderte Produktfunktion:
|
|
168
|
+
vorhandene Doku aktualisieren oder eine kleine neue Seite in `docs/` oder
|
|
169
|
+
`app/docs/` anlegen.
|
|
170
|
+
|
|
171
|
+
## Launch-Blocker
|
|
172
|
+
|
|
173
|
+
- Echte Betreiberangaben in `app/imprint/page.tsx`, `app/privacy/page.tsx` und
|
|
174
|
+
`app/terms/page.tsx` eintragen.
|
|
175
|
+
- Lokale und Server-Env pruefen. `POLAR_SERVER` darf nur `sandbox` oder
|
|
176
|
+
`production` sein.
|
|
177
|
+
- Monitoring setzen: `SENTRY_DSN` oder dokumentierte Alternative.
|
|
178
|
+
- `npm run ci` muss vor Production gruen sein.
|
|
179
|
+
|
|
180
|
+
## Wichtige Hinweise
|
|
181
|
+
|
|
182
|
+
- Dieses Projekt nutzt Next.js 16. Vor groesseren Next-Aenderungen die lokalen
|
|
183
|
+
Docs in `node_modules/next/dist/docs/` lesen.
|
|
184
|
+
- `templates/index.ts` ist generiert. Nicht manuell pflegen.
|
|
185
|
+
- JSON-Spalten muessen beim Schreiben mit `JSON.stringify(...)` gespeichert
|
|
186
|
+
werden.
|
|
187
|
+
- Secrets gehoeren nicht ins Repository.
|
|
188
|
+
- Vor einem Deploy `npm run ci` ausfuehren.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
export type ScalarType =
|
|
2
|
+
| "text"
|
|
3
|
+
| "markdown"
|
|
4
|
+
| "image"
|
|
5
|
+
| "icon"
|
|
6
|
+
| "color"
|
|
7
|
+
| "number"
|
|
8
|
+
| "range"
|
|
9
|
+
| "boolean"
|
|
10
|
+
| "select";
|
|
11
|
+
|
|
12
|
+
export type ScalarValue = string | number | boolean;
|
|
13
|
+
export type CollectionItem = Record<string, ScalarValue>;
|
|
14
|
+
export type CustomizationValue = ScalarValue | string[] | CollectionItem[];
|
|
15
|
+
export type CustomizationValues = Record<string, CustomizationValue>;
|
|
16
|
+
|
|
17
|
+
type FieldOptions = {
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
step?: number;
|
|
21
|
+
options?: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type CollectionField = FieldOptions & {
|
|
25
|
+
key: string;
|
|
26
|
+
name: string;
|
|
27
|
+
type: ScalarType;
|
|
28
|
+
defaultValue: ScalarValue;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type TemplateCustomization = FieldOptions & {
|
|
32
|
+
fieldId: number;
|
|
33
|
+
name: string;
|
|
34
|
+
type: ScalarType | "array" | "collection";
|
|
35
|
+
defaultValue: CustomizationValue;
|
|
36
|
+
fields?: CollectionField[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const SCALAR_TYPES: ScalarType[] = [
|
|
40
|
+
"text", "markdown", "image", "icon", "color",
|
|
41
|
+
"number", "range", "boolean", "select",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export function normalizeTemplateCustomizations(value: unknown) {
|
|
45
|
+
const parsed = typeof value === "string" ? parseJson(value, []) : value;
|
|
46
|
+
return Array.isArray(parsed)
|
|
47
|
+
? parsed.flatMap((item) => {
|
|
48
|
+
const field = readField(item, true);
|
|
49
|
+
return field ? [field] : [];
|
|
50
|
+
})
|
|
51
|
+
: [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validateTemplateCustomizations(value: unknown) {
|
|
55
|
+
if (!Array.isArray(value)) throw new Error("Template customizations must be an array.");
|
|
56
|
+
return value.map((item, index) => {
|
|
57
|
+
const field = readField(item, false);
|
|
58
|
+
if (!field) throw new Error(`Customization ${index + 1} is invalid.`);
|
|
59
|
+
return field;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function normalizeCustomizationValues(value: unknown): CustomizationValues | null {
|
|
64
|
+
const parsed = typeof value === "string" ? parseJson(value, null) : value;
|
|
65
|
+
return isRecord(parsed) && Object.values(parsed).every(isCustomizationValue)
|
|
66
|
+
? parsed as CustomizationValues
|
|
67
|
+
: null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function restoreCustomizationValues(
|
|
71
|
+
current: CustomizationValues,
|
|
72
|
+
previous: CustomizationValues,
|
|
73
|
+
fieldIds: string[],
|
|
74
|
+
) {
|
|
75
|
+
const restored = { ...current };
|
|
76
|
+
for (const fieldId of fieldIds) {
|
|
77
|
+
if (Object.hasOwn(previous, fieldId)) restored[fieldId] = previous[fieldId];
|
|
78
|
+
else delete restored[fieldId];
|
|
79
|
+
}
|
|
80
|
+
return restored;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeCustomizationValue(
|
|
84
|
+
field: TemplateCustomization,
|
|
85
|
+
value: unknown,
|
|
86
|
+
): CustomizationValue | undefined {
|
|
87
|
+
if (field.type === "array") {
|
|
88
|
+
return Array.isArray(value) && value.length <= 50 &&
|
|
89
|
+
value.every((item) => typeof item === "string" && item.length <= 20_000)
|
|
90
|
+
? value
|
|
91
|
+
: undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (field.type === "collection") {
|
|
95
|
+
if (!Array.isArray(value) || value.length > 50 || !field.fields?.length) return undefined;
|
|
96
|
+
const items = value.map((item) => {
|
|
97
|
+
if (!isRecord(item)) return null;
|
|
98
|
+
const normalized: CollectionItem = {};
|
|
99
|
+
for (const child of field.fields!) {
|
|
100
|
+
const childValue = normalizeScalarValue(child, item[child.key]);
|
|
101
|
+
if (childValue === undefined) return null;
|
|
102
|
+
normalized[child.key] = childValue;
|
|
103
|
+
}
|
|
104
|
+
return normalized;
|
|
105
|
+
});
|
|
106
|
+
return items.some((item) => item === null) ? undefined : items as CollectionItem[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const type = field.type;
|
|
110
|
+
if (!isScalarType(type)) return undefined;
|
|
111
|
+
return normalizeScalarValue({ ...field, type }, value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readField(value: unknown, loose: boolean): TemplateCustomization | null {
|
|
115
|
+
if (!isRecord(value)) return null;
|
|
116
|
+
const fieldId = Number(value.fieldId);
|
|
117
|
+
const name = text(value.name);
|
|
118
|
+
const type = value.type === "option" ? "select" : value.type;
|
|
119
|
+
if (!Number.isInteger(fieldId) || fieldId < 1 || !name) return null;
|
|
120
|
+
|
|
121
|
+
if (type === "array") {
|
|
122
|
+
const defaultValue = strings(value.defaultValue);
|
|
123
|
+
if (!loose && !defaultValue) return null;
|
|
124
|
+
return { fieldId, name, type, defaultValue: defaultValue ?? [] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (type === "collection") {
|
|
128
|
+
const fields = Array.isArray(value.fields)
|
|
129
|
+
? value.fields.flatMap((item) => {
|
|
130
|
+
const field = readCollectionField(item, loose);
|
|
131
|
+
return field ? [field] : [];
|
|
132
|
+
})
|
|
133
|
+
: [];
|
|
134
|
+
if (!loose && new Set(fields.map((field) => field.key)).size !== fields.length) return null;
|
|
135
|
+
const defaultValue = collection(value.defaultValue, fields);
|
|
136
|
+
if (!loose && (!Array.isArray(value.fields) || !defaultValue)) return null;
|
|
137
|
+
return { fieldId, name, type, fields, defaultValue: defaultValue ?? [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!isScalarType(type)) return null;
|
|
141
|
+
const defaultValue = scalar(value.defaultValue, type);
|
|
142
|
+
if (!loose && defaultValue === null) return null;
|
|
143
|
+
return { fieldId, name, type, defaultValue: defaultValue ?? scalarDefault(type), ...fieldOptions(value, type) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readCollectionField(value: unknown, loose: boolean): CollectionField | null {
|
|
147
|
+
if (!isRecord(value) || !isScalarType(value.type)) return null;
|
|
148
|
+
const key = text(value.key);
|
|
149
|
+
const name = text(value.name);
|
|
150
|
+
const defaultValue = scalar(value.defaultValue, value.type);
|
|
151
|
+
if (!key || !name || (!loose && defaultValue === null)) return null;
|
|
152
|
+
return { key, name, type: value.type, defaultValue: defaultValue ?? scalarDefault(value.type), ...fieldOptions(value, value.type) };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function fieldOptions(value: Record<string, unknown>, type: ScalarType) {
|
|
156
|
+
return {
|
|
157
|
+
...(number(value.min) !== null ? { min: number(value.min)! } : {}),
|
|
158
|
+
...(number(value.max) !== null ? { max: number(value.max)! } : {}),
|
|
159
|
+
...(number(value.step) !== null && number(value.step)! > 0 ? { step: number(value.step)! } : {}),
|
|
160
|
+
...(type === "select" ? { options: strings(value.options) ?? [] } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function collection(value: unknown, fields: CollectionField[]) {
|
|
165
|
+
if (!Array.isArray(value)) return null;
|
|
166
|
+
const items = value.flatMap((item) => {
|
|
167
|
+
if (!isRecord(item)) return [];
|
|
168
|
+
return [Object.fromEntries(fields.map((field) => [
|
|
169
|
+
field.key,
|
|
170
|
+
scalar(item[field.key], field.type) ?? field.defaultValue,
|
|
171
|
+
])) as CollectionItem];
|
|
172
|
+
});
|
|
173
|
+
return items.length === value.length ? items : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isCustomizationValue(value: unknown): value is CustomizationValue {
|
|
177
|
+
if (typeof value === "string" || typeof value === "boolean") return true;
|
|
178
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
179
|
+
if (!Array.isArray(value)) return false;
|
|
180
|
+
return value.every((item) => typeof item === "string") ||
|
|
181
|
+
value.every((item) => isRecord(item) && Object.values(item).every(isScalarValue));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function scalar(value: unknown, type: ScalarType): ScalarValue | null {
|
|
185
|
+
if (type === "number" || type === "range") return number(value);
|
|
186
|
+
if (type === "boolean") return typeof value === "boolean" ? value : null;
|
|
187
|
+
return typeof value === "string" ? value : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeScalarValue(
|
|
191
|
+
field: { type: ScalarType; min?: number; max?: number; options?: string[] },
|
|
192
|
+
value: unknown,
|
|
193
|
+
): ScalarValue | undefined {
|
|
194
|
+
if (field.type === "number" || field.type === "range") {
|
|
195
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
196
|
+
if (field.min !== undefined && value < field.min) return undefined;
|
|
197
|
+
if (field.max !== undefined && value > field.max) return undefined;
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
if (field.type === "boolean") return typeof value === "boolean" ? value : undefined;
|
|
201
|
+
if (typeof value !== "string" || value.length > 20_000) return undefined;
|
|
202
|
+
if (field.type === "select" && field.options?.length && !field.options.includes(value)) return undefined;
|
|
203
|
+
if (field.type === "color" && !/^#[\da-f]{3}([\da-f]{3})?$/i.test(value)) return undefined;
|
|
204
|
+
if (field.type === "icon" && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) return undefined;
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function scalarDefault(type: ScalarType): ScalarValue {
|
|
209
|
+
if (type === "number" || type === "range") return 0;
|
|
210
|
+
if (type === "boolean") return false;
|
|
211
|
+
if (type === "icon") return "circle";
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isScalarValue(value: unknown): value is ScalarValue {
|
|
216
|
+
return typeof value === "string" || typeof value === "boolean" ||
|
|
217
|
+
(typeof value === "number" && Number.isFinite(value));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isScalarType(value: unknown): value is ScalarType {
|
|
221
|
+
return SCALAR_TYPES.includes(value as ScalarType);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function number(value: unknown) {
|
|
225
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function strings(value: unknown) {
|
|
229
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function text(value: unknown) {
|
|
233
|
+
return typeof value === "string" ? value.trim() : "";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
237
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseJson<T>(value: string, fallback: T) {
|
|
241
|
+
let parsed: unknown = value;
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < 4 && typeof parsed === "string"; i++) {
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(parsed) as unknown;
|
|
246
|
+
} catch {
|
|
247
|
+
return fallback;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return parsed;
|
|
252
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nowebcli",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Create, preview and publish community templates for Noweb",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lnny8/noweb.git"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/customizations.ts",
|
|
12
|
+
"scripts/noweb.mjs",
|
|
13
|
+
"templates/compileTailwind.ts",
|
|
14
|
+
"templates/shadersRuntime.tsx",
|
|
15
|
+
"templates/templateCompiler.ts",
|
|
16
|
+
"templates/templateRuntime.tsx",
|
|
17
|
+
"templates/validateTemplateCode.ts",
|
|
18
|
+
"tsconfig.json"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"noweb": "scripts/noweb.mjs",
|
|
25
|
+
"nowebcli": "scripts/noweb.mjs"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "next dev --webpack",
|
|
29
|
+
"build": "next build",
|
|
30
|
+
"start": "next start",
|
|
31
|
+
"lint": "eslint --max-warnings=0",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:e2e": "playwright test",
|
|
35
|
+
"screenshots": "tsx scripts/generate-template-screenshots.ts",
|
|
36
|
+
"template": "node scripts/noweb.mjs",
|
|
37
|
+
"template:publish": "node --env-file-if-exists=.env --env-file-if-exists=.env.local scripts/noweb.mjs publish",
|
|
38
|
+
"migrate:cli-auth": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-cli-auth.ts",
|
|
39
|
+
"migrate:project-snapshots": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-project-snapshots.ts",
|
|
40
|
+
"migrate:project-data": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-project-data.ts",
|
|
41
|
+
"migrate:template-ids": "tsx scripts/migrate-positive-template-ids.ts",
|
|
42
|
+
"ci": "npm audit --omit=dev --audit-level=high && npm run typecheck && npm run lint && npm run test && npm run build && npm run test:e2e"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@ai-sdk/openai": "^4.0.24",
|
|
46
|
+
"@ai-sdk/react": "^4.0.45",
|
|
47
|
+
"@aws-sdk/client-s3": "^3.1055.0",
|
|
48
|
+
"@heroicons/react": "^2.2.0",
|
|
49
|
+
"@monaco-editor/react": "^4.7.0",
|
|
50
|
+
"@polar-sh/better-auth": "^1.8.4",
|
|
51
|
+
"@polar-sh/nextjs": "^0.9.6",
|
|
52
|
+
"@polar-sh/sdk": "^0.47.1",
|
|
53
|
+
"@react-three/drei": "^10.7.7",
|
|
54
|
+
"@react-three/fiber": "^9.6.1",
|
|
55
|
+
"@react-three/rapier": "^2.2.0",
|
|
56
|
+
"@tailwindcss/postcss": "^4",
|
|
57
|
+
"@types/pg": "^8.20.0",
|
|
58
|
+
"@uiw/react-textarea-code-editor": "^3.1.1",
|
|
59
|
+
"ai": "^7.0.42",
|
|
60
|
+
"better-auth": "1.7.2",
|
|
61
|
+
"clsx": "^2.1.1",
|
|
62
|
+
"esbuild": "^0.28.1",
|
|
63
|
+
"fumadocs-core": "^16.15.11",
|
|
64
|
+
"fumadocs-ui": "^16.15.11",
|
|
65
|
+
"geist": "^1.7.2",
|
|
66
|
+
"gsap": "^3.15.0",
|
|
67
|
+
"html2canvas-pro": "^2.3.3",
|
|
68
|
+
"lucide-react": "^1.14.0",
|
|
69
|
+
"monaco-editor": "^0.53.0",
|
|
70
|
+
"motion": "^12.40.0",
|
|
71
|
+
"next": "^16.2.10",
|
|
72
|
+
"ogl": "^1.0.11",
|
|
73
|
+
"pg": "^8.22.0",
|
|
74
|
+
"playwright": "^1.61.1",
|
|
75
|
+
"postcss": "^8.5.6",
|
|
76
|
+
"postgres": "^3.4.9",
|
|
77
|
+
"react": "19.2.4",
|
|
78
|
+
"react-dom": "19.2.4",
|
|
79
|
+
"react-markdown": "^10.1.0",
|
|
80
|
+
"remark-gfm": "^4.0.1",
|
|
81
|
+
"shaders": "^3.0.445",
|
|
82
|
+
"shiki": "^4.3.0",
|
|
83
|
+
"tailwindcss": "^4",
|
|
84
|
+
"three": "^0.184.0",
|
|
85
|
+
"tsx": "^4.22.4",
|
|
86
|
+
"typescript": "^5",
|
|
87
|
+
"zod": "^4.4.3"
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"@playwright/test": "^1.61.1",
|
|
91
|
+
"@tailwindcss/vite": "^4.3.1",
|
|
92
|
+
"@types/node": "^20",
|
|
93
|
+
"@types/react": "^19",
|
|
94
|
+
"@types/react-dom": "^19",
|
|
95
|
+
"@types/three": "^0.184.1",
|
|
96
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
97
|
+
"eslint": "^9",
|
|
98
|
+
"eslint-config-next": "^16.2.10",
|
|
99
|
+
"shadcn": "^4.16.1",
|
|
100
|
+
"vite": "^8.1.0",
|
|
101
|
+
"vitest": "^4.1.10"
|
|
102
|
+
}
|
|
103
|
+
}
|