Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/contentful--create-contentful-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ npx create-contentful-app <app-name>
# npm
npm init contentful-app <app-name>

# pnpm
pnpm init contentful-app <app-name>

# Yarn
yarn create contentful-app <app-name>
```
Expand All @@ -38,11 +41,9 @@ yarn create contentful-app <app-name>

### Package Manager

`--npm` or `--yarn`

Use npm or Yarn to manage dependencies. If omitted, defaults to the manager used to run `create-contentful-app`.
`--npm` or `--pnpm` or `--yarn`

Both flags are mutually exclusive.
Use npm, pnpm, or Yarn to manage dependencies. If omitted, or if more than one flag is passed, will default to the manager used to run `create-contentful-app`.

### Template

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Analytics } from '@segment/analytics-node';
import type { PackageManager } from './types';

// Public write key scoped to data source
const SEGMENT_WRITE_KEY = 'IzCq3j4dQlTAgLdMykRW9oBHQKUy1xMm';

interface CCAEventProperties {
template?: string; // can be example, source, or JS or TS
manager: 'npm' | 'yarn';
manager: PackageManager;
interactive: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,10 @@ async function promptExampleSelection(): Promise<string> {
// get available templates from examples
const availableTemplates = await getGithubFolderNames();
// filter out the ignored ones that are listed as templates instead of examples
const filteredTemplates = availableTemplates
.filter(
(template) =>
!IGNORED_EXAMPLE_FOLDERS.includes(template as (typeof IGNORED_EXAMPLE_FOLDERS)[number])
)
const filteredTemplates = availableTemplates.filter(
(template) =>
!IGNORED_EXAMPLE_FOLDERS.includes(template as (typeof IGNORED_EXAMPLE_FOLDERS)[number])
);
console.log(availableTemplates.length, filteredTemplates.length);

// ask user to select a template from the available examples
Expand Down
50 changes: 34 additions & 16 deletions packages/contentful--create-contentful-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import { program } from 'commander';
import inquirer from 'inquirer';
import tildify from 'tildify';
import { cloneTemplateIn } from './template';
import { detectManager, exec, normalizeOptions, isContentfulTemplate } from './utils';
import { CLIOptions } from './types';
import {
detectActivePackageManager,
getNormalizedPackageManager,
exec,
normalizeOptions,
isContentfulTemplate,
} from './utils';
import type { CLIOptions, PackageManager } from './types';
import { code, error, highlight, success, warn, wrapInBlanks } from './logger';
import chalk from 'chalk';
import { CREATE_APP_DEFINITION_GUIDE_URL, EXAMPLES_REPO_URL } from './constants';
Expand All @@ -19,7 +25,15 @@ import fs from 'fs';

const DEFAULT_APP_NAME = 'contentful-app';

function successMessage(folder: string, useYarn: boolean) {
function successMessage(folder: string, packageManager: PackageManager) {
let command = '';
if (packageManager === 'yarn') {
command = 'yarn create-app-definition';
} else if (packageManager === 'pnpm') {
command = 'pnpm create-app-definition';
} else {
command = 'npm run create-app-definition';
}
console.log(`
${success('Success!')} Created a new Contentful app in ${highlight(tildify(folder))}.`);

Expand All @@ -28,7 +42,7 @@ ${success('Success!')} Created a new Contentful app in ${highlight(tildify(folde
console.log(`Now create an app definition for your app by running

${code(`cd ${tildify(folder)}`)}
${code(useYarn ? 'yarn create-app-definition' : 'npm run create-app-definition')}
${code(command)}

or you can create it manually in web app:
${highlight(CREATE_APP_DEFINITION_GUIDE_URL)}
Expand All @@ -37,7 +51,7 @@ ${success('Success!')} Created a new Contentful app in ${highlight(tildify(folde
console.log(`Then kick it off by running

${code(`cd ${tildify(folder)}`)}
${code(`${useYarn ? 'yarn' : 'npm'} start`)}
${code(`${packageManager} start`)}
`);
}

Expand Down Expand Up @@ -100,6 +114,9 @@ async function validateAppName(appName: string): Promise<string> {

async function initProject(appName: string, options: CLIOptions) {
const normalizedOptions = normalizeOptions(options);
const activePackageManager = detectActivePackageManager();
const packageManager = getNormalizedPackageManager(normalizedOptions, activePackageManager);

try {
appName = await validateAppName(appName);

Expand All @@ -115,28 +132,28 @@ async function initProject(appName: string, options: CLIOptions) {

updatePackageName(fullAppFolder);

const useYarn = normalizedOptions.yarn || detectManager() === 'yarn';

wrapInBlanks(
highlight(
`---- Installing the dependencies for your app (using ${chalk.cyan(
useYarn ? 'yarn' : 'npm'
)})...`
`---- Installing the dependencies for your app (using ${chalk.cyan(packageManager)})...`
)
);
if (useYarn) {

if (packageManager === 'yarn') {
await exec('yarn', [], { cwd: fullAppFolder });
} else if (packageManager === 'pnpm') {
await exec('pnpm', ['install'], { cwd: fullAppFolder });
} else {
await exec('npm', ['install', '--no-audit', '--no-fund'], { cwd: fullAppFolder });
}
successMessage(fullAppFolder, useYarn);
successMessage(fullAppFolder, packageManager);
} catch (err) {
error(`Failed to create ${highlight(chalk.cyan(appName))}`, err);
process.exit(1);
}

async function addAppExample(fullAppFolder: string) {
const isInteractive = !normalizedOptions.example &&
const isInteractive =
!normalizedOptions.example &&
!normalizedOptions.source &&
!normalizedOptions.javascript &&
!normalizedOptions.typescript &&
Expand All @@ -146,7 +163,7 @@ async function initProject(appName: string, options: CLIOptions) {

track({
template: templateSource,
manager: normalizedOptions.npm ? 'npm' : 'yarn',
manager: packageManager,
interactive: isInteractive,
});

Expand All @@ -163,7 +180,7 @@ async function initProject(appName: string, options: CLIOptions) {
}

async function addFunctionTemplate(fullAppFolder: string) {
if (!fs.existsSync(fullAppFolder)) {
if (!fs.existsSync(fullAppFolder)) {
fs.mkdirSync(fullAppFolder, { recursive: true });
}

Expand All @@ -188,7 +205,7 @@ async function initProject(appName: string, options: CLIOptions) {
example: normalizedOptions.function,
language: normalizedOptions.javascript ? 'javascript' : 'typescript',
name: functionName,
keepPackageJson: normalizedOptions.skipUi === true
keepPackageJson: normalizedOptions.skipUi === true,
} as any);
}
}
Expand All @@ -212,6 +229,7 @@ async function initProject(appName: string, options: CLIOptions) {
)
.argument('[app-name]', 'app name')
.option('--npm', 'use npm')
.option('--pnpm', 'use pnpm')
.option('--yarn', 'use Yarn')
.option('-ts, --typescript', 'use TypeScript template (default)')
.option('-js, --javascript', 'use JavaScript template')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function validate(destination: string): void {
}

function cleanUp(destination: string) {
rmIfExists(resolve(destination, 'pnpm-lock.json'));
rmIfExists(resolve(destination, 'package-lock.json'));
rmIfExists(resolve(destination, 'yarn.lock'));
}
Expand Down
3 changes: 3 additions & 0 deletions packages/contentful--create-contentful-app/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type CLIOptions = Partial<{
npm: boolean;
yarn: boolean;
pnpm: boolean;
javascript: boolean;
typescript: boolean;
source: string;
Expand All @@ -9,6 +10,8 @@ export type CLIOptions = Partial<{
skipUi: boolean;
}>;

export type PackageManager = 'npm' | 'yarn' | 'pnpm';

export const ContentfulExample = {
Javascript: 'javascript',
Typescript: 'typescript',
Expand Down
69 changes: 60 additions & 9 deletions packages/contentful--create-contentful-app/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { spawn, SpawnOptionsWithoutStdio } from 'child_process';
import { existsSync, rmSync } from 'fs';
import { existsSync, readFileSync, rmSync } from 'fs';
import { basename } from 'path';
import { choice, highlight, warn } from './logger';
import { CLIOptions, ContentfulExample } from './types';
import { CLIOptions, ContentfulExample, PackageManager } from './types';
import { EXAMPLES_PATH } from './constants';

const MUTUALLY_EXCLUSIVE_OPTIONS = ['source', 'example', 'typescript', 'javascript'] as const;
Expand All @@ -26,31 +26,82 @@ export function rmIfExists(path: string) {
}
}

export function detectManager() {
export function detectActivePackageManager(): PackageManager {
if (existsSync('pnpm-lock.yaml')) return 'pnpm';
if (existsSync('yarn.lock')) return 'yarn';
if (existsSync('package-lock.json')) return 'npm';
warn('No lock files found, we will try to detect the active package manager from package.json.');
try {
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
if (pkg.packageManager?.startsWith('pnpm')) return 'pnpm';
if (pkg.packageManager?.startsWith('yarn')) return 'yarn';
if (pkg.packageManager?.startsWith('npm')) return 'npm';
} catch {
warn(
`Unable to determine active package manager from package.json. We will try to detect it from npm_execpath.`
);
}

switch (basename(process.env.npm_execpath || '')) {
case 'yarn.js':
return 'yarn';
case 'pnpm.cjs':
return 'pnpm';
case 'npx-cli.js':
case 'npm-cli.js':
default:
return 'npm';
}
}

// By the time this function is called, the options have already been normalized
// so we would not need to consider multiple package manager flags at once
export function getNormalizedPackageManager(
options: CLIOptions,
activePackageManager: PackageManager
): PackageManager {
// Prefer to get the package manager from options
if (options.pnpm) {
return 'pnpm';
} else if (options.yarn) {
return 'yarn';
} else if (options.npm) {
return 'npm';
}

// Fallback to active package manager
return activePackageManager;
}

export function normalizeOptions(options: CLIOptions): CLIOptions {
const normalizedOptions: CLIOptions = { ...options };

if (normalizedOptions.npm && normalizedOptions.yarn) {
const selectedPackageManagers = [
['npm', normalizedOptions.npm],
['pnpm', normalizedOptions.pnpm],
['yarn', normalizedOptions.yarn],
].filter(([, n]) => n);
const activePackageManager = detectActivePackageManager();

if (selectedPackageManagers.length > 1) {
warn(
`Provided both ${highlight('--yarn')} and ${highlight('--npm')} flags, using ${choice(
'--npm'
`Too many package manager flags were provided, we will use ${choice(
`--${activePackageManager}`
)}.`
);
delete normalizedOptions.yarn;

// Delete all package manager options
selectedPackageManagers.forEach(([packageManager]) => {
delete normalizedOptions[packageManager as keyof CLIOptions];
});

// Select active package manager
(normalizedOptions as CLIOptions)[activePackageManager] = true;
}

if (!normalizedOptions.yarn) {
normalizedOptions.npm = true;
// No package manager flags were provided, use active package manager
if (selectedPackageManagers.length === 0) {
(normalizedOptions as CLIOptions)[activePackageManager] = true;
}

let fallbackOption = '--typescript';
Expand Down
Loading