neonctl 1.32.0 → 1.33.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) {
@@ -52,6 +58,256 @@ function getExecutorProgram(packageManager) {
52
58
  return 'bunx';
53
59
  }
54
60
  }
61
+ function writeEnvFile({ fileName, secrets, }) {
62
+ let content = '';
63
+ for (const secret of secrets) {
64
+ content += `${secret.key}=${secret.value}\n`;
65
+ }
66
+ writeFileSync(fileName, content, 'utf8');
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
+ }
55
311
  const bootstrap = async (props) => {
56
312
  const out = writer(props);
57
313
  if (isCi()) {
@@ -63,6 +319,7 @@ const bootstrap = async (props) => {
63
319
  name: 'path',
64
320
  message: 'What is your project named?',
65
321
  initial: 'my-app',
322
+ max: 10,
66
323
  validate: (name) => {
67
324
  // We resolve to normalize the path name first, so that if the user enters
68
325
  // something like "/hello", we get back just "hello" and not "/hello".
@@ -98,10 +355,10 @@ const bootstrap = async (props) => {
98
355
  const root = resolve(resolvedProjectPath);
99
356
  const appName = basename(root);
100
357
  const folderExists = existsSync(root);
101
- if (folderExists && !isFolderEmpty(root, appName, out.text)) {
358
+ if (folderExists && !isFolderEmpty(root, appName, (data) => out.text(data))) {
102
359
  throw new Error(`Could not create a project called ${chalk.red(`"${projectName}"`)} because the folder ${chalk.red(`"${resolvedProjectPath}"`)} is not empty.`);
103
360
  }
104
- const finalOptions = {
361
+ const options = {
105
362
  auth: 'auth.js',
106
363
  framework: 'Next.js',
107
364
  deployment: 'vercel',
@@ -127,11 +384,11 @@ const bootstrap = async (props) => {
127
384
  choices: packageManagerOptions,
128
385
  initial: 0,
129
386
  });
130
- finalOptions.packageManager = packageManagerOptions[packageManagerOption]
387
+ options.packageManager = packageManagerOptions[packageManagerOption]
131
388
  .title;
132
389
  trackEvent('create-app', {
133
390
  phase: 'package-manager',
134
- meta: { packageManager: finalOptions.packageManager },
391
+ meta: { packageManager: options.packageManager },
135
392
  });
136
393
  const frameworkOptions = [
137
394
  {
@@ -155,11 +412,11 @@ const bootstrap = async (props) => {
155
412
  initial: 0,
156
413
  warn: 'Coming soon',
157
414
  });
158
- finalOptions.framework = frameworkOptions[framework]
415
+ options.framework = frameworkOptions[framework]
159
416
  .title;
160
417
  trackEvent('create-app', {
161
418
  phase: 'framework',
162
- meta: { framework: finalOptions.framework },
419
+ meta: { framework: options.framework },
163
420
  });
164
421
  const { orm } = await prompts({
165
422
  onState: onPromptState,
@@ -168,14 +425,14 @@ const bootstrap = async (props) => {
168
425
  message: `What ORM would you like to use?`,
169
426
  choices: [
170
427
  { title: 'Drizzle', value: 'drizzle' },
171
- { title: 'Prisma', value: 'prisma', disabled: true },
428
+ { title: 'Prisma', value: 'prisma' },
172
429
  { title: 'No ORM', value: -1, disabled: true },
173
430
  ],
174
431
  initial: 0,
175
432
  warn: 'Coming soon',
176
433
  });
177
- finalOptions.orm = orm;
178
- trackEvent('create-app', { phase: 'orm', meta: { orm: finalOptions.orm } });
434
+ options.orm = orm;
435
+ trackEvent('create-app', { phase: 'orm', meta: { orm: options.orm } });
179
436
  const { auth } = await prompts({
180
437
  onState: onPromptState,
181
438
  type: 'select',
@@ -183,14 +440,14 @@ const bootstrap = async (props) => {
183
440
  message: `What authentication framework do you want to use?`,
184
441
  choices: [
185
442
  { title: 'Auth.js', value: 'auth.js' },
186
- { title: 'No Authentication', value: -1 },
443
+ { title: 'No Authentication', value: 'no-auth' },
187
444
  ],
188
445
  initial: 0,
189
446
  });
190
- finalOptions.auth = auth;
447
+ options.auth = auth;
191
448
  trackEvent('create-app', {
192
449
  phase: 'auth',
193
- meta: { auth: finalOptions.auth },
450
+ meta: { auth: options.auth },
194
451
  });
195
452
  const PROJECTS_LIST_LIMIT = 100;
196
453
  const getList = async (fn) => {
@@ -237,30 +494,87 @@ const bootstrap = async (props) => {
237
494
  trackEvent('create-app', { phase: 'neon-project' });
238
495
  let projectCreateRequest;
239
496
  let project;
240
- let connectionString;
497
+ let devConnectionString;
498
+ const devBranchName = `dev-${cryptoRandomString({
499
+ length: 10,
500
+ type: 'url-safe',
501
+ })}`;
241
502
  if (neonProject === -1) {
242
503
  try {
243
504
  // Call the API directly. This code is inspired from the `create` code in
244
505
  // `projects.ts`.
245
506
  projectCreateRequest = {
246
- name: `${appName}-db`,
507
+ name: `${appName}-project`,
247
508
  branch: {},
248
509
  };
249
- const { data } = await props.apiClient.createProject({
510
+ const { data: createProjectData } = await retryOnLock(() => props.apiClient.createProject({
250
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,
523
+ });
524
+ const database = await createDatabase({
525
+ appName,
526
+ apiClient: props.apiClient,
527
+ branchId: branch.id,
528
+ projectId: project.id,
251
529
  });
252
- project = data.project;
253
- const out = writer(props);
254
- out.write(project, { fields: PROJECT_FIELDS, title: 'Project' });
255
- out.write(data.connection_uris, {
256
- fields: ['connection_uri'],
257
- title: 'Connection URIs',
530
+ writer(props).end(branch, {
531
+ fields: BRANCH_FIELDS,
532
+ title: 'Branch',
258
533
  });
259
- out.end();
260
- connectionString = data.connection_uris[0].connection_uri;
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();
261
575
  }
262
576
  catch (error) {
263
- 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)}`);
264
578
  }
265
579
  }
266
580
  else {
@@ -268,41 +582,30 @@ const bootstrap = async (props) => {
268
582
  if (!project) {
269
583
  throw new Error('An unexpected error occured while selecting the Neon project to use.');
270
584
  }
271
- const { data: { branches }, } = await props.apiClient.listProjectBranches(project.id);
272
- let branchId;
273
- if (branches.length === 0) {
274
- throw new Error(`No branches found for the project ${project.name}.`);
275
- }
276
- else if (branches.length === 1) {
277
- branchId = branches[0].id;
278
- }
279
- else {
280
- const branchChoices = branches.map((branch) => {
281
- return {
282
- title: branch.name,
283
- value: branch.id,
284
- };
285
- });
286
- const { branchIdChoice } = await prompts({
287
- onState: onPromptState,
288
- type: 'select',
289
- name: 'branchIdChoice',
290
- message: `What branch would you like to use?`,
291
- choices: branchChoices,
292
- initial: 0,
293
- });
294
- branchId = branchIdChoice;
295
- trackEvent('create-app', { phase: 'neon-branch' });
296
- }
297
- const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(project.id, branchId);
298
- const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
299
- if (!endpoint) {
300
- throw new Error(`No read-write endpoint found for the project ${project.name}.`);
301
- }
302
- 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);
303
606
  let role;
304
607
  if (roles.length === 0) {
305
- throw new Error(`No roles found for the branch: ${branchId}`);
608
+ throw new Error(`No roles found for the branch: ${branch.id}`);
306
609
  }
307
610
  else if (roles.length === 1) {
308
611
  role = roles[0];
@@ -326,36 +629,12 @@ const bootstrap = async (props) => {
326
629
  if (!role) {
327
630
  throw new Error(`No role found for the name: ${roleName}`);
328
631
  }
329
- trackEvent('create-app', { phase: 'neon-role' });
330
- }
331
- const { data: { databases: branchDatabases }, } = await props.apiClient.listProjectBranchDatabases(project.id, branchId);
332
- let database;
333
- if (branchDatabases.length === 0) {
334
- throw new Error(`No databases found for the branch: ${branchId}`);
335
- }
336
- else if (branchDatabases.length === 1) {
337
- database = branchDatabases[0];
632
+ trackEvent('create-app', { phase: 'neon-role-dev' });
338
633
  }
339
- else {
340
- const databaseChoices = branchDatabases.map((db) => {
341
- return {
342
- title: db.name,
343
- value: db.id,
344
- };
345
- });
346
- const { databaseId } = await prompts({
347
- onState: onPromptState,
348
- type: 'select',
349
- name: 'databaseId',
350
- message: `What database would you like to use?`,
351
- choices: databaseChoices,
352
- initial: 0,
353
- });
354
- database = branchDatabases.find((d) => d.id === databaseId);
355
- if (!database) {
356
- throw new Error(`No database found with ID: ${databaseId}`);
357
- }
358
- 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}.`);
359
638
  }
360
639
  const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(project.id, endpoint.branch_id, role.name);
361
640
  const host = endpoint.host;
@@ -363,105 +642,147 @@ const bootstrap = async (props) => {
363
642
  connectionUrl.pathname = database.name;
364
643
  connectionUrl.username = role.name;
365
644
  connectionUrl.password = password;
366
- connectionString = connectionUrl.toString();
645
+ devConnectionString = connectionUrl.toString();
367
646
  }
368
647
  const environmentVariables = [];
369
- if (finalOptions.framework === 'Next.js') {
648
+ if (options.framework === 'Next.js') {
370
649
  let template;
371
- if (finalOptions.auth === 'auth.js') {
650
+ if (options.auth === 'auth.js' && options.orm === 'drizzle') {
372
651
  template =
373
652
  'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-drizzle-authjs';
374
653
  }
375
- else {
654
+ else if (options.auth === 'no-auth' && options.orm === 'drizzle') {
376
655
  template =
377
656
  'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-drizzle';
378
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
+ }
379
666
  let packageManager = '--use-npm';
380
- if (finalOptions.packageManager === 'bun') {
667
+ if (options.packageManager === 'bun') {
381
668
  packageManager = '--use-bun';
382
669
  }
383
- else if (finalOptions.packageManager === 'pnpm') {
670
+ else if (options.packageManager === 'pnpm') {
384
671
  packageManager = '--use-pnpm';
385
672
  }
386
673
  try {
387
- execSync(`${getCreateNextAppCommand(finalOptions.packageManager)} \
674
+ execSync(`${getCreateNextAppCommand(options.packageManager)} \
388
675
  ${packageManager} \
389
676
  --example ${template} \
390
677
  ${appName}`, { stdio: 'inherit' });
391
678
  }
392
679
  catch (error) {
393
- throw new Error(`Creating a Next.js project failed: ${error}.`);
394
- }
395
- if (finalOptions.auth === 'auth.js') {
396
- // Generate AUTH_SECRET using openssl
397
- const authSecret = execSync('openssl rand -base64 33').toString().trim();
398
- // Content for the .env.local file
399
- const content = `DATABASE_URL=${connectionString}
400
- AUTH_SECRET=${authSecret}`;
401
- // Write the content to the .env.local file
402
- writeFileSync(`${appName}/.env.local`, content, 'utf8');
403
- writeFileSync(`${appName}/.dev.vars`, content, 'utf8'); // cloudflare
680
+ throw new Error(`Creating a Next.js project failed: ${String(error)}.`);
681
+ }
682
+ if (options.auth === 'auth.js') {
683
+ const devAuthSecret = getAuthjsSecret();
684
+ const prodAuthSecret = getAuthjsSecret();
404
685
  environmentVariables.push({
405
686
  key: 'DATABASE_URL',
406
- value: connectionString,
687
+ value: devConnectionString,
407
688
  kind: 'build',
689
+ environment: 'development',
408
690
  });
409
691
  environmentVariables.push({
410
692
  key: 'DATABASE_URL',
411
- value: connectionString,
693
+ value: devConnectionString,
412
694
  kind: 'runtime',
695
+ environment: 'development',
413
696
  });
414
697
  environmentVariables.push({
415
698
  key: 'AUTH_SECRET',
416
- value: authSecret,
699
+ value: devAuthSecret,
417
700
  kind: 'build',
701
+ environment: 'development',
418
702
  });
419
703
  environmentVariables.push({
420
704
  key: 'AUTH_SECRET',
421
- value: authSecret,
705
+ value: devAuthSecret,
422
706
  kind: 'runtime',
707
+ environment: 'development',
708
+ });
709
+ environmentVariables.push({
710
+ key: 'AUTH_SECRET',
711
+ value: prodAuthSecret,
712
+ kind: 'build',
713
+ environment: 'production',
714
+ });
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
722
+ writeEnvFile({
723
+ fileName: `${appName}/.env`,
724
+ secrets: environmentVariables.filter((e) => e.kind === 'runtime' && e.environment === 'development'),
423
725
  });
424
726
  }
425
727
  else {
426
- // Content for the .env.local file
427
- const content = `DATABASE_URL=${connectionString}`;
428
- // Write the content to the .env.local file
429
- writeFileSync(`${appName}/.env.local`, content, 'utf8');
430
- writeFileSync(`${appName}/.dev.vars`, content, 'utf8'); // cloudflare
431
728
  environmentVariables.push({
432
729
  key: 'DATABASE_URL',
433
- value: connectionString,
730
+ value: devConnectionString,
434
731
  kind: 'build',
732
+ environment: 'development',
435
733
  });
436
734
  environmentVariables.push({
437
735
  key: 'DATABASE_URL',
438
- value: connectionString,
736
+ value: devConnectionString,
439
737
  kind: 'runtime',
738
+ environment: 'development',
739
+ });
740
+ // Write the content to the .env file
741
+ writeEnvFile({
742
+ fileName: `${appName}/.env`,
743
+ secrets: environmentVariables.filter((e) => e.kind === 'runtime' && e.environment === 'development'),
440
744
  });
441
745
  }
442
- 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`)}`);
443
747
  }
444
- if (finalOptions.orm === 'drizzle') {
748
+ if (options.orm === 'drizzle') {
445
749
  try {
446
- execSync(`${finalOptions.packageManager} run db:generate -- --name init_db`, {
750
+ execSync(`${options.packageManager} run db:generate -- --name init_db`, {
447
751
  cwd: appName,
448
752
  stdio: 'inherit',
449
753
  });
450
754
  }
451
755
  catch (error) {
452
- 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
+ });
453
765
  }
454
- // If the user doesn't specify Auth.js, there is no schema to be applied.
455
- if (finalOptions.auth === 'auth.js') {
456
- try {
457
- execSync(`${finalOptions.packageManager} run db:migrate`, {
458
- cwd: appName,
459
- stdio: 'inherit',
460
- });
461
- }
462
- catch (error) {
463
- throw new Error(`Applying the schema failed: ${error}.`);
464
- }
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)}.`);
465
786
  }
466
787
  out.text(`Database schema generated and applied.\n`);
467
788
  }
@@ -480,83 +801,40 @@ AUTH_SECRET=${authSecret}`;
480
801
  title: 'Cloudflare',
481
802
  value: 'cloudflare',
482
803
  description: 'We will install the Wrangler CLI globally.',
804
+ // Making Prisma work on Cloudflare is a bit tricky.
805
+ disabled: options.orm === 'prisma',
483
806
  },
484
- { title: 'Nowhere', value: -1 },
807
+ { title: 'Skip this step', value: 'no-deployment' },
485
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,
486
813
  initial: 0,
487
814
  });
488
- finalOptions.deployment = deployment;
815
+ options.deployment = deployment;
489
816
  trackEvent('create-app', {
490
817
  phase: 'deployment',
491
- meta: { deployment: finalOptions.deployment },
818
+ meta: { deployment: options.deployment },
492
819
  });
493
- if (finalOptions.deployment === 'vercel') {
494
- try {
495
- let envVarsStr = '';
496
- for (let i = 0; i < environmentVariables.length; i++) {
497
- const envVar = environmentVariables[i];
498
- envVarsStr += `${envVar.kind === 'build' ? '--build-env' : '--env'} ${envVar.key}=${envVar.value} `;
499
- }
500
- execSync(`${getExecutorProgram(finalOptions.packageManager)} vercel@34.3.1 deploy ${envVarsStr}`, {
501
- cwd: appName,
502
- stdio: 'inherit',
503
- });
504
- }
505
- catch (error) {
506
- throw new Error(`Deploying to Vercel failed: ${error}.`);
507
- }
820
+ if (options.deployment !== 'no-deployment') {
821
+ await deployApp({
822
+ options,
823
+ props,
824
+ devBranchName,
825
+ project,
826
+ appName,
827
+ environmentVariables,
828
+ });
508
829
  }
509
- else if (finalOptions.deployment === 'cloudflare') {
510
- try {
511
- execSync('command -v wrangler', {
512
- cwd: appName,
513
- stdio: 'ignore',
514
- });
515
- }
516
- catch (error) {
517
- try {
518
- execSync(`${finalOptions.packageManager} install -g @cloudflare/wrangler`, {
519
- cwd: appName,
520
- stdio: 'inherit',
521
- });
522
- }
523
- catch (error) {
524
- throw new Error(`Failed to install the Cloudflare CLI: ${error}.`);
525
- }
526
- }
527
- const wranglerToml = `name = "${appName}"
528
- compatibility_flags = [ "nodejs_compat" ]
529
- pages_build_output_dir = ".vercel/output/static"
530
- compatibility_date = "2022-11-30"
830
+ trackEvent('create-app', { phase: 'success-finish' });
831
+ if (options.framework === 'Next.js') {
832
+ log.info(chalk.green(`
531
833
 
532
- [vars]
533
- ${environmentVariables
534
- .map((envVar) => {
535
- if (envVar.kind === 'runtime') {
536
- return `${envVar.key} = "${envVar.value}"`;
537
- }
538
- })
539
- .join('\n')}
540
- `;
541
- writeFileSync(`${appName}/wrangler.toml`, wranglerToml, 'utf8');
542
- try {
543
- execSync(`${getExecutorProgram(finalOptions.packageManager)} @cloudflare/next-on-pages@1.12.1`, {
544
- cwd: appName,
545
- stdio: 'inherit',
546
- });
547
- }
548
- catch (error) {
549
- throw new Error(`Failed to build Next.js app with next-on-pages: ${error}.`);
550
- }
551
- try {
552
- execSync(`wrangler pages deploy`, {
553
- cwd: appName,
554
- stdio: 'inherit',
555
- });
556
- }
557
- catch (error) {
558
- throw new Error(`Failed to deploy to Cloudflare Pages: ${error}.`);
559
- }
834
+ You can now run:
835
+
836
+ cd ${appName} && ${options.packageManager} run dev
837
+
838
+ to start the app locally.`));
560
839
  }
561
- trackEvent('create-app', { phase: 'success-finish' });
562
840
  };