create-nexora-next 0.3.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nexora-next",
3
- "version": "0.3.6",
3
+ "version": "0.4.7",
4
4
  "description": "The official Next.js scaffolding CLI by Rayan — batteries included.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -4,24 +4,23 @@ import pc from 'picocolors'
4
4
  import path from 'path'
5
5
  import { existsSync } from 'fs'
6
6
  import { createRequire } from 'module'
7
+ import { parseArgs, hasAnyFlag, printHelp } from './utils/args.js'
7
8
 
8
9
  const require = createRequire(import.meta.url)
9
10
  const { version } = require('../package.json')
10
11
 
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'
12
+ import { stepCreateNextApp } from './steps/01-create-next-app.js'
13
+ import { stepSetupShadcn } from './steps/02-shadcn.js'
14
+ import { stepWriteBaseFiles } from './steps/03-base-files.js'
15
+ import { stepWriteProviders } from './steps/04-providers.js'
16
+ import { stepSetupI18n } from './steps/05-i18n.js'
17
+ import { stepSetupAuth } from './steps/06-auth.js'
18
+ import { stepWriteProxy } from './steps/07-proxy.js'
19
+ import { stepInstallDeps } from './steps/08-install-deps.js'
20
+ import { stepSetupHusky } from './steps/09-husky.js'
21
+ import { stepSetupAxios } from './steps/10-axios.js'
21
22
  import { stepPatchPackageJson } from './steps/11-patch-pkg.js'
22
23
 
23
- // ─── Helpers ──────────────────────────────────────────────────────────────────
24
-
25
24
  function bail(msg) {
26
25
  p.cancel(pc.red(msg))
27
26
  process.exit(1)
@@ -35,8 +34,6 @@ function section(title) {
35
34
  p.log.step(pc.bold(pc.cyan(`\n ${title}`)))
36
35
  }
37
36
 
38
- // ─── Banner ───────────────────────────────────────────────────────────────────
39
-
40
37
  function printBanner() {
41
38
  const art = [
42
39
  ' ███╗ ██╗███████╗██╗ ██╗ ██████╗ ██████╗ █████╗ ',
@@ -46,9 +43,7 @@ function printBanner() {
46
43
  ' ██║ ╚████║███████╗██╔╝ ██╗╚██████╔╝██║ ██║██║ ██║',
47
44
  ' ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝',
48
45
  ]
49
- // Fade from cyan → blue top to bottom
50
46
  const gradient = [pc.cyan, pc.cyan, pc.cyan, pc.blue, pc.blue, pc.blue]
51
-
52
47
  console.log()
53
48
  art.forEach((line, i) => console.log(pc.bold(gradient[i](line))))
54
49
  console.log()
@@ -56,8 +51,7 @@ function printBanner() {
56
51
  const centre = (str) => {
57
52
  const pad = W - str.length
58
53
  const l = Math.floor(pad / 2)
59
- const r = pad - l
60
- return ' '.repeat(l) + str + ' '.repeat(r)
54
+ return ' '.repeat(l) + str + ' '.repeat(pad - l)
61
55
  }
62
56
  const bar = '═'.repeat(W)
63
57
  console.log(pc.bold(pc.cyan(
@@ -69,8 +63,6 @@ function printBanner() {
69
63
  console.log()
70
64
  }
71
65
 
72
- // ─── Summary ──────────────────────────────────────────────────────────────────
73
-
74
66
  function printSummary(opts, projectName, pm) {
75
67
  const yes = pc.green('✓')
76
68
  const skip = pc.dim('–')
@@ -103,103 +95,97 @@ function printSummary(opts, projectName, pm) {
103
95
  console.log()
104
96
  }
105
97
 
106
- // ─── Main ─────────────────────────────────────────────────────────────────────
98
+ async function ask(condition, prompt) {
99
+ if (condition !== null) return condition
100
+ const val = await prompt()
101
+ if (p.isCancel(val)) onCancel()
102
+ return Boolean(val)
103
+ }
107
104
 
108
105
  async function main() {
106
+ const parsed = parseArgs(process.argv)
107
+
109
108
  printBanner()
110
- p.intro(pc.bgCyan(pc.black(" create-nexora-next ")))
111
109
 
112
- // ── Phase 1: Identity ──────────────────────────────────────────────────────
110
+ if (parsed.version) {
111
+ console.log(` v${version}`)
112
+ process.exit(0)
113
+ }
114
+
115
+ if (parsed.help) {
116
+ printHelp(version)
117
+ process.exit(0)
118
+ }
119
+
120
+ p.intro(pc.bgCyan(pc.black(' create-nexora-next ')))
121
+
122
+ const flagMode = hasAnyFlag(parsed)
123
+
124
+ if (flagMode) {
125
+ p.log.info(pc.dim(' Running in flag mode — skipping answered prompts'))
126
+ console.log()
127
+ }
128
+
113
129
  section('Phase 1 — Project identity')
114
130
 
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()
131
+ let name = parsed.name
132
+ if (!name) {
133
+ const res = await p.text({
134
+ message: 'What is your project name?',
135
+ placeholder: 'my-app',
136
+ validate: (v) => {
137
+ if (!v || !v.trim()) return 'Project name is required.'
138
+ if (!/^[a-z0-9-_]+$/.test(v.trim()))
139
+ return 'Use lowercase letters, numbers, hyphens or underscores only.'
140
+ },
141
+ })
142
+ if (p.isCancel(res)) onCancel()
143
+ name = res.trim()
144
+ } else {
145
+ p.log.step(`Project name: ${pc.cyan(name)}`)
146
+ }
126
147
 
127
148
  const targetDir = path.resolve(process.cwd(), name)
128
- if (existsSync(targetDir)) {
129
- bail(`Directory "${name}" already exists. Choose a different name.`)
149
+ if (existsSync(targetDir)) bail(`Directory "${name}" already exists.`)
150
+
151
+ let pm = parsed.pm
152
+ if (!pm) {
153
+ const res = await p.select({
154
+ message: 'Which package manager?',
155
+ options: [
156
+ { value: 'pnpm', label: 'pnpm', hint: 'recommended' },
157
+ { value: 'bun', label: 'bun', hint: 'fastest' },
158
+ { value: 'npm', label: 'npm', hint: 'classic' },
159
+ { value: 'yarn', label: 'yarn', hint: 'classic alt' },
160
+ ],
161
+ })
162
+ if (p.isCancel(res)) onCancel()
163
+ pm = res
164
+ } else {
165
+ p.log.step(`Package manager: ${pc.cyan(pm)}`)
130
166
  }
131
167
 
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
168
  section('Phase 2 — Features')
145
169
 
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 ────────────────────────────────────────────────────────────────
170
+ const defaults = { theming: true, husky: true, reactCompiler: true }
171
+
172
+ const i18n = await ask(parsed.i18n, () => p.confirm({ message: 'Set up localization? (next-intl)', initialValue: false }))
173
+ const query = await ask(parsed.query, () => p.confirm({ message: 'Set up TanStack Query + local persistence?', initialValue: false }))
174
+ const theming = await ask(parsed.theming, () => p.confirm({ message: 'Set up theming? (next-themes dark/light)', initialValue: true }))
175
+ const animations = await ask(parsed.animations, () => p.confirm({ message: 'Install animation libraries? (motion + gsap)', initialValue: false }))
176
+ const auth = await ask(parsed.auth, () => p.confirm({ message: 'Set up auth proxy + zod validators?', initialValue: false }))
177
+ const axios = await ask(parsed.axios, () => p.confirm({ message: 'Set up axios client/server instances?', initialValue: false }))
178
+ const husky = await ask(parsed.husky, () => p.confirm({ message: 'Configure Husky pre-commit hooks?', initialValue: true }))
179
+ const reactCompiler = await ask(parsed.reactCompiler, () => p.confirm({ message: 'Enable React Compiler? (babel-plugin-react-compiler)', initialValue: true }))
180
+
195
181
  const features = [
196
- i18n && 'i18n',
197
- query && 'query',
198
- theming && 'theming',
199
- animations && 'animations',
200
- auth && 'auth',
201
- axios && 'axios',
202
- husky && 'husky',
182
+ i18n && 'i18n',
183
+ query && 'query',
184
+ theming && 'theming',
185
+ animations && 'animations',
186
+ auth && 'auth',
187
+ axios && 'axios',
188
+ husky && 'husky',
203
189
  reactCompiler && 'react-compiler',
204
190
  ].filter(Boolean)
205
191
 
@@ -211,23 +197,21 @@ async function main() {
211
197
  )
212
198
  console.log()
213
199
 
214
- const go = await p.confirm({
215
- message: 'Ready to scaffold?',
216
- initialValue: true,
217
- })
218
- if (p.isCancel(go) || !go) onCancel()
200
+ if (!parsed.yes && !flagMode) {
201
+ const go = await p.confirm({ message: 'Ready to scaffold?', initialValue: true })
202
+ if (p.isCancel(go) || !go) onCancel()
203
+ }
219
204
 
220
- // ── Phase 3: Execution ─────────────────────────────────────────────────────
221
205
  section('Phase 3 — Building your project')
222
206
 
223
207
  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),
208
+ i18n: Boolean(i18n),
209
+ query: Boolean(query),
210
+ theming: Boolean(theming),
211
+ animations: Boolean(animations),
212
+ auth: Boolean(auth),
213
+ axios: Boolean(axios),
214
+ husky: Boolean(husky),
231
215
  reactCompiler: Boolean(reactCompiler),
232
216
  }
233
217
 
@@ -236,26 +220,17 @@ async function main() {
236
220
  await stepWriteBaseFiles(targetDir, opts)
237
221
  await stepWriteProviders(targetDir, opts)
238
222
 
239
- if (opts.i18n) {
240
- await stepSetupI18n(targetDir, pm, opts)
241
- }
242
-
243
- if (opts.auth || opts.i18n) {
244
- await stepSetupAuth(targetDir, opts)
245
- }
223
+ if (opts.i18n) await stepSetupI18n(targetDir, pm, opts)
224
+ if (opts.auth || opts.i18n) await stepSetupAuth(targetDir, opts)
246
225
 
247
226
  await stepWriteProxy(targetDir, opts)
248
227
 
249
- if (opts.axios) {
250
- await stepSetupAxios(targetDir)
251
- }
228
+ if (opts.axios) await stepSetupAxios(targetDir)
252
229
 
253
230
  await stepInstallDeps(targetDir, pm, opts)
254
231
  await stepPatchPackageJson(targetDir)
255
232
 
256
- if (opts.husky) {
257
- await stepSetupHusky(targetDir, pm)
258
- }
233
+ if (opts.husky) await stepSetupHusky(targetDir, pm)
259
234
 
260
235
  printSummary(opts, name, pm)
261
236
  p.outro(pc.bgGreen(pc.black(' Done! ')))
@@ -0,0 +1,119 @@
1
+ const VALID_PMS = ['npm', 'pnpm', 'bun', 'yarn']
2
+
3
+ const FLAGS = {
4
+ '--all': { key: 'all' },
5
+ '--i18n': { key: 'i18n' },
6
+ '--query': { key: 'query' },
7
+ '--theming': { key: 'theming' },
8
+ '--animations': { key: 'animations' },
9
+ '--auth': { key: 'auth' },
10
+ '--axios': { key: 'axios' },
11
+ '--husky': { key: 'husky' },
12
+ '--rc': { key: 'reactCompiler' },
13
+ '--react-compiler': { key: 'reactCompiler' },
14
+ '--no-theming': { key: 'theming', value: false },
15
+ '--no-husky': { key: 'husky', value: false },
16
+ '--no-rc': { key: 'reactCompiler', value: false },
17
+ '--pm': { key: 'pm', takesValue: true },
18
+ '-y': { key: 'yes' },
19
+ '--yes': { key: 'yes' },
20
+ '--help': { key: 'help' },
21
+ '-h': { key: 'help' },
22
+ '--version': { key: 'version' },
23
+ '-v': { key: 'version' },
24
+ }
25
+
26
+ export function parseArgs(argv) {
27
+ const args = argv.slice(2)
28
+ const result = {
29
+ name: null,
30
+ pm: null,
31
+ yes: false,
32
+ help: false,
33
+ version: false,
34
+ all: false,
35
+ i18n: null,
36
+ query: null,
37
+ theming: null,
38
+ animations: null,
39
+ auth: null,
40
+ axios: null,
41
+ husky: null,
42
+ reactCompiler: null,
43
+ }
44
+
45
+ let i = 0
46
+ while (i < args.length) {
47
+ const arg = args[i]
48
+ const def = FLAGS[arg]
49
+
50
+ if (def) {
51
+ if (def.takesValue) {
52
+ result[def.key] = args[++i] ?? null
53
+ } else if (def.value === false) {
54
+ result[def.key] = false
55
+ } else {
56
+ result[def.key] = true
57
+ }
58
+ } else if (!arg.startsWith('-') && !result.name) {
59
+ result.name = arg
60
+ }
61
+ i++
62
+ }
63
+
64
+ if (result.all) {
65
+ for (const key of ['i18n','query','theming','animations','auth','axios','husky','reactCompiler']) {
66
+ if (result[key] === null) result[key] = true
67
+ }
68
+ }
69
+
70
+ if (result.pm && !VALID_PMS.includes(result.pm)) {
71
+ result.pm = null
72
+ }
73
+
74
+ return result
75
+ }
76
+
77
+ export function hasAnyFlag(parsed) {
78
+ return (
79
+ parsed.yes ||
80
+ parsed.all ||
81
+ parsed.name !== null ||
82
+ parsed.pm !== null ||
83
+ ['i18n','query','theming','animations','auth','axios','husky','reactCompiler']
84
+ .some(k => parsed[k] !== null)
85
+ )
86
+ }
87
+
88
+ export function printHelp(version) {
89
+ console.log(`
90
+ create-nexora-next v${version}
91
+
92
+ Usage:
93
+ npx create-nexora-next [name] [flags]
94
+
95
+ Flags:
96
+ --pm <npm|pnpm|bun|yarn> Package manager (default: prompt)
97
+ --all Enable every feature
98
+ --i18n Localization via next-intl
99
+ --query TanStack Query + persistence
100
+ --theming Theming via next-themes
101
+ --animations Motion + GSAP
102
+ --auth Auth proxy + validators
103
+ --axios Axios client/server instances
104
+ --husky Husky pre-commit hooks
105
+ --rc / --react-compiler React Compiler
106
+ --no-theming Skip theming
107
+ --no-husky Skip Husky
108
+ --no-rc Skip React Compiler
109
+ -y / --yes Accept all defaults (no prompts)
110
+ -v / --version Print version
111
+ -h / --help Show this help
112
+
113
+ Examples:
114
+ npx create-nexora-next my-app --pm pnpm --all
115
+ npx create-nexora-next my-app --pm bun --i18n --query --theming
116
+ npx create-nexora-next my-app --pm bun --all --no-animations --no-auth
117
+ npx create-nexora-next my-app -y
118
+ `)
119
+ }