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.
- package/auth.js +7 -3
- package/commands/auth.js +3 -2
- package/commands/auth.test.js +5 -7
- package/commands/bootstrap/authjs-secret.js +6 -0
- package/commands/bootstrap/index.js +477 -199
- package/commands/bootstrap/index.test.js +5 -16
- package/commands/bootstrap/is-folder-empty.js +1 -1
- package/commands/branches.js +19 -20
- package/commands/branches.test.js +106 -195
- package/commands/connection_string.test.js +48 -105
- package/commands/databases.js +4 -4
- package/commands/databases.test.js +10 -22
- package/commands/help.test.js +4 -6
- package/commands/ip_allow.test.js +39 -63
- package/commands/operations.js +1 -1
- package/commands/operations.test.js +3 -7
- package/commands/orgs.test.js +3 -7
- package/commands/projects.test.js +80 -124
- package/commands/roles.js +3 -3
- package/commands/roles.test.js +16 -21
- package/commands/set_context.js +1 -1
- package/commands/set_context.test.js +65 -62
- package/config.js +1 -1
- package/context.js +2 -2
- package/help.js +1 -1
- package/index.js +7 -3
- package/package.json +9 -6
- package/test_utils/fixtures.js +84 -0
- package/writer.js +1 -1
- package/test_utils/mock_server.js +0 -16
- package/test_utils/test_cli_command.js +0 -80
|
@@ -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
|
|
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
|
-
|
|
387
|
+
options.packageManager = packageManagerOptions[packageManagerOption]
|
|
131
388
|
.title;
|
|
132
389
|
trackEvent('create-app', {
|
|
133
390
|
phase: 'package-manager',
|
|
134
|
-
meta: { 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
|
-
|
|
415
|
+
options.framework = frameworkOptions[framework]
|
|
159
416
|
.title;
|
|
160
417
|
trackEvent('create-app', {
|
|
161
418
|
phase: 'framework',
|
|
162
|
-
meta: { 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'
|
|
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
|
-
|
|
178
|
-
trackEvent('create-app', { phase: 'orm', meta: { 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: -
|
|
443
|
+
{ title: 'No Authentication', value: 'no-auth' },
|
|
187
444
|
],
|
|
188
445
|
initial: 0,
|
|
189
446
|
});
|
|
190
|
-
|
|
447
|
+
options.auth = auth;
|
|
191
448
|
trackEvent('create-app', {
|
|
192
449
|
phase: 'auth',
|
|
193
|
-
meta: { 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
|
|
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}-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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: ${
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
645
|
+
devConnectionString = connectionUrl.toString();
|
|
367
646
|
}
|
|
368
647
|
const environmentVariables = [];
|
|
369
|
-
if (
|
|
648
|
+
if (options.framework === 'Next.js') {
|
|
370
649
|
let template;
|
|
371
|
-
if (
|
|
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 (
|
|
667
|
+
if (options.packageManager === 'bun') {
|
|
381
668
|
packageManager = '--use-bun';
|
|
382
669
|
}
|
|
383
|
-
else if (
|
|
670
|
+
else if (options.packageManager === 'pnpm') {
|
|
384
671
|
packageManager = '--use-pnpm';
|
|
385
672
|
}
|
|
386
673
|
try {
|
|
387
|
-
execSync(`${getCreateNextAppCommand(
|
|
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 (
|
|
396
|
-
|
|
397
|
-
const
|
|
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:
|
|
687
|
+
value: devConnectionString,
|
|
407
688
|
kind: 'build',
|
|
689
|
+
environment: 'development',
|
|
408
690
|
});
|
|
409
691
|
environmentVariables.push({
|
|
410
692
|
key: 'DATABASE_URL',
|
|
411
|
-
value:
|
|
693
|
+
value: devConnectionString,
|
|
412
694
|
kind: 'runtime',
|
|
695
|
+
environment: 'development',
|
|
413
696
|
});
|
|
414
697
|
environmentVariables.push({
|
|
415
698
|
key: 'AUTH_SECRET',
|
|
416
|
-
value:
|
|
699
|
+
value: devAuthSecret,
|
|
417
700
|
kind: 'build',
|
|
701
|
+
environment: 'development',
|
|
418
702
|
});
|
|
419
703
|
environmentVariables.push({
|
|
420
704
|
key: 'AUTH_SECRET',
|
|
421
|
-
value:
|
|
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:
|
|
730
|
+
value: devConnectionString,
|
|
434
731
|
kind: 'build',
|
|
732
|
+
environment: 'development',
|
|
435
733
|
});
|
|
436
734
|
environmentVariables.push({
|
|
437
735
|
key: 'DATABASE_URL',
|
|
438
|
-
value:
|
|
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} && ${
|
|
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 (
|
|
748
|
+
if (options.orm === 'drizzle') {
|
|
445
749
|
try {
|
|
446
|
-
execSync(`${
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
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: '
|
|
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
|
-
|
|
815
|
+
options.deployment = deployment;
|
|
489
816
|
trackEvent('create-app', {
|
|
490
817
|
phase: 'deployment',
|
|
491
|
-
meta: { deployment:
|
|
818
|
+
meta: { deployment: options.deployment },
|
|
492
819
|
});
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
};
|