Przeglądaj źródła

feat(create): Improved getting started experience (#3128)

Michael Bromley 1 rok temu
rodzic
commit
adb43842a9

+ 0 - 2
docs/docs/guides/getting-started/installation/index.md

@@ -66,8 +66,6 @@ Follow the instructions to move into the new directory created for your project,
 ```bash
 cd my-shop
 
-yarn dev
-# or
 npm run dev
 ```
 

+ 1 - 1
package-lock.json

@@ -28269,7 +28269,6 @@
             "version": "8.4.2",
             "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
             "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
-            "license": "MIT",
             "dependencies": {
                 "define-lazy-prop": "^2.0.0",
                 "is-docker": "^2.1.1",
@@ -36837,6 +36836,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"

+ 5 - 1
packages/core/src/cli/populate.ts

@@ -150,5 +150,9 @@ export async function importProductsFromCsv(
         languageCode,
         channelOrToken: channel,
     });
-    return lastValueFrom(importer.parseAndImport(productData, ctx, true));
+    const createEnvVar: import('@vendure/common/lib/shared-constants').CREATING_VENDURE_APP =
+        'CREATING_VENDURE_APP';
+    // Turn off progress bar when running in the context of the @vendure/create script
+    const reportProgress = process.env[createEnvVar] === 'true' ? false : true;
+    return lastValueFrom(importer.parseAndImport(productData, ctx, reportProgress));
 }

+ 2 - 32
packages/create/README.md

@@ -4,47 +4,17 @@ A CLI tool for rapidly scaffolding a new Vendure server application. Heavily ins
 
 ## Usage
 
-Vendure Create requires [Node.js](https://nodejs.org/en/) v8.9.0+ to be installed.
-
-To create a new project, you may choose one of the following methods:
-
-### npx
+Vendure Create requires [Node.js](https://nodejs.org/en/) v18+ to be installed.
 
 ```sh
 npx @vendure/create my-app
 ```
 
-*[npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b) comes with npm 5.2+ and higher.*
-
-### npm
-
-```sh
-npm init @vendure my-app
-```
-
-*`npm init <initializer>` is available in npm 6+*
-
-### Yarn
-
-```sh
-yarn create @vendure my-app
-```
-
-*`yarn create` is available in Yarn 0.25+*
-
-
-It will create a directory called `my-app` inside the current folder.
-
 ## Options
 
-### `--use-npm`
-
-By default, Vendure Create will detect whether a compatible version of Yarn is installed, and if so will display a prompt to select the preferred package manager.
-You can override this and force it to use npm with the `--use-npm` flag.
-
 ### `--log-level`
 
-You can control how much output is generated during the installation and setup with this flag. Valid options are `silent`, `info` and `verbose`. The default is `silent`
+You can control how much output is generated during the installation and setup with this flag. Valid options are `silent`, `info` and `verbose`. The default is `info`
 
 Example:
 

+ 1 - 0
packages/create/package.json

@@ -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"

+ 226 - 61
packages/create/src/create-vendure-app.ts

@@ -1,24 +1,33 @@
-/* eslint-disable no-console */
 import { intro, note, outro, select, spinner } from '@clack/prompts';
 import { program } from 'commander';
 import fs from 'fs-extra';
+import { ChildProcess, spawn } from 'node:child_process';
+import { setTimeout as sleep } from 'node:timers/promises';
+import open from 'open';
 import os from 'os';
 import path from 'path';
 import pc from 'picocolors';
 
 import { REQUIRED_NODE_VERSION, SERVER_PORT } from './constants';
-import { checkCancel, gatherCiUserResponses, gatherUserResponses } from './gather-user-responses';
 import {
+    getCiConfiguration,
+    getManualConfiguration,
+    getQuickStartConfiguration,
+} from './gather-user-responses';
+import {
+    checkCancel,
     checkDbConnection,
     checkNodeVersion,
     checkThatNpmCanReadCwd,
+    cleanUpDockerResources,
     getDependencies,
     installPackages,
     isSafeToCreateProjectIn,
     isServerPortInUse,
     scaffoldAlreadyExists,
-    yarnIsAvailable,
+    startPostgresDatabase,
 } from './helpers';
+import { log, setLogLevel } from './logger';
 import { CliLogLevel, PackageManager } from './types';
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -44,14 +53,23 @@ program
         '--log-level <logLevel>',
         "Log level, either 'silent', 'info', or 'verbose'",
         /^(silent|info|verbose)$/i,
-        'silent',
+        'info',
+    )
+    .option('--verbose', 'Alias for --log-level verbose', false)
+    .option(
+        '--use-npm',
+        'Uses npm rather than as the default package manager. DEPRECATED: Npm is now the default',
     )
-    .option('--use-npm', 'Uses npm rather than Yarn as the default package manager')
-    .option('--ci', 'Runs without prompts for use in CI scenarios')
+    .option('--ci', 'Runs without prompts for use in CI scenarios', false)
     .parse(process.argv);
 
 const options = program.opts();
-void createVendureApp(projectName, options.useNpm, options.logLevel || 'silent', options.ci);
+void createVendureApp(
+    projectName,
+    options.useNpm,
+    options.verbose ? 'verbose' : options.logLevel || 'info',
+    options.ci,
+);
 
 export async function createVendureApp(
     name: string | undefined,
@@ -59,6 +77,7 @@ export async function createVendureApp(
     logLevel: CliLogLevel,
     isCi: boolean = false,
 ) {
+    setLogLevel(logLevel);
     if (!runPreChecks(name, useNpm)) {
         return;
     }
@@ -67,6 +86,22 @@ export async function createVendureApp(
         `Let's create a ${pc.blue(pc.bold('Vendure App'))} ✨ ${pc.dim(`v${packageJson.version as string}`)}`,
     );
 
+    const mode = isCi
+        ? 'ci'
+        : ((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;
@@ -90,27 +125,15 @@ export async function createVendureApp(
     const appName = path.basename(root);
     const scaffoldExists = scaffoldAlreadyExists(root, name);
 
-    const yarnAvailable = yarnIsAvailable();
-    let packageManager: PackageManager = 'npm';
-    if (yarnAvailable && !useNpm) {
-        packageManager = (await select({
-            message: 'Which package manager should be used?',
-            options: [
-                { label: 'npm', value: 'npm' },
-                { label: 'yarn', value: 'yarn' },
-            ],
-            initialValue: 'yarn' as PackageManager,
-        })) as PackageManager;
-        checkCancel(packageManager);
-    }
+    const packageManager: PackageManager = 'npm';
 
     if (scaffoldExists) {
-        console.log(
+        log(
             pc.yellow(
                 'It appears that a new Vendure project scaffold already exists. Re-using the existing files...',
             ),
+            { newline: 'after' },
         );
-        console.log();
     }
     const {
         dbType,
@@ -123,10 +146,12 @@ export async function createVendureApp(
         dockerfileSource,
         dockerComposeSource,
         populateProducts,
-    } = isCi
-        ? await gatherCiUserResponses(root, packageManager)
-        : await gatherUserResponses(root, scaffoldExists, packageManager);
-    const originalDirectory = process.cwd();
+    } =
+        mode === 'ci'
+            ? await getCiConfiguration(root, packageManager)
+            : mode === 'manual'
+              ? await getManualConfiguration(root, packageManager)
+              : await getQuickStartConfiguration(root, packageManager);
     process.chdir(root);
     if (packageManager !== 'npm' && !checkThatNpmCanReadCwd()) {
         process.exit(1);
@@ -139,11 +164,11 @@ export async function createVendureApp(
         scripts: {
             'dev:server': 'ts-node ./src/index.ts',
             'dev:worker': 'ts-node ./src/index-worker.ts',
-            dev: packageManager === 'yarn' ? 'concurrently yarn:dev:*' : 'concurrently npm:dev:*',
+            dev: 'concurrently npm:dev:*',
             build: 'tsc',
             'start:server': 'node ./dist/index.js',
             'start:worker': 'node ./dist/index-worker.js',
-            start: packageManager === 'yarn' ? 'concurrently yarn:start:*' : 'concurrently npm:start:*',
+            start: 'concurrently npm:start:*',
         },
     };
 
@@ -152,7 +177,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);
@@ -162,9 +186,9 @@ export async function createVendureApp(
     const installSpinner = spinner();
     installSpinner.start(`Installing ${dependencies[0]} + ${dependencies.length - 1} more dependencies`);
     try {
-        await installPackages(root, packageManager === 'yarn', dependencies, false, logLevel, isCi);
+        await installPackages({ dependencies, logLevel });
     } catch (e) {
-        outro(pc.red(`Failed to install dependencies. Please try again.`));
+        outro(pc.red(`Failed to inst all dependencies. Please try again.`));
         process.exit(1);
     }
     installSpinner.stop(`Successfully installed ${dependencies.length} dependencies`);
@@ -175,7 +199,7 @@ export async function createVendureApp(
             `Installing ${devDependencies[0]} + ${devDependencies.length - 1} more dev dependencies`,
         );
         try {
-            await installPackages(root, packageManager === 'yarn', devDependencies, true, logLevel, isCi);
+            await installPackages({ dependencies: devDependencies, isDevDependencies: true, logLevel });
         } catch (e) {
             outro(pc.red(`Failed to install dev dependencies. Please try again.`));
             process.exit(1);
@@ -185,6 +209,10 @@ export async function createVendureApp(
 
     const scaffoldSpinner = spinner();
     scaffoldSpinner.start(`Generating app scaffold`);
+    // We add this pause so that the above output is displayed before the
+    // potentially lengthy file operations begin, which can prevent that
+    // from displaying and thus make the user think that the process has hung.
+    await sleep(500);
     fs.ensureDirSync(path.join(root, 'src'));
     const assetPath = (fileName: string) => path.join(__dirname, '../assets', fileName);
     const configFile = srcPathScript('vendure-config');
@@ -199,34 +227,87 @@ 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') {
+        cleanUpDockerResources(name);
+        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`,
+        `📖 Check out the Vendure documentation at https://docs.vendure.io`,
+        `💬 Join our Discord community to chat with other Vendure developers: https://vendure.io/community`,
+        '💡 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`,
+        `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`,
+        `Order flows are fully customizable to suit your business requirements`,
+        `Role-based permissions allow you to control access to every part of the system`,
+        `Customers can be grouped for targeted promotions & custom pricing`,
+        `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();
 
+    let superAdminCredentials: { identifier: string; password: string } | undefined;
     try {
         const { populate } = await import(path.join(root, 'node_modules/@vendure/core/cli/populate'));
-        const { bootstrap, DefaultLogger, LogLevel, JobQueueService } = await import(
+        const { bootstrap, DefaultLogger, LogLevel, JobQueueService, ConfigModule } = await import(
             path.join(root, 'node_modules/@vendure/core/dist/index')
         );
         const { config } = await import(configFile);
         const assetsDir = path.join(__dirname, '../assets');
-
+        superAdminCredentials = config.authOptions.superadminCredentials;
         const initialDataPath = path.join(assetsDir, 'initial-data.json');
         const vendureLogLevel =
-            logLevel === 'silent'
+            logLevel === 'info' || logLevel === 'silent'
                 ? LogLevel.Error
                 : logLevel === 'verbose'
                   ? LogLevel.Verbose
@@ -240,7 +321,6 @@ export async function createVendureApp(
                     ...(config.apiOptions ?? {}),
                     port,
                 },
-                silent: logLevel === 'silent',
                 dbConnectionOptions: {
                     ...config.dbConnectionOptions,
                     synchronize: true,
@@ -262,35 +342,116 @@ export async function createVendureApp(
 
         // Pause to ensure the worker jobs have time to complete.
         if (isCi) {
-            console.log('[CI] Pausing before close...');
+            log('[CI] Pausing before close...');
         }
-        await new Promise(resolve => setTimeout(resolve, isCi ? 30000 : 2000));
+        await sleep(isCi ? 30000 : 2000);
         await app.close();
         if (isCi) {
-            console.log('[CI] Pausing after close...');
-            await new Promise(resolve => setTimeout(resolve, 10000));
+            log('[CI] Pausing after close...');
+            await sleep(10000);
         }
-    } catch (e) {
-        console.log(e);
+        populateSpinner.stop(`Server successfully initialized${populateProducts ? ' and populated' : ''}`);
+        clearTimeout(timer);
+        /**
+         * This is currently disabled because I am running into issues actually getting the server
+         * to quite properly in response to a SIGINT.
+         * This means that the server runs, but cannot be ended, without forcefully
+         * killing the process.
+         *
+         * Once this has been resolved, the following code can be re-enabled by
+         * setting `autoRunServer` to `true`.
+         */
+        const autoRunServer = false;
+        if (mode === 'quick' && autoRunServer) {
+            // In quick-start mode, we want to now run the server and open up
+            // a browser window to the Admin UI.
+            try {
+                const adminUiUrl = `http://localhost:${port}/admin`;
+                const quickStartInstructions = [
+                    '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)}`,
+                    `Open your browser and navigate to: ${pc.green(adminUiUrl)}`,
+                    '',
+                ];
+                note(quickStartInstructions.join('\n'));
+
+                const npmCommand = os.platform() === 'win32' ? 'npm.cmd' : 'npm';
+                let quickStartProcess: ChildProcess | undefined;
+                try {
+                    quickStartProcess = spawn(npmCommand, ['run', 'dev'], {
+                        cwd: root,
+                        stdio: 'inherit',
+                    });
+                } catch (e: any) {
+                    /* empty */
+                }
+
+                // process.stdin.resume();
+                process.on('SIGINT', function () {
+                    displayOutro(root, name, superAdminCredentials);
+                    quickStartProcess?.kill('SIGINT');
+                    process.exit(0);
+                });
+
+                // Give enough time for the server to get up and running
+                // before opening the window.
+                await sleep(10_000);
+                try {
+                    await open(adminUiUrl, {
+                        newInstance: true,
+                    });
+                } catch (e: any) {
+                    /* empty */
+                }
+            } catch (e: any) {
+                log(pc.red(`Failed to start the server: ${e.message as string}`), {
+                    newline: 'after',
+                    level: 'verbose',
+                });
+            }
+        } else {
+            clearTimeout(timer);
+            displayOutro(root, name, superAdminCredentials);
+            process.exit(0);
+        }
+    } 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' : ''}`);
+}
 
-    const startCommand = packageManager === 'yarn' ? 'yarn dev' : 'npm run dev';
+function displayOutro(
+    root: string,
+    name: string,
+    superAdminCredentials?: { identifier: string; password: string },
+) {
+    const startCommand = 'npm run dev';
     const nextSteps = [
-        `${pc.green('Success!')} Created a new Vendure server at:`,
-        `\n`,
-        pc.italic(root),
-        `\n`,
-        `We suggest that you start by typing:`,
+        `Your new Vendure server was created!`,
+        pc.gray(root),
         `\n`,
+        `Next, run:`,
         pc.gray('$ ') + pc.blue(pc.bold(`cd ${name}`)),
         pc.gray('$ ') + pc.blue(pc.bold(`${startCommand}`)),
+        `\n`,
+        `This will start the server in development mode.`,
+        `To access the Admin UI, open your browser and navigate to:`,
+        `\n`,
+        pc.green(`http://localhost:3000/admin`),
+        `\n`,
+        `Use the following credentials to log in:`,
+        `Username: ${pc.green(superAdminCredentials?.identifier ?? 'superadmin')}`,
+        `Password: ${pc.green(superAdminCredentials?.password ?? 'superadmin')}`,
+        '\n',
+        '➡️ Docs: https://docs.vendure.io',
+        '➡️ Discord community: https://vendure.io/community',
+        '➡️ Star us on GitHub:',
+        '   https://github.com/vendure-ecommerce/vendure',
     ];
-    note(nextSteps.join('\n'));
+    note(nextSteps.join('\n'), pc.green('Setup complete!'));
     outro(`Happy hacking!`);
-    process.exit(0);
 }
 
 /**
@@ -299,17 +460,21 @@ export async function createVendureApp(
  */
 function runPreChecks(name: string | undefined, useNpm: boolean): name is string {
     if (typeof name === 'undefined') {
-        console.error('Please specify the project directory:');
-        console.log(`  ${pc.cyan(program.name())} ${pc.green('<project-directory>')}`);
-        console.log();
-        console.log('For example:');
-        console.log(`  ${pc.cyan(program.name())} ${pc.green('my-vendure-app')}`);
+        log(pc.red(`Please specify the project directory:`));
+        log(`  ${pc.cyan(program.name())} ${pc.green('<project-directory>')}`, { newline: 'after' });
+        log('For example:');
+        log(`  ${pc.cyan(program.name())} ${pc.green('my-vendure-app')}`);
         process.exit(1);
         return false;
     }
 
     const root = path.resolve(name);
-    fs.ensureDirSync(name);
+    try {
+        fs.ensureDirSync(name);
+    } catch (e: any) {
+        log(pc.red(`Could not create project directory ${name}: ${e.message as string}`));
+        return false;
+    }
     if (!isSafeToCreateProjectIn(root, name)) {
         process.exit(1);
     }
@@ -332,6 +497,6 @@ async function copyEmailTemplates(root: string) {
     try {
         await fs.copy(templateDir, path.join(root, 'static', 'email', 'templates'));
     } catch (err: any) {
-        console.error(pc.red('Failed to copy email templates.'));
+        log(pc.red('Failed to copy email templates.'));
     }
 }

+ 67 - 21
packages/create/src/gather-user-responses.ts

@@ -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 {
@@ -23,12 +24,72 @@ interface PromptAnswers {
 
 /* eslint-disable no-console */
 
+export async function getQuickStartConfiguration(
+    root: string,
+    packageManager: PackageManager,
+): Promise<UserResponses> {
+    // 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 ? '6543' : '',
+        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 gatherUserResponses(
+export async function getManualConfiguration(
     root: string,
-    alreadyRanScaffold: boolean,
     packageManager: PackageManager,
 ): Promise<UserResponses> {
     const dbType = (await select({
@@ -38,13 +99,12 @@ export async function gatherUserResponses(
             { label: 'MariaDB', value: 'mariadb' },
             { label: 'Postgres', value: 'postgres' },
             { label: 'SQLite', value: 'sqlite' },
-            { label: 'SQL.js', value: 'sqljs' },
         ],
         initialValue: 'sqlite' as DbType,
     })) as DbType;
     checkCancel(dbType);
 
-    const hasConnection = dbType !== 'sqlite' && dbType !== 'sqljs';
+    const hasConnection = dbType !== 'sqlite';
     const dbHost = hasConnection
         ? await text({
               message: "What's the database host address?",
@@ -146,7 +206,7 @@ export async function gatherUserResponses(
 /**
  * Returns mock "user response" without prompting, for use in CI
  */
-export async function gatherCiUserResponses(
+export async function getCiConfiguration(
     root: string,
     packageManager: PackageManager,
 ): Promise<UserResponses> {
@@ -171,14 +231,6 @@ export async function gatherCiUserResponses(
     };
 }
 
-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.
  */
@@ -200,12 +252,10 @@ async function generateSources(
 
     const templateContext = {
         ...answers,
-        useYarn: packageManager === 'yarn',
         dbType: answers.dbType === 'sqlite' ? 'better-sqlite3' : answers.dbType,
         name: path.basename(root),
         isSQLite: answers.dbType === 'sqlite',
-        isSQLjs: answers.dbType === 'sqljs',
-        requiresConnection: answers.dbType !== 'sqlite' && answers.dbType !== 'sqljs',
+        requiresConnection: answers.dbType !== 'sqlite',
         cookieSecret: randomBytes(16).toString('base64url'),
     };
 
@@ -233,10 +283,6 @@ function defaultDBPort(dbType: DbType): number {
             return 3306;
         case 'postgres':
             return 5432;
-        case 'mssql':
-            return 1433;
-        case 'oracle':
-            return 1521;
         default:
             return 3306;
     }

+ 187 - 80
packages/create/src/helpers.ts

@@ -1,12 +1,15 @@
-/* eslint-disable no-console */
-import { execSync } from 'child_process';
+import { cancel, isCancel, spinner } from '@clack/prompts';
 import spawn from 'cross-spawn';
 import fs from 'fs-extra';
+import { execFile, execSync, execFileSync } from 'node:child_process';
+import { platform } from 'node:os';
+import { promisify } from 'node:util';
 import path from 'path';
 import pc from 'picocolors';
 import semver from 'semver';
 
-import { SERVER_PORT, TYPESCRIPT_VERSION } from './constants';
+import { TYPESCRIPT_VERSION } from './constants';
+import { log } from './logger';
 import { CliLogLevel, DbType } from './types';
 
 /**
@@ -46,7 +49,6 @@ export function isSafeToCreateProjectIn(root: string, name: string) {
         'tsconfig.json',
         'yarn.lock',
     ];
-    console.log();
 
     const conflicts = fs
         .readdirSync(root)
@@ -57,13 +59,13 @@ export function isSafeToCreateProjectIn(root: string, name: string) {
         .filter(file => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0));
 
     if (conflicts.length > 0) {
-        console.log(`The directory ${pc.green(name)} contains files that could conflict:`);
-        console.log();
+        log(`The directory ${pc.green(name)} contains files that could conflict:`, { newline: 'after' });
         for (const file of conflicts) {
-            console.log(`  ${file}`);
+            log(`  ${file}`);
         }
-        console.log();
-        console.log('Either try using a new directory name, or remove the files listed above.');
+        log('Either try using a new directory name, or remove the files listed above.', {
+            newline: 'before',
+        });
 
         return false;
     }
@@ -89,38 +91,23 @@ export function scaffoldAlreadyExists(root: string, name: string): boolean {
 
 export function checkNodeVersion(requiredVersion: string) {
     if (!semver.satisfies(process.version, requiredVersion)) {
-        console.error(
+        log(
             pc.red(
-                'You are running Node %s.\n' +
-                    'Vendure requires Node %s or higher. \n' +
+                `You are running Node ${process.version}.` +
+                    `Vendure requires Node ${requiredVersion} or higher.` +
                     'Please update your version of Node.',
             ),
-            process.version,
-            requiredVersion,
         );
         process.exit(1);
     }
 }
 
-export function yarnIsAvailable() {
-    try {
-        const yarnVersion = execSync('yarnpkg --version');
-        if (semver.major(yarnVersion.toString()) > 1) {
-            return true;
-        } else {
-            return false;
-        }
-    } catch (e: any) {
-        return false;
-    }
-}
-
 // Bun support should not be exposed yet, see
 // https://github.com/oven-sh/bun/issues/4947
 // https://github.com/lovell/sharp/issues/3511
 export function bunIsAvailable() {
     try {
-        execSync('bun --version', { stdio: 'ignore' });
+        execFileSync('bun', ['--version'], { stdio: 'ignore' });
         return true;
     } catch (e: any) {
         return false;
@@ -160,7 +147,7 @@ export function checkThatNpmCanReadCwd() {
     if (npmCWD === cwd) {
         return true;
     }
-    console.error(
+    log(
         pc.red(
             'Could not start an npm process in the right directory.\n\n' +
                 `The current directory is: ${pc.bold(cwd)}\n` +
@@ -169,7 +156,7 @@ export function checkThatNpmCanReadCwd() {
         ),
     );
     if (process.platform === 'win32') {
-        console.error(
+        log(
             pc.red('On Windows, this can usually be fixed by running:\n\n') +
                 `  ${pc.cyan('reg')} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
                 `  ${pc.cyan(
@@ -185,61 +172,32 @@ export function checkThatNpmCanReadCwd() {
 }
 
 /**
- * Install packages via npm or yarn.
+ * Install packages via npm.
  * Based on the install function from https://github.com/facebook/create-react-app
  */
-export function installPackages(
-    root: string,
-    useYarn: boolean,
-    dependencies: string[],
-    isDev: boolean,
-    logLevel: CliLogLevel,
-    isCi: boolean = false,
-): Promise<void> {
+export function installPackages(options: {
+    dependencies: string[];
+    isDevDependencies?: boolean;
+    logLevel: CliLogLevel;
+}): Promise<void> {
+    const { dependencies, isDevDependencies = false, logLevel } = options;
     return new Promise((resolve, reject) => {
-        let command: string;
-        let args: string[];
-        if (useYarn) {
-            command = 'yarnpkg';
-            args = ['add', '--exact', '--ignore-engines'];
-            if (isDev) {
-                args.push('--dev');
-            }
-            if (isCi) {
-                // In CI, publish to Verdaccio
-                // See https://github.com/yarnpkg/yarn/issues/6029
-                args.push('--registry http://localhost:4873/');
-                // Increase network timeout
-                // See https://github.com/yarnpkg/yarn/issues/4890#issuecomment-358179301
-                args.push('--network-timeout 300000');
-            }
-            args = args.concat(dependencies);
-
-            // Explicitly set cwd() to work around issues like
-            // https://github.com/facebook/create-react-app/issues/3326.
-            // Unfortunately we can only do this for Yarn because npm support for
-            // equivalent --prefix flag doesn't help with this issue.
-            // This is why for npm, we run checkThatNpmCanReadCwd() early instead.
-            args.push('--cwd');
-            args.push(root);
-        } else {
-            command = 'npm';
-            args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat(dependencies);
-            if (isDev) {
-                args.push('--save-dev');
-            }
+        const command = 'npm';
+        const args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat(dependencies);
+        if (isDevDependencies) {
+            args.push('--save-dev');
         }
 
         if (logLevel === 'verbose') {
             args.push('--verbose');
         }
 
-        const child = spawn(command, args, { stdio: logLevel === 'silent' ? 'ignore' : 'inherit' });
+        const child = spawn(command, args, { stdio: logLevel === 'verbose' ? 'inherit' : 'ignore' });
         child.on('close', code => {
             if (code !== 0) {
                 let message = 'An error occurred when installing dependencies.';
                 if (logLevel === 'silent') {
-                    message += ' Try running with `--log-level info` or `--log-level verbose` to diagnose.';
+                    message += ' Try running with `--log-level verbose` to diagnose.';
                 }
                 reject({
                     message,
@@ -285,15 +243,9 @@ function dbDriverPackage(dbType: DbType): string {
             return 'pg';
         case 'sqlite':
             return 'better-sqlite3';
-        case 'sqljs':
-            return 'sql.js';
-        case 'mssql':
-            return 'mssql';
-        case 'oracle':
-            return 'oracledb';
         default:
             const n: never = dbType;
-            console.error(pc.red(`No driver package configured for type "${dbType as string}"`));
+            log(pc.red(`No driver package configured for type "${dbType as string}"`));
             return '';
     }
 }
@@ -383,6 +335,133 @@ async function checkPostgresDbExists(options: any, root: string): Promise<true>
     return true;
 }
 
+/**
+ * Check to see if Docker is installed and running.
+ * If not, attempt to start it.
+ * If that is not possible, return false.
+ *
+ * Refs:
+ * - https://stackoverflow.com/a/48843074/772859
+ */
+export async function isDockerAvailable(): Promise<{ result: 'not-found' | 'not-running' | 'running' }> {
+    const dockerSpinner = spinner();
+
+    function isDaemonRunning(): boolean {
+        try {
+            execFileSync('docker', ['stats', '--no-stream'], { stdio: 'ignore' });
+            return true;
+        } catch (e: any) {
+            return false;
+        }
+    }
+
+    dockerSpinner.start('Checking for Docker');
+    try {
+        execFileSync('docker', ['-v'], { stdio: 'ignore' });
+        dockerSpinner.message('Docker was found!');
+    } catch (e: any) {
+        dockerSpinner.stop('Docker was not found on this machine. We will use SQLite for the database.');
+        return { result: 'not-found' };
+    }
+    // Now we need to check if the docker daemon is running
+    const isRunning = isDaemonRunning();
+    if (isRunning) {
+        dockerSpinner.stop('Docker is running');
+        return { result: 'running' };
+    }
+    dockerSpinner.message('Docker daemon is not running. Attempting to start');
+    // detect the current OS
+    const currentPlatform = platform();
+    try {
+        if (currentPlatform === 'win32') {
+            // https://stackoverflow.com/a/44182489/772859
+            execSync('"C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"', { stdio: 'ignore' });
+        } else if (currentPlatform === 'darwin') {
+            execSync('open -a Docker', { stdio: 'ignore' });
+        } else {
+            execSync('systemctl start docker', { stdio: 'ignore' });
+        }
+    } catch (e: any) {
+        dockerSpinner.stop('Could not start Docker.');
+        log(e.message, { level: 'verbose' });
+        return { result: 'not-running' };
+    }
+    // Verify that the daemon is now running
+    let attempts = 1;
+    do {
+        log(`Checking for Docker daemon... (attempt ${attempts})`, { level: 'verbose' });
+        if (isDaemonRunning()) {
+            log(`Docker daemon is now running (after ${attempts} attempts).`, { level: 'verbose' });
+            dockerSpinner.stop('Docker is running');
+            return { result: 'running' };
+        }
+        await new Promise(resolve => setTimeout(resolve, 50));
+        attempts++;
+    } while (attempts < 100);
+    dockerSpinner.stop('Docker daemon could not be started');
+    return { result: 'not-running' };
+}
+
+export async function startPostgresDatabase(root: string): Promise<boolean> {
+    // Now we need to run the postgres database via Docker
+    let containerName: string | undefined;
+    const postgresContainerSpinner = spinner();
+    postgresContainerSpinner.start('Starting PostgreSQL database');
+    try {
+        const result = await promisify(execFile)(`docker`, [
+            `compose`,
+            `-f`,
+            path.join(root, 'docker-compose.yml'),
+            `up`,
+            `-d`,
+            `postgres_db`,
+        ]);
+        containerName = result.stderr.match(/Container\s+(.+-postgres_db[^ ]*)/)?.[1];
+        if (!containerName) {
+            // guess the container name based on the directory name
+            containerName = path.basename(root).replace(/[^a-z0-9]/gi, '') + '-postgres_db-1';
+            postgresContainerSpinner.message(
+                'Could not find container name. Guessing it is: ' + containerName,
+            );
+            log(pc.red('Could not find container name. Guessing it is: ' + containerName), {
+                newline: 'before',
+                level: 'verbose',
+            });
+        } else {
+            log(pc.green(`Started PostgreSQL database in container "${containerName}"`), {
+                newline: 'before',
+                level: 'verbose',
+            });
+        }
+    } catch (e: any) {
+        log(pc.red(`Failed to start PostgreSQL database: ${e.message as string}`));
+        postgresContainerSpinner.stop('Failed to start PostgreSQL database');
+        return false;
+    }
+    postgresContainerSpinner.message(`Waiting for PostgreSQL database to be ready...`);
+    let attempts = 1;
+    let isReady = false;
+    do {
+        // We now need to ensure that the database is ready to accept connections
+        try {
+            const result = execFileSync(`docker`, [`exec`, `-i`, containerName, `pg_isready`]);
+            isReady = result?.toString().includes('accepting connections');
+            if (!isReady) {
+                log(pc.yellow(`PostgreSQL database not yet ready. Attempt ${attempts}...`), {
+                    level: 'verbose',
+                });
+            }
+        } catch (e: any) {
+            // ignore
+            log('is_ready error:' + (e.message as string), { level: 'verbose', newline: 'before' });
+        }
+        await new Promise(resolve => setTimeout(resolve, 50));
+        attempts++;
+    } while (!isReady && attempts < 100);
+    postgresContainerSpinner.stop('PostgreSQL database is ready');
+    return true;
+}
+
 function throwConnectionError(err: any) {
     throw new Error(
         'Could not connect to the database. ' +
@@ -420,7 +499,35 @@ export function isServerPortInUse(port: number): Promise<boolean> {
     try {
         return tcpPortUsed.check(port);
     } catch (e: any) {
-        console.log(pc.yellow(`Warning: could not determine whether port ${port} is available`));
+        log(pc.yellow(`Warning: could not determine whether port ${port} is available`));
         return Promise.resolve(false);
     }
 }
+
+/**
+ * Checks if the response from a Clack prompt was a cancellation symbol, and if so,
+ * ends the interactive process.
+ */
+export function checkCancel<T>(value: T | symbol): value is T {
+    if (isCancel(value)) {
+        cancel('Setup cancelled.');
+        process.exit(0);
+    }
+    return true;
+}
+
+export function cleanUpDockerResources(name: string) {
+    try {
+        execSync(`docker stop $(docker ps -a -q --filter "label=io.vendure.create.name=${name}")`, {
+            stdio: 'ignore',
+        });
+        execSync(`docker rm $(docker ps -a -q --filter "label=io.vendure.create.name=${name}")`, {
+            stdio: 'ignore',
+        });
+        execSync(`docker volume rm $(docker volume ls --filter "label=io.vendure.create.name=${name}" -q)`, {
+            stdio: 'ignore',
+        });
+    } catch (e) {
+        log(pc.yellow(`Could not clean up Docker resources`), { level: 'verbose' });
+    }
+}

+ 24 - 0
packages/create/src/logger.ts

@@ -0,0 +1,24 @@
+/* eslint-disable no-console */
+import { CliLogLevel } from './types';
+
+let logLevel: CliLogLevel = 'info';
+
+export function setLogLevel(level: CliLogLevel = 'info') {
+    logLevel = level;
+}
+
+export function log(
+    message?: string,
+    options?: { level?: CliLogLevel; newline?: 'before' | 'after' | 'both' },
+) {
+    const { level = 'info' } = options || {};
+    if (logLevel !== 'silent' && (logLevel === 'verbose' || level === 'info')) {
+        if (options?.newline === 'before' || options?.newline === 'both') {
+            console.log();
+        }
+        console.log('   ' + (message ?? ''));
+        if (options?.newline === 'after' || options?.newline === 'both') {
+            console.log();
+        }
+    }
+}

+ 2 - 2
packages/create/src/types.ts

@@ -1,4 +1,4 @@
-export type DbType = 'mysql' | 'mariadb' | 'postgres' | 'sqlite' | 'sqljs' | 'mssql' | 'oracle';
+export type DbType = 'mysql' | 'mariadb' | 'postgres' | 'sqlite';
 
 export interface FileSources {
     indexSource: string;
@@ -18,6 +18,6 @@ export interface UserResponses extends FileSources {
     superadminPassword: string;
 }
 
-export type PackageManager = 'npm' | 'yarn';
+export type PackageManager = 'npm';
 
 export type CliLogLevel = 'silent' | 'info' | 'verbose';

+ 3 - 3
packages/create/templates/Dockerfile.hbs

@@ -3,7 +3,7 @@ FROM node:20
 WORKDIR /usr/src/app
 
 COPY package.json ./
-COPY {{#if useYarn}}yarn.lock{{else}}package-lock.json{{/if}} ./
-RUN {{#if useYarn}}yarn{{else}}npm install{{/if}} --production
+COPY package-lock.json ./
+RUN npm install --production
 COPY . .
-RUN {{#if useYarn}}yarn{{else}}npm run{{/if}} build
+RUN npm run build

+ 114 - 38
packages/create/templates/docker-compose.hbs

@@ -1,39 +1,115 @@
-version: "3"
+# INFORMATION
+# We are not exposing the default ports for the services in this file.
+# This is to avoid conflicts with existing services on your machine.
+# In case you don't have any services running on the default ports, you can expose them by changing the
+# ports section in the services block. Please don't forget to update the ports in the .env file as well.
+
 services:
-  server:
-    build:
-      context: .
-      dockerfile: Dockerfile
-    ports:
-      - 3000:3000
-    command: [{{#if useYarn}}"yarn"{{else}}"npm", "run"{{/if}}, "start:server"]
-    volumes:
-      - /usr/src/app
-    environment:
-      DB_HOST: database
-      DB_PORT: 5432
-      DB_NAME: vendure
-      DB_USERNAME: postgres
-      DB_PASSWORD: password
-  worker:
-    build:
-      context: .
-      dockerfile: Dockerfile
-    command: [{{#if useYarn}}"yarn"{{else}}"npm", "run"{{/if}}, "start:worker"]
-    volumes:
-      - /usr/src/app
-    environment:
-      DB_HOST: database
-      DB_PORT: 5432
-      DB_NAME: vendure
-      DB_USERNAME: postgres
-      DB_PASSWORD: password
-  database:
-    image: postgres
-    volumes:
-      - /var/lib/postgresql/data
-    ports:
-      - 5432:5432
-    environment:
-      POSTGRES_PASSWORD: password
-      POSTGRES_DB: vendure
+    postgres_db:
+        image: postgres:16-alpine
+        volumes:
+            - postgres_db_data:/var/lib/postgresql/data
+        ports:
+            - "6543:5432"
+        environment:
+            POSTGRES_DB: {{{ escapeSingle dbName }}}
+            POSTGRES_USER: {{{ escapeSingle dbUserName }}}
+            POSTGRES_PASSWORD: {{{ escapeSingle dbPassword }}}
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+
+    mysql_db:
+        image: mysql:8
+        volumes:
+            - mysql_db_data:/var/lib/mysql
+        environment:
+            MYSQL_ROOT_PASSWORD: 'ROOT'
+            MYSQL_DATABASE: {{{ escapeSingle dbName }}}
+            MYSQL_USER: {{{ escapeSingle dbUserName }}}
+            MYSQL_PASSWORD: {{{ escapeSingle dbPassword }}}
+        ports:
+            - "4306:3306"
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+
+    mariadb_db:
+        image: mariadb:10
+        volumes:
+            - mariadb_db_data:/var/lib/mysql
+        environment:
+            MARIADB_ROOT_PASSWORD: 'ROOT'
+            MARIADB_DATABASE: {{{ escapeSingle dbName }}}
+            MARIADB_USER: {{{ escapeSingle dbUserName }}}
+            MARIADB_PASSWORD: {{{ escapeSingle dbPassword }}}
+        ports:
+            - "3306:3306"
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+
+    # RECOMMENDED (especially for production)
+    # Want to use our BullMQ with Redis instead of our default database job queue?
+    # Checkout our BullMQ plugin: https://docs.vendure.io/reference/core-plugins/job-queue-plugin/bull-mqjob-queue-plugin/
+    redis:
+        image: redis:7-alpine
+        ports:
+            - "6479:6379"
+        volumes:
+            - redis_data:/data
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+
+    # RECOMMENDED
+    # Want to use Typesense instead of our default search engine?
+    # Checkout our advanced search plugin: https://vendure.io/hub/vendure-plus-advanced-search-plugin
+    # To run the typesense container run "docker compose up -d typesense"
+    typesense:
+        image: typesense/typesense:27
+        command: [ '--data-dir', '/data', '--api-key', 'SuperSecret' ]
+        ports:
+            - "8208:8108"
+        volumes:
+            - typesense_data:/data
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+
+    # Want to use Elasticsearch instead of our default database engine?
+    # Checkout our Elasticsearch plugin: https://docs.vendure.io/reference/core-plugins/elasticsearch-plugin/
+    # To run the elasticsearch container run "docker compose up -d elasticsearch"
+    elasticsearch:
+        image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
+        environment:
+            discovery.type: single-node
+            bootstrap.memory_lock: true
+            ES_JAVA_OPTS: -Xms512m -Xmx512m
+        volumes:
+            - elasticsearch_data:/usr/share/elasticsearch/data
+        ports:
+            - "9300:9200"
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+
+volumes:
+    postgres_db_data:
+        driver: local
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+    mysql_db_data:
+        driver: local
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+    mariadb_db_data:
+        driver: local
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+    typesense_data:
+        driver: local
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+    elasticsearch_data:
+        driver: local
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"
+    redis_data:
+        driver: local
+        labels:
+            - "io.vendure.create.name={{{ escapeSingle name }}}"

+ 5 - 5
packages/create/templates/readme.hbs

@@ -17,7 +17,7 @@ Useful links:
 ## Development
 
 ```
-{{#if useYarn}}yarn dev{{else}}npm run dev{{/if}}
+npm run dev
 ```
 
 will start the Vendure server and [worker](https://www.vendure.io/docs/developer-guide/vendure-worker/) processes from
@@ -26,7 +26,7 @@ the `src` directory.
 ## Build
 
 ```
-{{#if useYarn}}yarn build{{else}}npm run build{{/if}}
+npm run build
 ```
 
 will compile the TypeScript sources into the `/dist` directory.
@@ -41,7 +41,7 @@ hosting environment.
 You can run the built files directly with the `start` script:
 
 ```
-{{#if useYarn}}yarn start{{else}}npm run start{{/if}}
+npm run start
 ```
 
 You could also consider using a process manager like [pm2](https://pm2.keymetrics.io/) to run and manage
@@ -92,7 +92,7 @@ These should be located in the `./src/plugins` directory.
 To create a new plugin run:
 
 ```
-{{#if useYarn}}yarn{{else}}npx{{/if}} vendure add
+npx vendure add
 ```
 
 and select `[Plugin] Create a new Vendure plugin`.
@@ -105,7 +105,7 @@ will be required whenever you make changes to the `customFields` config or defin
 To generate a new migration, run:
 
 ```
-{{#if useYarn}}yarn{{else}}npx{{/if}} vendure migrate
+npx vendure migrate
 ```
 
 The generated migration file will be found in the `./src/migrations/` directory, and should be committed to source control.