소스 검색

feat(create): Allow selection of package manager

Currently only Yarn and npm are supported, but this sets up future support
for other package managers & runtimes such as pnpm & Bun
Michael Bromley 2 년 전
부모
커밋
6561bb7b57
4개의 변경된 파일57개의 추가작업 그리고 22개의 파일을 삭제
  1. 27 13
      packages/create/src/create-vendure-app.ts
  2. 15 8
      packages/create/src/gather-user-responses.ts
  3. 13 1
      packages/create/src/helpers.ts
  4. 2 0
      packages/create/src/types.ts

+ 27 - 13
packages/create/src/create-vendure-app.ts

@@ -1,5 +1,5 @@
 /* eslint-disable no-console */
-import { intro, note, outro, spinner } from '@clack/prompts';
+import { intro, note, outro, select, spinner } from '@clack/prompts';
 import { program } from 'commander';
 import detectPort from 'detect-port';
 import fs from 'fs-extra';
@@ -8,7 +8,7 @@ import path from 'path';
 import pc from 'picocolors';
 
 import { REQUIRED_NODE_VERSION, SERVER_PORT } from './constants';
-import { gatherCiUserResponses, gatherUserResponses } from './gather-user-responses';
+import { checkCancel, gatherCiUserResponses, gatherUserResponses } from './gather-user-responses';
 import {
     checkDbConnection,
     checkNodeVersion,
@@ -18,9 +18,9 @@ import {
     isSafeToCreateProjectIn,
     isServerPortInUse,
     scaffoldAlreadyExists,
-    shouldUseYarn,
+    yarnIsAvailable,
 } from './helpers';
-import { CliLogLevel } from './types';
+import { CliLogLevel, DbType, PackageManager } from './types';
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const packageJson = require('../package.json');
@@ -75,7 +75,21 @@ export async function createVendureApp(
     const root = path.resolve(name);
     const appName = path.basename(root);
     const scaffoldExists = scaffoldAlreadyExists(root, name);
-    const useYarn = useNpm ? false : shouldUseYarn();
+
+    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);
+    }
+
     if (scaffoldExists) {
         console.log(
             pc.yellow(
@@ -97,11 +111,11 @@ export async function createVendureApp(
         dockerComposeSource,
         populateProducts,
     } = isCi
-        ? await gatherCiUserResponses(root, useYarn)
-        : await gatherUserResponses(root, scaffoldExists, useYarn);
+        ? await gatherCiUserResponses(root, packageManager)
+        : await gatherUserResponses(root, scaffoldExists, packageManager);
     const originalDirectory = process.cwd();
     process.chdir(root);
-    if (!useYarn && !checkThatNpmCanReadCwd()) {
+    if (packageManager !== 'npm' && !checkThatNpmCanReadCwd()) {
         process.exit(1);
     }
 
@@ -112,11 +126,11 @@ export async function createVendureApp(
         scripts: {
             'dev:server': 'ts-node ./src/index.ts',
             'dev:worker': 'ts-node ./src/index-worker.ts',
-            dev: useYarn ? 'concurrently yarn:dev:*' : 'concurrently npm:dev:*',
+            dev: packageManager === 'yarn' ? 'concurrently yarn:dev:*' : 'concurrently npm:dev:*',
             build: 'tsc',
             'start:server': 'node ./dist/index.js',
             'start:worker': 'node ./dist/index-worker.js',
-            start: useYarn ? 'concurrently yarn:start:*' : 'concurrently npm:start:*',
+            start: packageManager === 'yarn' ? 'concurrently yarn:start:*' : 'concurrently npm:start:*',
             'migration:generate': 'ts-node migration generate',
             'migration:run': 'ts-node migration run',
             'migration:revert': 'ts-node migration revert',
@@ -138,7 +152,7 @@ export async function createVendureApp(
     const installSpinner = spinner();
     installSpinner.start(`Installing ${dependencies[0]} + ${dependencies.length - 1} more dependencies`);
     try {
-        await installPackages(root, useYarn, dependencies, false, logLevel, isCi);
+        await installPackages(root, packageManager === 'yarn', dependencies, false, logLevel, isCi);
     } catch (e) {
         outro(pc.red(`Failed to install dependencies. Please try again.`));
         process.exit(1);
@@ -151,7 +165,7 @@ export async function createVendureApp(
             `Installing ${devDependencies[0]} + ${devDependencies.length - 1} more dev dependencies`,
         );
         try {
-            await installPackages(root, useYarn, devDependencies, true, logLevel, isCi);
+            await installPackages(root, packageManager === 'yarn', devDependencies, true, logLevel, isCi);
         } catch (e) {
             outro(pc.red(`Failed to install dev dependencies. Please try again.`));
             process.exit(1);
@@ -255,7 +269,7 @@ export async function createVendureApp(
     }
     populateSpinner.stop(`Server successfully initialized${populateProducts ? ' and populated' : ''}`);
 
-    const startCommand = useYarn ? 'yarn dev' : 'npm run dev';
+    const startCommand = packageManager === 'yarn' ? 'yarn dev' : 'npm run dev';
     const nextSteps = [
         `${pc.green('Success!')} Created a new Vendure server at:`,
         `\n`,

+ 15 - 8
packages/create/src/gather-user-responses.ts

@@ -4,7 +4,7 @@ import fs from 'fs-extra';
 import Handlebars from 'handlebars';
 import path from 'path';
 
-import { DbType, FileSources, UserResponses } from './types';
+import { DbType, FileSources, PackageManager, UserResponses } from './types';
 
 interface PromptAnswers {
     dbType: DbType;
@@ -27,7 +27,7 @@ interface PromptAnswers {
 export async function gatherUserResponses(
     root: string,
     alreadyRanScaffold: boolean,
-    useYarn: boolean,
+    packageManager: PackageManager,
 ): Promise<UserResponses> {
     const dbType = (await select({
         message: 'Which database are you using?',
@@ -119,7 +119,7 @@ export async function gatherUserResponses(
     };
 
     return {
-        ...(await generateSources(root, answers, useYarn)),
+        ...(await generateSources(root, answers, packageManager)),
         dbType,
         populateProducts: answers.populateProducts as boolean,
         superadminIdentifier: answers.superadminIdentifier as string,
@@ -130,7 +130,10 @@ export async function gatherUserResponses(
 /**
  * Returns mock "user response" without prompting, for use in CI
  */
-export async function gatherCiUserResponses(root: string, useYarn: boolean): Promise<UserResponses> {
+export async function gatherCiUserResponses(
+    root: string,
+    packageManager: PackageManager,
+): Promise<UserResponses> {
     const ciAnswers = {
         dbType: 'sqlite' as const,
         dbHost: '',
@@ -144,7 +147,7 @@ export async function gatherCiUserResponses(root: string, useYarn: boolean): Pro
     };
 
     return {
-        ...(await generateSources(root, ciAnswers, useYarn)),
+        ...(await generateSources(root, ciAnswers, packageManager)),
         dbType: ciAnswers.dbType,
         populateProducts: ciAnswers.populateProducts,
         superadminIdentifier: ciAnswers.superadminIdentifier,
@@ -152,7 +155,7 @@ export async function gatherCiUserResponses(root: string, useYarn: boolean): Pro
     };
 }
 
-function checkCancel<T>(value: T | symbol): value is T {
+export function checkCancel<T>(value: T | symbol): value is T {
     if (isCancel(value)) {
         cancel('Setup cancelled.');
         process.exit(0);
@@ -163,7 +166,11 @@ function checkCancel<T>(value: T | symbol): value is T {
 /**
  * Create the server index, worker and config source code based on the options specified by the CLI prompts.
  */
-async function generateSources(root: string, answers: PromptAnswers, useYarn: boolean): Promise<FileSources> {
+async function generateSources(
+    root: string,
+    answers: PromptAnswers,
+    packageManager: PackageManager,
+): Promise<FileSources> {
     const assetPath = (fileName: string) => path.join(__dirname, '../assets', fileName);
 
     /**
@@ -177,7 +184,7 @@ async function generateSources(root: string, answers: PromptAnswers, useYarn: bo
 
     const templateContext = {
         ...answers,
-        useYarn,
+        useYarn: packageManager === 'yarn',
         dbType: answers.dbType === 'sqlite' ? 'better-sqlite3' : answers.dbType,
         name: path.basename(root),
         isSQLite: answers.dbType === 'sqlite',

+ 13 - 1
packages/create/src/helpers.ts

@@ -102,7 +102,7 @@ export function checkNodeVersion(requiredVersion: string) {
     }
 }
 
-export function shouldUseYarn() {
+export function yarnIsAvailable() {
     try {
         execSync('yarnpkg --version', { stdio: 'ignore' });
         return true;
@@ -111,6 +111,18 @@ export function shouldUseYarn() {
     }
 }
 
+// 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' });
+        return true;
+    } catch (e: any) {
+        return false;
+    }
+}
+
 export function checkThatNpmCanReadCwd() {
     const cwd = process.cwd();
     let childOutput = null;

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

@@ -19,4 +19,6 @@ export interface UserResponses extends FileSources {
     superadminPassword: string;
 }
 
+export type PackageManager = 'npm' | 'yarn';
+
 export type CliLogLevel = 'silent' | 'info' | 'verbose';