neonctl 2.9.0 → 2.9.1
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/commands/auth.js +10 -4
- package/commands/index.js +0 -2
- package/index.js +0 -2
- package/package.json +2 -3
- package/commands/bootstrap/authjs-secret.js +0 -6
- package/commands/bootstrap/index.js +0 -842
- package/commands/bootstrap/index.test.js +0 -93
- package/commands/bootstrap/is-folder-empty.js +0 -56
- package/commands/bootstrap/validate-pkg.js +0 -15
package/commands/auth.js
CHANGED
|
@@ -25,10 +25,16 @@ export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceA
|
|
|
25
25
|
clientId: clientId,
|
|
26
26
|
});
|
|
27
27
|
const credentialsPath = join(configDir, CREDENTIALS_FILE);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
try {
|
|
29
|
+
await preserveCredentials(credentialsPath, tokenSet, getApiClient({
|
|
30
|
+
apiKey: tokenSet.access_token || '',
|
|
31
|
+
apiHost,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
log.error('Failed to save credentials');
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
32
38
|
log.info('Auth complete');
|
|
33
39
|
return tokenSet.access_token || '';
|
|
34
40
|
};
|
package/commands/index.js
CHANGED
|
@@ -10,7 +10,6 @@ import * as roles from './roles.js';
|
|
|
10
10
|
import * as operations from './operations.js';
|
|
11
11
|
import * as cs from './connection_string.js';
|
|
12
12
|
import * as setContext from './set_context.js';
|
|
13
|
-
import * as bootstrap from './bootstrap/index.js';
|
|
14
13
|
export default [
|
|
15
14
|
auth,
|
|
16
15
|
users,
|
|
@@ -24,5 +23,4 @@ export default [
|
|
|
24
23
|
operations,
|
|
25
24
|
cs,
|
|
26
25
|
setContext,
|
|
27
|
-
bootstrap,
|
|
28
26
|
];
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git+ssh://git@github.com/neondatabase/neonctl.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "2.9.
|
|
8
|
+
"version": "2.9.1",
|
|
9
9
|
"description": "CLI tool for NeonDB Cloud management",
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"author": "NeonDB",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"strip-ansi": "^7.1.0",
|
|
52
52
|
"typescript": "^4.7.4",
|
|
53
53
|
"typescript-eslint": "v8.0.0-alpha.41",
|
|
54
|
-
"vitest": "^1.6.
|
|
54
|
+
"vitest": "^1.6.1"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@neondatabase/api-client": "1.12.0",
|
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
"cli-table": "^0.3.11",
|
|
63
63
|
"crypto-random-string": "^5.0.0",
|
|
64
64
|
"diff": "^5.2.0",
|
|
65
|
-
"inquirer": "^9.2.6",
|
|
66
65
|
"open": "^10.1.0",
|
|
67
66
|
"openid-client": "^5.6.5",
|
|
68
67
|
"prompts": "2.4.2",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
// See https://github.com/nextauthjs/cli/blob/8443988fe7e7f078ead32288dcd1b01b9443f13a/commands/secret.js#L9
|
|
2
|
-
// for reference.
|
|
3
|
-
export function getAuthjsSecret() {
|
|
4
|
-
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
5
|
-
return Buffer.from(bytes.toString(), 'base64').toString('base64');
|
|
6
|
-
}
|
|
@@ -1,842 +0,0 @@
|
|
|
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
|
-
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';
|
|
19
|
-
export const command = 'create-app';
|
|
20
|
-
export const aliases = ['bootstrap'];
|
|
21
|
-
export const describe = 'Initialize a new Neon project';
|
|
22
|
-
export const builder = (yargs) => yargs.option('context-file', {
|
|
23
|
-
hidden: true,
|
|
24
|
-
});
|
|
25
|
-
const onPromptState = (state) => {
|
|
26
|
-
if (state.aborted) {
|
|
27
|
-
// If we don't re-enable the terminal cursor before exiting
|
|
28
|
-
// the program, the cursor will remain hidden
|
|
29
|
-
process.stdout.write('\x1B[?25h');
|
|
30
|
-
process.stdout.write('\n');
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
export const handler = async (args) => {
|
|
35
|
-
await bootstrap(args);
|
|
36
|
-
};
|
|
37
|
-
export const DEFAULT_NEON_ROLE_NAME = 'neondb_owner';
|
|
38
|
-
// `getCreateNextAppCommand` returns the command for creating a Next app
|
|
39
|
-
// with `create-next-app` for different package managers.
|
|
40
|
-
function getCreateNextAppCommand(packageManager) {
|
|
41
|
-
const createNextAppVersion = '14.2.4';
|
|
42
|
-
switch (packageManager) {
|
|
43
|
-
case 'npm':
|
|
44
|
-
return `npx create-next-app@${createNextAppVersion}`;
|
|
45
|
-
case 'bun':
|
|
46
|
-
return `bunx create-next-app@${createNextAppVersion}`;
|
|
47
|
-
case 'pnpm':
|
|
48
|
-
return `pnpm create next-app@${createNextAppVersion}`;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function getExecutorProgram(packageManager) {
|
|
52
|
-
switch (packageManager) {
|
|
53
|
-
case 'npm':
|
|
54
|
-
return 'npx';
|
|
55
|
-
case 'pnpm':
|
|
56
|
-
return 'pnpx';
|
|
57
|
-
case 'bun':
|
|
58
|
-
return 'bunx';
|
|
59
|
-
}
|
|
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({
|
|
139
|
-
projectId: project.id,
|
|
140
|
-
});
|
|
141
|
-
branches = branches.filter((branch) => branch.name !== devBranchName);
|
|
142
|
-
let branchId;
|
|
143
|
-
if (branches.length === 0) {
|
|
144
|
-
throw new Error(`No branches found for the project ${project.name}.`);
|
|
145
|
-
}
|
|
146
|
-
else if (branches.length === 1) {
|
|
147
|
-
branchId = branches[0].id;
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
// Excludes dev branch we created above.
|
|
151
|
-
const branchChoices = branches.map((branch) => {
|
|
152
|
-
return {
|
|
153
|
-
title: branch.name,
|
|
154
|
-
value: branch.id,
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
const { branchIdChoice } = await prompts({
|
|
158
|
-
onState: onPromptState,
|
|
159
|
-
type: 'select',
|
|
160
|
-
name: 'branchIdChoice',
|
|
161
|
-
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)`,
|
|
162
|
-
choices: branchChoices,
|
|
163
|
-
initial: 0,
|
|
164
|
-
});
|
|
165
|
-
branchId = branchIdChoice;
|
|
166
|
-
trackEvent('create-app', { phase: 'neon-branch-deploy' });
|
|
167
|
-
}
|
|
168
|
-
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(project.id, branchId);
|
|
169
|
-
const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
|
|
170
|
-
if (!endpoint) {
|
|
171
|
-
throw new Error(`No read-write endpoint found for the project ${project.name}.`);
|
|
172
|
-
}
|
|
173
|
-
const { data: { roles }, } = await props.apiClient.listProjectBranchRoles(project.id, branchId);
|
|
174
|
-
let role;
|
|
175
|
-
if (roles.length === 0) {
|
|
176
|
-
throw new Error(`No roles found for the branch: ${branchId}`);
|
|
177
|
-
}
|
|
178
|
-
else if (roles.length === 1) {
|
|
179
|
-
role = roles[0];
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
const roleChoices = roles.map((r) => {
|
|
183
|
-
return {
|
|
184
|
-
title: r.name,
|
|
185
|
-
value: r.name,
|
|
186
|
-
};
|
|
187
|
-
});
|
|
188
|
-
const { roleName } = await prompts({
|
|
189
|
-
onState: onPromptState,
|
|
190
|
-
type: 'select',
|
|
191
|
-
name: 'roleName',
|
|
192
|
-
message: `What role would you like to use?`,
|
|
193
|
-
choices: roleChoices,
|
|
194
|
-
initial: 0,
|
|
195
|
-
});
|
|
196
|
-
role = roles.find((r) => r.name === roleName);
|
|
197
|
-
if (!role) {
|
|
198
|
-
throw new Error(`No role found for the name: ${roleName}`);
|
|
199
|
-
}
|
|
200
|
-
trackEvent('create-app', { phase: 'neon-role-deploy' });
|
|
201
|
-
}
|
|
202
|
-
const database = await createDatabase({
|
|
203
|
-
appName,
|
|
204
|
-
apiClient: props.apiClient,
|
|
205
|
-
branchId,
|
|
206
|
-
projectId: project.id,
|
|
207
|
-
});
|
|
208
|
-
writer(props).end(database, {
|
|
209
|
-
fields: DATABASE_FIELDS,
|
|
210
|
-
title: 'Database',
|
|
211
|
-
});
|
|
212
|
-
const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(project.id, endpoint.branch_id, role.name);
|
|
213
|
-
const host = endpoint.host;
|
|
214
|
-
const connectionUrl = new URL(`postgresql://${host}`);
|
|
215
|
-
connectionUrl.pathname = database.name;
|
|
216
|
-
connectionUrl.username = role.name;
|
|
217
|
-
connectionUrl.password = password;
|
|
218
|
-
const deployConnectionString = connectionUrl.toString();
|
|
219
|
-
environmentVariables.push({
|
|
220
|
-
key: 'DATABASE_URL',
|
|
221
|
-
value: deployConnectionString,
|
|
222
|
-
kind: 'build',
|
|
223
|
-
environment: 'production',
|
|
224
|
-
});
|
|
225
|
-
environmentVariables.push({
|
|
226
|
-
key: 'DATABASE_URL',
|
|
227
|
-
value: deployConnectionString,
|
|
228
|
-
kind: 'runtime',
|
|
229
|
-
environment: 'production',
|
|
230
|
-
});
|
|
231
|
-
// If the user doesn't specify Auth.js, there is no schema to be applied
|
|
232
|
-
// in Drizzle.
|
|
233
|
-
if (options.auth === 'auth.js' || options.orm === 'prisma') {
|
|
234
|
-
applyMigrations({
|
|
235
|
-
options,
|
|
236
|
-
appName,
|
|
237
|
-
connectionString: deployConnectionString,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
if (options.deployment === 'vercel') {
|
|
241
|
-
try {
|
|
242
|
-
const envVarsStr = environmentVariables
|
|
243
|
-
.filter((envVar) => envVar.environment === 'production')
|
|
244
|
-
.reduce((acc, envVar) => {
|
|
245
|
-
acc.push(envVar.kind === 'build' ? '--build-env' : '--env');
|
|
246
|
-
acc.push(`${envVar.key}=${envVar.value}`);
|
|
247
|
-
return acc;
|
|
248
|
-
}, [])
|
|
249
|
-
.join(' ');
|
|
250
|
-
execSync(`${getExecutorProgram(options.packageManager)} vercel@34.3.1 deploy ${envVarsStr}`, {
|
|
251
|
-
cwd: appName,
|
|
252
|
-
stdio: 'inherit',
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
throw new Error(`Deploying to Vercel failed: ${String(error)}.`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
else if (options.deployment === 'cloudflare') {
|
|
260
|
-
try {
|
|
261
|
-
execSync('command -v wrangler', {
|
|
262
|
-
cwd: appName,
|
|
263
|
-
stdio: 'ignore',
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
try {
|
|
268
|
-
execSync(`${options.packageManager} install -g @cloudflare/wrangler`, {
|
|
269
|
-
cwd: appName,
|
|
270
|
-
stdio: 'inherit',
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
throw new Error(`Failed to install the Cloudflare CLI: ${String(error)}.`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
const wranglerToml = `name = "${appName}"
|
|
278
|
-
compatibility_flags = [ "nodejs_compat" ]
|
|
279
|
-
pages_build_output_dir = ".vercel/output/static"
|
|
280
|
-
compatibility_date = "2022-11-30"
|
|
281
|
-
|
|
282
|
-
[vars]
|
|
283
|
-
${environmentVariables
|
|
284
|
-
.filter((envVar) => envVar.environment === 'production')
|
|
285
|
-
.map((envVar) => {
|
|
286
|
-
if (envVar.kind === 'runtime') {
|
|
287
|
-
return `${envVar.key} = "${envVar.value}"`;
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
.join('\n')}
|
|
291
|
-
`;
|
|
292
|
-
writeFileSync(`${appName}/wrangler.toml`, wranglerToml, 'utf8');
|
|
293
|
-
try {
|
|
294
|
-
execSync(`${getExecutorProgram(options.packageManager)} @cloudflare/next-on-pages@1.12.1`, {
|
|
295
|
-
cwd: appName,
|
|
296
|
-
stdio: 'inherit',
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
throw new Error(`Failed to build Next.js app with next-on-pages: ${String(error)}.`);
|
|
301
|
-
}
|
|
302
|
-
try {
|
|
303
|
-
execSync(`wrangler pages deploy`, {
|
|
304
|
-
cwd: appName,
|
|
305
|
-
stdio: 'inherit',
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
throw new Error(`Failed to deploy to Cloudflare Pages: ${String(error)}.`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
const bootstrap = async (props) => {
|
|
314
|
-
const out = writer(props);
|
|
315
|
-
if (isCi()) {
|
|
316
|
-
throw new Error('Cannot run interactive auth in CI');
|
|
317
|
-
}
|
|
318
|
-
const res = await prompts({
|
|
319
|
-
onState: onPromptState,
|
|
320
|
-
type: 'text',
|
|
321
|
-
name: 'path',
|
|
322
|
-
message: 'What is your project named?',
|
|
323
|
-
initial: 'my-app',
|
|
324
|
-
max: 10,
|
|
325
|
-
validate: (name) => {
|
|
326
|
-
// We resolve to normalize the path name first, so that if the user enters
|
|
327
|
-
// something like "/hello", we get back just "hello" and not "/hello".
|
|
328
|
-
// This avoids issues where relative paths might lead to different results
|
|
329
|
-
// depending on the current working directory. It also prevents issues
|
|
330
|
-
// related to invalid symlinks.
|
|
331
|
-
const validation = validateNpmName(basename(resolve(name)));
|
|
332
|
-
if (validation.valid) {
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
return 'Invalid project name: ' + validation.problems[0];
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
trackEvent('create-app', { phase: 'start' });
|
|
339
|
-
if (typeof res.path !== 'string') {
|
|
340
|
-
throw new Error('Could not get project path');
|
|
341
|
-
}
|
|
342
|
-
// We resolve to normalize the path name first, so that if the user enters
|
|
343
|
-
// something like "/hello", we get back just "hello" and not "/hello".
|
|
344
|
-
// This avoids issues where relative paths might lead to different results
|
|
345
|
-
// depending on the current working directory. It also prevents issues
|
|
346
|
-
// related to invalid symlinks.
|
|
347
|
-
const projectPath = res.path.trim();
|
|
348
|
-
const resolvedProjectPath = resolve(projectPath);
|
|
349
|
-
const projectName = basename(resolvedProjectPath);
|
|
350
|
-
const validation = validateNpmName(projectName);
|
|
351
|
-
if (!validation.valid) {
|
|
352
|
-
throw new Error(`Could not create a project called ${chalk.red(`"${projectName}"`)} because of npm package naming restrictions:`);
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Verify the project dir is empty or doesn't exist
|
|
356
|
-
*/
|
|
357
|
-
const root = resolve(resolvedProjectPath);
|
|
358
|
-
const appName = basename(root);
|
|
359
|
-
const folderExists = existsSync(root);
|
|
360
|
-
if (folderExists && !isFolderEmpty(root, appName, (data) => out.text(data))) {
|
|
361
|
-
throw new Error(`Could not create a project called ${chalk.red(`"${projectName}"`)} because the folder ${chalk.red(`"${resolvedProjectPath}"`)} is not empty.`);
|
|
362
|
-
}
|
|
363
|
-
const options = {
|
|
364
|
-
auth: 'auth.js',
|
|
365
|
-
framework: 'Next.js',
|
|
366
|
-
deployment: 'vercel',
|
|
367
|
-
orm: 'drizzle',
|
|
368
|
-
packageManager: 'npm',
|
|
369
|
-
};
|
|
370
|
-
const packageManagerOptions = [
|
|
371
|
-
{
|
|
372
|
-
title: 'npm',
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
title: 'pnpm',
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
title: 'bun',
|
|
379
|
-
},
|
|
380
|
-
];
|
|
381
|
-
const { packageManagerOption } = await prompts({
|
|
382
|
-
onState: onPromptState,
|
|
383
|
-
type: 'select',
|
|
384
|
-
name: 'packageManagerOption',
|
|
385
|
-
message: `Which package manager would you like to use?`,
|
|
386
|
-
choices: packageManagerOptions,
|
|
387
|
-
initial: 0,
|
|
388
|
-
});
|
|
389
|
-
options.packageManager = packageManagerOptions[packageManagerOption]
|
|
390
|
-
.title;
|
|
391
|
-
trackEvent('create-app', {
|
|
392
|
-
phase: 'package-manager',
|
|
393
|
-
meta: { packageManager: options.packageManager },
|
|
394
|
-
});
|
|
395
|
-
const frameworkOptions = [
|
|
396
|
-
{
|
|
397
|
-
title: 'Next.js',
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
title: 'SvelteKit',
|
|
401
|
-
disabled: true,
|
|
402
|
-
},
|
|
403
|
-
{
|
|
404
|
-
title: 'Nuxt.js',
|
|
405
|
-
disabled: true,
|
|
406
|
-
},
|
|
407
|
-
];
|
|
408
|
-
const { framework } = await prompts({
|
|
409
|
-
onState: onPromptState,
|
|
410
|
-
type: 'select',
|
|
411
|
-
name: 'framework',
|
|
412
|
-
message: `What framework would you like to use?`,
|
|
413
|
-
choices: frameworkOptions,
|
|
414
|
-
initial: 0,
|
|
415
|
-
warn: 'Coming soon',
|
|
416
|
-
});
|
|
417
|
-
options.framework = frameworkOptions[framework]
|
|
418
|
-
.title;
|
|
419
|
-
trackEvent('create-app', {
|
|
420
|
-
phase: 'framework',
|
|
421
|
-
meta: { framework: options.framework },
|
|
422
|
-
});
|
|
423
|
-
const { orm } = await prompts({
|
|
424
|
-
onState: onPromptState,
|
|
425
|
-
type: 'select',
|
|
426
|
-
name: 'orm',
|
|
427
|
-
message: `What ORM would you like to use?`,
|
|
428
|
-
choices: [
|
|
429
|
-
{ title: 'Drizzle', value: 'drizzle' },
|
|
430
|
-
{ title: 'Prisma', value: 'prisma' },
|
|
431
|
-
{ title: 'No ORM', value: -1, disabled: true },
|
|
432
|
-
],
|
|
433
|
-
initial: 0,
|
|
434
|
-
warn: 'Coming soon',
|
|
435
|
-
});
|
|
436
|
-
options.orm = orm;
|
|
437
|
-
trackEvent('create-app', { phase: 'orm', meta: { orm: options.orm } });
|
|
438
|
-
const { auth } = await prompts({
|
|
439
|
-
onState: onPromptState,
|
|
440
|
-
type: 'select',
|
|
441
|
-
name: 'auth',
|
|
442
|
-
message: `What authentication framework do you want to use?`,
|
|
443
|
-
choices: [
|
|
444
|
-
{ title: 'Auth.js', value: 'auth.js' },
|
|
445
|
-
{ title: 'No Authentication', value: 'no-auth' },
|
|
446
|
-
],
|
|
447
|
-
initial: 0,
|
|
448
|
-
});
|
|
449
|
-
options.auth = auth;
|
|
450
|
-
trackEvent('create-app', {
|
|
451
|
-
phase: 'auth',
|
|
452
|
-
meta: { auth: options.auth },
|
|
453
|
-
});
|
|
454
|
-
const PROJECTS_LIST_LIMIT = 100;
|
|
455
|
-
const getList = async (fn) => {
|
|
456
|
-
const result = [];
|
|
457
|
-
let cursor;
|
|
458
|
-
let end = false;
|
|
459
|
-
while (!end) {
|
|
460
|
-
const { data } = await fn({
|
|
461
|
-
limit: PROJECTS_LIST_LIMIT,
|
|
462
|
-
cursor,
|
|
463
|
-
});
|
|
464
|
-
result.push(...data.projects);
|
|
465
|
-
cursor = data.pagination?.cursor;
|
|
466
|
-
log.debug('Got %d projects, with cursor: %s', data.projects.length, cursor);
|
|
467
|
-
if (data.projects.length < PROJECTS_LIST_LIMIT) {
|
|
468
|
-
end = true;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
return result;
|
|
472
|
-
};
|
|
473
|
-
const [ownedProjects, sharedProjects] = await Promise.all([
|
|
474
|
-
getList(props.apiClient.listProjects),
|
|
475
|
-
getList(props.apiClient.listSharedProjects),
|
|
476
|
-
]);
|
|
477
|
-
const allProjects = [...ownedProjects, ...sharedProjects];
|
|
478
|
-
const projectChoices = [
|
|
479
|
-
{ title: 'Create a new Neon project', value: -1 },
|
|
480
|
-
...allProjects.map((project) => {
|
|
481
|
-
return {
|
|
482
|
-
title: project.name,
|
|
483
|
-
value: project.id,
|
|
484
|
-
};
|
|
485
|
-
}),
|
|
486
|
-
];
|
|
487
|
-
// `neonProject` will either be -1 or a string (project ID)
|
|
488
|
-
const { neonProject } = await prompts({
|
|
489
|
-
onState: onPromptState,
|
|
490
|
-
type: 'select',
|
|
491
|
-
name: 'neonProject',
|
|
492
|
-
message: `What Neon project would you like to use?`,
|
|
493
|
-
choices: projectChoices,
|
|
494
|
-
initial: 0,
|
|
495
|
-
});
|
|
496
|
-
trackEvent('create-app', { phase: 'neon-project' });
|
|
497
|
-
let projectCreateRequest;
|
|
498
|
-
let project;
|
|
499
|
-
let devConnectionString;
|
|
500
|
-
const devBranchName = `dev/${cryptoRandomString({
|
|
501
|
-
length: 10,
|
|
502
|
-
type: 'url-safe',
|
|
503
|
-
})}`;
|
|
504
|
-
if (neonProject === -1) {
|
|
505
|
-
try {
|
|
506
|
-
// Call the API directly. This code is inspired from the `create` code in
|
|
507
|
-
// `projects.ts`.
|
|
508
|
-
projectCreateRequest = {
|
|
509
|
-
name: `${appName}-project`,
|
|
510
|
-
branch: {},
|
|
511
|
-
};
|
|
512
|
-
const { data: createProjectData } = await retryOnLock(() => props.apiClient.createProject({
|
|
513
|
-
project: projectCreateRequest,
|
|
514
|
-
}));
|
|
515
|
-
project = createProjectData.project;
|
|
516
|
-
writer(props).end(project, {
|
|
517
|
-
fields: PROJECT_FIELDS,
|
|
518
|
-
title: 'Project',
|
|
519
|
-
});
|
|
520
|
-
const branch = await createBranch({
|
|
521
|
-
appName,
|
|
522
|
-
apiClient: props.apiClient,
|
|
523
|
-
projectId: project.id,
|
|
524
|
-
name: devBranchName,
|
|
525
|
-
});
|
|
526
|
-
const database = await createDatabase({
|
|
527
|
-
appName,
|
|
528
|
-
apiClient: props.apiClient,
|
|
529
|
-
branchId: branch.id,
|
|
530
|
-
projectId: project.id,
|
|
531
|
-
});
|
|
532
|
-
writer(props).end(branch, {
|
|
533
|
-
fields: BRANCH_FIELDS,
|
|
534
|
-
title: 'Branch',
|
|
535
|
-
});
|
|
536
|
-
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(project.id, branch.id);
|
|
537
|
-
const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
|
|
538
|
-
if (!endpoint) {
|
|
539
|
-
throw new Error(`No read-write endpoint found for the project ${project.name}.`);
|
|
540
|
-
}
|
|
541
|
-
const { data: { roles }, } = await props.apiClient.listProjectBranchRoles(project.id, branch.id);
|
|
542
|
-
let role;
|
|
543
|
-
if (roles.length === 0) {
|
|
544
|
-
throw new Error(`No roles found for the branch: ${branch.id}`);
|
|
545
|
-
}
|
|
546
|
-
else if (roles.length === 1) {
|
|
547
|
-
role = roles[0];
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
const roleChoices = roles.map((r) => {
|
|
551
|
-
return {
|
|
552
|
-
title: r.name,
|
|
553
|
-
value: r.name,
|
|
554
|
-
};
|
|
555
|
-
});
|
|
556
|
-
const { roleName } = await prompts({
|
|
557
|
-
onState: onPromptState,
|
|
558
|
-
type: 'select',
|
|
559
|
-
name: 'roleName',
|
|
560
|
-
message: `What role would you like to use?`,
|
|
561
|
-
choices: roleChoices,
|
|
562
|
-
initial: 0,
|
|
563
|
-
});
|
|
564
|
-
role = roles.find((r) => r.name === roleName);
|
|
565
|
-
if (!role) {
|
|
566
|
-
throw new Error(`No role found for the name: ${roleName}`);
|
|
567
|
-
}
|
|
568
|
-
trackEvent('create-app', { phase: 'neon-role-dev' });
|
|
569
|
-
}
|
|
570
|
-
const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(project.id, endpoint.branch_id, role.name);
|
|
571
|
-
const host = endpoint.host;
|
|
572
|
-
const connectionUrl = new URL(`postgresql://${host}`);
|
|
573
|
-
connectionUrl.pathname = database.name;
|
|
574
|
-
connectionUrl.username = role.name;
|
|
575
|
-
connectionUrl.password = password;
|
|
576
|
-
devConnectionString = connectionUrl.toString();
|
|
577
|
-
}
|
|
578
|
-
catch (error) {
|
|
579
|
-
throw new Error(`An error occurred while creating a new Neon project: ${String(error)}`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
project = allProjects.find((p) => p.id === neonProject);
|
|
584
|
-
if (!project) {
|
|
585
|
-
throw new Error('An unexpected error occured while selecting the Neon project to use.');
|
|
586
|
-
}
|
|
587
|
-
const branch = await createBranch({
|
|
588
|
-
appName,
|
|
589
|
-
apiClient: props.apiClient,
|
|
590
|
-
projectId: project.id,
|
|
591
|
-
name: devBranchName,
|
|
592
|
-
});
|
|
593
|
-
writer(props).end(branch, {
|
|
594
|
-
fields: BRANCH_FIELDS,
|
|
595
|
-
title: 'Branch',
|
|
596
|
-
});
|
|
597
|
-
const database = await createDatabase({
|
|
598
|
-
appName,
|
|
599
|
-
apiClient: props.apiClient,
|
|
600
|
-
branchId: branch.id,
|
|
601
|
-
projectId: project.id,
|
|
602
|
-
});
|
|
603
|
-
writer(props).end(database, {
|
|
604
|
-
fields: DATABASE_FIELDS,
|
|
605
|
-
title: 'Database',
|
|
606
|
-
});
|
|
607
|
-
const { data: { roles }, } = await props.apiClient.listProjectBranchRoles(project.id, branch.id);
|
|
608
|
-
let role;
|
|
609
|
-
if (roles.length === 0) {
|
|
610
|
-
throw new Error(`No roles found for the branch: ${branch.id}`);
|
|
611
|
-
}
|
|
612
|
-
else if (roles.length === 1) {
|
|
613
|
-
role = roles[0];
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
const roleChoices = roles.map((r) => {
|
|
617
|
-
return {
|
|
618
|
-
title: r.name,
|
|
619
|
-
value: r.name,
|
|
620
|
-
};
|
|
621
|
-
});
|
|
622
|
-
const { roleName } = await prompts({
|
|
623
|
-
onState: onPromptState,
|
|
624
|
-
type: 'select',
|
|
625
|
-
name: 'roleName',
|
|
626
|
-
message: `What role would you like to use?`,
|
|
627
|
-
choices: roleChoices,
|
|
628
|
-
initial: 0,
|
|
629
|
-
});
|
|
630
|
-
role = roles.find((r) => r.name === roleName);
|
|
631
|
-
if (!role) {
|
|
632
|
-
throw new Error(`No role found for the name: ${roleName}`);
|
|
633
|
-
}
|
|
634
|
-
trackEvent('create-app', { phase: 'neon-role-dev' });
|
|
635
|
-
}
|
|
636
|
-
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(project.id, branch.id);
|
|
637
|
-
const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
|
|
638
|
-
if (!endpoint) {
|
|
639
|
-
throw new Error(`No read-write endpoint found for the project ${project.name}.`);
|
|
640
|
-
}
|
|
641
|
-
const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(project.id, endpoint.branch_id, role.name);
|
|
642
|
-
const host = endpoint.host;
|
|
643
|
-
const connectionUrl = new URL(`postgresql://${host}`);
|
|
644
|
-
connectionUrl.pathname = database.name;
|
|
645
|
-
connectionUrl.username = role.name;
|
|
646
|
-
connectionUrl.password = password;
|
|
647
|
-
devConnectionString = connectionUrl.toString();
|
|
648
|
-
}
|
|
649
|
-
const environmentVariables = [];
|
|
650
|
-
if (options.framework === 'Next.js') {
|
|
651
|
-
let template;
|
|
652
|
-
if (options.auth === 'auth.js' && options.orm === 'drizzle') {
|
|
653
|
-
template =
|
|
654
|
-
'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-drizzle-authjs';
|
|
655
|
-
}
|
|
656
|
-
else if (options.auth === 'no-auth' && options.orm === 'drizzle') {
|
|
657
|
-
template =
|
|
658
|
-
'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-drizzle';
|
|
659
|
-
}
|
|
660
|
-
else if (options.auth === 'auth.js' && options.orm === 'prisma') {
|
|
661
|
-
template =
|
|
662
|
-
'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-prisma-authjs';
|
|
663
|
-
}
|
|
664
|
-
else if (options.auth === 'no-auth' && options.orm === 'prisma') {
|
|
665
|
-
template =
|
|
666
|
-
'https://github.com/neondatabase/neonctl-create-app-templates/tree/main/next-prisma';
|
|
667
|
-
}
|
|
668
|
-
let packageManager = '--use-npm';
|
|
669
|
-
if (options.packageManager === 'bun') {
|
|
670
|
-
packageManager = '--use-bun';
|
|
671
|
-
}
|
|
672
|
-
else if (options.packageManager === 'pnpm') {
|
|
673
|
-
packageManager = '--use-pnpm';
|
|
674
|
-
}
|
|
675
|
-
try {
|
|
676
|
-
execSync(`${getCreateNextAppCommand(options.packageManager)} \
|
|
677
|
-
${packageManager} \
|
|
678
|
-
--example ${template} \
|
|
679
|
-
${appName}`, { stdio: 'inherit' });
|
|
680
|
-
}
|
|
681
|
-
catch (error) {
|
|
682
|
-
throw new Error(`Creating a Next.js project failed: ${String(error)}.`);
|
|
683
|
-
}
|
|
684
|
-
if (options.auth === 'auth.js') {
|
|
685
|
-
const devAuthSecret = getAuthjsSecret();
|
|
686
|
-
const prodAuthSecret = getAuthjsSecret();
|
|
687
|
-
environmentVariables.push({
|
|
688
|
-
key: 'DATABASE_URL',
|
|
689
|
-
value: devConnectionString,
|
|
690
|
-
kind: 'build',
|
|
691
|
-
environment: 'development',
|
|
692
|
-
});
|
|
693
|
-
environmentVariables.push({
|
|
694
|
-
key: 'DATABASE_URL',
|
|
695
|
-
value: devConnectionString,
|
|
696
|
-
kind: 'runtime',
|
|
697
|
-
environment: 'development',
|
|
698
|
-
});
|
|
699
|
-
environmentVariables.push({
|
|
700
|
-
key: 'AUTH_SECRET',
|
|
701
|
-
value: devAuthSecret,
|
|
702
|
-
kind: 'build',
|
|
703
|
-
environment: 'development',
|
|
704
|
-
});
|
|
705
|
-
environmentVariables.push({
|
|
706
|
-
key: 'AUTH_SECRET',
|
|
707
|
-
value: devAuthSecret,
|
|
708
|
-
kind: 'runtime',
|
|
709
|
-
environment: 'development',
|
|
710
|
-
});
|
|
711
|
-
environmentVariables.push({
|
|
712
|
-
key: 'AUTH_SECRET',
|
|
713
|
-
value: prodAuthSecret,
|
|
714
|
-
kind: 'build',
|
|
715
|
-
environment: 'production',
|
|
716
|
-
});
|
|
717
|
-
environmentVariables.push({
|
|
718
|
-
key: 'AUTH_SECRET',
|
|
719
|
-
value: prodAuthSecret,
|
|
720
|
-
kind: 'runtime',
|
|
721
|
-
environment: 'production',
|
|
722
|
-
});
|
|
723
|
-
// Write the content to the .env file
|
|
724
|
-
writeEnvFile({
|
|
725
|
-
fileName: `${appName}/.env`,
|
|
726
|
-
secrets: environmentVariables.filter((e) => e.kind === 'runtime' && e.environment === 'development'),
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
else {
|
|
730
|
-
environmentVariables.push({
|
|
731
|
-
key: 'DATABASE_URL',
|
|
732
|
-
value: devConnectionString,
|
|
733
|
-
kind: 'build',
|
|
734
|
-
environment: 'development',
|
|
735
|
-
});
|
|
736
|
-
environmentVariables.push({
|
|
737
|
-
key: 'DATABASE_URL',
|
|
738
|
-
value: devConnectionString,
|
|
739
|
-
kind: 'runtime',
|
|
740
|
-
environment: 'development',
|
|
741
|
-
});
|
|
742
|
-
// Write the content to the .env file
|
|
743
|
-
writeEnvFile({
|
|
744
|
-
fileName: `${appName}/.env`,
|
|
745
|
-
secrets: environmentVariables.filter((e) => e.kind === 'runtime' && e.environment === 'development'),
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
out.text(`Created a Next.js project in ${chalk.blue(appName)}.\n\nYou can now run ${chalk.blue(`cd ${appName} && ${options.packageManager} run dev`)}`);
|
|
749
|
-
}
|
|
750
|
-
if (options.orm === 'drizzle') {
|
|
751
|
-
try {
|
|
752
|
-
execSync(`${options.packageManager} run db:generate -- --name init_db`, {
|
|
753
|
-
cwd: appName,
|
|
754
|
-
stdio: 'inherit',
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
catch (error) {
|
|
758
|
-
throw new Error(`Generating the database schema failed: ${String(error)}.`);
|
|
759
|
-
}
|
|
760
|
-
// If the user doesn't specify Auth.js, there is no schema to be applied
|
|
761
|
-
// with Drizzle.
|
|
762
|
-
if (options.auth === 'auth.js') {
|
|
763
|
-
applyMigrations({
|
|
764
|
-
options,
|
|
765
|
-
appName,
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
out.text(`Database schema generated and applied.\n`);
|
|
769
|
-
}
|
|
770
|
-
else if (options.orm === 'prisma') {
|
|
771
|
-
try {
|
|
772
|
-
execSync(`${options.packageManager} run db:generate`, {
|
|
773
|
-
cwd: appName,
|
|
774
|
-
stdio: 'inherit',
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
throw new Error(`Generating the Prisma client failed: ${String(error)}.`);
|
|
779
|
-
}
|
|
780
|
-
try {
|
|
781
|
-
execSync(`${options.packageManager} run db:migrate -- --name init --skip-generate`, {
|
|
782
|
-
cwd: appName,
|
|
783
|
-
stdio: 'inherit',
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
catch (error) {
|
|
787
|
-
throw new Error(`Applying the schema failed: ${String(error)}.`);
|
|
788
|
-
}
|
|
789
|
-
out.text(`Database schema generated and applied.\n`);
|
|
790
|
-
}
|
|
791
|
-
const { deployment } = await prompts({
|
|
792
|
-
onState: onPromptState,
|
|
793
|
-
type: 'select',
|
|
794
|
-
name: 'deployment',
|
|
795
|
-
message: `Where would you like to deploy?`,
|
|
796
|
-
choices: [
|
|
797
|
-
{
|
|
798
|
-
title: 'Vercel',
|
|
799
|
-
value: 'vercel',
|
|
800
|
-
description: 'We will install the Vercel CLI globally.',
|
|
801
|
-
},
|
|
802
|
-
{
|
|
803
|
-
title: 'Cloudflare',
|
|
804
|
-
value: 'cloudflare',
|
|
805
|
-
description: 'We will install the Wrangler CLI globally.',
|
|
806
|
-
// Making Prisma work on Cloudflare is a bit tricky.
|
|
807
|
-
disabled: options.orm === 'prisma',
|
|
808
|
-
},
|
|
809
|
-
{ title: 'Skip this step', value: 'no-deployment' },
|
|
810
|
-
],
|
|
811
|
-
// Making Prisma work on Cloudflare is a bit tricky.
|
|
812
|
-
warn: options.orm === 'prisma'
|
|
813
|
-
? 'We do not yet support Cloudflare deployments with Prisma.'
|
|
814
|
-
: undefined,
|
|
815
|
-
initial: 0,
|
|
816
|
-
});
|
|
817
|
-
options.deployment = deployment;
|
|
818
|
-
trackEvent('create-app', {
|
|
819
|
-
phase: 'deployment',
|
|
820
|
-
meta: { deployment: options.deployment },
|
|
821
|
-
});
|
|
822
|
-
if (options.deployment !== 'no-deployment') {
|
|
823
|
-
await deployApp({
|
|
824
|
-
options,
|
|
825
|
-
props,
|
|
826
|
-
devBranchName,
|
|
827
|
-
project,
|
|
828
|
-
appName,
|
|
829
|
-
environmentVariables,
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
trackEvent('create-app', { phase: 'success-finish' });
|
|
833
|
-
if (options.framework === 'Next.js') {
|
|
834
|
-
log.info(chalk.green(`
|
|
835
|
-
|
|
836
|
-
You can now run:
|
|
837
|
-
|
|
838
|
-
cd ${appName} && ${options.packageManager} run dev
|
|
839
|
-
|
|
840
|
-
to start the app locally.`));
|
|
841
|
-
}
|
|
842
|
-
};
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { expect, describe } from 'vitest';
|
|
2
|
-
import { fork } from 'node:child_process';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { log } from '../../log';
|
|
5
|
-
import { test } from '../../test_utils/fixtures';
|
|
6
|
-
describe('bootstrap/create-app', () => {
|
|
7
|
-
// We create an app without a schema and without deploying it, as
|
|
8
|
-
// a very simple check that the CLI works. Eventually, we need
|
|
9
|
-
// to have a much more complete test suite that actually verifies
|
|
10
|
-
// that launching all different app combinations works.
|
|
11
|
-
test.skip('very simple CLI interaction test', async ({ runMockServer }) => {
|
|
12
|
-
const server = await runMockServer('main');
|
|
13
|
-
// Most of this forking code is copied from `test_cli_command.ts`.
|
|
14
|
-
const cp = fork(join(process.cwd(), './dist/index.js'), [
|
|
15
|
-
'--api-host',
|
|
16
|
-
`http://localhost:${server.address().port}`,
|
|
17
|
-
'--output',
|
|
18
|
-
'yaml',
|
|
19
|
-
'--api-key',
|
|
20
|
-
'test-key',
|
|
21
|
-
'--no-analytics',
|
|
22
|
-
'create-app',
|
|
23
|
-
], {
|
|
24
|
-
stdio: 'pipe',
|
|
25
|
-
env: {
|
|
26
|
-
PATH: `mocks/bin:${process.env.PATH}`,
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
process.on('SIGINT', () => {
|
|
30
|
-
cp.kill();
|
|
31
|
-
});
|
|
32
|
-
let neonProjectCreated = false;
|
|
33
|
-
return new Promise((resolve, reject) => {
|
|
34
|
-
cp.stdout?.on('data', (data) => {
|
|
35
|
-
const stdout = data.toString();
|
|
36
|
-
log.info(stdout);
|
|
37
|
-
// For some unknown, weird reason, when we send TAB clicks (\t),
|
|
38
|
-
// they only affect the next question. So, we send TAB below
|
|
39
|
-
// in order to affect the answer to the following prompt, not the
|
|
40
|
-
// current one.
|
|
41
|
-
if (stdout.includes('What is your project named')) {
|
|
42
|
-
cp.stdin?.write('my-app\n');
|
|
43
|
-
}
|
|
44
|
-
else if (stdout.includes('Which package manager would you like to use')) {
|
|
45
|
-
cp.stdin?.write('\n');
|
|
46
|
-
}
|
|
47
|
-
else if (stdout.includes('What framework would you like to use')) {
|
|
48
|
-
cp.stdin?.write('\n');
|
|
49
|
-
}
|
|
50
|
-
else if (stdout.includes('What ORM would you like to use')) {
|
|
51
|
-
cp.stdin?.write('\t'); // change auth.js
|
|
52
|
-
cp.stdin?.write('\n');
|
|
53
|
-
}
|
|
54
|
-
else if (stdout.includes('What authentication framework do you want to use')) {
|
|
55
|
-
cp.stdin?.write('\n');
|
|
56
|
-
}
|
|
57
|
-
else if (stdout.includes('What Neon project would you like to use')) {
|
|
58
|
-
neonProjectCreated = true;
|
|
59
|
-
cp.stdin?.write('\t'); // change deployment
|
|
60
|
-
cp.stdin?.write('\t');
|
|
61
|
-
cp.stdin?.write('\n');
|
|
62
|
-
}
|
|
63
|
-
else if (stdout.includes('Where would you like to deploy')) {
|
|
64
|
-
cp.stdin?.write('\n');
|
|
65
|
-
cp.stdin?.write('\n');
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
cp.stderr?.on('data', (data) => {
|
|
69
|
-
log.error(data.toString());
|
|
70
|
-
});
|
|
71
|
-
cp.on('error', (err) => {
|
|
72
|
-
throw err;
|
|
73
|
-
});
|
|
74
|
-
cp.on('close', (code) => {
|
|
75
|
-
// If we got to the point that a Neon project was successfully
|
|
76
|
-
// created, we consider the test run to be a success. We can't
|
|
77
|
-
// currently check that the template is properly generated, and that
|
|
78
|
-
// the project runs. We'll have to do that with containerization in
|
|
79
|
-
// the future, most likely.
|
|
80
|
-
if (neonProjectCreated) {
|
|
81
|
-
resolve();
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
expect(code).toBe(0);
|
|
85
|
-
resolve();
|
|
86
|
-
}
|
|
87
|
-
catch (err) {
|
|
88
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
}, 1000 * 60 * 5);
|
|
93
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// Code copied from `create-next-app`.
|
|
2
|
-
import { lstatSync, readdirSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
// `isFolderEmpty` checks if a folder is empty and ready to onboard a Next.js package into it.
|
|
6
|
-
// It will actually log to stdout as part of its execution.
|
|
7
|
-
export function isFolderEmpty(root, name, writeStdout) {
|
|
8
|
-
const validFiles = new Set([
|
|
9
|
-
'.DS_Store',
|
|
10
|
-
'.git',
|
|
11
|
-
'.gitattributes',
|
|
12
|
-
'.gitignore',
|
|
13
|
-
'.gitlab-ci.yml',
|
|
14
|
-
'.hg',
|
|
15
|
-
'.hgcheck',
|
|
16
|
-
'.hgignore',
|
|
17
|
-
'.idea',
|
|
18
|
-
'.npmignore',
|
|
19
|
-
'.travis.yml',
|
|
20
|
-
'LICENSE',
|
|
21
|
-
'Thumbs.db',
|
|
22
|
-
'docs',
|
|
23
|
-
'mkdocs.yml',
|
|
24
|
-
'npm-debug.log',
|
|
25
|
-
'yarn-debug.log',
|
|
26
|
-
'yarn-error.log',
|
|
27
|
-
'yarnrc.yml',
|
|
28
|
-
'.yarn',
|
|
29
|
-
]);
|
|
30
|
-
const conflicts = readdirSync(root).filter((file) => !validFiles.has(file) &&
|
|
31
|
-
// Support IntelliJ IDEA-based editors
|
|
32
|
-
!file.endsWith('.iml'));
|
|
33
|
-
if (conflicts.length > 0) {
|
|
34
|
-
writeStdout(`The directory ${chalk.green(name)} contains files that could conflict:\n`);
|
|
35
|
-
writeStdout('');
|
|
36
|
-
for (const file of conflicts) {
|
|
37
|
-
try {
|
|
38
|
-
const stats = lstatSync(join(root, file));
|
|
39
|
-
if (stats.isDirectory()) {
|
|
40
|
-
writeStdout(` ${chalk.blue(file)}/\n`);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
writeStdout(` ${file}\n`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
writeStdout(` ${file}\n`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
writeStdout('\n');
|
|
51
|
-
writeStdout('Either try using a new directory name, or remove the files listed above.\n');
|
|
52
|
-
writeStdout('\n');
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Code copied from `create-next-app`.
|
|
2
|
-
import validateProjectName from 'validate-npm-package-name';
|
|
3
|
-
export function validateNpmName(name) {
|
|
4
|
-
const nameValidation = validateProjectName(name);
|
|
5
|
-
if (nameValidation.validForNewPackages) {
|
|
6
|
-
return { valid: true };
|
|
7
|
-
}
|
|
8
|
-
return {
|
|
9
|
-
valid: false,
|
|
10
|
-
problems: [
|
|
11
|
-
...(nameValidation.errors || []),
|
|
12
|
-
...(nameValidation.warnings || []),
|
|
13
|
-
],
|
|
14
|
-
};
|
|
15
|
-
}
|