create-nexora-next 0.1.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 +46 -0
- package/package.json +35 -0
- package/src/index.js +267 -0
- package/src/steps/01-create-next-app.js +19 -0
- package/src/steps/02-shadcn.js +39 -0
- package/src/steps/03-base-files.js +110 -0
- package/src/steps/04-providers.js +51 -0
- package/src/steps/05-i18n.js +42 -0
- package/src/steps/06-auth.js +31 -0
- package/src/steps/07-proxy.js +17 -0
- package/src/steps/08-install-deps.js +83 -0
- package/src/steps/09-husky.js +59 -0
- package/src/steps/10-axios.js +15 -0
- package/src/steps/11-patch-pkg.js +22 -0
- package/src/templates/files.js +908 -0
- package/src/utils/runner.js +50 -0
- package/src/utils/safe-step.js +43 -0
- package/src/utils/writer.js +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# create-nexora-next
|
|
2
|
+
|
|
3
|
+
> The official Next.js scaffolding CLI by Rayan — batteries included.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-nexora-next
|
|
9
|
+
pnpm dlx create-nexora-next
|
|
10
|
+
bunx create-nexora-next
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What it sets up
|
|
14
|
+
|
|
15
|
+
### Always included
|
|
16
|
+
- Next.js (TypeScript, Tailwind CSS v4, ESLint, App Router, `~/` alias)
|
|
17
|
+
- shadcn/ui initialized with defaults
|
|
18
|
+
- Custom `globals.css` (squircle utilities, container, skeleton animation)
|
|
19
|
+
- `.prettierrc` with `prettier-plugin-tailwindcss`
|
|
20
|
+
- `.env.local` pre-populated
|
|
21
|
+
- Poppins font via `next/font/google`
|
|
22
|
+
- `cn()`, `toSlug()`, `trycatch()`, and other utils
|
|
23
|
+
- Typed constants (`TZ_COOKIE`, `SITE_URL`)
|
|
24
|
+
- SEO metadata scaffold
|
|
25
|
+
- `SileoToaster` component
|
|
26
|
+
- Providers pattern (`src/components/providers/`)
|
|
27
|
+
- nuqs adapter (URL state management)
|
|
28
|
+
- Core deps: `zustand`, `zod`, `react-hook-form`, `date-fns`, `nuqs`, `cookies-next`, `nextjs-toploader`
|
|
29
|
+
- Dev deps: `prettier`, `prettier-plugin-tailwindcss`, `babel-plugin-react-compiler`
|
|
30
|
+
|
|
31
|
+
### Optional features (prompted)
|
|
32
|
+
|
|
33
|
+
| Feature | What gets installed / created |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Husky | `husky`, `lint-staged`, pre-commit hook |
|
|
36
|
+
| Localization | `next-intl`, `src/i18n/`, `locales/en.json`, `[locale]/page.tsx` |
|
|
37
|
+
| TanStack Query | `@tanstack/react-query`, persist client, query-client provider |
|
|
38
|
+
| Auth | proxy chain, auth validators, `src/apis/` |
|
|
39
|
+
| Theming | `next-themes`, theme provider |
|
|
40
|
+
| Animations | `motion`, `gsap` |
|
|
41
|
+
| Axios | `axios`, `axios.client.ts`, `axios.server.ts` |
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Node.js 18+
|
|
46
|
+
- npm, pnpm, bun, or yarn
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-nexora-next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The official Next.js scaffolding CLI by Rayan — batteries included.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-nexora-next": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node src/index.js",
|
|
14
|
+
"dev": "node src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@clack/prompts": "^0.9.0",
|
|
18
|
+
"picocolors": "^1.1.1"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"nextjs",
|
|
25
|
+
"scaffold",
|
|
26
|
+
"cli",
|
|
27
|
+
"create-app",
|
|
28
|
+
"rayan",
|
|
29
|
+
"next-intl",
|
|
30
|
+
"shadcn",
|
|
31
|
+
"tanstack"
|
|
32
|
+
],
|
|
33
|
+
"author": "Rayan",
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as p from '@clack/prompts'
|
|
3
|
+
import pc from 'picocolors'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { existsSync } from 'fs'
|
|
6
|
+
import { createRequire } from 'module'
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url)
|
|
9
|
+
const { version } = require('../package.json')
|
|
10
|
+
|
|
11
|
+
import { stepCreateNextApp } from './steps/01-create-next-app.js'
|
|
12
|
+
import { stepSetupShadcn } from './steps/02-shadcn.js'
|
|
13
|
+
import { stepWriteBaseFiles } from './steps/03-base-files.js'
|
|
14
|
+
import { stepWriteProviders } from './steps/04-providers.js'
|
|
15
|
+
import { stepSetupI18n } from './steps/05-i18n.js'
|
|
16
|
+
import { stepSetupAuth } from './steps/06-auth.js'
|
|
17
|
+
import { stepWriteProxy } from './steps/07-proxy.js'
|
|
18
|
+
import { stepInstallDeps } from './steps/08-install-deps.js'
|
|
19
|
+
import { stepSetupHusky } from './steps/09-husky.js'
|
|
20
|
+
import { stepSetupAxios } from './steps/10-axios.js'
|
|
21
|
+
import { stepPatchPackageJson } from './steps/11-patch-pkg.js'
|
|
22
|
+
|
|
23
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function bail(msg) {
|
|
26
|
+
p.cancel(pc.red(msg))
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function onCancel() {
|
|
31
|
+
bail('Setup cancelled.')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function section(title) {
|
|
35
|
+
p.log.step(pc.bold(pc.cyan(`\n ${title}`)))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function printBanner() {
|
|
41
|
+
const art = [
|
|
42
|
+
' ███╗ ██╗███████╗██╗ ██╗ ██████╗ ██████╗ █████╗ ',
|
|
43
|
+
' ████╗ ██║██╔════╝╚██╗██╔╝██╔═══██╗██╔══██╗██╔══██╗',
|
|
44
|
+
' ██╔██╗ ██║█████╗ ╚███╔╝ ██║ ██║██████╔╝███████║',
|
|
45
|
+
' ██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██║██╔══██╗██╔══██║',
|
|
46
|
+
' ██║ ╚████║███████╗██╔╝ ██╗╚██████╔╝██║ ██║██║ ██║',
|
|
47
|
+
' ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝',
|
|
48
|
+
]
|
|
49
|
+
// Fade from cyan → blue top to bottom
|
|
50
|
+
const gradient = [pc.cyan, pc.cyan, pc.cyan, pc.blue, pc.blue, pc.blue]
|
|
51
|
+
|
|
52
|
+
console.log()
|
|
53
|
+
art.forEach((line, i) => console.log(pc.bold(gradient[i](line))))
|
|
54
|
+
console.log()
|
|
55
|
+
const W = 44
|
|
56
|
+
const centre = (str) => {
|
|
57
|
+
const pad = W - str.length
|
|
58
|
+
const l = Math.floor(pad / 2)
|
|
59
|
+
const r = pad - l
|
|
60
|
+
return ' '.repeat(l) + str + ' '.repeat(r)
|
|
61
|
+
}
|
|
62
|
+
const bar = '═'.repeat(W)
|
|
63
|
+
console.log(pc.bold(pc.cyan(
|
|
64
|
+
` ╔${bar}╗\n` +
|
|
65
|
+
` ║${centre(`create-nexora-next v${version}`)}║\n` +
|
|
66
|
+
` ║${centre('Next.js scaffold · by Rayan')}║\n` +
|
|
67
|
+
` ╚${bar}╝`
|
|
68
|
+
)))
|
|
69
|
+
console.log()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Summary ──────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function printSummary(opts, projectName, pm) {
|
|
75
|
+
const yes = pc.green('✓')
|
|
76
|
+
const skip = pc.dim('–')
|
|
77
|
+
const row = (label, on) =>
|
|
78
|
+
` ${on ? yes : skip} ${on ? pc.white(label) : pc.dim(label)}`
|
|
79
|
+
|
|
80
|
+
console.log()
|
|
81
|
+
p.log.success(pc.bold(pc.green(` 🚀 ${projectName} is ready!`)))
|
|
82
|
+
console.log()
|
|
83
|
+
console.log(pc.bold(' What was set up:'))
|
|
84
|
+
console.log(row('Next.js 15 + TypeScript + Tailwind + ESLint', true))
|
|
85
|
+
console.log(row('shadcn/ui + custom globals.css utilities', true))
|
|
86
|
+
console.log(row('Prettier + tailwind class sorting', true))
|
|
87
|
+
console.log(row('Poppins font + path alias ~/', true))
|
|
88
|
+
console.log(row('Localization via next-intl', opts.i18n))
|
|
89
|
+
console.log(row('TanStack Query + local persistence', opts.query))
|
|
90
|
+
console.log(row('Theming with next-themes', opts.theming))
|
|
91
|
+
console.log(row('Animations — motion + gsap', opts.animations))
|
|
92
|
+
console.log(row('Auth proxy + zod validators', opts.auth))
|
|
93
|
+
console.log(row('Axios client + server instances', opts.axios))
|
|
94
|
+
console.log(row('Husky pre-commit hooks', opts.husky))
|
|
95
|
+
console.log(row('React Compiler', opts.reactCompiler))
|
|
96
|
+
console.log()
|
|
97
|
+
console.log(pc.bold(' Next steps:'))
|
|
98
|
+
console.log(` ${pc.cyan('cd')} ${projectName}`)
|
|
99
|
+
const devCmd = pm === 'npm' ? 'npm run dev' : `${pm} dev`
|
|
100
|
+
console.log(` ${pc.cyan(devCmd)}`)
|
|
101
|
+
console.log()
|
|
102
|
+
console.log(pc.dim(' Happy shipping, Rayan. 🔥'))
|
|
103
|
+
console.log()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async function main() {
|
|
109
|
+
printBanner()
|
|
110
|
+
p.intro(pc.bgCyan(pc.black(" create-nexora-next ")))
|
|
111
|
+
|
|
112
|
+
// ── Phase 1: Identity ──────────────────────────────────────────────────────
|
|
113
|
+
section('Phase 1 — Project identity')
|
|
114
|
+
|
|
115
|
+
const projectName = await p.text({
|
|
116
|
+
message: 'What is your project name?',
|
|
117
|
+
placeholder: 'my-app',
|
|
118
|
+
validate: (v) => {
|
|
119
|
+
if (!v || !v.trim()) return 'Project name is required.'
|
|
120
|
+
if (!/^[a-z0-9-_]+$/.test(v.trim()))
|
|
121
|
+
return 'Use lowercase letters, numbers, hyphens or underscores only.'
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
if (p.isCancel(projectName)) onCancel()
|
|
125
|
+
const name = projectName.trim()
|
|
126
|
+
|
|
127
|
+
const targetDir = path.resolve(process.cwd(), name)
|
|
128
|
+
if (existsSync(targetDir)) {
|
|
129
|
+
bail(`Directory "${name}" already exists. Choose a different name.`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const pm = await p.select({
|
|
133
|
+
message: 'Which package manager?',
|
|
134
|
+
options: [
|
|
135
|
+
{ value: 'pnpm', label: 'pnpm', hint: 'recommended' },
|
|
136
|
+
{ value: 'bun', label: 'bun', hint: 'fastest' },
|
|
137
|
+
{ value: 'npm', label: 'npm', hint: 'classic' },
|
|
138
|
+
{ value: 'yarn', label: 'yarn', hint: 'classic alt' },
|
|
139
|
+
],
|
|
140
|
+
})
|
|
141
|
+
if (p.isCancel(pm)) onCancel()
|
|
142
|
+
|
|
143
|
+
// ── Phase 2: Feature flags ─────────────────────────────────────────────────
|
|
144
|
+
section('Phase 2 — Features')
|
|
145
|
+
|
|
146
|
+
const i18n = await p.confirm({
|
|
147
|
+
message: 'Set up localization? (next-intl)',
|
|
148
|
+
initialValue: false,
|
|
149
|
+
})
|
|
150
|
+
if (p.isCancel(i18n)) onCancel()
|
|
151
|
+
|
|
152
|
+
const query = await p.confirm({
|
|
153
|
+
message: 'Set up TanStack Query + local persistence?',
|
|
154
|
+
initialValue: false,
|
|
155
|
+
})
|
|
156
|
+
if (p.isCancel(query)) onCancel()
|
|
157
|
+
|
|
158
|
+
const theming = await p.confirm({
|
|
159
|
+
message: 'Set up theming? (next-themes dark/light)',
|
|
160
|
+
initialValue: true,
|
|
161
|
+
})
|
|
162
|
+
if (p.isCancel(theming)) onCancel()
|
|
163
|
+
|
|
164
|
+
const animations = await p.confirm({
|
|
165
|
+
message: 'Install animation libraries? (motion + gsap)',
|
|
166
|
+
initialValue: false,
|
|
167
|
+
})
|
|
168
|
+
if (p.isCancel(animations)) onCancel()
|
|
169
|
+
|
|
170
|
+
const auth = await p.confirm({
|
|
171
|
+
message: 'Set up auth proxy + zod validators?',
|
|
172
|
+
initialValue: false,
|
|
173
|
+
})
|
|
174
|
+
if (p.isCancel(auth)) onCancel()
|
|
175
|
+
|
|
176
|
+
const axios = await p.confirm({
|
|
177
|
+
message: 'Set up axios client/server instances?',
|
|
178
|
+
initialValue: false,
|
|
179
|
+
})
|
|
180
|
+
if (p.isCancel(axios)) onCancel()
|
|
181
|
+
|
|
182
|
+
const husky = await p.confirm({
|
|
183
|
+
message: 'Configure Husky pre-commit hooks?',
|
|
184
|
+
initialValue: true,
|
|
185
|
+
})
|
|
186
|
+
if (p.isCancel(husky)) onCancel()
|
|
187
|
+
|
|
188
|
+
const reactCompiler = await p.confirm({
|
|
189
|
+
message: 'Enable React Compiler? (babel-plugin-react-compiler)',
|
|
190
|
+
initialValue: true,
|
|
191
|
+
})
|
|
192
|
+
if (p.isCancel(reactCompiler)) onCancel()
|
|
193
|
+
|
|
194
|
+
// ── Confirm ────────────────────────────────────────────────────────────────
|
|
195
|
+
const features = [
|
|
196
|
+
i18n && 'i18n',
|
|
197
|
+
query && 'query',
|
|
198
|
+
theming && 'theming',
|
|
199
|
+
animations && 'animations',
|
|
200
|
+
auth && 'auth',
|
|
201
|
+
axios && 'axios',
|
|
202
|
+
husky && 'husky',
|
|
203
|
+
reactCompiler && 'react-compiler',
|
|
204
|
+
].filter(Boolean)
|
|
205
|
+
|
|
206
|
+
console.log()
|
|
207
|
+
p.log.info(
|
|
208
|
+
pc.bold(` ${pc.cyan(name)}`) +
|
|
209
|
+
pc.dim(` · ${pm}`) +
|
|
210
|
+
(features.length ? pc.dim(' · ' + features.join(' · ')) : '')
|
|
211
|
+
)
|
|
212
|
+
console.log()
|
|
213
|
+
|
|
214
|
+
const go = await p.confirm({
|
|
215
|
+
message: 'Ready to scaffold?',
|
|
216
|
+
initialValue: true,
|
|
217
|
+
})
|
|
218
|
+
if (p.isCancel(go) || !go) onCancel()
|
|
219
|
+
|
|
220
|
+
// ── Phase 3: Execution ─────────────────────────────────────────────────────
|
|
221
|
+
section('Phase 3 — Building your project')
|
|
222
|
+
|
|
223
|
+
const opts = {
|
|
224
|
+
i18n: Boolean(i18n),
|
|
225
|
+
query: Boolean(query),
|
|
226
|
+
theming: Boolean(theming),
|
|
227
|
+
animations: Boolean(animations),
|
|
228
|
+
auth: Boolean(auth),
|
|
229
|
+
axios: Boolean(axios),
|
|
230
|
+
husky: Boolean(husky),
|
|
231
|
+
reactCompiler: Boolean(reactCompiler),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await stepCreateNextApp(name, pm, targetDir)
|
|
235
|
+
await stepSetupShadcn(targetDir, pm)
|
|
236
|
+
await stepWriteBaseFiles(targetDir, opts)
|
|
237
|
+
await stepWriteProviders(targetDir, opts)
|
|
238
|
+
|
|
239
|
+
if (opts.i18n) {
|
|
240
|
+
await stepSetupI18n(targetDir, pm, opts)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (opts.auth || opts.i18n) {
|
|
244
|
+
await stepSetupAuth(targetDir, opts)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await stepWriteProxy(targetDir, opts)
|
|
248
|
+
|
|
249
|
+
if (opts.axios) {
|
|
250
|
+
await stepSetupAxios(targetDir)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await stepInstallDeps(targetDir, pm, opts)
|
|
254
|
+
await stepPatchPackageJson(targetDir)
|
|
255
|
+
|
|
256
|
+
if (opts.husky) {
|
|
257
|
+
await stepSetupHusky(targetDir, pm)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
printSummary(opts, name, pm)
|
|
261
|
+
p.outro(pc.bgGreen(pc.black(' Done! ')))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
main().catch((err) => {
|
|
265
|
+
console.error(pc.red('\n Unexpected error:'), err.message || err)
|
|
266
|
+
process.exit(1)
|
|
267
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { run, execCmd } from '../utils/runner.js'
|
|
3
|
+
import { safeStep } from '../utils/safe-step.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} projectName
|
|
7
|
+
* @param {'npm'|'pnpm'|'bun'|'yarn'} pm
|
|
8
|
+
* @param {string} targetDir Absolute path where the project will live
|
|
9
|
+
*/
|
|
10
|
+
export async function stepCreateNextApp(projectName, pm, targetDir) {
|
|
11
|
+
await safeStep('Scaffolding Next.js app', () => {
|
|
12
|
+
const x = execCmd(pm)
|
|
13
|
+
// create-next-app flags: TypeScript, Tailwind, ESLint, App Router, src dir, no Turbopack prompt, import alias ~
|
|
14
|
+
run(
|
|
15
|
+
`${x} create-next-app@latest ${projectName} --typescript --tailwind --eslint --app --src-dir --no-turbopack --import-alias "~/*" --use-${pm === 'npm' ? 'npm' : pm}`,
|
|
16
|
+
path.dirname(targetDir),
|
|
17
|
+
)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { run, execCmd } from '../utils/runner.js'
|
|
4
|
+
import { appendFile } from '../utils/writer.js'
|
|
5
|
+
import { safeStep } from '../utils/safe-step.js'
|
|
6
|
+
import { globalsCssAppend } from '../templates/files.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} targetDir
|
|
10
|
+
* @param {'npm'|'pnpm'|'bun'|'yarn'} pm
|
|
11
|
+
*/
|
|
12
|
+
export async function stepSetupShadcn(targetDir, pm) {
|
|
13
|
+
await safeStep('Initialising shadcn/ui', () => {
|
|
14
|
+
const x = execCmd(pm)
|
|
15
|
+
run(`${x} shadcn@latest init -y -d`, targetDir)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
await safeStep('Patching shadcn config + cleaning up', () => {
|
|
19
|
+
// 1. Delete src/lib/utils.ts if shadcn created it (we use src/lib/utils/index.ts)
|
|
20
|
+
const shadcnUtils = path.join(targetDir, 'src', 'lib', 'utils.ts')
|
|
21
|
+
if (fs.existsSync(shadcnUtils)) fs.unlinkSync(shadcnUtils)
|
|
22
|
+
|
|
23
|
+
// 2. Update components.json utils path to point at our index.ts
|
|
24
|
+
const componentsJson = path.join(targetDir, 'components.json')
|
|
25
|
+
if (fs.existsSync(componentsJson)) {
|
|
26
|
+
const raw = fs.readFileSync(componentsJson, 'utf8')
|
|
27
|
+
const patched = raw.replace(
|
|
28
|
+
'"utils": "~/lib/utils"',
|
|
29
|
+
'"utils": "~/lib/utils/index.ts"',
|
|
30
|
+
)
|
|
31
|
+
fs.writeFileSync(componentsJson, patched, 'utf8')
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
await safeStep('Appending custom styles to globals.css', () => {
|
|
36
|
+
const globalsPath = path.join(targetDir, 'src', 'app', 'globals.css')
|
|
37
|
+
appendFile(globalsPath, globalsCssAppend)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { writeFile, mkdir } from '../utils/writer.js'
|
|
4
|
+
import { safeStep } from '../utils/safe-step.js'
|
|
5
|
+
import {
|
|
6
|
+
envLocal,
|
|
7
|
+
prettierRc,
|
|
8
|
+
fontsTs,
|
|
9
|
+
utilsTs,
|
|
10
|
+
utilsWithAxiosTs,
|
|
11
|
+
constantsTs,
|
|
12
|
+
seoTs,
|
|
13
|
+
seoWithLocaleTs,
|
|
14
|
+
sileoUiTsx,
|
|
15
|
+
sileoProviderTsx,
|
|
16
|
+
nuqsAdapterTsx,
|
|
17
|
+
rootLayoutI18nTs,
|
|
18
|
+
rootLayoutTs,
|
|
19
|
+
localeLayoutTs,
|
|
20
|
+
eslintConfigMjs,
|
|
21
|
+
sitemapTs,
|
|
22
|
+
} from '../templates/files.js'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} targetDir
|
|
26
|
+
* @param {{ i18n: boolean, axios: boolean, query: boolean }} opts
|
|
27
|
+
*/
|
|
28
|
+
export async function stepWriteBaseFiles(targetDir, opts) {
|
|
29
|
+
await safeStep('Writing base config files', () => {
|
|
30
|
+
const p = (...parts) => path.join(targetDir, ...parts)
|
|
31
|
+
|
|
32
|
+
// .env.local
|
|
33
|
+
writeFile(p('.env.local'), envLocal)
|
|
34
|
+
|
|
35
|
+
// .prettierrc
|
|
36
|
+
writeFile(p('.prettierrc'), prettierRc)
|
|
37
|
+
|
|
38
|
+
// src/lib/fonts.ts
|
|
39
|
+
writeFile(p('src', 'lib', 'fonts.ts'), fontsTs)
|
|
40
|
+
|
|
41
|
+
// src/lib/utils/index.ts
|
|
42
|
+
writeFile(
|
|
43
|
+
p('src', 'lib', 'utils', 'index.ts'),
|
|
44
|
+
opts.axios ? utilsWithAxiosTs : utilsTs,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// src/constants/index.ts
|
|
48
|
+
writeFile(p('src', 'constants', 'index.ts'), constantsTs(opts))
|
|
49
|
+
|
|
50
|
+
// src/lib/seo/index.ts
|
|
51
|
+
writeFile(
|
|
52
|
+
p('src', 'lib', 'seo', 'index.ts'),
|
|
53
|
+
opts.i18n ? seoWithLocaleTs : seoTs,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// src/components/ui/sileo.tsx
|
|
57
|
+
writeFile(p('src', 'components', 'ui', 'sileo.tsx'), sileoUiTsx)
|
|
58
|
+
|
|
59
|
+
// src/components/providers/sileo.provider.tsx
|
|
60
|
+
writeFile(p('src', 'components', 'providers', 'sileo.provider.tsx'), sileoProviderTsx)
|
|
61
|
+
|
|
62
|
+
// src/components/adapters/nuqs.adapter.tsx
|
|
63
|
+
writeFile(p('src', 'components', 'adapters', 'nuqs.adapter.tsx'), nuqsAdapterTsx)
|
|
64
|
+
|
|
65
|
+
// Empty structural folders
|
|
66
|
+
const emptyDirs = [
|
|
67
|
+
['src', 'hooks'],
|
|
68
|
+
['src', 'components', 'atoms'],
|
|
69
|
+
['src', 'components', 'molecules'],
|
|
70
|
+
['src', 'components', 'organisms'],
|
|
71
|
+
['src', 'components', 'primitives'],
|
|
72
|
+
]
|
|
73
|
+
for (const parts of emptyDirs) {
|
|
74
|
+
mkdir(p(...parts))
|
|
75
|
+
writeFile(p(...parts, 'index.ts'), `// ${parts[parts.length - 1]}\n`)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// ── Layouts ────────────────────────────────────────────────────────────────
|
|
80
|
+
await safeStep('Setting up app layouts', () => {
|
|
81
|
+
const p = (...parts) => path.join(targetDir, ...parts)
|
|
82
|
+
|
|
83
|
+
if (opts.i18n) {
|
|
84
|
+
// Replace root layout with passthrough shell
|
|
85
|
+
writeFile(p('src', 'app', 'layout.tsx'), rootLayoutI18nTs)
|
|
86
|
+
|
|
87
|
+
// Remove the default src/app/page.tsx (locale takes over routing)
|
|
88
|
+
const defaultPage = p('src', 'app', 'page.tsx')
|
|
89
|
+
if (fs.existsSync(defaultPage)) fs.unlinkSync(defaultPage)
|
|
90
|
+
|
|
91
|
+
// Create src/app/[locale]/layout.tsx
|
|
92
|
+
writeFile(p('src', 'app', '[locale]', 'layout.tsx'), localeLayoutTs)
|
|
93
|
+
} else {
|
|
94
|
+
// Standard root layout wiring up Providers
|
|
95
|
+
writeFile(p('src', 'app', 'layout.tsx'), rootLayoutTs)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// ── ESLint config ──────────────────────────────────────────────────────────
|
|
100
|
+
await safeStep('Replacing ESLint config', () => {
|
|
101
|
+
const p = (...parts) => path.join(targetDir, ...parts)
|
|
102
|
+
writeFile(p('eslint.config.mjs'), eslintConfigMjs(opts))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ── Sitemap ────────────────────────────────────────────────────────────────
|
|
106
|
+
await safeStep('Writing sitemap', () => {
|
|
107
|
+
const p = (...parts) => path.join(targetDir, ...parts)
|
|
108
|
+
writeFile(p('src', 'app', 'sitemap.ts'), sitemapTs)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import {
|
|
3
|
+
localeProviderTsx,
|
|
4
|
+
providersIndexTsx,
|
|
5
|
+
queryClientProviderTsx,
|
|
6
|
+
themeProviderTsx,
|
|
7
|
+
tzProviderTsx,
|
|
8
|
+
} from "../templates/files.js";
|
|
9
|
+
import { safeStep } from "../utils/safe-step.js";
|
|
10
|
+
import { writeFile } from "../utils/writer.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} targetDir
|
|
14
|
+
* @param {{ i18n: boolean, theming: boolean, query: boolean }} opts
|
|
15
|
+
*/
|
|
16
|
+
export async function stepWriteProviders(targetDir, opts) {
|
|
17
|
+
await safeStep("Generating providers", () => {
|
|
18
|
+
const p = (...parts) => path.join(targetDir, ...parts);
|
|
19
|
+
|
|
20
|
+
// Dynamic providers/index.ts
|
|
21
|
+
writeFile(
|
|
22
|
+
p("src", "components", "providers", "index.tsx"),
|
|
23
|
+
providersIndexTsx(opts),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (opts.theming) {
|
|
27
|
+
writeFile(
|
|
28
|
+
p("src", "components", "providers", "theme.provider.tsx"),
|
|
29
|
+
themeProviderTsx,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (opts.i18n) {
|
|
34
|
+
writeFile(
|
|
35
|
+
p("src", "components", "providers", "locale.provider.tsx"),
|
|
36
|
+
localeProviderTsx,
|
|
37
|
+
);
|
|
38
|
+
writeFile(
|
|
39
|
+
p("src", "components", "providers", "tz.provider.tsx"),
|
|
40
|
+
tzProviderTsx,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (opts.query) {
|
|
45
|
+
writeFile(
|
|
46
|
+
p("src", "components", "providers", "query-client.provider.tsx"),
|
|
47
|
+
queryClientProviderTsx,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { writeFile } from '../utils/writer.js'
|
|
3
|
+
import { run, installCmd } from '../utils/runner.js'
|
|
4
|
+
import { safeStep } from '../utils/safe-step.js'
|
|
5
|
+
import {
|
|
6
|
+
i18nRoutingTs,
|
|
7
|
+
i18nRequestTs,
|
|
8
|
+
i18nNavigationTs,
|
|
9
|
+
nextIntlTs,
|
|
10
|
+
localesEnJson,
|
|
11
|
+
localPageTsx,
|
|
12
|
+
localeDateFormatsTs,
|
|
13
|
+
nextConfigI18nTs,
|
|
14
|
+
} from '../templates/files.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} targetDir
|
|
18
|
+
* @param {'npm'|'pnpm'|'bun'|'yarn'} pm
|
|
19
|
+
* @param {{ reactCompiler: boolean }} opts
|
|
20
|
+
*/
|
|
21
|
+
export async function stepSetupI18n(targetDir, pm, opts) {
|
|
22
|
+
await safeStep('Installing next-intl', () => {
|
|
23
|
+
run(installCmd(pm, ['next-intl']), targetDir)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await safeStep('Writing i18n files', () => {
|
|
27
|
+
const p = (...parts) => path.join(targetDir, ...parts)
|
|
28
|
+
|
|
29
|
+
writeFile(p('locales', 'en.json'), localesEnJson)
|
|
30
|
+
writeFile(p('src', 'i18n', 'routing.ts'), i18nRoutingTs)
|
|
31
|
+
writeFile(p('src', 'i18n', 'request.ts'), i18nRequestTs)
|
|
32
|
+
writeFile(p('src', 'i18n', 'navigation.ts'), i18nNavigationTs)
|
|
33
|
+
writeFile(p('next-intl.ts'), nextIntlTs)
|
|
34
|
+
writeFile(p('src', 'app', '[locale]', 'page.tsx'), localPageTsx)
|
|
35
|
+
writeFile(p('src', 'lib', 'utils', 'locale-date-formats.ts'), localeDateFormatsTs)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await safeStep('Updating next.config.ts for next-intl', () => {
|
|
39
|
+
const p = (...parts) => path.join(targetDir, ...parts)
|
|
40
|
+
writeFile(p('next.config.ts'), nextConfigI18nTs(opts))
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { writeFile } from '../utils/writer.js'
|
|
3
|
+
import { safeStep } from '../utils/safe-step.js'
|
|
4
|
+
import {
|
|
5
|
+
proxyLibTs,
|
|
6
|
+
proxyAuthTs,
|
|
7
|
+
validatorsAuthTs,
|
|
8
|
+
validatorsTs,
|
|
9
|
+
apisClientTs,
|
|
10
|
+
apisServerTs,
|
|
11
|
+
} from '../templates/files.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} targetDir
|
|
15
|
+
* @param {{ i18n: boolean, auth: boolean, axios: boolean }} opts
|
|
16
|
+
*/
|
|
17
|
+
export async function stepSetupAuth(targetDir, opts) {
|
|
18
|
+
await safeStep('Writing auth & proxy files', () => {
|
|
19
|
+
const p = (...parts) => path.join(targetDir, ...parts)
|
|
20
|
+
|
|
21
|
+
writeFile(p('src', 'lib', 'proxy', 'index.ts'), proxyLibTs)
|
|
22
|
+
|
|
23
|
+
if (opts.auth) {
|
|
24
|
+
writeFile(p('src', 'lib', 'proxy', 'auth.ts'), proxyAuthTs)
|
|
25
|
+
writeFile(p('src', 'lib', 'validators', 'index.ts'), validatorsTs)
|
|
26
|
+
writeFile(p('src', 'lib', 'validators', 'auth.ts'), validatorsAuthTs)
|
|
27
|
+
writeFile(p('src', 'apis', 'client', 'index.ts'), apisClientTs)
|
|
28
|
+
writeFile(p('src', 'apis', 'server', 'index.ts'), apisServerTs)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { writeFile } from '../utils/writer.js'
|
|
3
|
+
import { safeStep } from '../utils/safe-step.js'
|
|
4
|
+
import { proxyTs } from '../templates/files.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} targetDir
|
|
8
|
+
* @param {{ i18n: boolean, auth: boolean }} opts
|
|
9
|
+
*/
|
|
10
|
+
export async function stepWriteProxy(targetDir, opts) {
|
|
11
|
+
if (!opts.i18n && !opts.auth) return
|
|
12
|
+
|
|
13
|
+
await safeStep('Writing Next.js middleware (proxy)', () => {
|
|
14
|
+
const p = (...parts) => path.join(targetDir, ...parts)
|
|
15
|
+
writeFile(p('src', 'proxy.ts'), proxyTs(opts))
|
|
16
|
+
})
|
|
17
|
+
}
|