@akash-chowdhury-24/deployhub 1.0.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/LICENSE +21 -0
- package/README.md +176 -0
- package/install.ps1 +55 -0
- package/install.sh +99 -0
- package/package.json +86 -0
- package/src/adapters/dotnet.adapter.js +41 -0
- package/src/adapters/go.adapter.js +40 -0
- package/src/adapters/index.js +48 -0
- package/src/adapters/java.adapter.js +46 -0
- package/src/adapters/node.adapter.js +77 -0
- package/src/adapters/php.adapter.js +43 -0
- package/src/adapters/python.adapter.js +54 -0
- package/src/adapters/rails.adapter.js +66 -0
- package/src/artifact/engine.js +473 -0
- package/src/cli/index.js +45 -0
- package/src/commands/artifact.js +88 -0
- package/src/commands/build.js +44 -0
- package/src/commands/clean.js +50 -0
- package/src/commands/deploy.js +82 -0
- package/src/commands/doctor.js +630 -0
- package/src/commands/init.js +795 -0
- package/src/commands/logs.js +33 -0
- package/src/commands/rollback.js +50 -0
- package/src/commands/storage.js +116 -0
- package/src/commands/update.js +63 -0
- package/src/commands/verify.js +55 -0
- package/src/core/config.js +168 -0
- package/src/core/pipeline.js +77 -0
- package/src/core/stages.js +210 -0
- package/src/deployment/index.js +156 -0
- package/src/deployment/providers/azure-vm.js +7 -0
- package/src/deployment/providers/docker.js +30 -0
- package/src/deployment/providers/ec2.js +7 -0
- package/src/deployment/providers/gcp-vm.js +7 -0
- package/src/deployment/providers/kubernetes.js +30 -0
- package/src/deployment/providers/platforms/_shared.js +167 -0
- package/src/deployment/providers/platforms/aws-amplify.js +164 -0
- package/src/deployment/providers/platforms/azure-static-web-apps.js +68 -0
- package/src/deployment/providers/platforms/cloudflare-pages.js +103 -0
- package/src/deployment/providers/platforms/firebase-app-hosting.js +95 -0
- package/src/deployment/providers/platforms/firebase-hosting.js +99 -0
- package/src/deployment/providers/platforms/index.js +44 -0
- package/src/deployment/providers/platforms/netlify.js +102 -0
- package/src/deployment/providers/platforms/vercel.js +92 -0
- package/src/deployment/providers/ssh.js +365 -0
- package/src/detectors/angular.js +23 -0
- package/src/detectors/backend.detector.js +304 -0
- package/src/detectors/dotnet.js +18 -0
- package/src/detectors/frontend.detector.js +219 -0
- package/src/detectors/go.js +20 -0
- package/src/detectors/index.js +78 -0
- package/src/detectors/java.js +24 -0
- package/src/detectors/nextjs.js +23 -0
- package/src/detectors/node.js +28 -0
- package/src/detectors/php.js +17 -0
- package/src/detectors/python.js +22 -0
- package/src/detectors/react.js +29 -0
- package/src/detectors/vue.js +23 -0
- package/src/logger/index.js +44 -0
- package/src/notifications/email.js +53 -0
- package/src/notifications/index.js +34 -0
- package/src/notifications/slack.js +18 -0
- package/src/notifications/webhook.js +21 -0
- package/src/rollback/engine.js +102 -0
- package/src/storage/index.js +109 -0
- package/src/storage/providers/aws.js +96 -0
- package/src/storage/providers/azure.js +45 -0
- package/src/storage/providers/dropbox.js +49 -0
- package/src/storage/providers/ftp.js +69 -0
- package/src/storage/providers/gcp.js +45 -0
- package/src/storage/providers/gdrive.js +80 -0
- package/src/storage/providers/local.js +61 -0
- package/src/utils/author.js +141 -0
- package/src/utils/checksums.js +53 -0
- package/src/utils/firebase-config-generator.js +35 -0
- package/src/utils/github-actions.js +389 -0
- package/src/utils/init-platform.js +229 -0
- package/src/utils/nginx.js +34 -0
- package/src/utils/platform-env.js +132 -0
- package/src/utils/version.js +31 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PLATFORM_ENV_MAP, PLATFORM_CLI_MAP } from './platform-env.js';
|
|
4
|
+
import { getWorkflowHeaderComment } from './author.js';
|
|
5
|
+
|
|
6
|
+
/** @typedef {'aws'|'azure'|'gcp'|'gdrive'|'dropbox'|'local'|'ftp'|'ssh'} ProviderEnvKey */
|
|
7
|
+
|
|
8
|
+
const PROVIDER_ENV_MAP = {
|
|
9
|
+
aws: [
|
|
10
|
+
'AWS_ACCESS_KEY_ID',
|
|
11
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
12
|
+
'AWS_BUCKET',
|
|
13
|
+
'AWS_REGION',
|
|
14
|
+
],
|
|
15
|
+
azure: ['AZURE_CONNECTION_STRING', 'AZURE_CONTAINER'],
|
|
16
|
+
gcp: ['GCP_PROJECT_ID', 'GCP_KEY_FILE', 'GCP_BUCKET'],
|
|
17
|
+
gdrive: [
|
|
18
|
+
'GDRIVE_CLIENT_ID',
|
|
19
|
+
'GDRIVE_CLIENT_SECRET',
|
|
20
|
+
'GDRIVE_REFRESH_TOKEN',
|
|
21
|
+
'GDRIVE_FOLDER_ID',
|
|
22
|
+
],
|
|
23
|
+
dropbox: ['DROPBOX_ACCESS_TOKEN'],
|
|
24
|
+
local: [],
|
|
25
|
+
ftp: ['FTP_HOST', 'FTP_USER', 'FTP_PASSWORD'],
|
|
26
|
+
ssh: ['SSH_HOST', 'SSH_USER', 'SSH_KEY'],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const BACKEND_SSH_ENV_VARS = [
|
|
30
|
+
'SSH_DEPLOY_PATH',
|
|
31
|
+
'SSH_APP_NAME',
|
|
32
|
+
'SSH_PORT',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} cliSource
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function getCliInstallSpec(cliSource) {
|
|
40
|
+
if (!cliSource || cliSource === 'npm:deployhub') {
|
|
41
|
+
return 'deployhub@latest';
|
|
42
|
+
}
|
|
43
|
+
if (cliSource.startsWith('github:')) {
|
|
44
|
+
return cliSource;
|
|
45
|
+
}
|
|
46
|
+
if (cliSource.startsWith('file:')) {
|
|
47
|
+
return cliSource;
|
|
48
|
+
}
|
|
49
|
+
return cliSource;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
54
|
+
* @returns {string[]}
|
|
55
|
+
*/
|
|
56
|
+
function getBackendSetupSteps(config) {
|
|
57
|
+
if (!config) return [];
|
|
58
|
+
|
|
59
|
+
const projectType = config.projectType || 'frontend';
|
|
60
|
+
if (projectType === 'frontend') return [];
|
|
61
|
+
|
|
62
|
+
/** @type {string[]} */
|
|
63
|
+
const steps = [];
|
|
64
|
+
|
|
65
|
+
if (projectType === 'backend' || projectType === 'both') {
|
|
66
|
+
const framework = config.backend?.framework || config.framework || 'express';
|
|
67
|
+
if (['express', 'nestjs', 'fastify', 'koa', 'nextjs'].includes(framework)) {
|
|
68
|
+
steps.push(` - uses: actions/setup-node@v4
|
|
69
|
+
with:
|
|
70
|
+
node-version: '20'`);
|
|
71
|
+
}
|
|
72
|
+
if (['fastapi', 'django', 'flask'].includes(framework)) {
|
|
73
|
+
steps.push(` - uses: actions/setup-python@v5
|
|
74
|
+
with:
|
|
75
|
+
python-version: '3.11'`);
|
|
76
|
+
}
|
|
77
|
+
if (['spring', 'java'].includes(framework)) {
|
|
78
|
+
steps.push(` - uses: actions/setup-java@v4
|
|
79
|
+
with:
|
|
80
|
+
distribution: 'temurin'
|
|
81
|
+
java-version: '17'`);
|
|
82
|
+
}
|
|
83
|
+
if (framework === 'go') {
|
|
84
|
+
steps.push(` - uses: actions/setup-go@v5
|
|
85
|
+
with:
|
|
86
|
+
go-version: '1.22'`);
|
|
87
|
+
}
|
|
88
|
+
if (framework === 'dotnet') {
|
|
89
|
+
steps.push(` - uses: actions/setup-dotnet@v4
|
|
90
|
+
with:
|
|
91
|
+
dotnet-version: '8.0.x'`);
|
|
92
|
+
}
|
|
93
|
+
if (framework === 'rails') {
|
|
94
|
+
steps.push(` - uses: ruby/setup-ruby@v1
|
|
95
|
+
with:
|
|
96
|
+
ruby-version: '3.2'
|
|
97
|
+
bundler-cache: true`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return steps;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {Record<string, unknown>[]} environments
|
|
106
|
+
* @returns {string[]}
|
|
107
|
+
*/
|
|
108
|
+
function getPlatformInstallSteps(environments) {
|
|
109
|
+
/** @type {Set<string>} */
|
|
110
|
+
const steps = new Set();
|
|
111
|
+
|
|
112
|
+
for (const env of environments) {
|
|
113
|
+
if (env.deploymentType !== 'platform' && env.frontendDeploymentType !== 'platform') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const platform = env.platform;
|
|
117
|
+
if (!platform) continue;
|
|
118
|
+
const cli = PLATFORM_CLI_MAP[platform];
|
|
119
|
+
if (cli?.globalInstall) {
|
|
120
|
+
steps.add(` - name: Install ${platform} CLI\n run: ${cli.globalInstall}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Array.from(steps);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string[]} deployEnvironments
|
|
129
|
+
* @param {Record<string, Record<string, unknown>>} environments
|
|
130
|
+
*/
|
|
131
|
+
function collectPlatformEnvVars(deployEnvironments, environments) {
|
|
132
|
+
/** @type {Set<string>} */
|
|
133
|
+
const envVars = new Set();
|
|
134
|
+
|
|
135
|
+
for (const envName of deployEnvironments) {
|
|
136
|
+
const env = environments[envName];
|
|
137
|
+
if (!env) continue;
|
|
138
|
+
|
|
139
|
+
if (env.deploymentType === 'platform' || env.frontendDeploymentType === 'platform') {
|
|
140
|
+
const platform = env.platform;
|
|
141
|
+
if (!platform) continue;
|
|
142
|
+
const keys = PLATFORM_ENV_MAP[platform] || [];
|
|
143
|
+
for (const key of keys) {
|
|
144
|
+
envVars.add(`${key}: \${{ secrets.${key} }}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return envVars;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string[]} storageProviders
|
|
154
|
+
* @param {string[]} deployEnvironments
|
|
155
|
+
* @param {Record<string, { type: string }>} environments
|
|
156
|
+
* @param {string} [cliSource]
|
|
157
|
+
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
export function generateWorkflowYaml(
|
|
161
|
+
storageProviders,
|
|
162
|
+
deployEnvironments,
|
|
163
|
+
environments,
|
|
164
|
+
cliSource = 'npm:deployhub',
|
|
165
|
+
config = null
|
|
166
|
+
) {
|
|
167
|
+
/** @type {Set<string>} */
|
|
168
|
+
const envVars = new Set(['DEPLOYHUB_ENV: production']);
|
|
169
|
+
|
|
170
|
+
for (const provider of storageProviders) {
|
|
171
|
+
const keys = PROVIDER_ENV_MAP[provider] || [];
|
|
172
|
+
for (const key of keys) {
|
|
173
|
+
envVars.add(`${key}: \${{ secrets.${key} }}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const envName of deployEnvironments) {
|
|
178
|
+
const env = environments[envName];
|
|
179
|
+
if (!env) continue;
|
|
180
|
+
|
|
181
|
+
if (env.deploymentType !== 'platform') {
|
|
182
|
+
const keys = PROVIDER_ENV_MAP[env.type] || [];
|
|
183
|
+
for (const key of keys) {
|
|
184
|
+
envVars.add(`${key}: \${{ secrets.${key} }}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (env.type === 'ssh' && config) {
|
|
189
|
+
const projectType = config.projectType || 'frontend';
|
|
190
|
+
if (projectType === 'backend' || projectType === 'both') {
|
|
191
|
+
for (const key of BACKEND_SSH_ENV_VARS) {
|
|
192
|
+
envVars.add(`${key}: \${{ secrets.${key} }}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const line of collectPlatformEnvVars(deployEnvironments, environments)) {
|
|
199
|
+
envVars.add(line);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const envBlock = Array.from(envVars)
|
|
203
|
+
.map((line) => ` ${line}`)
|
|
204
|
+
.join('\n');
|
|
205
|
+
|
|
206
|
+
const installSpec = getCliInstallSpec(cliSource);
|
|
207
|
+
const backendSteps = getBackendSetupSteps(config);
|
|
208
|
+
const uniqueBackendSteps = [...new Set(backendSteps)].join('\n');
|
|
209
|
+
|
|
210
|
+
const envList = deployEnvironments
|
|
211
|
+
.map((name) => environments[name])
|
|
212
|
+
.filter(Boolean);
|
|
213
|
+
const platformSteps = getPlatformInstallSteps(envList).join('\n');
|
|
214
|
+
|
|
215
|
+
const projectType = config?.projectType || 'frontend';
|
|
216
|
+
const installDepsCommand =
|
|
217
|
+
projectType === 'backend' || projectType === 'both'
|
|
218
|
+
? getInstallDepsCommand(config)
|
|
219
|
+
: 'npm install';
|
|
220
|
+
|
|
221
|
+
const workflow = `${getWorkflowHeaderComment()}name: DeployHub
|
|
222
|
+
on:
|
|
223
|
+
push:
|
|
224
|
+
branches: [main]
|
|
225
|
+
jobs:
|
|
226
|
+
deploy:
|
|
227
|
+
runs-on: ubuntu-latest
|
|
228
|
+
steps:
|
|
229
|
+
- uses: actions/checkout@v4
|
|
230
|
+
${uniqueBackendSteps ? `${uniqueBackendSteps}\n` : ''} - uses: actions/setup-node@v4
|
|
231
|
+
with:
|
|
232
|
+
node-version: '20'
|
|
233
|
+
- name: Install project dependencies
|
|
234
|
+
run: ${installDepsCommand}
|
|
235
|
+
${platformSteps ? `${platformSteps}\n` : ''} - name: Install DeployHub CLI
|
|
236
|
+
run: npm install ${installSpec} --no-save
|
|
237
|
+
- run: npx deployhub build
|
|
238
|
+
env:
|
|
239
|
+
${envBlock}
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
return workflow;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
247
|
+
* @returns {string}
|
|
248
|
+
*/
|
|
249
|
+
function getInstallDepsCommand(config) {
|
|
250
|
+
if (!config) return 'npm install';
|
|
251
|
+
|
|
252
|
+
const framework =
|
|
253
|
+
config.backend?.framework || config.framework || 'express';
|
|
254
|
+
const language = config.backend?.language || config.language;
|
|
255
|
+
|
|
256
|
+
if (language === 'python' || ['fastapi', 'django', 'flask'].includes(framework)) {
|
|
257
|
+
return 'pip install -r requirements.txt';
|
|
258
|
+
}
|
|
259
|
+
if (['laravel', 'symfony'].includes(framework)) {
|
|
260
|
+
return 'composer install --no-interaction';
|
|
261
|
+
}
|
|
262
|
+
if (framework === 'spring' || framework === 'java') {
|
|
263
|
+
return 'mvn dependency:resolve || true';
|
|
264
|
+
}
|
|
265
|
+
if (framework === 'go') {
|
|
266
|
+
return 'go mod download';
|
|
267
|
+
}
|
|
268
|
+
if (framework === 'dotnet') {
|
|
269
|
+
return 'dotnet restore';
|
|
270
|
+
}
|
|
271
|
+
if (framework === 'rails') {
|
|
272
|
+
return 'bundle install';
|
|
273
|
+
}
|
|
274
|
+
return 'npm install';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {string[]} storageProviders
|
|
279
|
+
* @param {string[]} deployEnvironments
|
|
280
|
+
* @param {Record<string, { type: string }>} environments
|
|
281
|
+
* @param {string} [cwd]
|
|
282
|
+
* @param {string} [cliSource]
|
|
283
|
+
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
284
|
+
*/
|
|
285
|
+
export async function writeWorkflowFile(
|
|
286
|
+
storageProviders,
|
|
287
|
+
deployEnvironments,
|
|
288
|
+
environments,
|
|
289
|
+
cwd = process.cwd(),
|
|
290
|
+
cliSource = 'npm:deployhub',
|
|
291
|
+
config = null
|
|
292
|
+
) {
|
|
293
|
+
const workflowDir = path.join(cwd, '.github', 'workflows');
|
|
294
|
+
await fs.ensureDir(workflowDir);
|
|
295
|
+
const content = generateWorkflowYaml(
|
|
296
|
+
storageProviders,
|
|
297
|
+
deployEnvironments,
|
|
298
|
+
environments,
|
|
299
|
+
cliSource,
|
|
300
|
+
config
|
|
301
|
+
);
|
|
302
|
+
await fs.writeFile(path.join(workflowDir, 'deployhub.yml'), content);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {string} cliSource
|
|
307
|
+
* @param {string} [cwd]
|
|
308
|
+
*/
|
|
309
|
+
export async function addDeployhubToPackageJson(cliSource, cwd = process.cwd()) {
|
|
310
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
311
|
+
if (!(await fs.pathExists(pkgPath))) return;
|
|
312
|
+
|
|
313
|
+
const pkg = await fs.readJson(pkgPath);
|
|
314
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
315
|
+
pkg.devDependencies.deployhub = getCliInstallSpec(cliSource);
|
|
316
|
+
pkg.scripts = pkg.scripts || {};
|
|
317
|
+
pkg.scripts['deployhub:build'] = 'deployhub build';
|
|
318
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {string} [cwd]
|
|
323
|
+
* @returns {Promise<string>}
|
|
324
|
+
*/
|
|
325
|
+
export async function guessCliGithubRepo(cwd = process.cwd()) {
|
|
326
|
+
try {
|
|
327
|
+
const { execa } = await import('execa');
|
|
328
|
+
const { stdout } = await execa('git', ['remote', 'get-url', 'origin'], {
|
|
329
|
+
cwd,
|
|
330
|
+
stdio: 'pipe',
|
|
331
|
+
});
|
|
332
|
+
const match = stdout.match(/github\.com[:/](.+?)\/(.+?)(?:\.git)?$/);
|
|
333
|
+
if (match) {
|
|
334
|
+
return `github:${match[1]}/deployhub`;
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
// ignore
|
|
338
|
+
}
|
|
339
|
+
return 'github:YOUR_USERNAME/deployhub';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {string[]} storageProviders
|
|
344
|
+
* @param {string[]} deployEnvironments
|
|
345
|
+
* @param {Record<string, { type: string }>} environments
|
|
346
|
+
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
347
|
+
* @returns {string[]}
|
|
348
|
+
*/
|
|
349
|
+
export function getRequiredSecrets(
|
|
350
|
+
storageProviders,
|
|
351
|
+
deployEnvironments,
|
|
352
|
+
environments,
|
|
353
|
+
config = null
|
|
354
|
+
) {
|
|
355
|
+
/** @type {Set<string>} */
|
|
356
|
+
const secrets = new Set();
|
|
357
|
+
|
|
358
|
+
for (const provider of storageProviders) {
|
|
359
|
+
const keys = PROVIDER_ENV_MAP[provider] || [];
|
|
360
|
+
keys.forEach((k) => secrets.add(k));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const envName of deployEnvironments) {
|
|
364
|
+
const env = environments[envName];
|
|
365
|
+
if (!env) continue;
|
|
366
|
+
|
|
367
|
+
if (env.deploymentType === 'platform' || env.frontendDeploymentType === 'platform') {
|
|
368
|
+
const platform = env.platform;
|
|
369
|
+
if (platform) {
|
|
370
|
+
const keys = PLATFORM_ENV_MAP[platform] || [];
|
|
371
|
+
keys.forEach((k) => secrets.add(k));
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
const keys = PROVIDER_ENV_MAP[env.type] || [];
|
|
375
|
+
keys.forEach((k) => secrets.add(k));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (env.type === 'ssh' && config) {
|
|
379
|
+
const projectType = config.projectType || 'frontend';
|
|
380
|
+
if (projectType === 'backend' || projectType === 'both') {
|
|
381
|
+
BACKEND_SSH_ENV_VARS.forEach((k) => secrets.add(k));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return Array.from(secrets);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export { PROVIDER_ENV_MAP };
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import {
|
|
7
|
+
PLATFORM_CHOICES,
|
|
8
|
+
PLATFORM_COMPARISON,
|
|
9
|
+
getPlatformEnvExample,
|
|
10
|
+
} from './platform-env.js';
|
|
11
|
+
import { ensureFirebaseJson } from './firebase-config-generator.js';
|
|
12
|
+
import { appendEnv } from '../core/config.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} platform
|
|
16
|
+
* @param {string} projectName
|
|
17
|
+
* @param {string} buildOutput
|
|
18
|
+
* @param {string} [cwd]
|
|
19
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
20
|
+
*/
|
|
21
|
+
export async function promptPlatformQuestions(
|
|
22
|
+
platform,
|
|
23
|
+
projectName,
|
|
24
|
+
buildOutput,
|
|
25
|
+
cwd = process.cwd()
|
|
26
|
+
) {
|
|
27
|
+
/** @type {Record<string, unknown>} */
|
|
28
|
+
const config = { platform };
|
|
29
|
+
|
|
30
|
+
switch (platform) {
|
|
31
|
+
case 'vercel': {
|
|
32
|
+
const answers = await inquirer.prompt([
|
|
33
|
+
{
|
|
34
|
+
type: 'input',
|
|
35
|
+
name: 'projectName',
|
|
36
|
+
message: 'Vercel project name:',
|
|
37
|
+
default: projectName,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: 'confirm',
|
|
41
|
+
name: 'vercelLinked',
|
|
42
|
+
message: 'Have you already linked this project with Vercel?',
|
|
43
|
+
default: fs.existsSync(path.join(cwd, '.vercel', 'project.json')),
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
config.projectName = answers.projectName;
|
|
47
|
+
|
|
48
|
+
if (!answers.vercelLinked) {
|
|
49
|
+
console.log(chalk.gray('Running vercel link — follow the prompts...'));
|
|
50
|
+
try {
|
|
51
|
+
await execa('vercel', ['link'], { cwd, stdio: 'inherit' });
|
|
52
|
+
} catch {
|
|
53
|
+
console.log(
|
|
54
|
+
chalk.yellow(
|
|
55
|
+
'Could not run vercel link automatically. Run it manually, then add VERCEL_ORG_ID and VERCEL_PROJECT_ID to .env'
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case 'netlify': {
|
|
64
|
+
const answers = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: 'input',
|
|
67
|
+
name: 'siteId',
|
|
68
|
+
message: 'Netlify site ID (from Netlify dashboard):',
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
config.siteId = answers.siteId;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'cloudflare-pages': {
|
|
76
|
+
const answers = await inquirer.prompt([
|
|
77
|
+
{
|
|
78
|
+
type: 'input',
|
|
79
|
+
name: 'projectName',
|
|
80
|
+
message: 'Cloudflare Pages project name:',
|
|
81
|
+
default: projectName,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: 'input',
|
|
85
|
+
name: 'accountId',
|
|
86
|
+
message: 'Cloudflare account ID (from dashboard):',
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
config.projectName = answers.projectName;
|
|
90
|
+
config.accountId = answers.accountId;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'aws-amplify': {
|
|
95
|
+
const answers = await inquirer.prompt([
|
|
96
|
+
{
|
|
97
|
+
type: 'input',
|
|
98
|
+
name: 'appId',
|
|
99
|
+
message: 'Amplify App ID (from AWS console):',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'input',
|
|
103
|
+
name: 'region',
|
|
104
|
+
message: 'AWS region:',
|
|
105
|
+
default: 'us-east-1',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'confirm',
|
|
109
|
+
name: 'githubConnected',
|
|
110
|
+
message: 'GitHub already connected to Amplify?',
|
|
111
|
+
default: true,
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
config.appId = answers.appId;
|
|
115
|
+
config.region = answers.region;
|
|
116
|
+
config.githubConnected = answers.githubConnected;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'azure-static-web-apps': {
|
|
121
|
+
const answers = await inquirer.prompt([
|
|
122
|
+
{
|
|
123
|
+
type: 'input',
|
|
124
|
+
name: 'resourceName',
|
|
125
|
+
message: 'Resource name:',
|
|
126
|
+
default: projectName,
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
config.resourceName = answers.resourceName;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'firebase-hosting': {
|
|
134
|
+
const answers = await inquirer.prompt([
|
|
135
|
+
{
|
|
136
|
+
type: 'input',
|
|
137
|
+
name: 'projectId',
|
|
138
|
+
message: 'Firebase project ID:',
|
|
139
|
+
default: projectName,
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
config.projectId = answers.projectId;
|
|
143
|
+
await ensureFirebaseJson(buildOutput, cwd);
|
|
144
|
+
console.log(chalk.green('✓ firebase.json generated'));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'firebase-app-hosting': {
|
|
149
|
+
const answers = await inquirer.prompt([
|
|
150
|
+
{
|
|
151
|
+
type: 'input',
|
|
152
|
+
name: 'projectId',
|
|
153
|
+
message: 'Firebase project ID:',
|
|
154
|
+
default: projectName,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: 'input',
|
|
158
|
+
name: 'backendName',
|
|
159
|
+
message: 'Backend name:',
|
|
160
|
+
default: `${projectName}-backend`,
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
config.projectId = answers.projectId;
|
|
164
|
+
config.backendName = answers.backendName;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
default:
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return config;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {string} platform
|
|
177
|
+
* @param {string} [cwd]
|
|
178
|
+
*/
|
|
179
|
+
export async function appendPlatformEnvExample(platform, cwd = process.cwd()) {
|
|
180
|
+
const examples = getPlatformEnvExample(platform);
|
|
181
|
+
await appendEnv(examples, cwd);
|
|
182
|
+
|
|
183
|
+
const envExamplePath = path.join(cwd, '.env.example');
|
|
184
|
+
if (!(await fs.pathExists(envExamplePath))) return;
|
|
185
|
+
|
|
186
|
+
let content = await fs.readFile(envExamplePath, 'utf-8');
|
|
187
|
+
const sectionHeader = `\n# ${platform} deployment\n`;
|
|
188
|
+
const lines = Object.keys(examples)
|
|
189
|
+
.map((key) => `${key}=`)
|
|
190
|
+
.join('\n');
|
|
191
|
+
|
|
192
|
+
if (!content.includes(`${platform} deployment`)) {
|
|
193
|
+
content += `${sectionHeader}${lines}\n`;
|
|
194
|
+
await fs.writeFile(envExamplePath, content);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function showPlatformComparison() {
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(chalk.bold('Platform comparison:'));
|
|
201
|
+
console.log(chalk.gray(PLATFORM_COMPARISON));
|
|
202
|
+
console.log('');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} platform
|
|
207
|
+
* @param {string} projectName
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
export function suggestHealthUrl(platform, projectName) {
|
|
211
|
+
const urls = {
|
|
212
|
+
vercel: `https://${projectName}.vercel.app`,
|
|
213
|
+
netlify: `https://${projectName}.netlify.app`,
|
|
214
|
+
'cloudflare-pages': `https://${projectName}.pages.dev`,
|
|
215
|
+
'firebase-hosting': `https://${projectName}.web.app`,
|
|
216
|
+
'firebase-app-hosting': `https://${projectName}.web.app`,
|
|
217
|
+
};
|
|
218
|
+
return urls[platform] || '';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { PLATFORM_CHOICES };
|
|
222
|
+
|
|
223
|
+
export default {
|
|
224
|
+
promptPlatformQuestions,
|
|
225
|
+
appendPlatformEnvExample,
|
|
226
|
+
showPlatformComparison,
|
|
227
|
+
suggestHealthUrl,
|
|
228
|
+
PLATFORM_CHOICES,
|
|
229
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate nginx server block config for SPA frontend deployments.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} projectName
|
|
5
|
+
* @param {string} deployPath - Absolute deploy path on the server
|
|
6
|
+
* @param {string} [buildOutput='dist']
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function generateNginxConfig(projectName, deployPath, buildOutput = 'dist') {
|
|
10
|
+
const root = `${deployPath.replace(/\/$/, '')}/${buildOutput}`;
|
|
11
|
+
|
|
12
|
+
return `server {
|
|
13
|
+
listen 80;
|
|
14
|
+
server_name _;
|
|
15
|
+
root ${root};
|
|
16
|
+
index index.html;
|
|
17
|
+
|
|
18
|
+
location / {
|
|
19
|
+
try_files $uri $uri/ /index.html;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} projectName
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
export function getNginxSitePath(projectName) {
|
|
30
|
+
const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
31
|
+
return `/etc/nginx/sites-available/${safeName}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default { generateNginxConfig, getNginxSitePath };
|