neonctl 1.32.1 → 1.34.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.
@@ -11,6 +11,11 @@ import { EndpointType, } from '@neondatabase/api-client';
11
11
  import { PROJECT_FIELDS } from '../projects.js';
12
12
  import { execSync } from 'child_process';
13
13
  import { trackEvent } from '../../analytics.js';
14
+ import { BRANCH_FIELDS } from '../branches.js';
15
+ import cryptoRandomString from 'crypto-random-string';
16
+ import { retryOnLock } from '../../api.js';
17
+ import { DATABASE_FIELDS } from '../databases.js';
18
+ import { getAuthjsSecret } from './authjs-secret.js';
14
19
  export const command = 'create-app';
15
20
  export const aliases = ['bootstrap'];
16
21
  export const describe = 'Initialize a new Neon project';
@@ -29,6 +34,7 @@ const onPromptState = (state) => {
29
34
  export const handler = async (args) => {
30
35
  await bootstrap(args);
31
36
  };
37
+ export const DEFAULT_NEON_ROLE_NAME = 'neondb_owner';
32
38
  // `getCreateNextAppCommand` returns the command for creating a Next app
33
39
  // with `create-next-app` for different package managers.
34
40
  function getCreateNextAppCommand(packageManager) {
@@ -59,6 +65,249 @@ function writeEnvFile({ fileName, secrets, }) {
59
65
  }
60
66
  writeFileSync(fileName, content, 'utf8');
61
67
  }
68
+ async function createBranch({ projectId, apiClient, name, }) {
69
+ const { data: { branch }, } = await retryOnLock(() => apiClient.createProjectBranch(projectId, {
70
+ branch: {
71
+ name,
72
+ },
73
+ endpoints: [
74
+ {
75
+ type: EndpointType.ReadWrite,
76
+ },
77
+ ],
78
+ }));
79
+ return branch;
80
+ }
81
+ async function createDatabase({ appName, projectId, branchId, apiClient, ownerRole, }) {
82
+ const { data: { database }, } = await retryOnLock(() => apiClient.createProjectBranchDatabase(projectId, branchId, {
83
+ database: {
84
+ name: `${appName}-${cryptoRandomString({
85
+ length: 5,
86
+ type: 'url-safe',
87
+ })}-db`,
88
+ owner_name: ownerRole || DEFAULT_NEON_ROLE_NAME,
89
+ },
90
+ }));
91
+ return database;
92
+ }
93
+ function applyMigrations({ options, appName, connectionString, }) {
94
+ // We have to seed `env` with all of `process.env` so that things like
95
+ // `NODE_ENV` and `PATH` are available to the child process.
96
+ const env = {
97
+ ...process.env,
98
+ };
99
+ if (connectionString) {
100
+ env.DATABASE_URL = connectionString;
101
+ }
102
+ if (options.orm === 'drizzle') {
103
+ try {
104
+ execSync(`${options.packageManager} run db:migrate`, {
105
+ cwd: appName,
106
+ stdio: 'inherit',
107
+ env,
108
+ });
109
+ }
110
+ catch (error) {
111
+ throw new Error(`Applying the schema to the dev branch failed: ${String(error)}.`);
112
+ }
113
+ }
114
+ else if (options.orm === 'prisma') {
115
+ try {
116
+ execSync(`${options.packageManager} run db:generate`, {
117
+ cwd: appName,
118
+ stdio: 'inherit',
119
+ env,
120
+ });
121
+ }
122
+ catch (error) {
123
+ throw new Error(`Generating the Prisma client failed: ${String(error)}.`);
124
+ }
125
+ try {
126
+ execSync(`${options.packageManager} run db:migrate -- --skip-generate`, {
127
+ cwd: appName,
128
+ stdio: 'inherit',
129
+ env,
130
+ });
131
+ }
132
+ catch (error) {
133
+ throw new Error(`Applying the schema failed: ${String(error)}.`);
134
+ }
135
+ }
136
+ }
137
+ async function deployApp({ props, options, devBranchName, project, appName, environmentVariables, }) {
138
+ let { data: { branches }, } = await props.apiClient.listProjectBranches(project.id);
139
+ branches = branches.filter((branch) => branch.name !== devBranchName);
140
+ let branchId;
141
+ if (branches.length === 0) {
142
+ throw new Error(`No branches found for the project ${project.name}.`);
143
+ }
144
+ else if (branches.length === 1) {
145
+ branchId = branches[0].id;
146
+ }
147
+ else {
148
+ // Excludes dev branch we created above.
149
+ const branchChoices = branches.map((branch) => {
150
+ return {
151
+ title: branch.name,
152
+ value: branch.id,
153
+ };
154
+ });
155
+ const { branchIdChoice } = await prompts({
156
+ onState: onPromptState,
157
+ type: 'select',
158
+ name: 'branchIdChoice',
159
+ message: `What branch would you like to use for your deployment? (We have created a branch just for local development, which is not on this list)`,
160
+ choices: branchChoices,
161
+ initial: 0,
162
+ });
163
+ branchId = branchIdChoice;
164
+ trackEvent('create-app', { phase: 'neon-branch-deploy' });
165
+ }
166
+ const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(project.id, branchId);
167
+ const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
168
+ if (!endpoint) {
169
+ throw new Error(`No read-write endpoint found for the project ${project.name}.`);
170
+ }
171
+ const { data: { roles }, } = await props.apiClient.listProjectBranchRoles(project.id, branchId);
172
+ let role;
173
+ if (roles.length === 0) {
174
+ throw new Error(`No roles found for the branch: ${branchId}`);
175
+ }
176
+ else if (roles.length === 1) {
177
+ role = roles[0];
178
+ }
179
+ else {
180
+ const roleChoices = roles.map((r) => {
181
+ return {
182
+ title: r.name,
183
+ value: r.name,
184
+ };
185
+ });
186
+ const { roleName } = await prompts({
187
+ onState: onPromptState,
188
+ type: 'select',
189
+ name: 'roleName',
190
+ message: `What role would you like to use?`,
191
+ choices: roleChoices,
192
+ initial: 0,
193
+ });
194
+ role = roles.find((r) => r.name === roleName);
195
+ if (!role) {
196
+ throw new Error(`No role found for the name: ${roleName}`);
197
+ }
198
+ trackEvent('create-app', { phase: 'neon-role-deploy' });
199
+ }
200
+ const database = await createDatabase({
201
+ appName,
202
+ apiClient: props.apiClient,
203
+ branchId,
204
+ projectId: project.id,
205
+ });
206
+ writer(props).end(database, {
207
+ fields: DATABASE_FIELDS,
208
+ title: 'Database',
209
+ });
210
+ const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(project.id, endpoint.branch_id, role.name);
211
+ const host = endpoint.host;
212
+ const connectionUrl = new URL(`postgresql://${host}`);
213
+ connectionUrl.pathname = database.name;
214
+ connectionUrl.username = role.name;
215
+ connectionUrl.password = password;
216
+ const deployConnectionString = connectionUrl.toString();
217
+ environmentVariables.push({
218
+ key: 'DATABASE_URL',
219
+ value: deployConnectionString,
220
+ kind: 'build',
221
+ environment: 'production',
222
+ });
223
+ environmentVariables.push({
224
+ key: 'DATABASE_URL',
225
+ value: deployConnectionString,
226
+ kind: 'runtime',
227
+ environment: 'production',
228
+ });
229
+ // If the user doesn't specify Auth.js, there is no schema to be applied
230
+ // in Drizzle.
231
+ if (options.auth === 'auth.js' || options.orm === 'prisma') {
232
+ applyMigrations({
233
+ options,
234
+ appName,
235
+ connectionString: deployConnectionString,
236
+ });
237
+ }
238
+ if (options.deployment === 'vercel') {
239
+ try {
240
+ const envVarsStr = environmentVariables
241
+ .filter((envVar) => envVar.environment === 'production')
242
+ .reduce((acc, envVar) => {
243
+ acc.push(envVar.kind === 'build' ? '--build-env' : '--env');
244
+ acc.push(`${envVar.key}=${envVar.value}`);
245
+ return acc;
246
+ }, [])
247
+ .join(' ');
248
+ execSync(`${getExecutorProgram(options.packageManager)} vercel@34.3.1 deploy ${envVarsStr}`, {
249
+ cwd: appName,
250
+ stdio: 'inherit',
251
+ });
252
+ }
253
+ catch (error) {
254
+ throw new Error(`Deploying to Vercel failed: ${String(error)}.`);
255
+ }
256
+ }
257
+ else if (options.deployment === 'cloudflare') {
258
+ try {
259
+ execSync('command -v wrangler', {
260
+ cwd: appName,
261
+ stdio: 'ignore',
262
+ });
263
+ }
264
+ catch {
265
+ try {
266
+ execSync(`${options.packageManager} install -g @cloudflare/wrangler`, {
267
+ cwd: appName,
268
+ stdio: 'inherit',
269
+ });
270
+ }
271
+ catch (error) {
272
+ throw new Error(`Failed to install the Cloudflare CLI: ${String(error)}.`);
273
+ }
274
+ }
275
+ const wranglerToml = `name = "${appName}"
276
+ compatibility_flags = [ "nodejs_compat" ]
277
+ pages_build_output_dir = ".vercel/output/static"
278
+ compatibility_date = "2022-11-30"
279
+
280
+ [vars]
281
+ ${environmentVariables
282
+ .filter((envVar) => envVar.environment === 'production')
283
+ .map((envVar) => {
284
+ if (envVar.kind === 'runtime') {
285
+ return `${envVar.key} = "${envVar.value}"`;
286
+ }
287
+ })
288
+ .join('\n')}
289
+ `;
290
+ writeFileSync(`${appName}/wrangler.toml`, wranglerToml, 'utf8');
291
+ try {
292
+ execSync(`${getExecutorProgram(options.packageManager)} @cloudflare/next-on-pages@1.12.1`, {
293
+ cwd: appName,
294
+ stdio: 'inherit',
295
+ });
296
+ }
297
+ catch (error) {
298
+ throw new Error(`Failed to build Next.js app with next-on-pages: ${String(error)}.`);
299
+ }
300
+ try {
301
+ execSync(`wrangler pages deploy`, {
302
+ cwd: appName,
303
+ stdio: 'inherit',
304
+ });
305
+ }
306
+ catch (error) {
307
+ throw new Error(`Failed to deploy to Cloudflare Pages: ${String(error)}.`);
308
+ }
309
+ }
310
+ }
62
311
  const bootstrap = async (props) => {
63
312
  const out = writer(props);
64
313
  if (isCi()) {
@@ -70,6 +319,7 @@ const bootstrap = async (props) => {
70
319
  name: 'path',
71
320
  message: 'What is your project named?',
72
321
  initial: 'my-app',
322
+ max: 10,
73
323
  validate: (name) => {
74
324
  // We resolve to normalize the path name first, so that if the user enters
75
325
  // something like "/hello", we get back just "hello" and not "/hello".
@@ -105,10 +355,10 @@ const bootstrap = async (props) => {
105
355
  const root = resolve(resolvedProjectPath);
106
356
  const appName = basename(root);
107
357
  const folderExists = existsSync(root);
108
- if (folderExists && !isFolderEmpty(root, appName, out.text)) {
358
+ if (folderExists && !isFolderEmpty(root, appName, (data) => out.text(data))) {
109
359
  throw new Error(`Could not create a project called ${chalk.red(`"${projectName}"`)} because the folder ${chalk.red(`"${resolvedProjectPath}"`)} is not empty.`);
110
360
  }
111
- const finalOptions = {
361
+ const options = {
112
362
  auth: 'auth.js',
113
363
  framework: 'Next.js',
114
364
  deployment: 'vercel',
@@ -134,11 +384,11 @@ const bootstrap = async (props) => {
134
384
  choices: packageManagerOptions,
135
385
  initial: 0,
136
386
  });
137
- finalOptions.packageManager = packageManagerOptions[packageManagerOption]
387
+ options.packageManager = packageManagerOptions[packageManagerOption]
138
388
  .title;
139
389
  trackEvent('create-app', {
140
390
  phase: 'package-manager',
141
- meta: { packageManager: finalOptions.packageManager },
391
+ meta: { packageManager: options.packageManager },
142
392
  });
143
393
  const frameworkOptions = [
144
394
  {
@@ -162,11 +412,11 @@ const bootstrap = async (props) => {
162
412
  initial: 0,
163
413
  warn: 'Coming soon',
164
414
  });
165
- finalOptions.framework = frameworkOptions[framework]
415
+ options.framework = frameworkOptions[framework]
166
416
  .title;
167
417
  trackEvent('create-app', {
168
418
  phase: 'framework',
169
- meta: { framework: finalOptions.framework },
419
+ meta: { framework: options.framework },
170
420
  });
171
421
  const { orm } = await prompts({
172
422
  onState: onPromptState,
@@ -175,14 +425,14 @@ const bootstrap = async (props) => {
175
425
  message: `What ORM would you like to use?`,
176
426
  choices: [
177
427
  { title: 'Drizzle', value: 'drizzle' },
178
- { title: 'Prisma', value: 'prisma', disabled: true },
428
+ { title: 'Prisma', value: 'prisma' },
179
429
  { title: 'No ORM', value: -1, disabled: true },
180
430
  ],
181
431
  initial: 0,
182
432
  warn: 'Coming soon',
183
433
  });
184
- finalOptions.orm = orm;
185
- trackEvent('create-app', { phase: 'orm', meta: { orm: finalOptions.orm } });
434
+ options.orm = orm;
435
+ trackEvent('create-app', { phase: 'orm', meta: { orm: options.orm } });
186
436
  const { auth } = await prompts({
187
437
  onState: onPromptState,
188
438
  type: 'select',
@@ -190,14 +440,14 @@ const bootstrap = async (props) => {
190
440
  message: `What authentication framework do you want to use?`,
191
441
  choices: [
192
442
  { title: 'Auth.js', value: 'auth.js' },
193
- { title: 'No Authentication', value: -1 },
443
+ { title: 'No Authentication', value: 'no-auth' },
194
444
  ],
195
445
  initial: 0,
196
446
  });
197
- finalOptions.auth = auth;
447
+ options.auth = auth;
198
448
  trackEvent('create-app', {
199
449
  phase: 'auth',
200
- meta: { auth: finalOptions.auth },
450
+ meta: { auth: options.auth },
201
451
  });
202
452
  const PROJECTS_LIST_LIMIT = 100;
203
453
  const getList = async (fn) => {
@@ -244,30 +494,87 @@ const bootstrap = async (props) => {
244
494
  trackEvent('create-app', { phase: 'neon-project' });
245
495
  let projectCreateRequest;
246
496
  let project;
247
- let connectionString;
497
+ let devConnectionString;
498
+ const devBranchName = `dev-${cryptoRandomString({
499
+ length: 10,
500
+ type: 'url-safe',
501
+ })}`;
248
502
  if (neonProject === -1) {
249
503
  try {
250
504
  // Call the API directly. This code is inspired from the `create` code in
251
505
  // `projects.ts`.
252
506
  projectCreateRequest = {
253
- name: `${appName}-db`,
507
+ name: `${appName}-project`,
254
508
  branch: {},
255
509
  };
256
- const { data } = await props.apiClient.createProject({
510
+ const { data: createProjectData } = await retryOnLock(() => props.apiClient.createProject({
257
511
  project: projectCreateRequest,
512
+ }));
513
+ project = createProjectData.project;
514
+ writer(props).end(project, {
515
+ fields: PROJECT_FIELDS,
516
+ title: 'Project',
517
+ });
518
+ const branch = await createBranch({
519
+ appName,
520
+ apiClient: props.apiClient,
521
+ projectId: project.id,
522
+ name: devBranchName,
258
523
  });
259
- project = data.project;
260
- const out = writer(props);
261
- out.write(project, { fields: PROJECT_FIELDS, title: 'Project' });
262
- out.write(data.connection_uris, {
263
- fields: ['connection_uri'],
264
- title: 'Connection URIs',
524
+ const database = await createDatabase({
525
+ appName,
526
+ apiClient: props.apiClient,
527
+ branchId: branch.id,
528
+ projectId: project.id,
265
529
  });
266
- out.end();
267
- connectionString = data.connection_uris[0].connection_uri;
530
+ writer(props).end(branch, {
531
+ fields: BRANCH_FIELDS,
532
+ title: 'Branch',
533
+ });
534
+ const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(project.id, branch.id);
535
+ const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
536
+ if (!endpoint) {
537
+ throw new Error(`No read-write endpoint found for the project ${project.name}.`);
538
+ }
539
+ const { data: { roles }, } = await props.apiClient.listProjectBranchRoles(project.id, branch.id);
540
+ let role;
541
+ if (roles.length === 0) {
542
+ throw new Error(`No roles found for the branch: ${branch.id}`);
543
+ }
544
+ else if (roles.length === 1) {
545
+ role = roles[0];
546
+ }
547
+ else {
548
+ const roleChoices = roles.map((r) => {
549
+ return {
550
+ title: r.name,
551
+ value: r.name,
552
+ };
553
+ });
554
+ const { roleName } = await prompts({
555
+ onState: onPromptState,
556
+ type: 'select',
557
+ name: 'roleName',
558
+ message: `What role would you like to use?`,
559
+ choices: roleChoices,
560
+ initial: 0,
561
+ });
562
+ role = roles.find((r) => r.name === roleName);
563
+ if (!role) {
564
+ throw new Error(`No role found for the name: ${roleName}`);
565
+ }
566
+ trackEvent('create-app', { phase: 'neon-role-dev' });
567
+ }
568
+ const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(project.id, endpoint.branch_id, role.name);
569
+ const host = endpoint.host;
570
+ const connectionUrl = new URL(`postgresql://${host}`);
571
+ connectionUrl.pathname = database.name;
572
+ connectionUrl.username = role.name;
573
+ connectionUrl.password = password;
574
+ devConnectionString = connectionUrl.toString();
268
575
  }
269
576
  catch (error) {
270
- throw new Error(`An error occurred while creating a new Neon project: ${error}`);
577
+ throw new Error(`An error occurred while creating a new Neon project: ${String(error)}`);
271
578
  }
272
579
  }
273
580
  else {
@@ -275,41 +582,30 @@ const bootstrap = async (props) => {
275
582
  if (!project) {
276
583
  throw new Error('An unexpected error occured while selecting the Neon project to use.');
277
584
  }
278
- const { data: { branches }, } = await props.apiClient.listProjectBranches(project.id);
279
- let branchId;
280
- if (branches.length === 0) {
281
- throw new Error(`No branches found for the project ${project.name}.`);
282
- }
283
- else if (branches.length === 1) {
284
- branchId = branches[0].id;
285
- }
286
- else {
287
- const branchChoices = branches.map((branch) => {
288
- return {
289
- title: branch.name,
290
- value: branch.id,
291
- };
292
- });
293
- const { branchIdChoice } = await prompts({
294
- onState: onPromptState,
295
- type: 'select',
296
- name: 'branchIdChoice',
297
- message: `What branch would you like to use?`,
298
- choices: branchChoices,
299
- initial: 0,
300
- });
301
- branchId = branchIdChoice;
302
- trackEvent('create-app', { phase: 'neon-branch' });
303
- }
304
- const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(project.id, branchId);
305
- const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
306
- if (!endpoint) {
307
- throw new Error(`No read-write endpoint found for the project ${project.name}.`);
308
- }
309
- const { data: { roles }, } = await props.apiClient.listProjectBranchRoles(project.id, branchId);
585
+ const branch = await createBranch({
586
+ appName,
587
+ apiClient: props.apiClient,
588
+ projectId: project.id,
589
+ name: devBranchName,
590
+ });
591
+ writer(props).end(branch, {
592
+ fields: BRANCH_FIELDS,
593
+ title: 'Branch',
594
+ });
595
+ const database = await createDatabase({
596
+ appName,
597
+ apiClient: props.apiClient,
598
+ branchId: branch.id,
599
+ projectId: project.id,
600
+ });
601
+ writer(props).end(database, {
602
+ fields: DATABASE_FIELDS,
603
+ title: 'Database',
604
+ });
605
+ const { data: { roles }, } = await props.apiClient.listProjectBranchRoles(project.id, branch.id);
310
606
  let role;
311
607
  if (roles.length === 0) {
312
- throw new Error(`No roles found for the branch: ${branchId}`);
608
+ throw new Error(`No roles found for the branch: ${branch.id}`);
313
609
  }
314
610
  else if (roles.length === 1) {
315
611
  role = roles[0];
@@ -333,36 +629,12 @@ const bootstrap = async (props) => {
333
629
  if (!role) {
334
630
  throw new Error(`No role found for the name: ${roleName}`);
335
631
  }
336
- trackEvent('create-app', { phase: 'neon-role' });
632
+ trackEvent('create-app', { phase: 'neon-role-dev' });
337
633
  }
338
- const { data: { databases: branchDatabases }, } = await props.apiClient.listProjectBranchDatabases(project.id, branchId);
339
- let database;
340
- if (branchDatabases.length === 0) {
341
- throw new Error(`No databases found for the branch: ${branchId}`);
342
- }
343
- else if (branchDatabases.length === 1) {
344
- database = branchDatabases[0];
345
- }
346
- else {
347
- const databaseChoices = branchDatabases.map((db) => {
348
- return {
349
- title: db.name,
350
- value: db.id,
351
- };
352
- });
353
- const { databaseId } = await prompts({
354
- onState: onPromptState,
355
- type: 'select',
356
- name: 'databaseId',
357
- message: `What database would you like to use?`,
358
- choices: databaseChoices,
359
- initial: 0,
360
- });
361
- database = branchDatabases.find((d) => d.id === databaseId);
362
- if (!database) {
363
- throw new Error(`No database found with ID: ${databaseId}`);
364
- }
365
- trackEvent('create-app', { phase: 'neon-database' });
634
+ const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(project.id, branch.id);
635
+ const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
636
+ if (!endpoint) {
637
+ throw new Error(`No read-write endpoint found for the project ${project.name}.`);
366
638
  }
367
639
  const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(project.id, endpoint.branch_id, role.name);
368
640
  const host = endpoint.host;
@@ -370,104 +642,147 @@ const bootstrap = async (props) => {
370
642
  connectionUrl.pathname = database.name;
371
643
  connectionUrl.username = role.name;
372
644
  connectionUrl.password = password;
373
- connectionString = connectionUrl.toString();
645
+ devConnectionString = connectionUrl.toString();
374
646
  }
375
647
  const environmentVariables = [];
376
- if (finalOptions.framework === 'Next.js') {
648
+ if (options.framework === 'Next.js') {
377
649
  let template;
378
- if (finalOptions.auth === 'auth.js') {
650
+ if (options.auth === 'auth.js' && options.orm === 'drizzle') {
379
651
  template =
380
652
  'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-drizzle-authjs';
381
653
  }
382
- else {
654
+ else if (options.auth === 'no-auth' && options.orm === 'drizzle') {
383
655
  template =
384
656
  'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-drizzle';
385
657
  }
658
+ else if (options.auth === 'auth.js' && options.orm === 'prisma') {
659
+ template =
660
+ 'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-prisma-authjs';
661
+ }
662
+ else if (options.auth === 'no-auth' && options.orm === 'prisma') {
663
+ template =
664
+ 'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-prisma';
665
+ }
386
666
  let packageManager = '--use-npm';
387
- if (finalOptions.packageManager === 'bun') {
667
+ if (options.packageManager === 'bun') {
388
668
  packageManager = '--use-bun';
389
669
  }
390
- else if (finalOptions.packageManager === 'pnpm') {
670
+ else if (options.packageManager === 'pnpm') {
391
671
  packageManager = '--use-pnpm';
392
672
  }
393
673
  try {
394
- execSync(`${getCreateNextAppCommand(finalOptions.packageManager)} \
674
+ execSync(`${getCreateNextAppCommand(options.packageManager)} \
395
675
  ${packageManager} \
396
676
  --example ${template} \
397
677
  ${appName}`, { stdio: 'inherit' });
398
678
  }
399
679
  catch (error) {
400
- throw new Error(`Creating a Next.js project failed: ${error}.`);
680
+ throw new Error(`Creating a Next.js project failed: ${String(error)}.`);
401
681
  }
402
- if (finalOptions.auth === 'auth.js') {
403
- // Generate AUTH_SECRET using openssl
404
- const authSecret = execSync('openssl rand -base64 33').toString().trim();
682
+ if (options.auth === 'auth.js') {
683
+ const devAuthSecret = getAuthjsSecret();
684
+ const prodAuthSecret = getAuthjsSecret();
405
685
  environmentVariables.push({
406
686
  key: 'DATABASE_URL',
407
- value: connectionString,
687
+ value: devConnectionString,
408
688
  kind: 'build',
689
+ environment: 'development',
409
690
  });
410
691
  environmentVariables.push({
411
692
  key: 'DATABASE_URL',
412
- value: connectionString,
693
+ value: devConnectionString,
413
694
  kind: 'runtime',
695
+ environment: 'development',
414
696
  });
415
697
  environmentVariables.push({
416
698
  key: 'AUTH_SECRET',
417
- value: authSecret,
699
+ value: devAuthSecret,
418
700
  kind: 'build',
701
+ environment: 'development',
419
702
  });
420
703
  environmentVariables.push({
421
704
  key: 'AUTH_SECRET',
422
- value: authSecret,
705
+ value: devAuthSecret,
423
706
  kind: 'runtime',
707
+ environment: 'development',
708
+ });
709
+ environmentVariables.push({
710
+ key: 'AUTH_SECRET',
711
+ value: prodAuthSecret,
712
+ kind: 'build',
713
+ environment: 'production',
424
714
  });
425
- // Write the content to the .env.local file
715
+ environmentVariables.push({
716
+ key: 'AUTH_SECRET',
717
+ value: prodAuthSecret,
718
+ kind: 'runtime',
719
+ environment: 'production',
720
+ });
721
+ // Write the content to the .env file
426
722
  writeEnvFile({
427
- fileName: `${appName}/.env.local`,
428
- secrets: environmentVariables.filter((e) => e.kind === 'runtime'),
723
+ fileName: `${appName}/.env`,
724
+ secrets: environmentVariables.filter((e) => e.kind === 'runtime' && e.environment === 'development'),
429
725
  });
430
726
  }
431
727
  else {
432
728
  environmentVariables.push({
433
729
  key: 'DATABASE_URL',
434
- value: connectionString,
730
+ value: devConnectionString,
435
731
  kind: 'build',
732
+ environment: 'development',
436
733
  });
437
734
  environmentVariables.push({
438
735
  key: 'DATABASE_URL',
439
- value: connectionString,
736
+ value: devConnectionString,
440
737
  kind: 'runtime',
738
+ environment: 'development',
441
739
  });
442
- // Write the content to the .env.local file
740
+ // Write the content to the .env file
443
741
  writeEnvFile({
444
- fileName: `${appName}/.env.local`,
445
- secrets: environmentVariables.filter((e) => e.kind === 'runtime'),
742
+ fileName: `${appName}/.env`,
743
+ secrets: environmentVariables.filter((e) => e.kind === 'runtime' && e.environment === 'development'),
446
744
  });
447
745
  }
448
- out.text(`Created a Next.js project in ${chalk.blue(appName)}.\n\nYou can now run ${chalk.blue(`cd ${appName} && ${finalOptions.packageManager} run dev`)}`);
746
+ out.text(`Created a Next.js project in ${chalk.blue(appName)}.\n\nYou can now run ${chalk.blue(`cd ${appName} && ${options.packageManager} run dev`)}`);
449
747
  }
450
- if (finalOptions.orm === 'drizzle') {
748
+ if (options.orm === 'drizzle') {
451
749
  try {
452
- execSync(`${finalOptions.packageManager} run db:generate -- --name init_db`, {
750
+ execSync(`${options.packageManager} run db:generate -- --name init_db`, {
453
751
  cwd: appName,
454
752
  stdio: 'inherit',
455
753
  });
456
754
  }
457
755
  catch (error) {
458
- throw new Error(`Generating the database schema failed: ${error}.`);
756
+ throw new Error(`Generating the database schema failed: ${String(error)}.`);
757
+ }
758
+ // If the user doesn't specify Auth.js, there is no schema to be applied
759
+ // with Drizzle.
760
+ if (options.auth === 'auth.js') {
761
+ applyMigrations({
762
+ options,
763
+ appName,
764
+ });
459
765
  }
460
- // If the user doesn't specify Auth.js, there is no schema to be applied.
461
- if (finalOptions.auth === 'auth.js') {
462
- try {
463
- execSync(`${finalOptions.packageManager} run db:migrate`, {
464
- cwd: appName,
465
- stdio: 'inherit',
466
- });
467
- }
468
- catch (error) {
469
- throw new Error(`Applying the schema failed: ${error}.`);
470
- }
766
+ out.text(`Database schema generated and applied.\n`);
767
+ }
768
+ else if (options.orm === 'prisma') {
769
+ try {
770
+ execSync(`${options.packageManager} run db:generate`, {
771
+ cwd: appName,
772
+ stdio: 'inherit',
773
+ });
774
+ }
775
+ catch (error) {
776
+ throw new Error(`Generating the Prisma client failed: ${String(error)}.`);
777
+ }
778
+ try {
779
+ execSync(`${options.packageManager} run db:migrate -- --name init --skip-generate`, {
780
+ cwd: appName,
781
+ stdio: 'inherit',
782
+ });
783
+ }
784
+ catch (error) {
785
+ throw new Error(`Applying the schema failed: ${String(error)}.`);
471
786
  }
472
787
  out.text(`Database schema generated and applied.\n`);
473
788
  }
@@ -486,91 +801,39 @@ const bootstrap = async (props) => {
486
801
  title: 'Cloudflare',
487
802
  value: 'cloudflare',
488
803
  description: 'We will install the Wrangler CLI globally.',
804
+ // Making Prisma work on Cloudflare is a bit tricky.
805
+ disabled: options.orm === 'prisma',
489
806
  },
490
- { title: 'Skip this step', value: -1 },
807
+ { title: 'Skip this step', value: 'no-deployment' },
491
808
  ],
809
+ // Making Prisma work on Cloudflare is a bit tricky.
810
+ warn: options.orm === 'prisma'
811
+ ? 'We do not yet support Cloudflare deployments with Prisma.'
812
+ : undefined,
492
813
  initial: 0,
493
814
  });
494
- finalOptions.deployment = deployment;
815
+ options.deployment = deployment;
495
816
  trackEvent('create-app', {
496
817
  phase: 'deployment',
497
- meta: { deployment: finalOptions.deployment },
818
+ meta: { deployment: options.deployment },
498
819
  });
499
- if (finalOptions.deployment === 'vercel') {
500
- try {
501
- let envVarsStr = '';
502
- for (let i = 0; i < environmentVariables.length; i++) {
503
- const envVar = environmentVariables[i];
504
- envVarsStr += `${envVar.kind === 'build' ? '--build-env' : '--env'} ${envVar.key}=${envVar.value} `;
505
- }
506
- execSync(`${getExecutorProgram(finalOptions.packageManager)} vercel@34.3.1 deploy ${envVarsStr}`, {
507
- cwd: appName,
508
- stdio: 'inherit',
509
- });
510
- }
511
- catch (error) {
512
- throw new Error(`Deploying to Vercel failed: ${error}.`);
513
- }
514
- }
515
- else if (finalOptions.deployment === 'cloudflare') {
516
- try {
517
- execSync('command -v wrangler', {
518
- cwd: appName,
519
- stdio: 'ignore',
520
- });
521
- }
522
- catch (error) {
523
- try {
524
- execSync(`${finalOptions.packageManager} install -g @cloudflare/wrangler`, {
525
- cwd: appName,
526
- stdio: 'inherit',
527
- });
528
- }
529
- catch (error) {
530
- throw new Error(`Failed to install the Cloudflare CLI: ${error}.`);
531
- }
532
- }
533
- const wranglerToml = `name = "${appName}"
534
- compatibility_flags = [ "nodejs_compat" ]
535
- pages_build_output_dir = ".vercel/output/static"
536
- compatibility_date = "2022-11-30"
537
-
538
- [vars]
539
- ${environmentVariables
540
- .map((envVar) => {
541
- if (envVar.kind === 'runtime') {
542
- return `${envVar.key} = "${envVar.value}"`;
543
- }
544
- })
545
- .join('\n')}
546
- `;
547
- writeFileSync(`${appName}/wrangler.toml`, wranglerToml, 'utf8');
548
- try {
549
- execSync(`${getExecutorProgram(finalOptions.packageManager)} @cloudflare/next-on-pages@1.12.1`, {
550
- cwd: appName,
551
- stdio: 'inherit',
552
- });
553
- }
554
- catch (error) {
555
- throw new Error(`Failed to build Next.js app with next-on-pages: ${error}.`);
556
- }
557
- try {
558
- execSync(`wrangler pages deploy`, {
559
- cwd: appName,
560
- stdio: 'inherit',
561
- });
562
- }
563
- catch (error) {
564
- throw new Error(`Failed to deploy to Cloudflare Pages: ${error}.`);
565
- }
820
+ if (options.deployment !== 'no-deployment') {
821
+ await deployApp({
822
+ options,
823
+ props,
824
+ devBranchName,
825
+ project,
826
+ appName,
827
+ environmentVariables,
828
+ });
566
829
  }
567
830
  trackEvent('create-app', { phase: 'success-finish' });
568
- if (finalOptions.framework === 'Next.js') {
831
+ if (options.framework === 'Next.js') {
569
832
  log.info(chalk.green(`
570
833
 
571
834
  You can now run:
572
835
 
573
- cd ${appName} && ${finalOptions.packageManager} run dev
836
+ cd ${appName} && ${options.packageManager} run dev
574
837
 
575
838
  to start the app locally.`));
576
839
  }