@thinhnguyencth1204/nextcli 0.7.0 → 0.9.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 +37 -24
- package/dist/cli.js +168 -107
- package/package.json +5 -3
- package/templates/features/supabase/src/lib/supabase/rich-text-image-sync.ts +28 -0
- package/templates/next-base/.env +16 -0
- package/templates/next-base/.env.development +16 -0
- package/templates/next-base/.env.example +16 -0
- package/templates/next-base/PROJECT_STRUCTURE.md +29 -18
- package/templates/next-base/SETUP.md +62 -10
- package/templates/next-base/bun.lock +59 -414
- package/templates/next-base/messages/vi/auth.json +42 -0
- package/templates/next-base/messages/vi/common.json +34 -0
- package/templates/next-base/messages/vi/example.json +10 -0
- package/templates/next-base/next-env.d.ts +1 -1
- package/templates/next-base/next.config.ts +4 -1
- package/templates/next-base/nextcli.json +12 -4
- package/templates/next-base/package.json +25 -1
- package/templates/next-base/prisma/schema.prisma +84 -0
- package/templates/next-base/prisma.config.ts +16 -0
- package/templates/next-base/src/app/(auth)/.gitkeep +1 -0
- package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
- package/templates/next-base/src/app/(auth)/change-password/page.tsx +14 -0
- package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
- package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -0
- package/templates/next-base/src/app/(auth)/sign-in/page.tsx +14 -0
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +18 -0
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +13 -0
- package/templates/next-base/src/app/(dashboard)/layout.tsx +22 -0
- package/templates/next-base/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
- package/templates/next-base/src/app/api/v1/auth/login/route.ts +70 -0
- package/templates/next-base/src/app/api/v1/auth/logout/route.ts +28 -0
- package/templates/next-base/src/app/api/v1/auth/me/route.ts +24 -0
- package/templates/next-base/src/app/api/v1/auth/refresh/route.ts +32 -0
- package/templates/next-base/src/app/api/v1/example/route.ts +34 -0
- package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
- package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
- package/templates/next-base/src/app/blog-demo/page.tsx +9 -0
- package/templates/next-base/src/app/globals.css +57 -0
- package/templates/next-base/src/app/layout.tsx +14 -6
- package/templates/next-base/src/app/page.tsx +2 -25
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +44 -0
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +54 -0
- package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
- package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
- package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
- package/templates/next-base/src/components/providers/query-provider.tsx +17 -0
- package/templates/next-base/src/components/rich-text/adapters/textarea-field.tsx +50 -0
- package/templates/next-base/src/components/rich-text/client-only.tsx +23 -0
- package/templates/next-base/src/components/rich-text/editor-field.tsx +62 -0
- package/templates/next-base/src/components/rich-text/examples/blog-rich-text-demo.tsx +218 -0
- package/templates/next-base/src/components/rich-text/index.ts +11 -0
- package/templates/next-base/src/components/rich-text/lexical/extension.ts +37 -0
- package/templates/next-base/src/components/rich-text/lexical/nodes/image-node.tsx +187 -0
- package/templates/next-base/src/components/rich-text/lexical/plugins/image-plugin.tsx +40 -0
- package/templates/next-base/src/components/rich-text/lexical/plugins/initial-state-plugin.tsx +26 -0
- package/templates/next-base/src/components/rich-text/lexical/plugins/on-change-plugin.tsx +26 -0
- package/templates/next-base/src/components/rich-text/lexical/plugins/toolbar-plugin.tsx +190 -0
- package/templates/next-base/src/components/rich-text/lexical/rich-text-editor.tsx +121 -0
- package/templates/next-base/src/components/rich-text/lexical/theme.ts +18 -0
- package/templates/next-base/src/components/rich-text/rich-text-renderer.tsx +72 -0
- package/templates/next-base/src/components/rich-text/types.ts +60 -0
- package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
- package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
- package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
- package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
- package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
- package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
- package/templates/next-base/src/data/sidebar-modules.ts +11 -0
- package/templates/next-base/src/example/api/use-example.ts +21 -0
- package/templates/next-base/src/example/api/use-mutations.ts +20 -0
- package/templates/next-base/src/example/components/example-table.tsx +51 -0
- package/templates/next-base/src/example/services.ts +9 -0
- package/templates/next-base/src/example/validations.ts +8 -0
- package/templates/next-base/src/features/auth/components/account-panel.tsx +80 -0
- package/templates/next-base/src/features/auth/components/change-password-form.tsx +82 -0
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +95 -0
- package/templates/next-base/src/features/auth/validations.ts +14 -0
- package/templates/next-base/src/features/users/services.ts +132 -0
- package/templates/next-base/src/features/users/validations.ts +21 -0
- package/templates/next-base/src/hooks/index.ts +1 -1
- package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
- package/templates/next-base/src/hooks/use-mobile.ts +25 -0
- package/templates/next-base/src/i18n/config.ts +7 -0
- package/templates/next-base/src/i18n/namespaces.ts +5 -0
- package/templates/next-base/src/i18n/request.ts +25 -0
- package/templates/next-base/src/instrumentation.ts +14 -0
- package/templates/next-base/src/lib/api/axios.ts +145 -0
- package/templates/next-base/src/lib/api/response.ts +45 -0
- package/templates/next-base/src/lib/api/token-store.ts +13 -0
- package/templates/next-base/src/lib/auth/bootstrap.ts +95 -0
- package/templates/next-base/src/lib/auth/client.ts +7 -0
- package/templates/next-base/src/lib/auth/cookies.ts +15 -0
- package/templates/next-base/src/lib/auth/index.ts +1 -0
- package/templates/next-base/src/lib/auth/rbac.ts +59 -0
- package/templates/next-base/src/lib/auth/server.ts +21 -0
- package/templates/next-base/src/lib/constants.ts +10 -0
- package/templates/next-base/src/lib/db/prisma.ts +23 -0
- package/templates/next-base/src/lib/prisma.ts +23 -0
- package/templates/next-base/src/lib/rich-text/default-image-removal.ts +10 -0
- package/templates/next-base/src/lib/rich-text/image-urls.ts +41 -0
- package/templates/next-base/src/lib/rich-text/index.ts +12 -0
- package/templates/next-base/src/lib/rich-text/supabase-url.ts +67 -0
- package/templates/next-base/src/lib/rich-text/sync-removed-images.ts +48 -0
- package/templates/next-base/src/lib/supabase/client.ts +6 -0
- package/templates/next-base/src/lib/supabase/rich-text-image-sync.ts +28 -0
- package/templates/next-base/src/lib/supabase/storage-config.ts +69 -0
- package/templates/next-base/src/lib/supabase/storage.ts +164 -0
- package/templates/next-base/src/types/data-table.ts +4 -0
- package/templates/next-base/src/types/index.ts +0 -2
- package/templates/next-base/tsconfig.tsbuildinfo +1 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ node dist/cli.js --help
|
|
|
17
17
|
|
|
18
18
|
## Command: create
|
|
19
19
|
|
|
20
|
-
Create a new project from the
|
|
20
|
+
Create a new project from the full-stack base template:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
node dist/cli.js create
|
|
@@ -27,36 +27,48 @@ 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 (
|
|
30
|
+
- multi-select optional modules (`chat`, `supabase-realtime`, `seo`, `email`)
|
|
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
|
|
34
34
|
|
|
35
|
-
**Default output
|
|
35
|
+
**Default output includes:** Prisma + Postgres, Supabase client + Storage, Better Auth + RBAC, API client (Axios + React Query), i18n (next-intl), dashboard shell, example CRUD demo, branding, theme, UI primitives, and Lexical rich text adapters. Optional modules are added only when selected.
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Rich text (base)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
All generated projects include Lexical rich text building blocks:
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
| `auth` | Better Auth, sign-in pages, user APIs, bootstrap admin |
|
|
46
|
-
| `api` | Axios client, API envelope helpers, React Query provider |
|
|
47
|
-
| `i18n` | next-intl config, messages, locale switcher (auto-adds `api`) |
|
|
48
|
-
| `dashboard` | Protected dashboard shell, sidebar, data-table UI |
|
|
49
|
-
| `example` | Starter CRUD demo + `Example` Prisma model |
|
|
50
|
-
| `chat` | Chat routes, hooks, Prisma chat models (+ auto deps) |
|
|
51
|
-
| `supabase-realtime` | Realtime channel helpers (+ auto `supabase`) |
|
|
52
|
-
| `seo` | robots/sitemap/JSON-LD helpers |
|
|
53
|
-
| `email` | Email helper + React Email templates (SMTP or Resend) |
|
|
41
|
+
- `EditorField` — swappable `textarea` or `lexical` adapter for forms
|
|
42
|
+
- `RichTextRenderer` — read-only playback of Lexical JSON from API data
|
|
43
|
+
- Toolbar: bold/italic/underline, emoji picker, image URL insertion
|
|
44
|
+
- Demo page: `/blog-demo`
|
|
54
45
|
|
|
55
|
-
|
|
46
|
+
Lexical content is stored as JSON (`SerializedEditorState`). Supabase-managed image URLs trigger storage cleanup via `syncRemovedSupabaseRichTextImages` (included in base).
|
|
56
47
|
|
|
57
|
-
|
|
48
|
+
**Included in every project (base):**
|
|
58
49
|
|
|
59
|
-
|
|
50
|
+
| Module | Adds |
|
|
51
|
+
| ----------- | -------------------------------------------------------- |
|
|
52
|
+
| `database` | Prisma schema, client, `DATABASE_URL`, db scripts |
|
|
53
|
+
| `supabase` | Supabase browser client + Storage helpers |
|
|
54
|
+
| `auth` | Better Auth, sign-in pages, user APIs, bootstrap admin |
|
|
55
|
+
| `api` | Axios client, API envelope helpers, React Query provider |
|
|
56
|
+
| `i18n` | next-intl config, messages, locale switcher |
|
|
57
|
+
| `dashboard` | Protected dashboard shell, sidebar, data-table UI |
|
|
58
|
+
| `example` | Starter CRUD demo + `Example` Prisma model |
|
|
59
|
+
|
|
60
|
+
**Optional** during `create` and `add module`:
|
|
61
|
+
|
|
62
|
+
| Module | Adds |
|
|
63
|
+
| ------------------- | ------------------------------------------------------------------- |
|
|
64
|
+
| `chat` | Chat routes, hooks, Prisma chat models (+ auto `supabase-realtime`) |
|
|
65
|
+
| `supabase-realtime` | Realtime channel helpers |
|
|
66
|
+
| `seo` | robots/sitemap/JSON-LD helpers |
|
|
67
|
+
| `email` | Email helper + React Email templates (SMTP or Resend) |
|
|
68
|
+
|
|
69
|
+
## Auth (base)
|
|
70
|
+
|
|
71
|
+
Every generated project includes:
|
|
60
72
|
|
|
61
73
|
- Better Auth + Prisma adapter with JWT + username plugins
|
|
62
74
|
- username/password sign-in (`/sign-in`) and forced password change (`/change-password`)
|
|
@@ -70,7 +82,7 @@ When selected, generated projects include:
|
|
|
70
82
|
- `GET /api/v1/auth/me`
|
|
71
83
|
- `POST /api/v1/auth/change-password`
|
|
72
84
|
|
|
73
|
-
Axios setup (
|
|
85
|
+
Axios setup (included in base):
|
|
74
86
|
|
|
75
87
|
- `publicApi`: public calls
|
|
76
88
|
- `protectedApi`: bearer token calls with 401 refresh queue + retry
|
|
@@ -85,12 +97,12 @@ Project-owned routes under `/api/v1/*` use a unified envelope:
|
|
|
85
97
|
- Error:
|
|
86
98
|
- `{ success: false, error: { code, message, details? }, timestamp, requestId? }`
|
|
87
99
|
|
|
88
|
-
This is powered by `src/lib/api/response.ts`
|
|
100
|
+
This is powered by `src/lib/api/response.ts` (included in base).
|
|
89
101
|
The Better Auth passthrough route `/api/auth/[...all]` remains unwrapped.
|
|
90
102
|
|
|
91
103
|
## Command: add feature
|
|
92
104
|
|
|
93
|
-
Run inside a generated project root (
|
|
105
|
+
Run inside a generated project root (database and api are included in base):
|
|
94
106
|
|
|
95
107
|
```bash
|
|
96
108
|
node ../dist/cli.js add feature orders
|
|
@@ -174,6 +186,7 @@ Optional flags:
|
|
|
174
186
|
- shadcn-style shared UI folder (base)
|
|
175
187
|
- Zod validation (base)
|
|
176
188
|
- Sonner notifications (base)
|
|
189
|
+
- Lexical rich text editor + renderer (base)
|
|
177
190
|
- Optional: chat, supabase-realtime, seo, email
|
|
178
191
|
|
|
179
192
|
## Template structure
|
package/dist/cli.js
CHANGED
|
@@ -152,17 +152,40 @@ async function mergeEnvFile(envFilePath, entries, options = {}) {
|
|
|
152
152
|
existingKeys.add(key.trim());
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
let nextLines = [...lines];
|
|
155
156
|
const additions = [];
|
|
156
157
|
for (const [key, value] of Object.entries(entries)) {
|
|
158
|
+
if (options.overwrite && existingKeys.has(key)) {
|
|
159
|
+
nextLines = nextLines.map((line) => {
|
|
160
|
+
const trimmed = line.trim();
|
|
161
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
162
|
+
return line;
|
|
163
|
+
}
|
|
164
|
+
const [lineKey] = trimmed.split("=");
|
|
165
|
+
if (lineKey?.trim() === key) {
|
|
166
|
+
return `${key}=${formatEnvValue(value)}`;
|
|
167
|
+
}
|
|
168
|
+
return line;
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
157
172
|
if (!existingKeys.has(key)) {
|
|
158
173
|
additions.push(`${key}=${formatEnvValue(value)}`);
|
|
159
174
|
}
|
|
160
175
|
}
|
|
176
|
+
let content = currentContent;
|
|
177
|
+
if (options.overwrite) {
|
|
178
|
+
content = `${nextLines.join("\n").replace(/\n?$/, "\n")}`;
|
|
179
|
+
if (additions.length === 0) {
|
|
180
|
+
await writeFile(envFilePath, content, "utf8");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
161
184
|
if (additions.length === 0) {
|
|
162
185
|
return;
|
|
163
186
|
}
|
|
164
|
-
if (options.header &&
|
|
165
|
-
const nextContent2 =
|
|
187
|
+
if (options.header && content.includes(options.header)) {
|
|
188
|
+
const nextContent2 = content.replace(
|
|
166
189
|
options.header,
|
|
167
190
|
`${options.header}
|
|
168
191
|
${additions.join("\n")}`
|
|
@@ -173,8 +196,8 @@ ${additions.join("\n")}`
|
|
|
173
196
|
if (options.header) {
|
|
174
197
|
additions.unshift(options.header);
|
|
175
198
|
}
|
|
176
|
-
const separator =
|
|
177
|
-
const nextContent = `${
|
|
199
|
+
const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
200
|
+
const nextContent = `${content}${separator}${additions.join("\n")}
|
|
178
201
|
`;
|
|
179
202
|
await writeFile(envFilePath, nextContent, "utf8");
|
|
180
203
|
}
|
|
@@ -382,53 +405,6 @@ function getEmailProviderDependencies(provider) {
|
|
|
382
405
|
};
|
|
383
406
|
}
|
|
384
407
|
|
|
385
|
-
// src/core/module-selection.ts
|
|
386
|
-
var MODULE_ORDER = [
|
|
387
|
-
"database",
|
|
388
|
-
"supabase",
|
|
389
|
-
"auth",
|
|
390
|
-
"api",
|
|
391
|
-
"i18n",
|
|
392
|
-
"dashboard",
|
|
393
|
-
"example",
|
|
394
|
-
"supabase-realtime",
|
|
395
|
-
"chat",
|
|
396
|
-
"seo",
|
|
397
|
-
"email"
|
|
398
|
-
];
|
|
399
|
-
var MODULE_DEPENDENCIES = {
|
|
400
|
-
auth: ["database"],
|
|
401
|
-
i18n: ["api"],
|
|
402
|
-
dashboard: ["auth", "api", "i18n"],
|
|
403
|
-
example: ["dashboard", "database"],
|
|
404
|
-
chat: ["database", "supabase-realtime"],
|
|
405
|
-
"supabase-realtime": ["supabase"]
|
|
406
|
-
};
|
|
407
|
-
function sortModules(moduleIds) {
|
|
408
|
-
const set = new Set(moduleIds);
|
|
409
|
-
return MODULE_ORDER.filter((id) => set.has(id));
|
|
410
|
-
}
|
|
411
|
-
function normalizeModuleSelection(moduleIds) {
|
|
412
|
-
const requested = new Set(moduleIds);
|
|
413
|
-
const autoAdded = [];
|
|
414
|
-
function addDependencies(moduleId) {
|
|
415
|
-
for (const dep of MODULE_DEPENDENCIES[moduleId] ?? []) {
|
|
416
|
-
if (!requested.has(dep)) {
|
|
417
|
-
requested.add(dep);
|
|
418
|
-
autoAdded.push(dep);
|
|
419
|
-
addDependencies(dep);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
for (const moduleId of moduleIds) {
|
|
424
|
-
addDependencies(moduleId);
|
|
425
|
-
}
|
|
426
|
-
return {
|
|
427
|
-
selectedModules: sortModules([...requested]),
|
|
428
|
-
autoAddedModules: autoAdded
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
408
|
// src/core/templates.ts
|
|
433
409
|
import path4 from "path";
|
|
434
410
|
import { existsSync } from "fs";
|
|
@@ -453,7 +429,22 @@ var templatePaths = {
|
|
|
453
429
|
};
|
|
454
430
|
|
|
455
431
|
// src/core/modules.ts
|
|
456
|
-
var
|
|
432
|
+
var baseModuleIds = [
|
|
433
|
+
"database",
|
|
434
|
+
"supabase",
|
|
435
|
+
"auth",
|
|
436
|
+
"api",
|
|
437
|
+
"i18n",
|
|
438
|
+
"dashboard",
|
|
439
|
+
"example"
|
|
440
|
+
];
|
|
441
|
+
var optionalModuleIds = [
|
|
442
|
+
"chat",
|
|
443
|
+
"supabase-realtime",
|
|
444
|
+
"seo",
|
|
445
|
+
"email"
|
|
446
|
+
];
|
|
447
|
+
var moduleDefinitions = [
|
|
457
448
|
{
|
|
458
449
|
id: "database",
|
|
459
450
|
label: "Database (Prisma + Postgres)",
|
|
@@ -523,7 +514,7 @@ Create the bucket and RLS policies in Supabase Dashboard before uploads.`
|
|
|
523
514
|
| \`BETTER_AUTH_SECRET\` | Auto-generated on create; rotate in production |
|
|
524
515
|
| \`BETTER_AUTH_URL\` | Your app URL (e.g. \`http://localhost:3000\`) |
|
|
525
516
|
|
|
526
|
-
Requires \`database\` (
|
|
517
|
+
Requires \`database\` (included in base). Bootstrap seeds \`admin\` / \`admin1234\` on first dev start.`
|
|
527
518
|
},
|
|
528
519
|
{
|
|
529
520
|
id: "api",
|
|
@@ -567,7 +558,7 @@ Add more locales with \`nextcli add language\`.`
|
|
|
567
558
|
nuqs: "^2.8.1",
|
|
568
559
|
"date-fns": "^3.6.0"
|
|
569
560
|
},
|
|
570
|
-
setupSection: `Requires \`auth\`, \`api\`, and \`i18n\` (
|
|
561
|
+
setupSection: `Requires \`auth\`, \`api\`, and \`i18n\` (included in base). Protected routes redirect unauthenticated users to \`/sign-in\`.`
|
|
571
562
|
},
|
|
572
563
|
{
|
|
573
564
|
id: "example",
|
|
@@ -575,7 +566,7 @@ Add more locales with \`nextcli add language\`.`
|
|
|
575
566
|
description: "Adds starter example feature with API route, hooks, and Prisma model",
|
|
576
567
|
templatePath: templatePaths.example,
|
|
577
568
|
env: {},
|
|
578
|
-
setupSection: `Requires \`dashboard\` and \`database\` (
|
|
569
|
+
setupSection: `Requires \`dashboard\` and \`database\` (included in base). Includes demo \`Example\` model in \`prisma/schema.prisma\` \u2014 run \`db:migrate\` after create.`
|
|
579
570
|
},
|
|
580
571
|
{
|
|
581
572
|
id: "chat",
|
|
@@ -589,7 +580,7 @@ Add more locales with \`nextcli add language\`.`
|
|
|
589
580
|
| -------- | ------------ |
|
|
590
581
|
| \`NEXT_PUBLIC_ENABLE_CHAT\` | Set \`true\` when chat module is enabled (auto on add) |
|
|
591
582
|
|
|
592
|
-
Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
|
|
583
|
+
Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (base includes database/supabase; realtime auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
|
|
593
584
|
},
|
|
594
585
|
{
|
|
595
586
|
id: "supabase-realtime",
|
|
@@ -598,7 +589,7 @@ Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (auto-added). Run
|
|
|
598
589
|
templatePath: templatePaths.supabaseRealtime,
|
|
599
590
|
env: {},
|
|
600
591
|
dependencies: {},
|
|
601
|
-
setupSection: `Requires \`supabase\` (
|
|
592
|
+
setupSection: `Requires \`supabase\` (included in base). Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
|
|
602
593
|
},
|
|
603
594
|
{
|
|
604
595
|
id: "seo",
|
|
@@ -622,13 +613,58 @@ Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (auto-added). Run
|
|
|
622
613
|
Only the selected provider keys are merged into your env files.`
|
|
623
614
|
}
|
|
624
615
|
];
|
|
616
|
+
var optionalModules = moduleDefinitions.filter(
|
|
617
|
+
(module) => optionalModuleIds.includes(module.id)
|
|
618
|
+
);
|
|
625
619
|
function getModuleById(moduleId) {
|
|
626
|
-
const module =
|
|
620
|
+
const module = moduleDefinitions.find((item) => item.id === moduleId);
|
|
627
621
|
if (!module) {
|
|
628
|
-
throw new Error(`Unknown
|
|
622
|
+
throw new Error(`Unknown module: ${moduleId}`);
|
|
629
623
|
}
|
|
630
624
|
return module;
|
|
631
625
|
}
|
|
626
|
+
function isBaseModuleId(moduleId) {
|
|
627
|
+
return baseModuleIds.includes(moduleId);
|
|
628
|
+
}
|
|
629
|
+
function isOptionalModuleId(moduleId) {
|
|
630
|
+
return optionalModuleIds.includes(moduleId);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/core/module-selection.ts
|
|
634
|
+
var MODULE_ORDER = [
|
|
635
|
+
...baseModuleIds,
|
|
636
|
+
"supabase-realtime",
|
|
637
|
+
"chat",
|
|
638
|
+
"seo",
|
|
639
|
+
"email"
|
|
640
|
+
];
|
|
641
|
+
var MODULE_DEPENDENCIES = {
|
|
642
|
+
chat: ["supabase-realtime"]
|
|
643
|
+
};
|
|
644
|
+
function sortModules(moduleIds) {
|
|
645
|
+
const set = new Set(moduleIds);
|
|
646
|
+
return MODULE_ORDER.filter((id) => set.has(id));
|
|
647
|
+
}
|
|
648
|
+
function normalizeModuleSelection(moduleIds) {
|
|
649
|
+
const requested = /* @__PURE__ */ new Set([...baseModuleIds, ...moduleIds]);
|
|
650
|
+
const autoAdded = [];
|
|
651
|
+
function addDependencies(moduleId) {
|
|
652
|
+
for (const dep of MODULE_DEPENDENCIES[moduleId] ?? []) {
|
|
653
|
+
if (!requested.has(dep)) {
|
|
654
|
+
requested.add(dep);
|
|
655
|
+
autoAdded.push(dep);
|
|
656
|
+
addDependencies(dep);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const moduleId of moduleIds) {
|
|
661
|
+
addDependencies(moduleId);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
selectedModules: sortModules([...requested]),
|
|
665
|
+
autoAddedModules: autoAdded
|
|
666
|
+
};
|
|
667
|
+
}
|
|
632
668
|
|
|
633
669
|
// src/core/setup-docs.ts
|
|
634
670
|
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -721,12 +757,28 @@ function resolveModuleEnv(moduleId, emailProvider, authSecret) {
|
|
|
721
757
|
}
|
|
722
758
|
return env;
|
|
723
759
|
}
|
|
760
|
+
async function mergeAuthSecret(projectDir, authSecret) {
|
|
761
|
+
const envTargets = [".env", ".env.example", ".env.development"];
|
|
762
|
+
for (const envFile of envTargets) {
|
|
763
|
+
const envPath = path6.join(projectDir, envFile);
|
|
764
|
+
if (await pathExists(envPath)) {
|
|
765
|
+
await mergeEnvFile(
|
|
766
|
+
envPath,
|
|
767
|
+
{ BETTER_AUTH_SECRET: authSecret },
|
|
768
|
+
{ header: "# --- module: auth ---", overwrite: true }
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
724
773
|
async function applyModulesToProject(options) {
|
|
725
774
|
const { projectDir, moduleIds, emailProvider, safeCopy = false } = options;
|
|
726
775
|
const authSecret = options.authSecret ?? randomBytes(32).toString("base64url");
|
|
727
776
|
const { selectedModules, autoAddedModules } = normalizeModuleSelection(moduleIds);
|
|
728
777
|
const copyReports = [];
|
|
729
|
-
|
|
778
|
+
const modulesToCopy = selectedModules.filter(
|
|
779
|
+
(moduleId) => !baseModuleIds.includes(moduleId)
|
|
780
|
+
);
|
|
781
|
+
for (const moduleId of modulesToCopy) {
|
|
730
782
|
const module = getModuleById(moduleId);
|
|
731
783
|
if (safeCopy) {
|
|
732
784
|
copyReports.push({
|
|
@@ -742,11 +794,12 @@ async function applyModulesToProject(options) {
|
|
|
742
794
|
chatSchemaStatus = await ensureChatSchemaInProject(projectDir);
|
|
743
795
|
}
|
|
744
796
|
let exampleSchemaStatus;
|
|
745
|
-
if (selectedModules.includes("example")) {
|
|
797
|
+
if (selectedModules.includes("example") && modulesToCopy.includes("example")) {
|
|
746
798
|
exampleSchemaStatus = await ensureExampleSchemaInProject(projectDir);
|
|
747
799
|
}
|
|
748
800
|
const envTargets = [".env", ".env.example", ".env.development"];
|
|
749
|
-
|
|
801
|
+
const envModules = ["auth", ...modulesToCopy];
|
|
802
|
+
for (const moduleId of envModules) {
|
|
750
803
|
const moduleEnv = resolveModuleEnv(moduleId, emailProvider, authSecret);
|
|
751
804
|
if (Object.keys(moduleEnv).length === 0) {
|
|
752
805
|
continue;
|
|
@@ -761,11 +814,11 @@ async function applyModulesToProject(options) {
|
|
|
761
814
|
}
|
|
762
815
|
}
|
|
763
816
|
const packageJsonPath = path6.join(projectDir, "package.json");
|
|
764
|
-
if (await pathExists(packageJsonPath)) {
|
|
817
|
+
if (await pathExists(packageJsonPath) && modulesToCopy.length > 0) {
|
|
765
818
|
const dependencies = {};
|
|
766
819
|
const devDependencies = {};
|
|
767
820
|
const scripts = {};
|
|
768
|
-
for (const moduleId of
|
|
821
|
+
for (const moduleId of modulesToCopy) {
|
|
769
822
|
const module = getModuleById(moduleId);
|
|
770
823
|
Object.assign(dependencies, module.dependencies ?? {});
|
|
771
824
|
Object.assign(devDependencies, module.devDependencies ?? {});
|
|
@@ -811,12 +864,20 @@ import { readdir as readdir3, readFile as readFile6, writeFile as writeFile6 } f
|
|
|
811
864
|
import path7 from "path";
|
|
812
865
|
import { readdir as readdir2, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
813
866
|
var defaultManifest = {
|
|
814
|
-
cli: "0.
|
|
867
|
+
cli: "0.9.0",
|
|
815
868
|
defaultLocale: "vi",
|
|
816
|
-
locales: [],
|
|
817
|
-
namespaces: [],
|
|
818
|
-
modules: [
|
|
819
|
-
|
|
869
|
+
locales: ["vi"],
|
|
870
|
+
namespaces: ["common", "auth", "example"],
|
|
871
|
+
modules: [
|
|
872
|
+
"database",
|
|
873
|
+
"supabase",
|
|
874
|
+
"auth",
|
|
875
|
+
"api",
|
|
876
|
+
"i18n",
|
|
877
|
+
"dashboard",
|
|
878
|
+
"example"
|
|
879
|
+
],
|
|
880
|
+
features: ["example"]
|
|
820
881
|
};
|
|
821
882
|
function getManifestPath(projectDir) {
|
|
822
883
|
return path7.join(projectDir, "nextcli.json");
|
|
@@ -1756,7 +1817,9 @@ function registerAddCommand(program2) {
|
|
|
1756
1817
|
);
|
|
1757
1818
|
} else if (result.reason === "already-exists") {
|
|
1758
1819
|
const slug = featureName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
1759
|
-
log.error(
|
|
1820
|
+
log.error(
|
|
1821
|
+
`Feature already exists: ${path10.join(cwd, "src/features", slug)}`
|
|
1822
|
+
);
|
|
1760
1823
|
}
|
|
1761
1824
|
process.exitCode = 1;
|
|
1762
1825
|
return;
|
|
@@ -1785,9 +1848,6 @@ function registerAddCommand(program2) {
|
|
|
1785
1848
|
process.exitCode = 1;
|
|
1786
1849
|
return;
|
|
1787
1850
|
}
|
|
1788
|
-
const validIds = new Set(
|
|
1789
|
-
optionalModules.map((module) => module.id)
|
|
1790
|
-
);
|
|
1791
1851
|
const requestedValues = options.module ? options.module.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean) : [];
|
|
1792
1852
|
const requestedIds = [];
|
|
1793
1853
|
let emailProviderFromModuleAlias;
|
|
@@ -1798,14 +1858,6 @@ function registerAddCommand(program2) {
|
|
|
1798
1858
|
return;
|
|
1799
1859
|
}
|
|
1800
1860
|
for (const moduleId of requestedValues) {
|
|
1801
|
-
if (validIds.has(moduleId)) {
|
|
1802
|
-
requestedIds.push(moduleId);
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
if (moduleId === "supabase") {
|
|
1806
|
-
requestedIds.push("supabase");
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
1861
|
if (moduleId === "resend") {
|
|
1810
1862
|
log.info(
|
|
1811
1863
|
"Module 'resend' is now 'email'; selecting email with provider resend."
|
|
@@ -1814,11 +1866,18 @@ function registerAddCommand(program2) {
|
|
|
1814
1866
|
emailProviderFromModuleAlias = "resend";
|
|
1815
1867
|
continue;
|
|
1816
1868
|
}
|
|
1817
|
-
if (!
|
|
1818
|
-
|
|
1869
|
+
if (!isOptionalModuleId(moduleId)) {
|
|
1870
|
+
if (isBaseModuleId(moduleId)) {
|
|
1871
|
+
log.error(
|
|
1872
|
+
`Module '${moduleId}' is included in the base template and cannot be added separately.`
|
|
1873
|
+
);
|
|
1874
|
+
} else {
|
|
1875
|
+
log.error(`Unknown module: ${moduleId}`);
|
|
1876
|
+
}
|
|
1819
1877
|
process.exitCode = 1;
|
|
1820
1878
|
return;
|
|
1821
1879
|
}
|
|
1880
|
+
requestedIds.push(moduleId);
|
|
1822
1881
|
}
|
|
1823
1882
|
startPrompt("NexTCLI optional modules");
|
|
1824
1883
|
const rawModules = requestedIds.length > 0 ? [...new Set(requestedIds)] : options.yes ? [] : await askMultiSelect(
|
|
@@ -1872,15 +1931,18 @@ function registerAddCommand(program2) {
|
|
|
1872
1931
|
}
|
|
1873
1932
|
const state = await detectProjectState(cwd);
|
|
1874
1933
|
const modulesToInstall = selectedModules.filter(
|
|
1875
|
-
(moduleId) => !state.modules.includes(moduleId)
|
|
1934
|
+
(moduleId) => !state.modules.includes(moduleId) && !baseModuleIds.includes(moduleId)
|
|
1876
1935
|
);
|
|
1877
1936
|
if (modulesToInstall.length === 0) {
|
|
1878
1937
|
finishPrompt("Selected modules are already installed.");
|
|
1879
1938
|
return;
|
|
1880
1939
|
}
|
|
1940
|
+
const optionalToApply = rawModules.length > 0 ? rawModules : modulesToInstall.filter(
|
|
1941
|
+
(moduleId) => isOptionalModuleId(moduleId)
|
|
1942
|
+
);
|
|
1881
1943
|
const applyResult = await applyModulesToProject({
|
|
1882
1944
|
projectDir: cwd,
|
|
1883
|
-
moduleIds:
|
|
1945
|
+
moduleIds: optionalToApply,
|
|
1884
1946
|
emailProvider,
|
|
1885
1947
|
safeCopy: true
|
|
1886
1948
|
});
|
|
@@ -1927,7 +1989,7 @@ function registerAddCommand(program2) {
|
|
|
1927
1989
|
...state,
|
|
1928
1990
|
namespaces: [...namespaceSet],
|
|
1929
1991
|
modules: mergedModules,
|
|
1930
|
-
locales:
|
|
1992
|
+
locales: state.locales.length > 0 ? state.locales : ["vi"],
|
|
1931
1993
|
features: mergedModules.includes("example") ? [.../* @__PURE__ */ new Set([...state.features, "example"])] : state.features
|
|
1932
1994
|
});
|
|
1933
1995
|
await mergeModuleDocs(cwd, modulesToInstall, mergedModules);
|
|
@@ -2111,9 +2173,10 @@ function registerAddCommand(program2) {
|
|
|
2111
2173
|
}
|
|
2112
2174
|
|
|
2113
2175
|
// src/commands/create.ts
|
|
2176
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2114
2177
|
import { spawn as spawn2 } from "child_process";
|
|
2115
2178
|
import path11 from "path";
|
|
2116
|
-
var CLI_VERSION = "0.
|
|
2179
|
+
var CLI_VERSION = "0.9.0";
|
|
2117
2180
|
async function runInstall(packageManager, cwd) {
|
|
2118
2181
|
const installArgsMap = {
|
|
2119
2182
|
npm: ["install"],
|
|
@@ -2215,11 +2278,14 @@ function registerCreateCommand(program2) {
|
|
|
2215
2278
|
);
|
|
2216
2279
|
}
|
|
2217
2280
|
const shouldInstall = await askConfirm("Install dependencies now?", true);
|
|
2281
|
+
const authSecret = randomBytes2(32).toString("base64url");
|
|
2218
2282
|
await copyDirectory(templatePaths.base, targetPath);
|
|
2283
|
+
await mergeAuthSecret(targetPath, authSecret);
|
|
2219
2284
|
const applyResult = await applyModulesToProject({
|
|
2220
2285
|
projectDir: targetPath,
|
|
2221
2286
|
moduleIds: rawModules,
|
|
2222
|
-
emailProvider
|
|
2287
|
+
emailProvider,
|
|
2288
|
+
authSecret
|
|
2223
2289
|
});
|
|
2224
2290
|
await replaceTokensInDirectory(targetPath, {
|
|
2225
2291
|
__PROJECT_NAME__: projectSlug,
|
|
@@ -2227,28 +2293,18 @@ function registerCreateCommand(program2) {
|
|
|
2227
2293
|
});
|
|
2228
2294
|
await mergeModuleDocs(
|
|
2229
2295
|
targetPath,
|
|
2230
|
-
|
|
2296
|
+
[...baseModuleIds, ...rawModules],
|
|
2231
2297
|
applyResult.selectedModules
|
|
2232
2298
|
);
|
|
2233
2299
|
const manifest = await readManifest(targetPath);
|
|
2234
2300
|
if (manifest) {
|
|
2235
|
-
const namespaces = [...manifest.namespaces];
|
|
2236
|
-
if (applyResult.selectedModules.includes("i18n")) {
|
|
2237
|
-
namespaces.push("common");
|
|
2238
|
-
}
|
|
2239
|
-
if (applyResult.selectedModules.includes("auth")) {
|
|
2240
|
-
namespaces.push("auth");
|
|
2241
|
-
}
|
|
2242
|
-
if (applyResult.selectedModules.includes("example")) {
|
|
2243
|
-
namespaces.push("example");
|
|
2244
|
-
}
|
|
2245
2301
|
await writeManifest(targetPath, {
|
|
2246
2302
|
...manifest,
|
|
2247
2303
|
cli: CLI_VERSION,
|
|
2248
2304
|
modules: applyResult.selectedModules,
|
|
2249
|
-
namespaces: [
|
|
2250
|
-
locales:
|
|
2251
|
-
features:
|
|
2305
|
+
namespaces: ["common", "auth", "example"],
|
|
2306
|
+
locales: ["vi"],
|
|
2307
|
+
features: ["example"]
|
|
2252
2308
|
});
|
|
2253
2309
|
}
|
|
2254
2310
|
if (shouldInstall) {
|
|
@@ -2260,9 +2316,13 @@ function registerCreateCommand(program2) {
|
|
|
2260
2316
|
if (projectSlug !== projectDirectoryName) {
|
|
2261
2317
|
log.detail("Normalized project id", projectSlug);
|
|
2262
2318
|
}
|
|
2319
|
+
log.detail("Base modules", baseModuleIds.join(", "));
|
|
2320
|
+
const optionalInstalled = applyResult.selectedModules.filter(
|
|
2321
|
+
(moduleId) => !baseModuleIds.includes(moduleId)
|
|
2322
|
+
);
|
|
2263
2323
|
log.detail(
|
|
2264
|
-
"
|
|
2265
|
-
|
|
2324
|
+
"Optional modules",
|
|
2325
|
+
optionalInstalled.length > 0 ? optionalInstalled.join(", ") : "none"
|
|
2266
2326
|
);
|
|
2267
2327
|
if (applyResult.autoAddedModules.length > 0) {
|
|
2268
2328
|
log.detail("Auto-added", applyResult.autoAddedModules.join(", "));
|
|
@@ -2278,8 +2338,9 @@ function registerCreateCommand(program2) {
|
|
|
2278
2338
|
if (applyResult.exampleSchemaStatus === "added") {
|
|
2279
2339
|
log.info("Example model was appended to prisma/schema.prisma.");
|
|
2280
2340
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2341
|
+
log.step(
|
|
2342
|
+
`Next: cd ${projectName} && ${packageManager} run db:migrate && ${packageManager} run dev`
|
|
2343
|
+
);
|
|
2283
2344
|
});
|
|
2284
2345
|
}
|
|
2285
2346
|
|
|
@@ -2494,7 +2555,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
|
|
|
2494
2555
|
|
|
2495
2556
|
// src/cli.ts
|
|
2496
2557
|
var program = new NexTCLICommand();
|
|
2497
|
-
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.
|
|
2558
|
+
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.9.0");
|
|
2498
2559
|
registerCreateCommand(program);
|
|
2499
2560
|
registerAddCommand(program);
|
|
2500
2561
|
registerMigrateCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinhnguyencth1204/nextcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "CLI scaffolder for outsourced Next.js projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"build": "tsup",
|
|
16
16
|
"dev": "tsup --watch",
|
|
17
17
|
"typecheck": "tsc --noEmit",
|
|
18
|
-
"test": "bun test
|
|
19
|
-
"test:add-feature": "bun run build && bun test
|
|
18
|
+
"test": "bun test tests",
|
|
19
|
+
"test:add-feature": "bun run build && bun test tests/core",
|
|
20
|
+
"test:rich-text": "bun test tests/templates/next-base",
|
|
20
21
|
"smoke": "node dist/cli.js --help",
|
|
21
22
|
"smoke:full": "bun run build && bun run test && bun run scripts/pre-publish-smoke.ts",
|
|
22
23
|
"prepublishOnly": "npm run build"
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/node": "^22.15.29",
|
|
44
|
+
"lexical": "^0.45.0",
|
|
43
45
|
"tsup": "^8.1.0",
|
|
44
46
|
"typescript": "^5.5.4"
|
|
45
47
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { removeResource } from "@/lib/supabase/storage";
|
|
2
|
+
import {
|
|
3
|
+
isSupabaseManagedImageUrl,
|
|
4
|
+
syncRemovedRichTextImages,
|
|
5
|
+
type RemoveManagedImageFn,
|
|
6
|
+
} from "@/lib/rich-text";
|
|
7
|
+
import type { SerializedEditorState } from "lexical";
|
|
8
|
+
|
|
9
|
+
export const removeSupabaseManagedImage: RemoveManagedImageFn = async (
|
|
10
|
+
_url,
|
|
11
|
+
ref,
|
|
12
|
+
) => {
|
|
13
|
+
await removeResource(ref.path, ref.bucket);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Deletes Supabase storage objects for images removed from rich text content.
|
|
18
|
+
* Only URLs belonging to this project's configured Supabase bucket are removed.
|
|
19
|
+
*/
|
|
20
|
+
export async function syncRemovedSupabaseRichTextImages(
|
|
21
|
+
previous: SerializedEditorState | null | undefined,
|
|
22
|
+
next: SerializedEditorState | null | undefined,
|
|
23
|
+
) {
|
|
24
|
+
return syncRemovedRichTextImages(previous, next, {
|
|
25
|
+
isManagedUrl: isSupabaseManagedImageUrl,
|
|
26
|
+
removeManagedImage: removeSupabaseManagedImage,
|
|
27
|
+
});
|
|
28
|
+
}
|