neonctl 1.31.1 → 1.32.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/analytics.js +11 -0
- package/commands/auth.test.js +5 -10
- package/commands/bootstrap/index.js +562 -0
- package/commands/bootstrap/index.test.js +104 -0
- package/commands/bootstrap/is-folder-empty.js +56 -0
- package/commands/bootstrap/validate-pkg.js +15 -0
- package/commands/branches.js +12 -0
- package/commands/branches.test.js +63 -1
- package/commands/connection_string.test.js +1 -1
- package/commands/databases.test.js +1 -1
- package/commands/help.test.js +1 -1
- package/commands/index.js +2 -0
- package/commands/ip_allow.js +14 -1
- package/commands/ip_allow.test.js +18 -2
- package/commands/operations.test.js +1 -1
- package/commands/orgs.test.js +11 -0
- package/commands/projects.js +25 -1
- package/commands/projects.test.js +49 -1
- package/commands/roles.test.js +1 -1
- package/commands/set_context.test.js +1 -1
- package/index.js +5 -0
- package/package.json +15 -13
- package/test_utils/mock_server.js +1 -1
- package/test_utils/oauth_server.js +1 -1
- package/test_utils/test_cli_command.js +1 -1
- package/utils/compute_units.js +28 -0
- package/utils/formats.test.js +1 -1
- package/writer.test.js +1 -1
package/analytics.js
CHANGED
|
@@ -82,3 +82,14 @@ export const sendError = (err, errCode) => {
|
|
|
82
82
|
});
|
|
83
83
|
log.debug('Sent CLI error event: %s', errCode);
|
|
84
84
|
};
|
|
85
|
+
export const trackEvent = (event, properties) => {
|
|
86
|
+
if (!client) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
client.track({
|
|
90
|
+
event,
|
|
91
|
+
userId: userId ?? 'anonymous',
|
|
92
|
+
properties,
|
|
93
|
+
});
|
|
94
|
+
log.debug('Sent CLI event: %s', event);
|
|
95
|
+
};
|
package/commands/auth.test.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import { beforeAll, describe, test,
|
|
2
|
+
import { vi, beforeAll, describe, test, afterAll, expect } from 'vitest';
|
|
3
3
|
import { mkdtempSync, rmSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { startOauthServer } from '../test_utils/oauth_server';
|
|
5
5
|
import { runMockServer } from '../test_utils/mock_server';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return axios.get(url);
|
|
10
|
-
}),
|
|
11
|
-
}));
|
|
12
|
-
// "open" module should be imported after mocking
|
|
13
|
-
const authModule = await import('./auth');
|
|
6
|
+
import { authFlow } from './auth';
|
|
7
|
+
vi.mock('open', () => ({ default: vi.fn((url) => axios.get(url)) }));
|
|
8
|
+
vi.mock('../pkg.ts', () => ({ default: { version: '0.0.0' } }));
|
|
14
9
|
describe('auth', () => {
|
|
15
10
|
let configDir = '';
|
|
16
11
|
let oauthServer;
|
|
@@ -26,7 +21,7 @@ describe('auth', () => {
|
|
|
26
21
|
await new Promise((resolve) => mockServer.close(resolve));
|
|
27
22
|
});
|
|
28
23
|
test('should auth', async () => {
|
|
29
|
-
await
|
|
24
|
+
await authFlow({
|
|
30
25
|
_: ['auth'],
|
|
31
26
|
apiHost: `http://localhost:${mockServer.address().port}`,
|
|
32
27
|
clientId: 'test-client-id',
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { writer } from '../../writer.js';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import { validateNpmName } from './validate-pkg.js';
|
|
4
|
+
import { basename, resolve } from 'path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { isCi } from '../../env.js';
|
|
7
|
+
import { log } from '../../log.js';
|
|
8
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
9
|
+
import { isFolderEmpty } from './is-folder-empty.js';
|
|
10
|
+
import { EndpointType, } from '@neondatabase/api-client';
|
|
11
|
+
import { PROJECT_FIELDS } from '../projects.js';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { trackEvent } from '../../analytics.js';
|
|
14
|
+
export const command = 'create-app';
|
|
15
|
+
export const aliases = ['bootstrap'];
|
|
16
|
+
export const describe = 'Initialize a new Neon project';
|
|
17
|
+
export const builder = (yargs) => yargs.option('context-file', {
|
|
18
|
+
hidden: true,
|
|
19
|
+
});
|
|
20
|
+
const onPromptState = (state) => {
|
|
21
|
+
if (state.aborted) {
|
|
22
|
+
// If we don't re-enable the terminal cursor before exiting
|
|
23
|
+
// the program, the cursor will remain hidden
|
|
24
|
+
process.stdout.write('\x1B[?25h');
|
|
25
|
+
process.stdout.write('\n');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export const handler = async (args) => {
|
|
30
|
+
await bootstrap(args);
|
|
31
|
+
};
|
|
32
|
+
// `getCreateNextAppCommand` returns the command for creating a Next app
|
|
33
|
+
// with `create-next-app` for different package managers.
|
|
34
|
+
function getCreateNextAppCommand(packageManager) {
|
|
35
|
+
const createNextAppVersion = '14.2.4';
|
|
36
|
+
switch (packageManager) {
|
|
37
|
+
case 'npm':
|
|
38
|
+
return `npx create-next-app@${createNextAppVersion}`;
|
|
39
|
+
case 'bun':
|
|
40
|
+
return `bunx create-next-app@${createNextAppVersion}`;
|
|
41
|
+
case 'pnpm':
|
|
42
|
+
return `pnpm create next-app@${createNextAppVersion}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getExecutorProgram(packageManager) {
|
|
46
|
+
switch (packageManager) {
|
|
47
|
+
case 'npm':
|
|
48
|
+
return 'npx';
|
|
49
|
+
case 'pnpm':
|
|
50
|
+
return 'pnpx';
|
|
51
|
+
case 'bun':
|
|
52
|
+
return 'bunx';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const bootstrap = async (props) => {
|
|
56
|
+
const out = writer(props);
|
|
57
|
+
if (isCi()) {
|
|
58
|
+
throw new Error('Cannot run interactive auth in CI');
|
|
59
|
+
}
|
|
60
|
+
const res = await prompts({
|
|
61
|
+
onState: onPromptState,
|
|
62
|
+
type: 'text',
|
|
63
|
+
name: 'path',
|
|
64
|
+
message: 'What is your project named?',
|
|
65
|
+
initial: 'my-app',
|
|
66
|
+
validate: (name) => {
|
|
67
|
+
// We resolve to normalize the path name first, so that if the user enters
|
|
68
|
+
// something like "/hello", we get back just "hello" and not "/hello".
|
|
69
|
+
// This avoids issues where relative paths might lead to different results
|
|
70
|
+
// depending on the current working directory. It also prevents issues
|
|
71
|
+
// related to invalid symlinks.
|
|
72
|
+
const validation = validateNpmName(basename(resolve(name)));
|
|
73
|
+
if (validation.valid) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return 'Invalid project name: ' + validation.problems[0];
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
trackEvent('create-app', { phase: 'start' });
|
|
80
|
+
if (typeof res.path !== 'string') {
|
|
81
|
+
throw new Error('Could not get project path');
|
|
82
|
+
}
|
|
83
|
+
// We resolve to normalize the path name first, so that if the user enters
|
|
84
|
+
// something like "/hello", we get back just "hello" and not "/hello".
|
|
85
|
+
// This avoids issues where relative paths might lead to different results
|
|
86
|
+
// depending on the current working directory. It also prevents issues
|
|
87
|
+
// related to invalid symlinks.
|
|
88
|
+
const projectPath = res.path.trim();
|
|
89
|
+
const resolvedProjectPath = resolve(projectPath);
|
|
90
|
+
const projectName = basename(resolvedProjectPath);
|
|
91
|
+
const validation = validateNpmName(projectName);
|
|
92
|
+
if (!validation.valid) {
|
|
93
|
+
throw new Error(`Could not create a project called ${chalk.red(`"${projectName}"`)} because of npm package naming restrictions:`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Verify the project dir is empty or doesn't exist
|
|
97
|
+
*/
|
|
98
|
+
const root = resolve(resolvedProjectPath);
|
|
99
|
+
const appName = basename(root);
|
|
100
|
+
const folderExists = existsSync(root);
|
|
101
|
+
if (folderExists && !isFolderEmpty(root, appName, out.text)) {
|
|
102
|
+
throw new Error(`Could not create a project called ${chalk.red(`"${projectName}"`)} because the folder ${chalk.red(`"${resolvedProjectPath}"`)} is not empty.`);
|
|
103
|
+
}
|
|
104
|
+
const finalOptions = {
|
|
105
|
+
auth: 'auth.js',
|
|
106
|
+
framework: 'Next.js',
|
|
107
|
+
deployment: 'vercel',
|
|
108
|
+
orm: 'drizzle',
|
|
109
|
+
packageManager: 'npm',
|
|
110
|
+
};
|
|
111
|
+
const packageManagerOptions = [
|
|
112
|
+
{
|
|
113
|
+
title: 'npm',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: 'pnpm',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
title: 'bun',
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
const { packageManagerOption } = await prompts({
|
|
123
|
+
onState: onPromptState,
|
|
124
|
+
type: 'select',
|
|
125
|
+
name: 'packageManagerOption',
|
|
126
|
+
message: `Which package manager would you like to use?`,
|
|
127
|
+
choices: packageManagerOptions,
|
|
128
|
+
initial: 0,
|
|
129
|
+
});
|
|
130
|
+
finalOptions.packageManager = packageManagerOptions[packageManagerOption]
|
|
131
|
+
.title;
|
|
132
|
+
trackEvent('create-app', {
|
|
133
|
+
phase: 'package-manager',
|
|
134
|
+
meta: { packageManager: finalOptions.packageManager },
|
|
135
|
+
});
|
|
136
|
+
const frameworkOptions = [
|
|
137
|
+
{
|
|
138
|
+
title: 'Next.js',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
title: 'SvelteKit',
|
|
142
|
+
disabled: true,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
title: 'Nuxt.js',
|
|
146
|
+
disabled: true,
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
const { framework } = await prompts({
|
|
150
|
+
onState: onPromptState,
|
|
151
|
+
type: 'select',
|
|
152
|
+
name: 'framework',
|
|
153
|
+
message: `What framework would you like to use?`,
|
|
154
|
+
choices: frameworkOptions,
|
|
155
|
+
initial: 0,
|
|
156
|
+
warn: 'Coming soon',
|
|
157
|
+
});
|
|
158
|
+
finalOptions.framework = frameworkOptions[framework]
|
|
159
|
+
.title;
|
|
160
|
+
trackEvent('create-app', {
|
|
161
|
+
phase: 'framework',
|
|
162
|
+
meta: { framework: finalOptions.framework },
|
|
163
|
+
});
|
|
164
|
+
const { orm } = await prompts({
|
|
165
|
+
onState: onPromptState,
|
|
166
|
+
type: 'select',
|
|
167
|
+
name: 'orm',
|
|
168
|
+
message: `What ORM would you like to use?`,
|
|
169
|
+
choices: [
|
|
170
|
+
{ title: 'Drizzle', value: 'drizzle' },
|
|
171
|
+
{ title: 'Prisma', value: 'prisma', disabled: true },
|
|
172
|
+
{ title: 'No ORM', value: -1, disabled: true },
|
|
173
|
+
],
|
|
174
|
+
initial: 0,
|
|
175
|
+
warn: 'Coming soon',
|
|
176
|
+
});
|
|
177
|
+
finalOptions.orm = orm;
|
|
178
|
+
trackEvent('create-app', { phase: 'orm', meta: { orm: finalOptions.orm } });
|
|
179
|
+
const { auth } = await prompts({
|
|
180
|
+
onState: onPromptState,
|
|
181
|
+
type: 'select',
|
|
182
|
+
name: 'auth',
|
|
183
|
+
message: `What authentication framework do you want to use?`,
|
|
184
|
+
choices: [
|
|
185
|
+
{ title: 'Auth.js', value: 'auth.js' },
|
|
186
|
+
{ title: 'No Authentication', value: -1 },
|
|
187
|
+
],
|
|
188
|
+
initial: 0,
|
|
189
|
+
});
|
|
190
|
+
finalOptions.auth = auth;
|
|
191
|
+
trackEvent('create-app', {
|
|
192
|
+
phase: 'auth',
|
|
193
|
+
meta: { auth: finalOptions.auth },
|
|
194
|
+
});
|
|
195
|
+
const PROJECTS_LIST_LIMIT = 100;
|
|
196
|
+
const getList = async (fn) => {
|
|
197
|
+
const result = [];
|
|
198
|
+
let cursor;
|
|
199
|
+
let end = false;
|
|
200
|
+
while (!end) {
|
|
201
|
+
const { data } = await fn({
|
|
202
|
+
limit: PROJECTS_LIST_LIMIT,
|
|
203
|
+
cursor,
|
|
204
|
+
});
|
|
205
|
+
result.push(...data.projects);
|
|
206
|
+
cursor = data.pagination?.cursor;
|
|
207
|
+
log.debug('Got %d projects, with cursor: %s', data.projects.length, cursor);
|
|
208
|
+
if (data.projects.length < PROJECTS_LIST_LIMIT) {
|
|
209
|
+
end = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
};
|
|
214
|
+
const [ownedProjects, sharedProjects] = await Promise.all([
|
|
215
|
+
getList(props.apiClient.listProjects),
|
|
216
|
+
getList(props.apiClient.listSharedProjects),
|
|
217
|
+
]);
|
|
218
|
+
const allProjects = [...ownedProjects, ...sharedProjects];
|
|
219
|
+
const projectChoices = [
|
|
220
|
+
{ title: 'Create a new Neon project', value: -1 },
|
|
221
|
+
...allProjects.map((project) => {
|
|
222
|
+
return {
|
|
223
|
+
title: project.name,
|
|
224
|
+
value: project.id,
|
|
225
|
+
};
|
|
226
|
+
}),
|
|
227
|
+
];
|
|
228
|
+
// `neonProject` will either be -1 or a string (project ID)
|
|
229
|
+
const { neonProject } = await prompts({
|
|
230
|
+
onState: onPromptState,
|
|
231
|
+
type: 'select',
|
|
232
|
+
name: 'neonProject',
|
|
233
|
+
message: `What Neon project would you like to use?`,
|
|
234
|
+
choices: projectChoices,
|
|
235
|
+
initial: 0,
|
|
236
|
+
});
|
|
237
|
+
trackEvent('create-app', { phase: 'neon-project' });
|
|
238
|
+
let projectCreateRequest;
|
|
239
|
+
let project;
|
|
240
|
+
let connectionString;
|
|
241
|
+
if (neonProject === -1) {
|
|
242
|
+
try {
|
|
243
|
+
// Call the API directly. This code is inspired from the `create` code in
|
|
244
|
+
// `projects.ts`.
|
|
245
|
+
projectCreateRequest = {
|
|
246
|
+
name: `${appName}-db`,
|
|
247
|
+
branch: {},
|
|
248
|
+
};
|
|
249
|
+
const { data } = await props.apiClient.createProject({
|
|
250
|
+
project: projectCreateRequest,
|
|
251
|
+
});
|
|
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',
|
|
258
|
+
});
|
|
259
|
+
out.end();
|
|
260
|
+
connectionString = data.connection_uris[0].connection_uri;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
throw new Error(`An error occurred while creating a new Neon project: ${error}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
project = allProjects.find((p) => p.id === neonProject);
|
|
268
|
+
if (!project) {
|
|
269
|
+
throw new Error('An unexpected error occured while selecting the Neon project to use.');
|
|
270
|
+
}
|
|
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);
|
|
303
|
+
let role;
|
|
304
|
+
if (roles.length === 0) {
|
|
305
|
+
throw new Error(`No roles found for the branch: ${branchId}`);
|
|
306
|
+
}
|
|
307
|
+
else if (roles.length === 1) {
|
|
308
|
+
role = roles[0];
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
const roleChoices = roles.map((r) => {
|
|
312
|
+
return {
|
|
313
|
+
title: r.name,
|
|
314
|
+
value: r.name,
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
const { roleName } = await prompts({
|
|
318
|
+
onState: onPromptState,
|
|
319
|
+
type: 'select',
|
|
320
|
+
name: 'roleName',
|
|
321
|
+
message: `What role would you like to use?`,
|
|
322
|
+
choices: roleChoices,
|
|
323
|
+
initial: 0,
|
|
324
|
+
});
|
|
325
|
+
role = roles.find((r) => r.name === roleName);
|
|
326
|
+
if (!role) {
|
|
327
|
+
throw new Error(`No role found for the name: ${roleName}`);
|
|
328
|
+
}
|
|
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];
|
|
338
|
+
}
|
|
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' });
|
|
359
|
+
}
|
|
360
|
+
const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(project.id, endpoint.branch_id, role.name);
|
|
361
|
+
const host = endpoint.host;
|
|
362
|
+
const connectionUrl = new URL(`postgresql://${host}`);
|
|
363
|
+
connectionUrl.pathname = database.name;
|
|
364
|
+
connectionUrl.username = role.name;
|
|
365
|
+
connectionUrl.password = password;
|
|
366
|
+
connectionString = connectionUrl.toString();
|
|
367
|
+
}
|
|
368
|
+
const environmentVariables = [];
|
|
369
|
+
if (finalOptions.framework === 'Next.js') {
|
|
370
|
+
let template;
|
|
371
|
+
if (finalOptions.auth === 'auth.js') {
|
|
372
|
+
template =
|
|
373
|
+
'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-drizzle-authjs';
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
template =
|
|
377
|
+
'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-drizzle';
|
|
378
|
+
}
|
|
379
|
+
let packageManager = '--use-npm';
|
|
380
|
+
if (finalOptions.packageManager === 'bun') {
|
|
381
|
+
packageManager = '--use-bun';
|
|
382
|
+
}
|
|
383
|
+
else if (finalOptions.packageManager === 'pnpm') {
|
|
384
|
+
packageManager = '--use-pnpm';
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
execSync(`${getCreateNextAppCommand(finalOptions.packageManager)} \
|
|
388
|
+
${packageManager} \
|
|
389
|
+
--example ${template} \
|
|
390
|
+
${appName}`, { stdio: 'inherit' });
|
|
391
|
+
}
|
|
392
|
+
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
|
|
404
|
+
environmentVariables.push({
|
|
405
|
+
key: 'DATABASE_URL',
|
|
406
|
+
value: connectionString,
|
|
407
|
+
kind: 'build',
|
|
408
|
+
});
|
|
409
|
+
environmentVariables.push({
|
|
410
|
+
key: 'DATABASE_URL',
|
|
411
|
+
value: connectionString,
|
|
412
|
+
kind: 'runtime',
|
|
413
|
+
});
|
|
414
|
+
environmentVariables.push({
|
|
415
|
+
key: 'AUTH_SECRET',
|
|
416
|
+
value: authSecret,
|
|
417
|
+
kind: 'build',
|
|
418
|
+
});
|
|
419
|
+
environmentVariables.push({
|
|
420
|
+
key: 'AUTH_SECRET',
|
|
421
|
+
value: authSecret,
|
|
422
|
+
kind: 'runtime',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
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
|
+
environmentVariables.push({
|
|
432
|
+
key: 'DATABASE_URL',
|
|
433
|
+
value: connectionString,
|
|
434
|
+
kind: 'build',
|
|
435
|
+
});
|
|
436
|
+
environmentVariables.push({
|
|
437
|
+
key: 'DATABASE_URL',
|
|
438
|
+
value: connectionString,
|
|
439
|
+
kind: 'runtime',
|
|
440
|
+
});
|
|
441
|
+
}
|
|
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`)}`);
|
|
443
|
+
}
|
|
444
|
+
if (finalOptions.orm === 'drizzle') {
|
|
445
|
+
try {
|
|
446
|
+
execSync(`${finalOptions.packageManager} run db:generate -- --name init_db`, {
|
|
447
|
+
cwd: appName,
|
|
448
|
+
stdio: 'inherit',
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
throw new Error(`Generating the database schema failed: ${error}.`);
|
|
453
|
+
}
|
|
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
|
+
}
|
|
465
|
+
}
|
|
466
|
+
out.text(`Database schema generated and applied.\n`);
|
|
467
|
+
}
|
|
468
|
+
const { deployment } = await prompts({
|
|
469
|
+
onState: onPromptState,
|
|
470
|
+
type: 'select',
|
|
471
|
+
name: 'deployment',
|
|
472
|
+
message: `Where would you like to deploy?`,
|
|
473
|
+
choices: [
|
|
474
|
+
{
|
|
475
|
+
title: 'Vercel',
|
|
476
|
+
value: 'vercel',
|
|
477
|
+
description: 'We will install the Vercel CLI globally.',
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
title: 'Cloudflare',
|
|
481
|
+
value: 'cloudflare',
|
|
482
|
+
description: 'We will install the Wrangler CLI globally.',
|
|
483
|
+
},
|
|
484
|
+
{ title: 'Nowhere', value: -1 },
|
|
485
|
+
],
|
|
486
|
+
initial: 0,
|
|
487
|
+
});
|
|
488
|
+
finalOptions.deployment = deployment;
|
|
489
|
+
trackEvent('create-app', {
|
|
490
|
+
phase: 'deployment',
|
|
491
|
+
meta: { deployment: finalOptions.deployment },
|
|
492
|
+
});
|
|
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
|
+
}
|
|
508
|
+
}
|
|
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"
|
|
531
|
+
|
|
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
|
+
}
|
|
560
|
+
}
|
|
561
|
+
trackEvent('create-app', { phase: 'success-finish' });
|
|
562
|
+
};
|