Skip to content

refactor(cli): only install and start project after all questions #77

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@babel/types": "7.24.5",
"@clack/prompts": "^0.7.0",
"chalk": "^5.3.0",
"execa": "^9.2.0",
"ignore": "^5.3.1",
"lookpath": "^1.2.2",
"yargs-parser": "^21.1.1"
Expand All @@ -43,7 +44,6 @@
"@types/yargs-parser": "^21.0.3",
"esbuild": "^0.20.2",
"esbuild-node-externals": "^1.13.0",
"execa": "^9.2.0",
"fs-extra": "^11.2.0",
"vitest": "^1.6.0"
},
Expand Down
115 changes: 0 additions & 115 deletions packages/cli/src/commands/create/dependencies.ts

This file was deleted.

9 changes: 6 additions & 3 deletions packages/cli/src/commands/create/enterprise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { parseAstroConfig, replaceArgs } from './astro-config.js';
import { generate } from './babel.js';

export async function setupEnterpriseConfig(dest: string, flags: CreateOptions) {
let editorOrigin = flags.enterprise;

if (!flags.defaults && flags.enterprise === undefined) {
// early exit if `--defaults` is provided without `--enterprise`
return;
} else if (editorOrigin) {
}

let editorOrigin = flags.enterprise;

if (editorOrigin) {
const error = validateEditorOrigin(editorOrigin);

if (error) {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/create/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { pkg } from '../../pkg.js';
import { warnLabel } from '../../utils/messages.js';
import { runShellCommand } from '../../utils/shell.js';
import { assertNotCanceled, runTask } from '../../utils/tasks.js';
import { DEFAULT_VALUES, type CreateOptions } from './options.js';
import { DEFAULT_VALUES, readFlag, type CreateOptions } from './options.js';

export async function initGitRepo(cwd: string, flags: CreateOptions) {
let shouldInitGitRepo = flags.git ?? DEFAULT_VALUES.git;
let shouldInitGitRepo = readFlag(flags, 'git');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a small readFlag() utility which returns the value of the flag, or if the flag doesn't have a value, it will return the default ONLY if flags.default is true. It makes the logic a bit easier because you know that if the value is undefined, you can prompt the user for the question because it also means --defaults was not provided.


if (!flags.defaults && flags.git === undefined) {
if (shouldInitGitRepo === undefined) {
const answer = await prompts.confirm({
message: 'Initialize a new git repository?',
initialValue: DEFAULT_VALUES.git,
Expand Down
67 changes: 59 additions & 8 deletions packages/cli/src/commands/create/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import chalk from 'chalk';
import fs from 'node:fs';
import path from 'node:path';
import yargs from 'yargs-parser';
import { execa } from 'execa';
import { pkg } from '../../pkg.js';
import { errorLabel, primaryLabel, printHelp } from '../../utils/messages.js';
import { errorLabel, primaryLabel, printHelp, warnLabel } from '../../utils/messages.js';
import { generateProjectName } from '../../utils/project.js';
import { assertNotCanceled } from '../../utils/tasks.js';
import { installDependencies, type PackageManager } from './dependencies.js';
import { initGitRepo } from './git.js';
import { DEFAULT_VALUES, type CreateOptions } from './options.js';
import { setupEnterpriseConfig } from './enterprise.js';
import { copyTemplate } from './template.js';
import { selectPackageManager, type PackageManager } from './package-manager.js';
import { installAndStart } from './install-start.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Late to the party, but should we call the file install-and-start.js?


const TUTORIALKIT_VERSION = pkg.version;

Expand All @@ -24,6 +26,7 @@ export async function createTutorial(flags: yargs.Arguments) {
Options: [
['--dir, -d', 'The folder in which the tutorial gets created'],
['--install, --no-install', `Install dependencies (default ${chalk.yellow(DEFAULT_VALUES.install)})`],
['--start, --no-start', `Start project (default ${chalk.yellow(DEFAULT_VALUES.start)})`],
['--git, --no-git', `Initialize a local git repository (default ${chalk.yellow(DEFAULT_VALUES.git)})`],
['--dry-run', `Walk through steps without executing (default ${chalk.yellow(DEFAULT_VALUES.dryRun)})`],
[
Expand All @@ -46,6 +49,16 @@ export async function createTutorial(flags: yargs.Arguments) {
return 0;
}

applyAliases(flags);

try {
verifyFlags(flags);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not possible to do --start --no-install. You can't start the project without installing the dependencies. If that happens, we throw. This method could have additional checks in the future.

} catch (error) {
console.error(`${errorLabel()} ${error.message}`);

process.exit(1);
}

try {
return _createTutorial(flags);
} catch (error) {
Expand All @@ -60,8 +73,6 @@ export async function createTutorial(flags: yargs.Arguments) {
}

async function _createTutorial(flags: CreateOptions) {
applyAliases(flags);

prompts.intro(primaryLabel(pkg.name));

let tutorialName = flags._[1] !== undefined ? String(flags._[1]) : undefined;
Expand Down Expand Up @@ -131,21 +142,55 @@ async function _createTutorial(flags: CreateOptions) {

updatePackageJson(resolvedDest, tutorialName, flags);

const { selectedPackageManager, dependenciesInstalled } = await installDependencies(resolvedDest, flags);
const selectedPackageManager = await selectPackageManager(flags);

updateReadme(resolvedDest, selectedPackageManager, flags);

await setupEnterpriseConfig(resolvedDest, flags);

await initGitRepo(resolvedDest, flags);

const { install, start } = await installAndStart(flags);

prompts.log.success(chalk.green('Tutorial successfully created!'));

printNextSteps(dest, selectedPackageManager, dependenciesInstalled);
if (install || start) {
let message = 'Please wait while we install the dependencies and start your project...';

prompts.outro(`You're all set!`);
if (install && !start) {
// change the message if we're only installing dependencies
message = 'Please wait while we install the dependencies...';

console.log('Until next time 👋');
// print the next steps without the install step in case we only install dependencies
printNextSteps(dest, selectedPackageManager, true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we install the dependencies, but we don't start the project, we still print the next steps without the install step.

◆  Tutorial successfully created!
│
│  Next Steps
│
◇  1. cd ./dry-king - Navigate to project
│
◇  2. npm run dev - Start development server
│
◇  3. Head over to http://localhost:4321
│
└  Please wait while we install the dependencies...

}

prompts.outro(message);

await startProject(resolvedDest, selectedPackageManager, flags, start);
} else {
printNextSteps(dest, selectedPackageManager, false);

prompts.outro(`You're all set!`);

console.log('Until next time 👋');
}
}

async function startProject(cwd: string, packageManager: PackageManager, flags: CreateOptions, startProject: boolean) {
if (flags.dryRun) {
const message = startProject
? 'Skipped dependency installation and project start'
: 'Skipped dependency installation';

console.warn(`${warnLabel('DRY RUN')} ${message}`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't do anything in case of a dry run.

◆  Tutorial successfully created!
│
└  Please wait while we install the dependencies and start your project...

 DRY RUN  Skipped dependency installation and project start

} else {
await execa(packageManager, ['install'], { cwd, stdio: 'inherit' });

if (startProject) {
await execa(packageManager, ['run', 'dev'], { cwd, stdio: 'inherit' });
}
}
}

async function getTutorialDirectory(tutorialName: string, flags: CreateOptions) {
Expand Down Expand Up @@ -264,3 +309,9 @@ function applyAliases(flags: CreateOptions & Record<string, any>) {
flags.enterprise = flags.e;
}
}

function verifyFlags(flags: CreateOptions) {
if (flags.install === false && flags.start) {
throw new Error('Cannot start project without installing dependencies.');
}
}
44 changes: 44 additions & 0 deletions packages/cli/src/commands/create/install-start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as prompts from '@clack/prompts';
import { DEFAULT_VALUES, readFlag, type CreateOptions } from './options.js';
import { assertNotCanceled } from '../../utils/tasks.js';

export async function installAndStart(flags: CreateOptions) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function combines both the --install and --start flags.

let installDeps = readFlag(flags, 'install');
let startProject = readFlag(flags, 'start');

if (installDeps === false) {
// the user doesn't want to install the dependencies, which means we can't start the project either
return { install: false, start: false };
}

if (startProject) {
// the user wants to start the project, so we also have to install the dependencies
return { install: true, start: true };
}

if (installDeps) {
if (startProject === false) {
// the user wants to install the dependencies but expicitly not start the project
return { install: true, start: false };
} else {
// the user explicitly wants to install the dependencies, but we don't know if they want to start the project
const answer = await prompts.confirm({
message: 'Start project?',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If --install is provided, but not --start, we change the question to just Start project?.

initialValue: DEFAULT_VALUES.install,
});

assertNotCanceled(answer);

return { install: true, start: answer };
}
}

const answer = await prompts.confirm({
message: 'Install dependencies and start project?',
initialValue: DEFAULT_VALUES.install,
});

assertNotCanceled(answer);

return { install: answer, start: answer };
}
14 changes: 14 additions & 0 deletions packages/cli/src/commands/create/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
export interface CreateOptions {
_: Array<string | number>;
install?: boolean;
start?: boolean;
git?: boolean;
enterprise?: string;
dir?: string;
Expand All @@ -20,7 +21,20 @@ export const templatePath = path.resolve(__dirname, process.env.TUTORIALKIT_TEMP
export const DEFAULT_VALUES = {
git: process.env.CI ? false : true,
install: true,
start: true,
dryRun: false,
force: false,
packageManager: 'npm',
};

type Flags = Omit<CreateOptions, '_'>;

export function readFlag<Flag extends keyof Flags>(flags: Flags, flag: Flag): Flags[Flag] {
let value = flags[flag];

if (flags.defaults) {
value ??= (DEFAULT_VALUES as Flags)[flag];
}

return value;
}
Loading