Browse Source

feat: create @vendure/create package

Relates to #84
Michael Bromley 6 years ago
parent
commit
7c633860da

+ 0 - 52
packages/core/build/gulpfile.ts

@@ -3,67 +3,15 @@ import fs from 'fs-extra';
 import { dest, parallel, series, src } from 'gulp';
 import path from 'path';
 
-import { initialData } from '../mock-data/data-sources/initial-data';
-
-// tslint:disable:no-console
-
 function copySchemas() {
     return src(['../src/**/*.graphql']).pipe(dest('../dist'));
 }
 
-function copyEmailTemplates() {
-    return src(['../src/email/templates/**/*']).pipe(dest('../dist/cli/assets/email-templates'));
-}
-
 function copyI18nMessages() {
     return src(['../src/i18n/messages/**/*']).pipe(dest('../dist/i18n/messages'));
 }
 
-function copyCliAssets() {
-    return src(['../cli/assets/**/*']).pipe(dest('../dist/cli/assets'));
-}
-
-function copyCliImages() {
-    return src(['../mock-data/assets/**/*']).pipe(dest('../dist/cli/assets/images'));
-}
-
-function copyCliProductData() {
-    return src(['../mock-data/data-sources/products.csv']).pipe(dest('../dist/cli/assets'));
-}
-
-function copyCliInitialData() {
-    return fs.outputFile(
-        '../dist/cli/assets/initial-data.json',
-        JSON.stringify(initialData, null, 2),
-        'utf-8',
-    );
-}
-
-function buildAdminUi() {
-    return exec(
-        'yarn build --prod=true',
-        {
-            cwd: path.join(__dirname, '../../../admin-ui'),
-        },
-        error => {
-            if (error) {
-                console.log(error);
-            }
-        },
-    );
-}
-
-function copyAdminUi() {
-    return src(['../../../admin-ui/dist/vendure-admin/**/*']).pipe(dest('../dist/admin-ui'));
-}
-
 export const build = parallel(
     copySchemas,
-    copyEmailTemplates,
     copyI18nMessages,
-    copyCliAssets,
-    copyCliImages,
-    copyCliProductData,
-    copyCliInitialData,
-    // series(buildAdminUi, copyAdminUi),
 );

+ 1 - 2
packages/core/build/tsconfig.cli.json

@@ -1,8 +1,7 @@
 {
   "extends": "../tsconfig.json",
   "compilerOptions": {
-    "outDir": "../dist/cli",
-    "declaration": false
+    "outDir": "../dist/cli"
   },
   "files": [
     "../cli/vendure-cli.ts",

+ 22 - 25
packages/core/cli/populate.ts

@@ -15,28 +15,32 @@ try {
 }
 
 // tslint:disable:no-console
-export async function populateFromCli() {
-    logColored('\nPopulating... (this may take a minute or two)\n');
-    const initialDataPath = './assets/initial-data.json';
-    const sampleProductsFile = path.join(__dirname, 'assets', 'products.csv');
-    const imageSourcePath = path.join(__dirname, 'assets', 'images');
-    const app = await populate(getApplicationRef, initialDataPath, sampleProductsFile, imageSourcePath);
-    await app.close();
-    process.exit(0);
-}
-
-export async function populate(bootstrapFn: () => Promise<INestApplication | undefined>,
-                               initialDataPath: string,
-                               productsCsvPath: string,
-                               imageSourcePath: string): Promise<INestApplication> {
+export async function populate(
+    bootstrapFn: () => Promise<INestApplication | undefined>,
+    initialDataPath: string,
+): Promise<INestApplication>;
+export async function populate(
+    bootstrapFn: () => Promise<INestApplication | undefined>,
+    initialDataPath: string,
+    productsCsvPath: string,
+    imageSourcePath: string,
+): Promise<INestApplication>;
+export async function populate(
+    bootstrapFn: () => Promise<INestApplication | undefined>,
+    initialDataPath: string,
+    productsCsvPath?: string,
+    imageSourcePath?: string,
+): Promise<INestApplication> {
     const app = await bootstrapFn();
     if (!app) {
         throw new Error('Could not bootstrap the Vendure app');
     }
     const initialData = require(initialDataPath);
     await populateInitialData(app, initialData);
-    await populateProducts(app, initialData, productsCsvPath, imageSourcePath);
-    await populateCollections(app, initialData);
+    if (productsCsvPath && imageSourcePath) {
+        await importProductsFromFile(app, productsCsvPath, initialData.defaultLanguage);
+        await populateCollections(app, initialData);
+    }
     logColored('\nDone!');
     return app;
 }
@@ -108,7 +112,7 @@ async function getApplicationRef(): Promise<INestApplication | undefined> {
     return app;
 }
 
-async function populateInitialData(app: INestApplication, initialData: any) {
+async function populateInitialData(app: INestApplication, initialData: object) {
     const populator = app.get(Populator);
     try {
         await populator.populateInitialData(initialData);
@@ -117,7 +121,7 @@ async function populateInitialData(app: INestApplication, initialData: any) {
     }
 }
 
-async function populateCollections(app: INestApplication, initialData: any) {
+async function populateCollections(app: INestApplication, initialData: object) {
     const populator = app.get(Populator);
     try {
         await populator.populateCollections(initialData);
@@ -126,13 +130,6 @@ async function populateCollections(app: INestApplication, initialData: any) {
     }
 }
 
-async function populateProducts(app: INestApplication, initialData: any, csvPath: string, imageSourcePath: string) {
-    // copy the images to the import folder
-    const destination = path.join(process.cwd(), 'vendure', 'import-assets');
-    await fs.copy(imageSourcePath, destination);
-    await importProductsFromFile(app, csvPath, initialData.defaultLanguage);
-}
-
 async function importProductsFromFile(app: INestApplication, csvPath: string, languageCode: string) {
     // import the csv of same product data
     const importer = app.get(Importer);

+ 1 - 26
packages/core/cli/vendure-cli.ts

@@ -4,8 +4,7 @@ import path from 'path';
 import prompts from 'prompts';
 
 import { logColored } from './cli-utils';
-import { init } from './init';
-import { importProducts, populateFromCli } from './populate';
+import { importProducts } from './populate';
 // tslint:disable-next-line:no-var-requires
 const version = require('../../package.json').version;
 
@@ -20,30 +19,6 @@ logColored(`
                                        `);
 
 program.version(`Vendure CLI v${version}`, '-v --version').name('vendure');
-program
-    .command('init')
-    .description('Initialize a new Vendure server application')
-    .action(async (command: any) => {
-        const indexFile = await init();
-        const answer = await prompts({
-            type: 'toggle',
-            name: 'populate',
-            message: 'Populate the database with some data to get you started (recommended)?',
-            active: 'yes',
-            inactive: 'no',
-            initial: true as any,
-        });
-        if (answer.populate) {
-            await populateFromCli();
-        }
-        logColored(`\nAll done! Run "${indexFile}" to start the server.`);
-    });
-program
-    .command('populate')
-    .description('Populate a new Vendure server instance with some initial data')
-    .action(async () => {
-        await populateFromCli();
-    });
 program
     .command('import-products <csvFile>')
     .option('-l, --language', 'Specify ISO 639-1 language code, e.g. "de", "es". Defaults to "en"')

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "0.1.0-alpha.17",
+  "version": "0.1.0-alpha.18",
   "description": "A modern, headless ecommerce framework",
   "repository": {
     "type": "git",

+ 1 - 1
packages/core/src/api/common/request-context.service.ts

@@ -88,6 +88,6 @@ export class RequestContextService {
     private arraysIntersect<T>(arr1: T[], arr2: T[]): boolean {
         return arr1.reduce((intersects, role) => {
             return intersects || arr2.includes(role);
-        }, false);
+        }, false as boolean);
     }
 }

+ 1 - 2
packages/core/tsconfig.json

@@ -5,7 +5,6 @@
     "removeComments": true,
     "strictPropertyInitialization": false,
     "sourceMap": true,
-    "newLine": "LF",
-    "allowJs": true
+    "newLine": "LF"
   }
 }

+ 2 - 0
packages/create/.gitignore

@@ -0,0 +1,2 @@
+lib
+assets

+ 0 - 0
packages/create/.npmignore


+ 36 - 0
packages/create/build.ts

@@ -0,0 +1,36 @@
+import fs from 'fs-extra';
+import path from 'path';
+
+import { initialData } from '../core/mock-data/data-sources/initial-data';
+
+const dataDir = path.join(__dirname, '../core/mock-data');
+
+function copyTemplates() {
+    return fs.copy('./templates', './assets');
+}
+
+function copyImages() {
+    return fs.copy(path.join(dataDir, 'assets'), './assets/images');
+}
+
+function copyProductData() {
+    return fs.copy(path.join(dataDir, 'data-sources/products.csv'), './assets/products.csv');
+}
+
+function copyCliInitialData() {
+    return fs.outputFile(
+        './assets/initial-data.json',
+        JSON.stringify(initialData, null, 2),
+        'utf-8',
+    );
+}
+
+copyTemplates()
+    .then(copyImages)
+    .then(copyProductData)
+    .then(copyCliInitialData)
+    .then(() => process.exit(0))
+    .catch(err => {
+        console.error(err);
+        process.exit(1);
+    });

+ 2 - 0
packages/create/index.js

@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require('./lib/create-vendure-app');

+ 38 - 0
packages/create/package.json

@@ -0,0 +1,38 @@
+{
+  "name": "@vendure/create",
+  "version": "0.1.0-alpha.17",
+  "bin": {
+    "create": "./index.js"
+  },
+  "files": [
+    "index.js",
+    "lib/**/*",
+    "assets/**/*"
+  ],
+  "scripts": {
+    "copy-assets": "rimraf assets && ts-node ./build.ts",
+    "build": "yarn copy-assets && rimraf lib && tsc -p ./tsconfig.build.json"
+  },
+  "devDependencies": {
+    "@types/cross-spawn": "^6.0.0",
+    "@types/fs-extra": "^5.0.4",
+    "@types/handlebars": "^4.1.0",
+    "@types/listr": "^0.13.0",
+    "@types/semver": "^6.0.0",
+    "@vendure/common": ">=0.1.0-alpha.1",
+    "@vendure/core": ">=0.1.0-alpha.1",
+    "rimraf": "^2.6.3",
+    "ts-node": "^8.0.3",
+    "typescript": "^3.3.4000"
+  },
+  "dependencies": {
+    "chalk": "^2.4.2",
+    "commander": "^2.19.0",
+    "cross-spawn": "^6.0.5",
+    "fs-extra": "^7.0.1",
+    "handlebars": "^4.1.1",
+    "listr": "^0.14.3",
+    "prompts": "^2.0.1",
+    "semver": "^6.0.0"
+  }
+}

+ 185 - 0
packages/create/src/create-vendure-app.ts

@@ -0,0 +1,185 @@
+/* tslint:disable:no-console */
+import chalk from 'chalk';
+import program from 'commander';
+import fs from 'fs-extra';
+import Listr from 'listr';
+import os from 'os';
+import path from 'path';
+
+import { gatherUserResponses } from './gather-user-responses';
+import { checkNodeVersion, checkThatNpmCanReadCwd, getDependencies, installPackages, isSafeToCreateProjectIn, shouldUseYarn } from './helpers';
+
+// tslint:disable-next-line:no-var-requires
+const packageJson = require('../package.json');
+const REQUIRED_NODE_VERSION = '>=8.9.0';
+checkNodeVersion(REQUIRED_NODE_VERSION);
+
+let projectName: string | undefined;
+
+program
+    .version(packageJson.version)
+    .arguments('<project-directory>')
+    .usage(`${chalk.green('<project-directory>')} [options]`)
+    .action(name => {
+        projectName = name;
+    })
+    .option('--verbose', 'print additional logs')
+    .option('--use-npm')
+    .parse(process.argv);
+
+createApp(projectName, program.useNpm, program.verbose);
+
+async function createApp(name: string | undefined, useNpm: boolean, verbose: boolean) {
+    if (!runPreChecks(name, useNpm)) {
+        return;
+    }
+
+    const root = path.resolve(name);
+    const appName = path.basename(root);
+    const { dbType, usingTs, configSource, indexSource, populateProducts } = await gatherUserResponses(root);
+
+    const packageJsonContents = {
+        name: appName,
+        version: '0.1.0',
+        private: true,
+        scripts: {
+            start: usingTs ? 'ts-node index.ts' : 'node index.js',
+        },
+    };
+    const useYarn = useNpm ? false : shouldUseYarn();
+    const originalDirectory = process.cwd();
+    process.chdir(root);
+    if (!useYarn && !checkThatNpmCanReadCwd()) {
+        process.exit(1);
+    }
+
+    const tasks = new Listr([
+        {
+            title: 'Installing dependencies',
+            task: async () => {
+                fs.writeFileSync(
+                    path.join(root, 'package.json'),
+                    JSON.stringify(packageJsonContents, null, 2) + os.EOL,
+                );
+                const { dependencies, devDependencies } = getDependencies(usingTs, dbType);
+                await installPackages(root, useYarn, dependencies, false, verbose);
+                await installPackages(root, useYarn, devDependencies, true, verbose);
+            },
+        },
+        {
+            title: 'Generating app scaffold',
+            task: async (ctx) => {
+                const assetPath = (fileName: string) => path.join(__dirname, '../assets', fileName);
+                const rootPathScript = (fileName: string): string => path.join(root, `${fileName}.${usingTs ? 'ts' : 'js'}`);
+                ctx.configFile = rootPathScript('vendure-config');
+                await fs.writeFile(ctx.configFile, configSource);
+                await fs.writeFile(rootPathScript('index'), indexSource);
+                if (usingTs) {
+                    await fs.copyFile(assetPath('tsconfig.template.json'), path.join(root, 'tsconfig.json'));
+                }
+                await createDirectoryStructure(root);
+                await copyEmailTemplates(root);
+            },
+        },
+        {
+            title: 'Initializing server',
+            task: async (ctx) => {
+                try {
+                    if (usingTs) {
+                        // register ts-node so that the config file can be loaded
+                        require(path.join(root, 'node_modules/ts-node')).register();
+                    }
+                    const { populate } = await import(path.join(root, 'node_modules/@vendure/core/dist/cli/populate'));
+                    const { bootstrap } = await import(path.join(root, 'node_modules/@vendure/core/dist/bootstrap'));
+                    const { config } = await import(ctx.configFile);
+                    const assetsDir = path.join(__dirname, '../assets');
+
+                    const initialDataPath = path.join(assetsDir, 'initial-data.json');
+                    const bootstrapFn = () => bootstrap({
+                        ...config,
+                        dbConnectionOptions: {
+                            ...config.dbConnectionOptions,
+                            synchronize: true,
+                        },
+                        importExportOptions: {
+                            importAssetsDir: path.join(assetsDir, 'images'),
+                        },
+                    });
+                    let app: any;
+                    if (populateProducts) {
+                        app = await populate(
+                            bootstrapFn,
+                            initialDataPath,
+                            path.join(assetsDir, 'products.csv'),
+                            path.join(assetsDir, 'images'),
+                        );
+                    } else {
+                        app = await populate(
+                            bootstrapFn,
+                            initialDataPath,
+                        );
+                    }
+                    await app.close();
+                } catch (e) {
+                    console.log(e);
+                    throw e;
+                }
+            },
+        },
+    ]);
+
+    try {
+        await tasks.run();
+    } catch (e) {
+        console.error(chalk.red(e));
+    }
+    process.exit(0);
+}
+
+/**
+ * Run some initial checks to ensure that it is okay to proceed with creating
+ * a new Vendure project in the given location.
+ */
+function runPreChecks(name: string | undefined, useNpm: boolean): name is string {
+    if (typeof name === 'undefined') {
+        console.error('Please specify the project directory:');
+        console.log(
+            `  ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`,
+        );
+        console.log();
+        console.log('For example:');
+        console.log(`  ${chalk.cyan(program.name())} ${chalk.green('my-vendure-app')}`);
+        process.exit(1);
+        return false;
+    }
+
+    const root = path.resolve(name);
+    fs.ensureDirSync(name);
+    if (!isSafeToCreateProjectIn(root, name)) {
+        process.exit(1);
+    }
+    console.log(`Creating a new Vendure app in ${chalk.green(root)}.`);
+    console.log();
+    return true;
+}
+
+/**
+ * Generate the default directory structure for a new Vendure project
+ */
+async function createDirectoryStructure(root: string) {
+    await fs.ensureDir(path.join(root, 'vendure', 'email', 'test-emails'));
+    await fs.ensureDir(path.join(root, 'vendure', 'import-assets'));
+    await fs.ensureDir(path.join(root, 'vendure', 'assets'));
+}
+
+/**
+ * Copy the email templates into the app
+ */
+async function copyEmailTemplates(root: string) {
+    const templateDir = path.join(root, 'node_modules/@vendure/email-plugin/templates');
+    try {
+        await fs.copy(templateDir, path.join(root, 'vendure', 'email', 'templates'));
+    } catch (err) {
+        console.error(chalk.red(`Failed to copy email templates.`));
+    }
+}

+ 57 - 72
packages/core/cli/init.ts → packages/create/src/gather-user-responses.ts

@@ -1,3 +1,4 @@
+import chalk from 'chalk';
 import fs from 'fs-extra';
 import Handlebars from 'handlebars';
 import path from 'path';
@@ -5,29 +6,27 @@ import { PromptObject } from 'prompts';
 import prompts from 'prompts';
 
 // tslint:disable:no-console
-export async function init(): Promise<string> {
-    function defaultPort(_dbType: string) {
-        switch (_dbType) {
-            case 'mysql':
-                return 3306;
-            case 'postgres':
-                return 5432;
-            case 'mssql':
-                return 1433;
-            case 'oracle':
-                return 1521;
-            default:
-                return 3306;
-        }
-    }
 
+export type DbType = 'mysql' | 'postgres' | 'sqlite' | 'sqljs' | 'mssql' | 'oracle';
+export interface UserResponses {
+    usingTs: boolean;
+    dbType: DbType;
+    populateProducts: boolean;
+    indexSource: string;
+    configSource: string;
+}
+
+/**
+ * Prompts the user to determine how the new Vendure app should be configured.
+ */
+export async function gatherUserResponses(root: string): Promise<UserResponses> {
     function onSubmit(prompt: PromptObject, answer: any) {
         if (prompt.name === 'dbType') {
             dbType = answer;
         }
     }
 
-    let dbType: string;
+    let dbType: DbType;
 
     console.log(`Let's get started with a new Vendure server!\n`);
 
@@ -42,8 +41,9 @@ export async function init(): Promise<string> {
                     { title: 'Postgres', value: 'postgres' },
                     { title: 'SQLite', value: 'sqlite' },
                     { title: 'SQL.js', value: 'sqljs' },
-                    { title: 'MS SQL Server', value: 'mssql' },
-                    { title: 'Oracle', value: 'oracle' },
+                    // Don't show these until they have been tested.
+                    // { title: 'MS SQL Server', value: 'mssql' },
+                    // { title: 'Oracle', value: 'oracle' },
                 ],
                 initial: 0 as any,
             },
@@ -57,19 +57,13 @@ export async function init(): Promise<string> {
                 type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
                 name: 'dbPort',
                 message: `What port is the database listening on?`,
-                initial: (() => defaultPort(dbType)) as any,
+                initial: (() => defaultDBPort(dbType)) as any,
             },
             {
-                type: 'text',
+                type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
                 name: 'dbName',
-                message: () =>
-                    dbType === 'sqlite' || dbType === 'sqljs'
-                        ? `What is the path to the database file?`
-                        : `What's the name of the database?`,
-                initial: (() =>
-                    dbType === 'sqlite' || dbType === 'sqljs'
-                        ? path.join(process.cwd(), 'vendure.sqlite')
-                        : 'vendure') as any,
+                message: `What's the name of the database?`,
+                initial: 'vendure',
             },
             {
                 type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
@@ -89,6 +83,14 @@ export async function init(): Promise<string> {
                 choices: [{ title: 'TypeScript', value: 'ts' }, { title: 'JavaScript', value: 'js' }],
                 initial: 0 as any,
             },
+            {
+                type: 'toggle',
+                name: 'populateProducts',
+                message: 'Populate with some sample product data?',
+                initial: true,
+                active: 'yes',
+                inactive: 'no',
+            },
         ],
         {
             onSubmit,
@@ -103,39 +105,21 @@ export async function init(): Promise<string> {
         process.exit(0);
     }
 
-    await createDirectoryStructure();
-    await copyEmailTemplates();
-    return createFilesForBootstrap(answers);
-}
-
-/**
- * Generate the default directory structure for a new Vendure project
- */
-async function createDirectoryStructure() {
-    const cwd = process.cwd();
-    await fs.ensureDir(path.join(cwd, 'vendure', 'email', 'test-emails'));
-    await fs.ensureDir(path.join(cwd, 'vendure', 'import-assets'));
-    await fs.ensureDir(path.join(cwd, 'vendure', 'assets'));
-}
-
-/**
- * Copy the email templates into the app
- */
-async function copyEmailTemplates() {
-    const templateDir = path.join(__dirname, 'assets', 'email-templates');
-    try {
-        await fs.copy(templateDir, path.join(process.cwd(), 'vendure', 'email', 'templates'));
-    } catch (err) {
-        console.error(`Failed to copy email templates.`);
-    }
+    const { indexSource, configSource } = await generateSources(root, answers);
+    return {
+        indexSource,
+        configSource,
+        usingTs: answers.language === 'ts',
+        dbType: answers.dbType,
+        populateProducts: answers.populateProducts,
+    };
 }
 
 /**
- * Create the server index and config files based on the options specified by the CLI prompts.
+ * Create the server index and config source code based on the options specified by the CLI prompts.
  */
-async function createFilesForBootstrap(answers: any): Promise<string> {
-    const cwd = process.cwd();
-    const filePath = (fileName: string): string => path.join(cwd, `${fileName}.${answers.language}`);
+async function generateSources(root: string, answers: any): Promise<{ indexSource: string; configSource: string; }> {
+    const assetPath = (fileName: string) => path.join(__dirname, '../assets', fileName);
 
     const templateContext = {
         ...answers,
@@ -143,27 +127,28 @@ async function createFilesForBootstrap(answers: any): Promise<string> {
         isSQLite: answers.dbType === 'sqlite',
         isSQLjs: answers.dbType === 'sqljs',
         requiresConnection: answers.dbType !== 'sqlite' && answers.dbType !== 'sqljs',
-        normalizedDbName:
-            answers.dbType === 'sqlite' || answers.dbType === 'sqljs'
-                ? normalizeFilePath(answers.dbName)
-                : answers.dbName,
         sessionSecret: Math.random()
             .toString(36)
             .substr(3),
     };
-    const configTemplate = await fs.readFile(path.join(__dirname, 'assets', 'vendure-config.hbs'), 'utf-8');
+    const configTemplate = await fs.readFile(assetPath('vendure-config.hbs'), 'utf-8');
     const configSource = Handlebars.compile(configTemplate)(templateContext);
-    await fs.writeFile(filePath('vendure-config'), configSource);
-    const indexTemplate = await fs.readFile(path.join(__dirname, 'assets', 'index.hbs'), 'utf-8');
+    const indexTemplate = await fs.readFile(assetPath('index.hbs'), 'utf-8');
     const indexSource = Handlebars.compile(indexTemplate)(templateContext);
-    await fs.writeFile(filePath('index'), indexSource);
-
-    return filePath('index');
+    return { indexSource, configSource };
 }
 
-/**
- * Escape backslashed for Windows file paths.
- */
-function normalizeFilePath(filePath: string): string {
-    return String.raw`${filePath}`.replace(/\\/g, '\\\\');
+function defaultDBPort(dbType: DbType): number {
+    switch (dbType) {
+        case 'mysql':
+            return 3306;
+        case 'postgres':
+            return 5432;
+        case 'mssql':
+            return 1433;
+        case 'oracle':
+            return 1521;
+        default:
+            return 3306;
+    }
 }

+ 263 - 0
packages/create/src/helpers.ts

@@ -0,0 +1,263 @@
+/* tslint:disable:no-console */
+import chalk from 'chalk';
+import { execSync } from 'child_process';
+import spawn from 'cross-spawn';
+import fs from 'fs-extra';
+import path from 'path';
+import semver from 'semver';
+
+import { DbType } from './gather-user-responses';
+
+/**
+ * If project only contains files generated by GH, it’s safe.
+ * Also, if project contains remnant error logs from a previous
+ * installation, lets remove them now.
+ * We also special case IJ-based products .idea because it integrates with CRA:
+ * https://github.com/facebook/create-react-app/pull/368#issuecomment-243446094
+ */
+export function isSafeToCreateProjectIn(root: string, name: string) {
+    // These files should be allowed to remain on a failed install,
+    // but then silently removed during the next create.
+    const errorLogFilePatterns = [
+        'npm-debug.log',
+        'yarn-error.log',
+        'yarn-debug.log',
+    ];
+    const validFiles = [
+        '.DS_Store',
+        'Thumbs.db',
+        '.git',
+        '.gitignore',
+        '.idea',
+        'README.md',
+        'LICENSE',
+        '.hg',
+        '.hgignore',
+        '.hgcheck',
+        '.npmignore',
+        'mkdocs.yml',
+        'docs',
+        '.travis.yml',
+        '.gitlab-ci.yml',
+        '.gitattributes',
+    ];
+    console.log();
+
+    const conflicts = fs
+        .readdirSync(root)
+        .filter(file => !validFiles.includes(file))
+        // IntelliJ IDEA creates module files before CRA is launched
+        .filter(file => !/\.iml$/.test(file))
+        // Don't treat log files from previous installation as conflicts
+        .filter(
+            file => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0),
+        );
+
+    if (conflicts.length > 0) {
+        console.log(
+            `The directory ${chalk.green(name)} contains files that could conflict:`,
+        );
+        console.log();
+        for (const file of conflicts) {
+            console.log(`  ${file}`);
+        }
+        console.log();
+        console.log(
+            'Either try using a new directory name, or remove the files listed above.',
+        );
+
+        return false;
+    }
+
+    // Remove any remnant files from a previous installation
+    const currentFiles = fs.readdirSync(path.join(root));
+    currentFiles.forEach(file => {
+        errorLogFilePatterns.forEach(errorLogFilePattern => {
+            // This will catch `(npm-debug|yarn-error|yarn-debug).log*` files
+            if (file.indexOf(errorLogFilePattern) === 0) {
+                fs.removeSync(path.join(root, file));
+            }
+        });
+    });
+    return true;
+}
+
+export function checkNodeVersion(requiredVersion: string) {
+    if (!semver.satisfies(process.version, requiredVersion)) {
+        console.error(
+            chalk.red(
+                'You are running Node %s.\n' +
+                'Vendure requires Node %s or higher. \n' +
+                'Please update your version of Node.',
+            ),
+            process.version,
+            requiredVersion,
+        );
+        process.exit(1);
+    }
+}
+
+export function shouldUseYarn() {
+    try {
+        execSync('yarnpkg --version', { stdio: 'ignore' });
+        return true;
+    } catch (e) {
+        return false;
+    }
+}
+
+export function checkThatNpmCanReadCwd() {
+    const cwd = process.cwd();
+    let childOutput = null;
+    try {
+        // Note: intentionally using spawn over exec since
+        // the problem doesn't reproduce otherwise.
+        // `npm config list` is the only reliable way I could find
+        // to reproduce the wrong path. Just printing process.cwd()
+        // in a Node process was not enough.
+        childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
+    } catch (err) {
+        // Something went wrong spawning node.
+        // Not great, but it means we can't do this check.
+        // We might fail later on, but let's continue.
+        return true;
+    }
+    if (typeof childOutput !== 'string') {
+        return true;
+    }
+    const lines = childOutput.split('\n');
+    // `npm config list` output includes the following line:
+    // "; cwd = C:\path\to\current\dir" (unquoted)
+    // I couldn't find an easier way to get it.
+    const prefix = '; cwd = ';
+    const line = lines.find(l => l.indexOf(prefix) === 0);
+    if (typeof line !== 'string') {
+        // Fail gracefully. They could remove it.
+        return true;
+    }
+    const npmCWD = line.substring(prefix.length);
+    if (npmCWD === cwd) {
+        return true;
+    }
+    console.error(
+        chalk.red(
+            `Could not start an npm process in the right directory.\n\n` +
+            `The current directory is: ${chalk.bold(cwd)}\n` +
+            `However, a newly started npm process runs in: ${chalk.bold(
+                npmCWD,
+            )}\n\n` +
+            `This is probably caused by a misconfigured system terminal shell.`,
+        ),
+    );
+    if (process.platform === 'win32') {
+        console.error(
+            chalk.red(`On Windows, this can usually be fixed by running:\n\n`) +
+            `  ${chalk.cyan(
+                'reg',
+            )} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
+            `  ${chalk.cyan(
+                'reg',
+            )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
+            chalk.red(`Try to run the above two lines in the terminal.\n`) +
+            chalk.red(
+                `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`,
+            ),
+        );
+    }
+    return false;
+}
+
+/**
+ * Install packages via npm or yarn.
+ * Based on the install function from https://github.com/facebook/create-react-app
+ */
+export function installPackages(root: string, useYarn: boolean, dependencies: string[], isDev: boolean, verbose: boolean): Promise<void> {
+    return new Promise((resolve, reject) => {
+        let command: string;
+        let args: string[];
+        if (useYarn) {
+            command = 'yarnpkg';
+            args = ['add', '--exact'];
+            if (isDev) {
+                args.push('--dev');
+            }
+            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');
+            }
+        }
+
+        if (verbose) {
+            args.push('--verbose');
+        } else {
+            args.push('--loglevel info')
+        }
+
+        const child = spawn(command, args, {stdio: 'inherit'});
+        child.on('close', code => {
+            if (code !== 0) {
+                reject({
+                    command: `${command} ${args.join(' ')}`,
+                });
+                return;
+            }
+            console.log(chalk.green(`Successfully installed ${dependencies.join(', ')}`));
+            resolve();
+        });
+    });
+}
+
+export function getDependencies(usingTs: boolean, dbType: DbType): { dependencies: string[]; devDependencies: string[]; } {
+    const dependencies = [
+        '@vendure/core',
+        '@vendure/email-plugin',
+        '@vendure/asset-server-plugin',
+        '@vendure/admin-ui-plugin',
+        dbDriverPackage(dbType),
+    ];
+    const devDependencies = usingTs ? ['ts-node'] : [];
+
+    return {dependencies, devDependencies};
+}
+
+/**
+ * Returns the name of the npm driver package for the
+ * selected database.
+ */
+function dbDriverPackage(dbType: DbType): string {
+    switch (dbType) {
+        case 'mysql':
+            return 'mysql';
+        case 'postgres':
+            return 'pg';
+        case 'sqlite':
+            return 'sqlite3';
+        case 'sqljs':
+            return 'sql.js';
+        case 'mssql':
+            return 'mssql';
+        case 'oracle':
+            return 'oracledb';
+        default:
+            const n: never = dbType;
+            console.error(chalk.red(`No driver package configured for type "${dbType}"`));
+            return '';
+    }
+}

+ 0 - 0
packages/core/cli/assets/index.hbs → packages/create/templates/index.hbs


+ 15 - 0
packages/create/templates/tsconfig.template.json

@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "module": "commonjs",
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "target": "es2017",
+    "strict": true,
+    "sourceMap": false,
+    "outDir": "./dist",
+    "baseUrl": "./"
+  },
+  "exclude": ["node_modules"]
+}

+ 22 - 12
packages/core/cli/assets/vendure-config.hbs → packages/create/templates/vendure-config.hbs

@@ -1,13 +1,25 @@
 {{#if isTs }}import{{ else }}const{{/if}} {
-    AdminUiPlugin,
     examplePaymentHandler,
-    DefaultAssetServerPlugin,
-    DefaultEmailPlugin,
     DefaultSearchPlugin,
     {{#if isTs}}VendureConfig,{{/if}}
 } {{#if isTs}}from '@vendure/core'; {{ else }}= require('@vendure/core');{{/if}}
 {{#if isTs }}
-import * as path from 'path';
+import { EmailPlugin } from '@vendure/email-plugin';
+{{ else }}
+const { EmailPlugin } = require('@vendure/email-plugin');
+{{/if}}
+{{#if isTs }}
+import { AssetServerPlugin } from '@vendure/asset-server-plugin';
+{{ else }}
+const { AssetServerPlugin } = require('@vendure/asset-server-plugin');
+{{/if}}
+{{#if isTs }}
+import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+{{ else }}
+const { AdminUiPlugin } = require('@vendure/admin-ui-plugin');
+{{/if}}
+{{#if isTs }}
+import path from 'path';
 {{ else }}
 const path = require('path');
 {{/if}}
@@ -27,9 +39,9 @@ const path = require('path');
         synchronize: false, // not working with SQLite/SQL.js, see https://github.com/typeorm/typeorm/issues/2576
         {{/if}}
         logging: false,
-        database: {{#if isSQLjs}}new Uint8Array([]){{else}}'{{ normalizedDbName }}'{{/if}},
+        database: {{#if isSQLjs}}new Uint8Array([]){{else}}'{{ dbName }}'{{/if}},
         {{#if isSQLjs}}
-        location: '{{ normalizedDbName }}',
+        location: path.join(__dirname, 'vendure.sqlite'),
         autoSave: true,
         {{/if}}
         {{#if requiresConnection}}
@@ -43,18 +55,16 @@ const path = require('path');
         paymentMethodHandlers: [examplePaymentHandler],
     },
     customFields: {},
-    importExportOptions: {
-        importAssetsDir: path.join(__dirname, 'vendure/import-assets'),
-    },
     plugins: [
-        new DefaultAssetServerPlugin({
+        new AssetServerPlugin({
             route: 'assets',
             assetUploadDir: path.join(__dirname, 'vendure/assets'),
             port: 3001,
         }),
-        new DefaultEmailPlugin({
-            templatePath: path.join(__dirname, 'vendure/email/templates'),
+        new EmailPlugin({
             devMode: true,
+            templatePath: path.join(__dirname, 'vendure/email/templates'),
+            outputPath: path.join(__dirname, 'vendure/email/test-emails'),
             templateVars: {
                 shopUrl: 'http://www.example.com',
             }

+ 9 - 0
packages/create/tsconfig.build.json

@@ -0,0 +1,9 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "./lib"
+  },
+  "files": [
+    "./src/create-vendure-app.ts"
+  ]
+}

+ 11 - 0
packages/create/tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "declaration": true,
+    "removeComments": true,
+    "noLib": false,
+    "skipLibCheck": true,
+    "sourceMap": true,
+    "newLine": "LF"
+  }
+}

+ 0 - 9
packages/email-plugin/build.js

@@ -1,9 +0,0 @@
-/* tslint:disable:no-console */
-const fs = require('fs-extra');
-
-fs.copy('./templates', './lib/templates')
-    .then(() => process.exit(0))
-    .catch(err => {
-        console.error(err);
-        process.exit(1);
-    });

+ 1 - 1
packages/email-plugin/package.json

@@ -5,7 +5,7 @@
   "types": "lib/index.d.ts",
   "files": ["lib/**/*", "templates/**/*"],
   "scripts": {
-    "build": "rimraf dist && tsc -p ./tsconfig.build.json && node build.js"
+    "build": "rimraf dist && tsc -p ./tsconfig.build.json"
   },
   "dependencies": {
     "dateformat": "^3.0.3",

+ 48 - 3
yarn.lock

@@ -1186,6 +1186,13 @@
   dependencies:
     "@types/express" "*"
 
+"@types/cross-spawn@^6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.0.tgz#320aaf1d1a12979f1b84fe7a5590a7e860bf3a80"
+  integrity sha512-evp2ZGsFw9YKprDbg8ySgC9NA15g3YgiI8ANkGmKKvvi0P2aDGYLPxQIC5qfeKNUOe3TjABVGuah6omPRpIYhg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/csv-parse@^1.1.11":
   version "1.1.12"
   resolved "https://registry.yarnpkg.com/@types/csv-parse/-/csv-parse-1.1.12.tgz#b1ae62fc0334821f45595e62ca683da9a760915e"
@@ -1277,7 +1284,7 @@
     "@types/vinyl-fs" "*"
     chokidar "^2.1.2"
 
-"@types/handlebars@^4.0.40":
+"@types/handlebars@^4.0.40", "@types/handlebars@^4.1.0":
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.1.0.tgz#3fcce9bf88f85fe73dc932240ab3fb682c624850"
   integrity sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA==
@@ -1334,6 +1341,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/listr@^0.13.0":
+  version "0.13.0"
+  resolved "https://registry.yarnpkg.com/@types/listr/-/listr-0.13.0.tgz#6250bc4a04123cafa24fc73d1b880653a6ae6721"
+  integrity sha512-8DOy0JCGwwAf76xmU0sRzSZCWKSPPA9djRcTYTsyqBPnMdGOjZ5tjmNswC4J9mgKZudte2tuTo1l14R1/t5l/g==
+  dependencies:
+    "@types/node" "*"
+
 "@types/long@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef"
@@ -1410,6 +1424,11 @@
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
   integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
 
+"@types/semver@^6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.0.0.tgz#86ba89f02a414e39c68d02b351872e4ed31bd773"
+  integrity sha512-OO0srjOGH99a4LUN2its3+r6CBYcplhJ466yLqs+zvAWgphCpS8hYZEZ797tRDP/QKcqTdb/YCN6ifASoAWkrQ==
+
 "@types/serve-static@*":
   version "1.13.2"
   resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48"
@@ -1828,6 +1847,11 @@ are-we-there-yet@~1.1.2:
     delegates "^1.0.0"
     readable-stream "^2.0.6"
 
+arg@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0"
+  integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==
+
 argparse@^1.0.7:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -4991,7 +5015,7 @@ gulplog@^1.0.0:
   dependencies:
     glogg "^1.0.0"
 
-handlebars@*, handlebars@^4.0.12, handlebars@^4.1.0:
+handlebars@*, handlebars@^4.0.12, handlebars@^4.1.0, handlebars@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.1.tgz#6e4e41c18ebe7719ae4d38e5aca3d32fa3dd23d3"
   integrity sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==
@@ -6712,7 +6736,7 @@ listr-verbose-renderer@^0.5.0:
     date-fns "^1.27.2"
     figures "^2.0.0"
 
-listr@0.14.3, listr@^0.14.1:
+listr@0.14.3, listr@^0.14.1, listr@^0.14.3:
   version "0.14.3"
   resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586"
   integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==
@@ -9662,6 +9686,11 @@ semver@4.3.2:
   resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
   integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=
 
+semver@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65"
+  integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==
+
 semver@~5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -10653,6 +10682,17 @@ ts-node@^7.0.1:
     source-map-support "^0.5.6"
     yn "^2.0.0"
 
+ts-node@^8.0.3:
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.3.tgz#aa60b836a24dafd8bf21b54766841a232fdbc641"
+  integrity sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA==
+  dependencies:
+    arg "^4.1.0"
+    diff "^3.1.0"
+    make-error "^1.1.1"
+    source-map-support "^0.5.6"
+    yn "^3.0.0"
+
 tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
@@ -11486,6 +11526,11 @@ yn@^2.0.0:
   resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"
   integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=
 
+yn@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/yn/-/yn-3.0.0.tgz#0073c6b56e92aed652fbdfd62431f2d6b9a7a091"
+  integrity sha512-+Wo/p5VRfxUgBUGy2j/6KX2mj9AYJWOHuhMjMcbBFc3y54o9/4buK1ksBvuiK01C3kby8DH9lSmJdSxw+4G/2Q==
+
 zen-observable-ts@^0.8.18:
   version "0.8.18"
   resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz#ade44b1060cc4a800627856ec10b9c67f5f639c8"