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.
- 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 +459 -196
- package/commands/bootstrap/index.test.js +5 -16
- package/commands/bootstrap/is-folder-empty.js +1 -1
- package/commands/branches.js +27 -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 -7
- package/parameters.gen.js +4 -4
- 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) {
|
|
@@ -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
|
|
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
|
-
|
|
387
|
+
options.packageManager = packageManagerOptions[packageManagerOption]
|
|
138
388
|
.title;
|
|
139
389
|
trackEvent('create-app', {
|
|
140
390
|
phase: 'package-manager',
|
|
141
|
-
meta: { 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
|
-
|
|
415
|
+
options.framework = frameworkOptions[framework]
|
|
166
416
|
.title;
|
|
167
417
|
trackEvent('create-app', {
|
|
168
418
|
phase: 'framework',
|
|
169
|
-
meta: { 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'
|
|
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
|
-
|
|
185
|
-
trackEvent('create-app', { phase: 'orm', meta: { 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: -
|
|
443
|
+
{ title: 'No Authentication', value: 'no-auth' },
|
|
194
444
|
],
|
|
195
445
|
initial: 0,
|
|
196
446
|
});
|
|
197
|
-
|
|
447
|
+
options.auth = auth;
|
|
198
448
|
trackEvent('create-app', {
|
|
199
449
|
phase: 'auth',
|
|
200
|
-
meta: { 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
|
|
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}-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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: ${
|
|
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: {
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
throw new Error(`No
|
|
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
|
-
|
|
645
|
+
devConnectionString = connectionUrl.toString();
|
|
374
646
|
}
|
|
375
647
|
const environmentVariables = [];
|
|
376
|
-
if (
|
|
648
|
+
if (options.framework === 'Next.js') {
|
|
377
649
|
let template;
|
|
378
|
-
if (
|
|
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 (
|
|
667
|
+
if (options.packageManager === 'bun') {
|
|
388
668
|
packageManager = '--use-bun';
|
|
389
669
|
}
|
|
390
|
-
else if (
|
|
670
|
+
else if (options.packageManager === 'pnpm') {
|
|
391
671
|
packageManager = '--use-pnpm';
|
|
392
672
|
}
|
|
393
673
|
try {
|
|
394
|
-
execSync(`${getCreateNextAppCommand(
|
|
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 (
|
|
403
|
-
|
|
404
|
-
const
|
|
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:
|
|
687
|
+
value: devConnectionString,
|
|
408
688
|
kind: 'build',
|
|
689
|
+
environment: 'development',
|
|
409
690
|
});
|
|
410
691
|
environmentVariables.push({
|
|
411
692
|
key: 'DATABASE_URL',
|
|
412
|
-
value:
|
|
693
|
+
value: devConnectionString,
|
|
413
694
|
kind: 'runtime',
|
|
695
|
+
environment: 'development',
|
|
414
696
|
});
|
|
415
697
|
environmentVariables.push({
|
|
416
698
|
key: 'AUTH_SECRET',
|
|
417
|
-
value:
|
|
699
|
+
value: devAuthSecret,
|
|
418
700
|
kind: 'build',
|
|
701
|
+
environment: 'development',
|
|
419
702
|
});
|
|
420
703
|
environmentVariables.push({
|
|
421
704
|
key: 'AUTH_SECRET',
|
|
422
|
-
value:
|
|
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
|
-
|
|
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
|
|
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:
|
|
730
|
+
value: devConnectionString,
|
|
435
731
|
kind: 'build',
|
|
732
|
+
environment: 'development',
|
|
436
733
|
});
|
|
437
734
|
environmentVariables.push({
|
|
438
735
|
key: 'DATABASE_URL',
|
|
439
|
-
value:
|
|
736
|
+
value: devConnectionString,
|
|
440
737
|
kind: 'runtime',
|
|
738
|
+
environment: 'development',
|
|
441
739
|
});
|
|
442
|
-
// Write the content to the .env
|
|
740
|
+
// Write the content to the .env file
|
|
443
741
|
writeEnvFile({
|
|
444
|
-
fileName: `${appName}/.env
|
|
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} && ${
|
|
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 (
|
|
748
|
+
if (options.orm === 'drizzle') {
|
|
451
749
|
try {
|
|
452
|
-
execSync(`${
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
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: -
|
|
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
|
-
|
|
815
|
+
options.deployment = deployment;
|
|
495
816
|
trackEvent('create-app', {
|
|
496
817
|
phase: 'deployment',
|
|
497
|
-
meta: { deployment:
|
|
818
|
+
meta: { deployment: options.deployment },
|
|
498
819
|
});
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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 (
|
|
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} && ${
|
|
836
|
+
cd ${appName} && ${options.packageManager} run dev
|
|
574
837
|
|
|
575
838
|
to start the app locally.`));
|
|
576
839
|
}
|