Skip to content

Commit

Permalink
feat(create-strapi-app): add cloud project creation
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan-pichon committed Apr 24, 2024
1 parent 42d0785 commit 755dda4
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 45 deletions.
2 changes: 1 addition & 1 deletion packages/cli/cloud/src/bin.ts
@@ -1,5 +1,5 @@
import { Command } from 'commander';
import { createLogger } from './utils/logger';
import { createLogger } from './services/logger';
import { CLIContext } from './types';
import { cli } from './index';

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/cloud/src/create-project/action.ts
Expand Up @@ -2,7 +2,7 @@ import inquirer from 'inquirer';
import { AxiosError } from 'axios';
import { defaults } from 'lodash/fp';
import type { CLIContext, ProjectAnswers, ProjectInput } from '../types';
import { tokenServiceFactory, cloudApiFactory } from '../services';
import { tokenServiceFactory, cloudApiFactory, local } from '../services';

function handleError(ctx: CLIContext, error: Error) {
const tokenService = tokenServiceFactory(ctx);
Expand Down Expand Up @@ -52,6 +52,7 @@ export default (ctx: CLIContext) => {

try {
const { data } = await cloudApi.createProject(projectInput);
local.save({ project: data });
return data;
} catch (error: Error | unknown) {
handleError(ctx, error as Error);
Expand Down
14 changes: 7 additions & 7 deletions packages/cli/cloud/src/deploy-project/action.ts
@@ -1,6 +1,7 @@
import fs from 'fs';
import path from 'path';
import { AxiosError } from 'axios';
import * as crypto from 'node:crypto';
import { apiConfig } from '../config/api';
import { compressFilesToTar } from '../utils/compress-files';
import createProjectAction from '../create-project/action';
Expand All @@ -13,6 +14,7 @@ import { loadPkg } from '../utils/pkg';
const FILE_SIZE_LIMIT = 100 * 1024 * 1024; // 100MB

type PackageJson = {
name: string;
strapi?: {
uuid: string;
};
Expand All @@ -32,15 +34,11 @@ async function upload(ctx: CLIContext, project: ProjectInfos, token: string) {
);
return;
}
if (!packageJson.strapi || !packageJson.strapi.uuid) {
ctx.logger.error(
'The project is not a Strapi project. Please make sure the package.json file is correctly formatted. It should contain a `strapi` object with a `uuid` property.'
);
return;
}

ctx.logger.log('📦 Compressing project...');
const compressedFilename = `${packageJson.strapi.uuid}.tar.gz`;
// hash packageJson.name to avoid conflicts
const hashname = crypto.createHash('md5').update(packageJson.name).digest('hex');
const compressedFilename = `${hashname}.tar.gz`;
try {
ctx.logger.debug(
'Compression parameters\n',
Expand Down Expand Up @@ -99,6 +97,8 @@ async function upload(ctx: CLIContext, project: ProjectInfos, token: string) {
}

ctx.logger.debug(JSON.stringify(error));
} finally {
fs.rmSync(tarFilePath, { force: true });
}
process.exit(0);
} catch (e: any) {
Expand Down
46 changes: 25 additions & 21 deletions packages/cli/cloud/src/services/cli-api.ts
Expand Up @@ -15,7 +15,29 @@ export type ProjectInfos = {
};
export type ProjectInput = Omit<ProjectInfos, 'id'>;

export function cloudApiFactory(token?: string) {
export interface CloudApiService {
deploy(
deployInput: {
filePath: string;
project: { id: string };
},
{
onUploadProgress,
}: {
onUploadProgress: (progressEvent: { loaded: number; total?: number }) => void;
}
): Promise<AxiosResponse>;

createProject(projectInput: ProjectInput): Promise<AxiosResponse<ProjectInfos>>;

getUserInfo(): Promise<AxiosResponse>;

config(): Promise<AxiosResponse<CloudCliConfig>>;

listProjects(): Promise<AxiosResponse<ProjectInfos[]>>;
}

export function cloudApiFactory(token?: string): CloudApiService {
const axiosCloudAPI = axios.create({
baseURL: `${apiConfig.apiBaseUrl}/${VERSION}`,
headers: {
Expand All @@ -28,20 +50,7 @@ export function cloudApiFactory(token?: string) {
}

return {
deploy(
{
filePath,
project,
}: {
filePath: string;
project: { id: string };
},
{
onUploadProgress,
}: {
onUploadProgress: (progressEvent: { loaded: number; total?: number }) => void;
}
) {
deploy({ filePath, project }, { onUploadProgress }) {
return axiosCloudAPI.post(
`/deploy/${project.id}`,
{ file: fs.createReadStream(filePath) },
Expand All @@ -54,12 +63,7 @@ export function cloudApiFactory(token?: string) {
);
},

createProject({
name,
nodeVersion,
region,
plan,
}: ProjectInput): Promise<AxiosResponse<ProjectInfos>> {
createProject({ name, nodeVersion, region, plan }) {
return axiosCloudAPI.post('/project', {
projectName: name,
region,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/cloud/src/services/index.ts
@@ -1,3 +1,4 @@
export { cloudApiFactory } from './cli-api';
export * as local from './strapi-info-save';
export { tokenServiceFactory } from './token';
export { createLogger } from './logger';
File renamed without changes.
20 changes: 12 additions & 8 deletions packages/cli/cloud/src/services/strapi-info-save.ts
@@ -1,4 +1,5 @@
import fs from 'fs';
import path from 'path';
import type { ProjectInfos } from './cli-api';

export const LOCAL_SAVE_FILENAME = '.strapi-cloud.json';
Expand All @@ -7,21 +8,24 @@ export type LocalSave = {
project?: ProjectInfos;
};

export function save(data: LocalSave) {
export function save(data: LocalSave, { directoryPath }: { directoryPath?: string } = {}) {
const storedData = { ...retrieve(), ...data };
fs.writeFileSync(LOCAL_SAVE_FILENAME, JSON.stringify(storedData), 'utf8');
const pathToFile = path.join(directoryPath || process.cwd(), LOCAL_SAVE_FILENAME);
fs.writeFileSync(pathToFile, JSON.stringify(storedData), 'utf8');
}

export function retrieve(): LocalSave {
if (!fs.existsSync(LOCAL_SAVE_FILENAME)) {
export function retrieve({ directoryPath }: { directoryPath?: string } = {}): LocalSave {
const pathToFile = path.join(directoryPath || process.cwd(), LOCAL_SAVE_FILENAME);
if (!fs.existsSync(pathToFile)) {
return {};
}

return JSON.parse(fs.readFileSync(LOCAL_SAVE_FILENAME, 'utf8'));
return JSON.parse(fs.readFileSync(pathToFile, 'utf8'));
}

export function erase() {
if (fs.existsSync(LOCAL_SAVE_FILENAME)) {
fs.unlinkSync(LOCAL_SAVE_FILENAME);
export function erase({ directoryPath }: { directoryPath?: string } = {}) {
const pathToFile = path.join(directoryPath || process.cwd(), LOCAL_SAVE_FILENAME);
if (fs.existsSync(pathToFile)) {
fs.unlinkSync(pathToFile);
}
}
4 changes: 2 additions & 2 deletions packages/cli/cloud/src/services/token.ts
Expand Up @@ -13,7 +13,7 @@ interface DecodedToken {
[key: string]: any;
}

export function tokenServiceFactory({ logger }: CLIContext) {
export function tokenServiceFactory({ logger }: { logger: CLIContext['logger'] }) {
function saveToken(str: string) {
const appConfig = getLocalConfig();

Expand All @@ -40,7 +40,7 @@ export function tokenServiceFactory({ logger }: CLIContext) {
return appConfig.token;
}
}
return null;
return undefined;
}

async function validateToken(idToken: string, jwksUrl: string): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/cloud/src/types.ts
@@ -1,6 +1,6 @@
import type { Command } from 'commander';
import type { DistinctQuestion } from 'inquirer';
import { Logger } from './utils/logger';
import { Logger } from './services/logger';

export type ProjectAnswers = {
name: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/cloud/src/utils/pkg.ts
Expand Up @@ -3,7 +3,7 @@ import os from 'os';
import pkgUp from 'pkg-up';
import * as yup from 'yup';
import chalk from 'chalk';
import { Logger } from './logger';
import { Logger } from '../services/logger';

interface Export {
types?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/cli/create-strapi-app/package.json
Expand Up @@ -43,6 +43,7 @@
"watch": "pack-up watch"
},
"dependencies": {
"@strapi/cloud-cli": "4.23.1",
"@strapi/generate-new": "4.23.1",
"commander": "8.3.0",
"inquirer": "8.2.5"
Expand Down
52 changes: 49 additions & 3 deletions packages/cli/create-strapi-app/src/create-strapi-app.ts
@@ -1,7 +1,9 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import commander from 'commander';
import inquirer from 'inquirer';
import { checkInstallPath, generateNewApp, type NewOptions } from '@strapi/generate-new';
import { cli as cloudCli, services as cloudServices } from '@strapi/cloud-cli';
import promptUser from './utils/prompt-user';
import type { Program } from './types';

Expand Down Expand Up @@ -57,6 +59,52 @@ function generateApp(projectName: string, options: Partial<NewOptions>) {
});
}

async function handleCloudProject(projectName: string): Promise<void> {
// Ask user if they want to connect to Strapi Cloud and create a cloud project to deploy the app
const { isCloudProjectAsked } = await inquirer.prompt<{ isCloudProjectAsked: boolean }>([
{
type: 'confirm',
name: 'isCloudProjectAsked',
message: 'Do you want to connect to Strapi Cloud and host your Strapi project?',
},
]);

if (isCloudProjectAsked) {
const logger = cloudServices.createLogger({
silent: false,
debug: false,
timestamp: false,
});
const cliContext = {
logger,
cwd: process.cwd(),
};
const tokenService = cloudServices.tokenServiceFactory(cliContext);
// 2- Create a new project with @strapi/cloud-cli cloudApiFactory and cloudApi.createProject

try {
await cloudCli.login.action(cliContext);
const token = await tokenService.retrieveToken();

const cloudApiService = cloudServices.cloudApiFactory(token);
const { data: config } = await cloudApiService.config();
const defaultProjectValues = config.projectCreation?.defaults || {};
await cloudApiService.createProject({
nodeVersion: process.versions?.node?.slice(1, 3) || '20',
region: 'NYC',
plan: 'trial',
...defaultProjectValues,
name: projectName,
});
} catch (e) {
logger.debug(e);
logger.error(
'An error occurred while trying to interact with Strapi Cloud. Use strapi deploy command once the project is generated.'
);
}
}
}

async function initProject(projectName: string, programArgs: Program) {
if (projectName) {
await checkInstallPath(resolve(projectName));
Expand Down Expand Up @@ -106,8 +154,6 @@ async function initProject(projectName: string, programArgs: Program) {
...options,
};

// Do you want to login/signup to Strapi Cloud?
await handleCloudProject(directory);
await generateApp(directory, generateStrapiAppOptions);
// Try to create Cloud project if user is logged in
// If no trial available, log a message "You have already used your free trial for Strapi Cloud. We do not allow
}

0 comments on commit 755dda4

Please sign in to comment.