Skip to content

Commit

Permalink
feat(create): Implement quick start flow
Browse files Browse the repository at this point in the history
Relates to #3117
  • Loading branch information
michaelbromley committed Oct 10, 2024
1 parent 358d381 commit 532f4d5
Show file tree
Hide file tree
Showing 7 changed files with 37,641 additions and 37,241 deletions.
74,540 changes: 37,333 additions & 37,207 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/create/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"cross-spawn": "^7.0.3",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8",
"open": "^8.4.2",
"picocolors": "^1.0.0",
"semver": "^7.5.4",
"tcp-port-used": "^1.0.2"
Expand Down
117 changes: 105 additions & 12 deletions packages/create/src/create-vendure-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import path from 'path';
import pc from 'picocolors';

import { REQUIRED_NODE_VERSION, SERVER_PORT } from './constants';
import { checkCancel, getCiConfiguration, getManualConfiguration } from './gather-user-responses';
import {
getCiConfiguration,
getManualConfiguration,
getQuickStartConfiguration,
} from './gather-user-responses';
import {
checkCancel,
checkDbConnection,
checkNodeVersion,
checkThatNpmCanReadCwd,
Expand All @@ -16,9 +21,10 @@ import {
isSafeToCreateProjectIn,
isServerPortInUse,
scaffoldAlreadyExists,
startPostgresDatabase,
} from './helpers';
import { log, setLogLevel } from './logger';
import { CliLogLevel, PackageManager } from './types';
import { CliLogLevel, DbType, PackageManager } from './types';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require('../package.json');
Expand All @@ -43,7 +49,7 @@ program
'--log-level <logLevel>',
"Log level, either 'silent', 'info', or 'verbose'",
/^(silent|info|verbose)$/i,
'silent',
'info',
)
.option(
'--use-npm',
Expand All @@ -70,6 +76,20 @@ export async function createVendureApp(
`Let's create a ${pc.blue(pc.bold('Vendure App'))}${pc.dim(`v${packageJson.version as string}`)}`,
);

const mode = (await select({
message: 'How should we proceed?',
options: [
{ label: 'Quick Start', value: 'quick', hint: 'Get up an running in a single step' },
{
label: 'Manual Configuration',
value: 'manual',
hint: 'Customize your Vendure project with more advanced settings',
},
],
initialValue: 'quick' as 'quick' | 'manual',
})) as 'quick' | 'manual';
checkCancel(mode);

const portSpinner = spinner();
let port = SERVER_PORT;
const attemptedPortRange = 20;
Expand Down Expand Up @@ -116,8 +136,9 @@ export async function createVendureApp(
populateProducts,
} = isCi
? await getCiConfiguration(root, packageManager)
: await getManualConfiguration(root, scaffoldExists, packageManager);
const originalDirectory = process.cwd();
: mode === 'manual'
? await getManualConfiguration(root, packageManager)
: await getQuickStartConfiguration(root, packageManager);
process.chdir(root);
if (packageManager !== 'npm' && !checkThatNpmCanReadCwd()) {
process.exit(1);
Expand All @@ -143,7 +164,6 @@ export async function createVendureApp(
`Setting up your new Vendure project in ${pc.green(root)}\nThis may take a few minutes...`,
);

const rootPathScript = (fileName: string): string => path.join(root, `${fileName}.ts`);
const srcPathScript = (fileName: string): string => path.join(root, 'src', `${fileName}.ts`);

fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJsonContents, null, 2) + os.EOL);
Expand Down Expand Up @@ -190,19 +210,67 @@ export async function createVendureApp(
.then(() => fs.writeFile(path.join(root, 'README.md'), readmeSource))
.then(() => fs.writeFile(path.join(root, 'Dockerfile'), dockerfileSource))
.then(() => fs.writeFile(path.join(root, 'docker-compose.yml'), dockerComposeSource))
.then(() => fs.mkdir(path.join(root, 'src/plugins')))
.then(() => fs.ensureDir(path.join(root, 'src/plugins')))
.then(() => fs.copyFile(assetPath('gitignore.template'), path.join(root, '.gitignore')))
.then(() => fs.copyFile(assetPath('tsconfig.template.json'), path.join(root, 'tsconfig.json')))
.then(() => createDirectoryStructure(root))
.then(() => copyEmailTemplates(root));
} catch (e) {
outro(pc.red(`Failed to create app scaffold. Please try again.`));
} catch (e: any) {
outro(pc.red(`Failed to create app scaffold: ${e.message as string}`));
process.exit(1);
}
scaffoldSpinner.stop(`Generated app scaffold`);

if (mode === 'quick' && dbType === 'postgres') {
await startPostgresDatabase(root);
}

const populateSpinner = spinner();
populateSpinner.start(`Initializing your new Vendure server`);

// We want to display a set of tips and instructions to the user
// as the initialization process is running because it can take
// a few minutes to complete.
const tips = [
populateProducts
? 'We are populating sample data so that you can start testing right away'
: 'We are setting up your Vendure server',
'This can take a minute or two, so grab a coffee ☕️',
`We'd love it if you drop us a star on GitHub: https://github.com/vendure-ecommerce/vendure`,
'In the mean time, here are some tips to get you started',
`Vendure provides dedicated GraphQL APIs for both the Admin and Shop`,
`Almost every aspect of Vendure is customizable via plugins`,
`You can run 'vendure add' from the command line to add new plugins & features`,
`Use the EventBus in your plugins to react to events in the system`,
`Check out the Vendure documentation at https://docs.vendure.io`,
`Join our Discord community to chat with other Vendure developers: https://vendure.io/community`,
`Vendure supports multiple languages & currencies out of the box`,
`Did we mention this can take a while? ☕️`,
`Our custom fields feature allows you to add any kind of data to your entities`,
`Vendure is built with TypeScript, so you get full type safety`,
`Combined with GraphQL's static schema, your type safety is end-to-end`,
`Almost there now... thanks for your patience!`,
`Collections allow you to group products together`,
`Our AssetServerPlugin allows you to dynamically resize & optimize images`,
`You can find integrations in the Vendure Hub: https://vendure.io/hub`,
];

let tipIndex = 0;
let timer: any;
const tipInterval = 10_000;

function displayTip() {
populateSpinner.message(tips[tipIndex]);
tipIndex++;
if (tipIndex >= tips.length) {
// skip the intro tips if looping
tipIndex = 3;
}
timer = setTimeout(displayTip, tipInterval);
}

timer = setTimeout(displayTip, tipInterval);

// register ts-node so that the config file can be loaded
// eslint-disable-next-line @typescript-eslint/no-var-requires
require(path.join(root, 'node_modules/ts-node')).register();
Expand All @@ -217,7 +285,7 @@ export async function createVendureApp(

const initialDataPath = path.join(assetsDir, 'initial-data.json');
const vendureLogLevel =
logLevel === 'silent'
logLevel === 'info' || logLevel === 'silent'
? LogLevel.Error
: logLevel === 'verbose'
? LogLevel.Verbose
Expand All @@ -231,7 +299,6 @@ export async function createVendureApp(
...(config.apiOptions ?? {}),
port,
},
silent: logLevel === 'silent',
dbConnectionOptions: {
...config.dbConnectionOptions,
synchronize: true,
Expand Down Expand Up @@ -261,12 +328,38 @@ export async function createVendureApp(
log('[CI] Pausing after close...');
await new Promise(resolve => setTimeout(resolve, 10000));
}
populateSpinner.stop(`Server successfully initialized${populateProducts ? ' and populated' : ''}`);

if (mode === 'quick') {
// In quick-start mode, we want to now run the server and open up
// a browser window to the Admin UI.
const quickStartApp = await bootstrap({
...config,
apiOptions: {
...(config.apiOptions ?? {}),
port,
},
});
await quickStartApp.get(JobQueueService).start();
note(
[
'Use the following credentials to log in to the Admin UI:',
`Username: ${pc.green(config.authOptions.superadminCredentials?.identifier)}`,
`Password: ${pc.green(config.authOptions.superadminCredentials?.password)}`,
].join('\n'),
);
const adminUiUrl = `http://localhost:${port}/admin`;

// eslint-disable-next-line @typescript-eslint/no-var-requires
const open = require('open');
await open(adminUiUrl);
}
} catch (e: any) {
log(e.toString());
outro(pc.red(`Failed to initialize server. Please try again.`));
process.exit(1);
}
populateSpinner.stop(`Server successfully initialized${populateProducts ? ' and populated' : ''}`);
clearTimeout(timer);

const startCommand = 'npm run dev';
const nextSteps = [
Expand Down
68 changes: 57 additions & 11 deletions packages/create/src/gather-user-responses.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { cancel, isCancel, select, text } from '@clack/prompts';
import { select, text } from '@clack/prompts';
import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
import { randomBytes } from 'crypto';
import fs from 'fs-extra';
import Handlebars from 'handlebars';
import path from 'path';

import { checkCancel, isDockerAvailable } from './helpers';
import { DbType, FileSources, PackageManager, UserResponses } from './types';

interface PromptAnswers {
Expand All @@ -27,15 +28,68 @@ export async function getQuickStartConfiguration(
root: string,
packageManager: PackageManager,
): Promise<UserResponses> {
return getCiConfiguration(root, packageManager);
// First we want to detect whether Docker is running
const { result: dockerStatus } = await isDockerAvailable();
let usePostgres: boolean;
switch (dockerStatus) {
case 'running':
usePostgres = true;
break;
case 'not-found':
usePostgres = false;
break;
case 'not-running': {
let useSqlite = false;
let dockerIsNowRunning = false;
do {
const useSqliteResponse = await select({
message: 'We could not automatically start Docker. How should we proceed?',
options: [
{ label: `Let's use SQLite as the database`, value: true },
{ label: 'I have manually started Docker', value: false },
],
initialValue: true,
});
checkCancel(useSqlite);
useSqlite = useSqliteResponse as boolean;
if (useSqlite === false) {
const { result: dockerStatusManual } = await isDockerAvailable();
dockerIsNowRunning = dockerStatusManual === 'running';
}
} while (dockerIsNowRunning !== true && useSqlite === false);
usePostgres = !useSqlite;
break;
}
}
const quickStartAnswers: PromptAnswers = {
dbType: usePostgres ? 'postgres' : 'sqlite',
dbHost: usePostgres ? 'localhost' : '',
dbPort: usePostgres ? '5432' : '',
dbName: usePostgres ? 'vendure' : '',
dbUserName: usePostgres ? 'vendure' : '',
dbPassword: usePostgres ? randomBytes(16).toString('base64url') : '',
dbSchema: usePostgres ? 'public' : '',
populateProducts: true,
superadminIdentifier: SUPER_ADMIN_USER_IDENTIFIER,
superadminPassword: SUPER_ADMIN_USER_PASSWORD,
};

const responses = {
...(await generateSources(root, quickStartAnswers, packageManager)),
dbType: quickStartAnswers.dbType,
populateProducts: quickStartAnswers.populateProducts as boolean,
superadminIdentifier: quickStartAnswers.superadminIdentifier as string,
superadminPassword: quickStartAnswers.superadminPassword as string,
};

return responses;
}

/**
* Prompts the user to determine how the new Vendure app should be configured.
*/
export async function getManualConfiguration(
root: string,
alreadyRanScaffold: boolean,
packageManager: PackageManager,
): Promise<UserResponses> {
const dbType = (await select({
Expand Down Expand Up @@ -177,14 +231,6 @@ export async function getCiConfiguration(
};
}

export function checkCancel<T>(value: T | symbol): value is T {
if (isCancel(value)) {
cancel('Setup cancelled.');
process.exit(0);
}
return true;
}

/**
* Create the server index, worker and config source code based on the options specified by the CLI prompts.
*/
Expand Down
Loading

0 comments on commit 532f4d5

Please sign in to comment.