@thinhnguyencth1204/nextcli 0.4.2 → 0.5.0
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 +7 -6
- package/dist/cli.js +58 -86
- package/package.json +2 -1
- package/templates/next-base/.env +8 -6
- package/templates/next-base/.env.development +8 -6
- package/templates/next-base/.env.example +8 -6
- package/templates/next-base/PROJECT_STRUCTURE.md +1 -2
- package/templates/next-base/SETUP.md +11 -16
- package/templates/next-base/package.json +1 -0
- package/templates/{features/supabase → next-base}/src/lib/supabase/client.ts +1 -4
- package/templates/{features/supabase → next-base}/src/lib/supabase/storage.ts +1 -4
- /package/templates/{features/supabase → next-base}/src/lib/supabase/storage-config.ts +0 -0
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ This command is fully interactive:
|
|
|
27
27
|
|
|
28
28
|
- enter project name
|
|
29
29
|
- select package manager in CLI UI (`npm`, `pnpm`, `yarn`, `bun`)
|
|
30
|
-
- multi-select optional modules (`chat`, `supabase
|
|
30
|
+
- multi-select optional modules (`chat`, `supabase-realtime`, `seo`, `resend`)
|
|
31
31
|
- confirm install step
|
|
32
32
|
- normalizes project directory name into a safe project slug for generated `package.json` and env placeholders
|
|
33
33
|
- ships `SETUP.md` and `PROJECT_STRUCTURE.md` in the generated project root
|
|
@@ -95,7 +95,6 @@ Module copy is non-destructive: existing files are kept and reported as skipped
|
|
|
95
95
|
Interactive multiselect shows available module catalog:
|
|
96
96
|
|
|
97
97
|
- `chat`
|
|
98
|
-
- `supabase`
|
|
99
98
|
- `supabase-realtime`
|
|
100
99
|
- `seo`
|
|
101
100
|
- `resend`
|
|
@@ -103,9 +102,11 @@ Interactive multiselect shows available module catalog:
|
|
|
103
102
|
Non-interactive example:
|
|
104
103
|
|
|
105
104
|
```bash
|
|
106
|
-
node ../dist/cli.js add module --module seo --module supabase --yes
|
|
105
|
+
node ../dist/cli.js add module --module seo --module supabase-realtime --yes
|
|
107
106
|
```
|
|
108
107
|
|
|
108
|
+
`supabase` is no longer an optional module; Supabase client, Storage helpers, and Supabase Postgres env placeholders are part of the base template.
|
|
109
|
+
|
|
109
110
|
## Command: add auth-provider
|
|
110
111
|
|
|
111
112
|
Enable social auth providers after project creation (not created by default):
|
|
@@ -128,7 +129,7 @@ node ../dist/cli.js add auth-provider --provider google --provider facebook --ye
|
|
|
128
129
|
This command:
|
|
129
130
|
|
|
130
131
|
- updates `src/lib/auth.ts` provider block
|
|
131
|
-
- merges provider env keys into `.env
|
|
132
|
+
- merges provider env keys into `.env`, `.env.example`, and `.env.development`
|
|
132
133
|
- runs Better Auth schema generation helper (with install prompt if CLI is missing in interactive mode)
|
|
133
134
|
|
|
134
135
|
## Command: migrate
|
|
@@ -154,6 +155,7 @@ node ../dist/cli.js migrate --name init_auth --skip-generate
|
|
|
154
155
|
|
|
155
156
|
- Next.js App Router + TypeScript
|
|
156
157
|
- Better Auth + Prisma adapter
|
|
158
|
+
- Supabase as the default Postgres/Storage stack
|
|
157
159
|
- Prisma ORM (`prisma/schema.prisma`, `prisma/migrations`)
|
|
158
160
|
- Axios fetch wrapper
|
|
159
161
|
- TanStack React Query
|
|
@@ -164,7 +166,7 @@ node ../dist/cli.js migrate --name init_auth --skip-generate
|
|
|
164
166
|
- i18n base with `next-intl`
|
|
165
167
|
- Sonner notifications
|
|
166
168
|
- date-fns utility library
|
|
167
|
-
- Optional modules: chat, supabase
|
|
169
|
+
- Optional modules: chat, supabase-realtime, seo, resend
|
|
168
170
|
|
|
169
171
|
## Template structure
|
|
170
172
|
|
|
@@ -175,7 +177,6 @@ Base template lives in:
|
|
|
175
177
|
Optional module templates:
|
|
176
178
|
|
|
177
179
|
- `templates/features/chat`
|
|
178
|
-
- `templates/features/supabase`
|
|
179
180
|
- `templates/features/supabase-realtime`
|
|
180
181
|
- `templates/features/seo`
|
|
181
182
|
- `templates/features/resend`
|
package/dist/cli.js
CHANGED
|
@@ -137,7 +137,7 @@ async function replaceTokensInDirectory(directoryPath, replacements) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
async function mergeEnvFile(envFilePath, entries) {
|
|
140
|
+
async function mergeEnvFile(envFilePath, entries, options = {}) {
|
|
141
141
|
const envExists = await pathExists(envFilePath);
|
|
142
142
|
const currentContent = envExists ? await readFile(envFilePath, "utf8") : "";
|
|
143
143
|
const lines = currentContent ? currentContent.split("\n") : [];
|
|
@@ -161,6 +161,18 @@ async function mergeEnvFile(envFilePath, entries) {
|
|
|
161
161
|
if (additions.length === 0) {
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
|
+
if (options.header && currentContent.includes(options.header)) {
|
|
165
|
+
const nextContent2 = currentContent.replace(
|
|
166
|
+
options.header,
|
|
167
|
+
`${options.header}
|
|
168
|
+
${additions.join("\n")}`
|
|
169
|
+
);
|
|
170
|
+
await writeFile(envFilePath, nextContent2, "utf8");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (options.header) {
|
|
174
|
+
additions.unshift(options.header);
|
|
175
|
+
}
|
|
164
176
|
const separator = currentContent.endsWith("\n") || currentContent.length === 0 ? "" : "\n";
|
|
165
177
|
const nextContent = `${currentContent}${separator}${additions.join("\n")}
|
|
166
178
|
`;
|
|
@@ -202,7 +214,6 @@ var rootDir = path2.resolve(currentDir, "../");
|
|
|
202
214
|
var templatePaths = {
|
|
203
215
|
base: path2.join(rootDir, "templates/next-base"),
|
|
204
216
|
chat: path2.join(rootDir, "templates/features/chat"),
|
|
205
|
-
supabase: path2.join(rootDir, "templates/features/supabase"),
|
|
206
217
|
supabaseRealtime: path2.join(rootDir, "templates/features/supabase-realtime"),
|
|
207
218
|
seo: path2.join(rootDir, "templates/features/seo"),
|
|
208
219
|
resend: path2.join(rootDir, "templates/features/resend")
|
|
@@ -223,25 +234,6 @@ var optionalModules = [
|
|
|
223
234
|
| \`NEXT_PUBLIC_ENABLE_CHAT\` | Set \`true\` when chat module is enabled (auto on add) |
|
|
224
235
|
|
|
225
236
|
Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
id: "supabase",
|
|
229
|
-
label: "Supabase",
|
|
230
|
-
description: "Adds Supabase client and storage upload helpers",
|
|
231
|
-
templatePath: templatePaths.supabase,
|
|
232
|
-
env: {
|
|
233
|
-
NEXT_PUBLIC_SUPABASE_URL: "",
|
|
234
|
-
NEXT_PUBLIC_SUPABASE_ANON_KEY: "",
|
|
235
|
-
NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET: "public"
|
|
236
|
-
},
|
|
237
|
-
dependencies: {
|
|
238
|
-
"@supabase/supabase-js": "^2.44.2"
|
|
239
|
-
},
|
|
240
|
-
setupSection: `| Variable | Where to get |
|
|
241
|
-
| -------- | ------------ |
|
|
242
|
-
| \`NEXT_PUBLIC_SUPABASE_URL\` | Supabase Dashboard \u2192 Project Settings \u2192 API \u2192 Project URL |
|
|
243
|
-
| \`NEXT_PUBLIC_SUPABASE_ANON_KEY\` | Same page \u2192 Project API keys \u2192 \`anon\` \`public\` |
|
|
244
|
-
| \`NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET\` | Storage \u2192 create bucket \u2192 use bucket name (default scaffold: \`public\`) |`
|
|
245
237
|
},
|
|
246
238
|
{
|
|
247
239
|
id: "supabase-realtime",
|
|
@@ -255,7 +247,7 @@ Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014
|
|
|
255
247
|
dependencies: {
|
|
256
248
|
"@supabase/supabase-js": "^2.44.2"
|
|
257
249
|
},
|
|
258
|
-
setupSection: `Uses
|
|
250
|
+
setupSection: `Uses the base Supabase URL/anon key. Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
|
|
259
251
|
},
|
|
260
252
|
{
|
|
261
253
|
id: "seo",
|
|
@@ -560,7 +552,7 @@ import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } f
|
|
|
560
552
|
import path4 from "path";
|
|
561
553
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
562
554
|
var defaultManifest = {
|
|
563
|
-
cli: "0.
|
|
555
|
+
cli: "0.5.0",
|
|
564
556
|
defaultLocale: "vi",
|
|
565
557
|
locales: ["vi"],
|
|
566
558
|
namespaces: ["common", "auth", "example"],
|
|
@@ -625,7 +617,9 @@ async function reconcileManifest(projectDir) {
|
|
|
625
617
|
]
|
|
626
618
|
};
|
|
627
619
|
merged.defaultLocale = merged.locales.includes(merged.defaultLocale) ? merged.defaultLocale : merged.locales[0] ?? "vi";
|
|
628
|
-
merged.modules = [...new Set(merged.modules)]
|
|
620
|
+
merged.modules = [...new Set(merged.modules)].filter(
|
|
621
|
+
(moduleId) => moduleId !== "supabase"
|
|
622
|
+
);
|
|
629
623
|
merged.features = [...new Set(merged.features)];
|
|
630
624
|
await writeManifest(projectDir, merged);
|
|
631
625
|
return merged;
|
|
@@ -1307,24 +1301,6 @@ function normalizeModuleSelection(moduleIds) {
|
|
|
1307
1301
|
autoAddedModules: autoAdded
|
|
1308
1302
|
};
|
|
1309
1303
|
}
|
|
1310
|
-
async function upsertEnvValue(envFilePath, key, value) {
|
|
1311
|
-
if (!await pathExists(envFilePath)) {
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
const content = await readFile6(envFilePath, "utf8");
|
|
1315
|
-
const entry = `${key}=${value}`;
|
|
1316
|
-
const pattern = new RegExp(`^${key}=.*$`, "m");
|
|
1317
|
-
if (pattern.test(content)) {
|
|
1318
|
-
const next = content.replace(pattern, entry);
|
|
1319
|
-
if (next !== content) {
|
|
1320
|
-
await writeFile6(envFilePath, next, "utf8");
|
|
1321
|
-
}
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
1325
|
-
await writeFile6(envFilePath, `${content}${separator}${entry}
|
|
1326
|
-
`, "utf8");
|
|
1327
|
-
}
|
|
1328
1304
|
function registerAddCommand(program2) {
|
|
1329
1305
|
const add = program2.command("add").description("Add modules to an existing app");
|
|
1330
1306
|
add.command("feature").description("Scaffold a feature folder under src/features").argument("<feature-name>", "Feature name").action(async (featureName) => {
|
|
@@ -1452,9 +1428,22 @@ function registerAddCommand(program2) {
|
|
|
1452
1428
|
process.exitCode = 1;
|
|
1453
1429
|
return;
|
|
1454
1430
|
}
|
|
1455
|
-
const validIds = new Set(
|
|
1456
|
-
|
|
1457
|
-
|
|
1431
|
+
const validIds = new Set(
|
|
1432
|
+
optionalModules.map((module) => module.id)
|
|
1433
|
+
);
|
|
1434
|
+
const requestedValues = options.module ? options.module.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean) : [];
|
|
1435
|
+
const requestedIds = [];
|
|
1436
|
+
for (const moduleId of requestedValues) {
|
|
1437
|
+
if (validIds.has(moduleId)) {
|
|
1438
|
+
requestedIds.push(moduleId);
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (moduleId === "supabase") {
|
|
1442
|
+
log.info(
|
|
1443
|
+
"Supabase is now part of the base template; only supabase-realtime is optional."
|
|
1444
|
+
);
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1458
1447
|
if (!validIds.has(moduleId)) {
|
|
1459
1448
|
log.error(`Unknown module: ${moduleId}`);
|
|
1460
1449
|
process.exitCode = 1;
|
|
@@ -1493,35 +1482,21 @@ function registerAddCommand(program2) {
|
|
|
1493
1482
|
if (selectedModules.includes("chat")) {
|
|
1494
1483
|
chatSchemaStatus = await ensureChatSchemaInProject(cwd);
|
|
1495
1484
|
}
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
};
|
|
1503
|
-
},
|
|
1504
|
-
{}
|
|
1505
|
-
);
|
|
1506
|
-
if (Object.keys(envEntries).length > 0) {
|
|
1507
|
-
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1485
|
+
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1486
|
+
for (const moduleId of selectedModules) {
|
|
1487
|
+
const module = getModuleById(moduleId);
|
|
1488
|
+
if (Object.keys(module.env).length === 0) {
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1508
1491
|
for (const envFile of envTargets) {
|
|
1509
1492
|
const envPath = path7.join(cwd, envFile);
|
|
1510
1493
|
if (await pathExists(envPath)) {
|
|
1511
|
-
await mergeEnvFile(envPath,
|
|
1494
|
+
await mergeEnvFile(envPath, module.env, {
|
|
1495
|
+
header: `# --- module: ${module.id} ---`
|
|
1496
|
+
});
|
|
1512
1497
|
}
|
|
1513
1498
|
}
|
|
1514
1499
|
}
|
|
1515
|
-
if (selectedModules.includes("chat")) {
|
|
1516
|
-
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1517
|
-
for (const envFile of envTargets) {
|
|
1518
|
-
await upsertEnvValue(
|
|
1519
|
-
path7.join(cwd, envFile),
|
|
1520
|
-
"NEXT_PUBLIC_ENABLE_CHAT",
|
|
1521
|
-
"true"
|
|
1522
|
-
);
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
1500
|
const dependencyEntries = selectedModules.reduce(
|
|
1526
1501
|
(acc, moduleId) => {
|
|
1527
1502
|
const module = getModuleById(moduleId);
|
|
@@ -1735,7 +1710,9 @@ function registerAddCommand(program2) {
|
|
|
1735
1710
|
for (const envFile of envTargets) {
|
|
1736
1711
|
const envPath = path7.join(cwd, envFile);
|
|
1737
1712
|
if (await pathExists(envPath)) {
|
|
1738
|
-
await mergeEnvFile(envPath, envEntries
|
|
1713
|
+
await mergeEnvFile(envPath, envEntries, {
|
|
1714
|
+
header: "# --- auth providers ---"
|
|
1715
|
+
});
|
|
1739
1716
|
}
|
|
1740
1717
|
}
|
|
1741
1718
|
finishPrompt(`Enabled providers: ${mergedProviders.join(", ")}`);
|
|
@@ -1854,22 +1831,18 @@ function registerCreateCommand(program2) {
|
|
|
1854
1831
|
if (selectedModules.includes("chat")) {
|
|
1855
1832
|
chatSchemaStatus = await ensureChatSchemaInProject(targetPath);
|
|
1856
1833
|
}
|
|
1857
|
-
const
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
};
|
|
1864
|
-
},
|
|
1865
|
-
{}
|
|
1866
|
-
);
|
|
1867
|
-
if (Object.keys(moduleEnvEntries).length > 0) {
|
|
1868
|
-
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1834
|
+
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1835
|
+
for (const moduleId of selectedModules) {
|
|
1836
|
+
const module = getModuleById(moduleId);
|
|
1837
|
+
if (Object.keys(module.env).length === 0) {
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1869
1840
|
for (const envFile of envTargets) {
|
|
1870
1841
|
const envPath = path8.join(targetPath, envFile);
|
|
1871
1842
|
if (await pathExists(envPath)) {
|
|
1872
|
-
await mergeEnvFile(envPath,
|
|
1843
|
+
await mergeEnvFile(envPath, module.env, {
|
|
1844
|
+
header: `# --- module: ${module.id} ---`
|
|
1845
|
+
});
|
|
1873
1846
|
}
|
|
1874
1847
|
}
|
|
1875
1848
|
}
|
|
@@ -1892,9 +1865,8 @@ function registerCreateCommand(program2) {
|
|
|
1892
1865
|
const betterAuthSecret = randomBytes(32).toString("base64url");
|
|
1893
1866
|
await replaceTokensInDirectory(targetPath, {
|
|
1894
1867
|
__PROJECT_NAME__: projectSlug,
|
|
1895
|
-
__ENABLE_CHAT__: selectedModules.includes("chat") ? "true" : "false",
|
|
1896
1868
|
__BETTER_AUTH_SECRET__: betterAuthSecret,
|
|
1897
|
-
__NEXTCLI_VERSION__: "0.
|
|
1869
|
+
__NEXTCLI_VERSION__: "0.5.0"
|
|
1898
1870
|
});
|
|
1899
1871
|
await mergeModuleSetupSections(
|
|
1900
1872
|
targetPath,
|
|
@@ -1905,7 +1877,7 @@ function registerCreateCommand(program2) {
|
|
|
1905
1877
|
if (manifest) {
|
|
1906
1878
|
await writeManifest(targetPath, {
|
|
1907
1879
|
...manifest,
|
|
1908
|
-
cli: "0.
|
|
1880
|
+
cli: "0.5.0",
|
|
1909
1881
|
modules: selectedModules
|
|
1910
1882
|
});
|
|
1911
1883
|
}
|
|
@@ -2147,7 +2119,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
|
|
|
2147
2119
|
|
|
2148
2120
|
// src/cli.ts
|
|
2149
2121
|
var program = new NexTCLICommand();
|
|
2150
|
-
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.
|
|
2122
|
+
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.5.0");
|
|
2151
2123
|
registerCreateCommand(program);
|
|
2152
2124
|
registerAddCommand(program);
|
|
2153
2125
|
registerMigrateCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinhnguyencth1204/nextcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI scaffolder for outsourced Next.js projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"dev": "tsup --watch",
|
|
17
17
|
"typecheck": "tsc --noEmit",
|
|
18
18
|
"smoke": "node dist/cli.js --help",
|
|
19
|
+
"smoke:full": "bun run scripts/pre-publish-smoke.ts",
|
|
19
20
|
"prepublishOnly": "npm run build"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
package/templates/next-base/.env
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
# --- Database (Supabase) ---
|
|
2
|
+
DATABASE_URL="postgresql://postgres:your-supabase-db-password@db.your-project-ref.supabase.co:5432/postgres"
|
|
3
|
+
NEXT_PUBLIC_SUPABASE_URL=""
|
|
4
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=""
|
|
5
|
+
NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET="public"
|
|
6
|
+
|
|
7
|
+
# --- Auth ---
|
|
2
8
|
BETTER_AUTH_SECRET="__BETTER_AUTH_SECRET__"
|
|
3
9
|
BETTER_AUTH_URL="http://localhost:3000"
|
|
4
10
|
|
|
11
|
+
# --- App ---
|
|
5
12
|
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
6
|
-
NEXT_PUBLIC_ENABLE_CHAT=__ENABLE_CHAT__
|
|
7
13
|
NEXT_PUBLIC_DEFAULT_LOCALE="en"
|
|
8
|
-
GOOGLE_CLIENT_ID=""
|
|
9
|
-
GOOGLE_CLIENT_SECRET=""
|
|
10
|
-
FACEBOOK_CLIENT_ID=""
|
|
11
|
-
FACEBOOK_CLIENT_SECRET=""
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
# --- Database (Supabase) ---
|
|
2
|
+
DATABASE_URL="postgresql://postgres:your-supabase-db-password@db.your-project-ref.supabase.co:5432/postgres"
|
|
3
|
+
NEXT_PUBLIC_SUPABASE_URL=""
|
|
4
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=""
|
|
5
|
+
NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET="public"
|
|
6
|
+
|
|
7
|
+
# --- Auth ---
|
|
2
8
|
BETTER_AUTH_SECRET="__BETTER_AUTH_SECRET__"
|
|
3
9
|
BETTER_AUTH_URL="http://localhost:3000"
|
|
4
10
|
|
|
11
|
+
# --- App ---
|
|
5
12
|
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
6
|
-
NEXT_PUBLIC_ENABLE_CHAT=__ENABLE_CHAT__
|
|
7
13
|
NEXT_PUBLIC_DEFAULT_LOCALE="en"
|
|
8
|
-
GOOGLE_CLIENT_ID=""
|
|
9
|
-
GOOGLE_CLIENT_SECRET=""
|
|
10
|
-
FACEBOOK_CLIENT_ID=""
|
|
11
|
-
FACEBOOK_CLIENT_SECRET=""
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
# --- Database (Supabase) ---
|
|
2
|
+
DATABASE_URL="postgresql://postgres:your-supabase-db-password@db.your-project-ref.supabase.co:5432/postgres"
|
|
3
|
+
NEXT_PUBLIC_SUPABASE_URL=""
|
|
4
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=""
|
|
5
|
+
NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET="public"
|
|
6
|
+
|
|
7
|
+
# --- Auth ---
|
|
2
8
|
BETTER_AUTH_SECRET="your-32-plus-char-random-secret"
|
|
3
9
|
BETTER_AUTH_URL="http://localhost:3000"
|
|
4
10
|
|
|
11
|
+
# --- App ---
|
|
5
12
|
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
6
|
-
NEXT_PUBLIC_ENABLE_CHAT=__ENABLE_CHAT__
|
|
7
13
|
NEXT_PUBLIC_DEFAULT_LOCALE="en"
|
|
8
|
-
GOOGLE_CLIENT_ID=""
|
|
9
|
-
GOOGLE_CLIENT_SECRET=""
|
|
10
|
-
FACEBOOK_CLIENT_ID=""
|
|
11
|
-
FACEBOOK_CLIENT_SECRET=""
|
|
@@ -41,7 +41,7 @@ Generated by NexTCLI. Folders marked **(module)** or **(feature)** appear only w
|
|
|
41
41
|
| `rbac.ts` | Role hierarchy guards |
|
|
42
42
|
| `prisma.ts` | DB client |
|
|
43
43
|
| `api-response.ts` | `/api/v1/*` envelope helpers |
|
|
44
|
-
| `supabase/` |
|
|
44
|
+
| `supabase/` | Supabase client + Storage helpers |
|
|
45
45
|
| `resend/` | **(module: `resend`)** email client + send helper |
|
|
46
46
|
|
|
47
47
|
## `src/features/` vs `src/example/`
|
|
@@ -74,7 +74,6 @@ Locale config with `nextcli:locales` / `nextcli:namespaces` markers (patched by
|
|
|
74
74
|
| Module | Adds |
|
|
75
75
|
| ------------------- | ------------------------------------------------------------------- |
|
|
76
76
|
| `chat` | Chat routes, hooks, Prisma chat models (+ auto `supabase-realtime`) |
|
|
77
|
-
| `supabase` | `src/lib/supabase/*`, Storage upload helpers |
|
|
78
77
|
| `supabase-realtime` | Realtime channel helpers |
|
|
79
78
|
| `seo` | robots/sitemap/JSON-LD helpers |
|
|
80
79
|
| `resend` | `src/lib/resend/*`, `src/emails/*` |
|
|
@@ -4,7 +4,7 @@ Quick reference for env vars and branding after `nextcli create`.
|
|
|
4
4
|
|
|
5
5
|
## First run (required)
|
|
6
6
|
|
|
7
|
-
1.
|
|
7
|
+
1. Create a Supabase project and set `DATABASE_URL` in `.env`.
|
|
8
8
|
2. `bun run db:migrate` — applies `prisma/migrations` (includes `Role`, `User.username`, `requirePasswordChange`).
|
|
9
9
|
3. `bun run dev` — bootstrap seeds `admin` / `admin` on first start.
|
|
10
10
|
|
|
@@ -23,12 +23,15 @@ Quick reference for env vars and branding after `nextcli create`.
|
|
|
23
23
|
|
|
24
24
|
Set in `.env` / `.env.development` (secrets are generated at create time).
|
|
25
25
|
|
|
26
|
-
| Variable
|
|
27
|
-
|
|
|
28
|
-
| `DATABASE_URL`
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
26
|
+
| Variable | Purpose | Where to get |
|
|
27
|
+
| ------------------------------------- | -------------------------- | ---------------------------------------------------------------------- |
|
|
28
|
+
| `DATABASE_URL` | Supabase Postgres database | Supabase Dashboard → Project Settings → Database → Connection string |
|
|
29
|
+
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase client URL | Supabase Dashboard → Project Settings → API → Project URL |
|
|
30
|
+
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase browser anon key | Same page → Project API keys → `anon` `public` |
|
|
31
|
+
| `NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET` | Supabase Storage bucket | Storage → create bucket → use bucket name (default scaffold: `public`) |
|
|
32
|
+
| `BETTER_AUTH_SECRET` | Auth signing secret | Auto-generated on create; rotate in production |
|
|
33
|
+
| `BETTER_AUTH_URL` | Server auth base URL | Your app URL (e.g. `http://localhost:3000`) |
|
|
34
|
+
| `NEXT_PUBLIC_APP_URL` | Client-visible app URL | Same as public site URL |
|
|
32
35
|
|
|
33
36
|
### Default admin account
|
|
34
37
|
|
|
@@ -54,17 +57,9 @@ Reference for all optional modules. Matching keys are merged into `.env` when se
|
|
|
54
57
|
|
|
55
58
|
Requires `supabase-realtime` (auto-added). Run `db:migrate` after add — chat Prisma models are appended.
|
|
56
59
|
|
|
57
|
-
### Module: Supabase (`supabase`)
|
|
58
|
-
|
|
59
|
-
| Variable | Where to get |
|
|
60
|
-
| ------------------------------------- | ---------------------------------------------------------------------- |
|
|
61
|
-
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase Dashboard → Project Settings → API → Project URL |
|
|
62
|
-
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Same page → Project API keys → `anon` `public` |
|
|
63
|
-
| `NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET` | Storage → create bucket → use bucket name (default scaffold: `public`) |
|
|
64
|
-
|
|
65
60
|
### Module: Supabase Realtime (`supabase-realtime`)
|
|
66
61
|
|
|
67
|
-
Uses
|
|
62
|
+
Uses the base Supabase URL/anon key. Enable Realtime on tables in Supabase Dashboard → Database → Replication.
|
|
68
63
|
|
|
69
64
|
### Module: SEO pack (`seo`)
|
|
70
65
|
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@radix-ui/react-slot": "^1.2.3",
|
|
28
28
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
29
29
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
30
|
+
"@supabase/supabase-js": "^2.44.2",
|
|
30
31
|
"@tanstack/react-form": "^1.0.3",
|
|
31
32
|
"@tanstack/react-query": "^5.56.2",
|
|
32
33
|
"@tanstack/react-query-devtools": "^5.56.2",
|
|
@@ -3,7 +3,4 @@ import { createClient } from "@supabase/supabase-js";
|
|
|
3
3
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
4
4
|
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
5
5
|
|
|
6
|
-
export const supabase =
|
|
7
|
-
url && anonKey
|
|
8
|
-
? createClient(url, anonKey)
|
|
9
|
-
: null;
|
|
6
|
+
export const supabase = url && anonKey ? createClient(url, anonKey) : null;
|
|
@@ -86,10 +86,7 @@ function assertCategoryRules(
|
|
|
86
86
|
|
|
87
87
|
const mime = contentType ?? (file instanceof File ? file.type : "");
|
|
88
88
|
|
|
89
|
-
if (
|
|
90
|
-
!mime ||
|
|
91
|
-
!allowedMimePrefixes.some((prefix) => mime.startsWith(prefix))
|
|
92
|
-
) {
|
|
89
|
+
if (!mime || !allowedMimePrefixes.some((prefix) => mime.startsWith(prefix))) {
|
|
93
90
|
throw new StorageHelperError(
|
|
94
91
|
`File type "${mime || "unknown"}" is not allowed for category "${category}".`,
|
|
95
92
|
"VALIDATION",
|
|
File without changes
|