Skip to content

Commit

Permalink
Merge pull request #24997 from storybookjs/cli/empty-project-init
Browse files Browse the repository at this point in the history
Init: Allow users to generate a new project in empty directory
  • Loading branch information
Shaun Evening committed Dec 8, 2023
2 parents 617163f + cc81dad commit f091782
Show file tree
Hide file tree
Showing 12 changed files with 503 additions and 102 deletions.
112 changes: 112 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,98 @@ jobs:
command: yarn upload-bench $(yarn get-template --cadence << pipeline.parameters.workflow >> --task bench)
- report-workflow-on-failure:
template: $(yarn get-template --cadence << pipeline.parameters.workflow >> --task bench)
test-empty-init:
executor:
class: medium
name: sb_node_16_browsers
parameters:
packageManager:
type: string
template:
type: string
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
- attach_workspace:
at: .
- when:
condition:
equal: ["npm", << parameters.packageManager >>]
steps:
- run:
name: Storybook init from empty directory (NPM)
command: |
cd code
yarn local-registry --open &
cd ../../
mkdir empty-<< parameters.template >>
cd empty-<< parameters.template >>
npm set registry http://localhost:6001
npx storybook init --yes --package-manager npm
npm run storybook -- --smoke-test
environment:
IN_STORYBOOK_SANDBOX: true
STORYBOOK_INIT_EMPTY_TYPE: << parameters.template >>
STORYBOOK_DISABLE_TELEMETRY: true
- when:
condition:
equal: ["yarn1", << parameters.packageManager >>]
steps:
- run:
name: Storybook init from empty directory (Yarn 1)
command: |
cd code
yarn local-registry --open &
cd ../../
mkdir empty-<< parameters.template >>
cd empty-<< parameters.template >>
npx storybook init --yes --package-manager yarn1
yarn storybook --smoke-test
environment:
IN_STORYBOOK_SANDBOX: true
STORYBOOK_INIT_EMPTY_TYPE: << parameters.template >>
STORYBOOK_DISABLE_TELEMETRY: true
- when:
condition:
equal: ["yarn2", << parameters.packageManager >>]
steps:
- run:
name: Storybook init from empty directory (Yarn 2)
command: |
cd code
yarn local-registry --open &
cd ../../
mkdir empty-<< parameters.template >>
cd empty-<< parameters.template >>
yarn set version berry
yarn config set registry http://localhost:6001
yarn dlx storybook init --yes --package-manager yarn2
yarn storybook --smoke-test
environment:
IN_STORYBOOK_SANDBOX: true
STORYBOOK_INIT_EMPTY_TYPE: << parameters.template >>
STORYBOOK_DISABLE_TELEMETRY: true
- when:
condition:
equal: ["pnpm", << parameters.packageManager >>]
steps:
- run:
name: Storybook init from empty directory (PNPM)
command: |
cd code
yarn local-registry --open &
cd ../../
mkdir empty-<< parameters.template >>
cd empty-<< parameters.template >>
npm i -g pnpm
pnpm config set registry http://localhost:6001
pnpm dlx storybook init --yes --package-manager pnpm
pnpm run storybook --smoke-test
environment:
IN_STORYBOOK_SANDBOX: true
STORYBOOK_INIT_EMPTY_TYPE: << parameters.template >>
STORYBOOK_DISABLE_TELEMETRY: true
- report-workflow-on-failure

workflows:
docs:
Expand Down Expand Up @@ -640,6 +732,26 @@ workflows:
parallelism: 29
requires:
- build-sandboxes

- test-empty-init:
requires:
- build
matrix:
parameters:
packageManager:
- "npm"
# TODO: reenable once we find out the source of failure
# - "yarn1"
# - "yarn2"
# - "pnpm"
template:
- "react-vite-ts"
- "nextjs-ts"
- "vue-vite-ts"
# --smoke-test is not supported for the angular builder right now
# - "angular-cli"
- "lit-vite-ts"

# TODO: reenable once we find out the source of flakyness
# - test-runner-dev:
# parallelism: 4
Expand Down
6 changes: 4 additions & 2 deletions code/lib/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"name": "@storybook/cli",
"version": "8.0.0-alpha.1",
"description": "Storybook's CLI - easiest method of adding storybook to your projects",
"description": "Storybook's CLI - install, dev, build, upgrade, and more",
"keywords": [
"cli",
"generator",
"storybook"
"dev",
"build",
"upgrade"
],
"homepage": "https://github.com/storybookjs/storybook/tree/next/code/lib/cli",
"bugs": {
Expand Down
71 changes: 17 additions & 54 deletions code/lib/cli/src/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { NxProjectDetectedError } from '@storybook/core-events/server-errors';

import dedent from 'ts-dedent';
import boxen from 'boxen';
import { readdirSync } from 'fs-extra';
import type { Builder } from './project_types';
import { installableProjectTypes, ProjectType } from './project_types';
import { detect, isStorybookInstantiated, detectLanguage, detectPnp } from './detect';
Expand All @@ -29,11 +28,13 @@ import qwikGenerator from './generators/QWIK';
import svelteKitGenerator from './generators/SVELTEKIT';
import solidGenerator from './generators/SOLID';
import serverGenerator from './generators/SERVER';
import type { JsPackageManager, PackageManagerName } from './js-package-manager';
import type { JsPackageManager } from './js-package-manager';
import { JsPackageManagerFactory, useNpmWarning } from './js-package-manager';
import type { NpmOptions } from './NpmOptions';
import type { CommandOptions, GeneratorOptions } from './generators/types';
import { HandledError } from './HandledError';
import { currentDirectoryIsEmpty, scaffoldNewProject } from './scaffold-new-project';
import versions from './versions';

const logger = console;

Expand Down Expand Up @@ -222,45 +223,6 @@ const projectTypeInquirer = async (
process.exit(0);
};

const getEmptyDirMessage = (packageManagerType: PackageManagerName) => {
const generatorCommandsMap = {
vite: {
npm: 'npm create vite@latest',
yarn1: 'yarn create vite',
yarn2: 'yarn create vite',
pnpm: 'pnpm create vite',
},
angular: {
npm: 'npx -p @angular/cli ng new my-project --package-manager=npm',
yarn1: 'npx -p @angular/cli ng new my-project --package-manager=yarn',
yarn2: 'npx -p @angular/cli ng new my-project --package-manager=yarn',
pnpm: 'npx -p @angular/cli ng new my-project --package-manager=pnpm',
},
};

return dedent`
Storybook cannot be installed into an empty project. We recommend creating a new project with the following:
📦 Vite CLI for React/Vue/Web Components => ${chalk.green(
generatorCommandsMap.vite[packageManagerType]
)}
See ${chalk.yellowBright('https://vitejs.dev/guide/#scaffolding-your-first-vite-project')}
📦 Angular CLI => ${chalk.green(generatorCommandsMap.angular[packageManagerType])}
See ${chalk.yellowBright('https://angular.io/cli/new')}
📦 Any other tooling of your choice
Once you've created a project, please re-run ${chalk.green(
'npx storybook@latest init'
)} inside the project root. For more information, see ${chalk.yellowBright(
'https://storybook.js.org/docs'
)}
Good luck! 🚀
`;
};

async function doInitiate(
options: CommandOptions,
pkg: PackageJson
Expand All @@ -280,11 +242,10 @@ async function doInitiate(
pkgMgr = 'npm';
}

const cwdFolderEntries = readdirSync(process.cwd());
const isEmptyDir =
cwdFolderEntries.length === 0 || cwdFolderEntries.every((entry) => entry.startsWith('.'));
const packageManager = JsPackageManagerFactory.getPackageManager({
force: pkgMgr,
});

const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });
const welcomeMessage = 'storybook init - the simplest way to add a Storybook to your project.';
logger.log(chalk.inverse(`\n ${welcomeMessage} \n`));

Expand All @@ -295,15 +256,16 @@ async function doInitiate(
updateCheckInterval: 1000 * 60 * 60, // every hour (we could increase this later on.)
});

if (options.force !== true && isEmptyDir) {
logger.log(
boxen(getEmptyDirMessage(packageManager.type), {
borderStyle: 'round',
padding: 1,
borderColor: '#F1618C',
})
);
throw new HandledError('Project was initialized in an empty directory.');
// Check if the current directory is empty.
if (options.force !== true && currentDirectoryIsEmpty(packageManager.type)) {
// Prompt the user to create a new project from our list.
await scaffoldNewProject(packageManager.type, options);

if (process.env.IN_STORYBOOK_SANDBOX === 'true' || process.env.CI === 'true') {
packageManager.addPackageResolutions({
'@storybook/telemetry': versions['@storybook/telemetry'],
});
}
}

let projectType: ProjectType;
Expand Down Expand Up @@ -386,6 +348,7 @@ async function doInitiate(
projectType === ProjectType.ANGULAR
? `ng run ${installResult.projectName}:storybook`
: packageManager.getRunStorybookCommand();

logger.log(
boxen(
dedent`
Expand Down
49 changes: 35 additions & 14 deletions code/lib/cli/src/js-package-manager/JsPackageManagerFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,26 @@ const spawnSyncMock = spawnSync as jest.Mock;
jest.mock('find-up');
const findUpSyncMock = findUpSync as unknown as jest.Mock;

describe('JsPackageManagerFactory', () => {
describe('CLASS: JsPackageManagerFactory', () => {
beforeEach(() => {
findUpSyncMock.mockReturnValue(undefined);
delete process.env.npm_config_user_agent;
});

describe('getPackageManager', () => {
describe('return an NPM proxy', () => {
it('when `force` option is `npm`', () => {
describe('METHOD: getPackageManager', () => {
describe('NPM proxy', () => {
it('FORCE: it should return a NPM proxy when `force` option is `npm`', () => {
expect(JsPackageManagerFactory.getPackageManager({ force: 'npm' })).toBeInstanceOf(
NPMProxy
);
});

it('when all package managers are ok, but only a `package-lock.json` file', () => {
it('USER AGENT: it should infer npm from the user agent', () => {
process.env.npm_config_user_agent = 'npm/7.24.0';
expect(JsPackageManagerFactory.getPackageManager()).toBeInstanceOf(NPMProxy);
});

it('ALL EXIST: when all package managers are ok, but only a `package-lock.json` file is found', () => {
spawnSyncMock.mockImplementation((command) => {
// Yarn is ok
if (command === 'yarn') {
Expand Down Expand Up @@ -62,14 +68,19 @@ describe('JsPackageManagerFactory', () => {
});
});

describe('return a PNPM proxy', () => {
it('when `force` option is `pnpm`', () => {
describe('PNPM proxy', () => {
it('FORCE: it should return a PNPM proxy when `force` option is `pnpm`', () => {
expect(JsPackageManagerFactory.getPackageManager({ force: 'pnpm' })).toBeInstanceOf(
PNPMProxy
);
});

it('when all package managers are ok, but only a `pnpm-lock.yaml` file', () => {
it('USER AGENT: it should infer pnpm from the user agent', () => {
process.env.npm_config_user_agent = 'pnpm/7.4.0';
expect(JsPackageManagerFactory.getPackageManager()).toBeInstanceOf(PNPMProxy);
});

it('ALL EXIST: when all package managers are ok, but only a `pnpm-lock.yaml` file is found', () => {
spawnSyncMock.mockImplementation((command) => {
// Yarn is ok
if (command === 'yarn') {
Expand Down Expand Up @@ -104,7 +115,7 @@ describe('JsPackageManagerFactory', () => {
expect(JsPackageManagerFactory.getPackageManager()).toBeInstanceOf(PNPMProxy);
});

it('when a pnpm-lock.yaml file is closer than a yarn.lock', () => {
it('PNPM LOCK IF CLOSER: when a pnpm-lock.yaml file is closer than a yarn.lock', () => {
// Allow find-up to work as normal, we'll set the cwd to our fixture package
findUpSyncMock.mockImplementation(jest.requireActual('find-up').sync);

Expand Down Expand Up @@ -140,13 +151,18 @@ describe('JsPackageManagerFactory', () => {
});
});

describe('return a Yarn 1 proxy', () => {
it('when `force` option is `yarn1`', () => {
describe('Yarn 1 proxy', () => {
it('FORCE: it should return a Yarn1 proxy when `force` option is `yarn1`', () => {
expect(JsPackageManagerFactory.getPackageManager({ force: 'yarn1' })).toBeInstanceOf(
Yarn1Proxy
);
});

it('USER AGENT: it should infer yarn1 from the user agent', () => {
process.env.npm_config_user_agent = 'yarn/1.22.11';
expect(JsPackageManagerFactory.getPackageManager()).toBeInstanceOf(Yarn1Proxy);
});

it('when Yarn command is ok, Yarn version is <2, NPM is ko, PNPM is ko', () => {
spawnSyncMock.mockImplementation((command) => {
// Yarn is ok
Expand Down Expand Up @@ -251,14 +267,19 @@ describe('JsPackageManagerFactory', () => {
});
});

describe('return a Yarn 2 proxy', () => {
it('when `force` option is `yarn2`', () => {
describe('Yarn 2 proxy', () => {
it('FORCE: it should return a Yarn2 proxy when `force` option is `yarn2`', () => {
expect(JsPackageManagerFactory.getPackageManager({ force: 'yarn2' })).toBeInstanceOf(
Yarn2Proxy
);
});

it('when Yarn command is ok, Yarn version is >=2, NPM is ko, PNPM is ko', () => {
it('USER AGENT: it should infer yarn2 from the user agent', () => {
process.env.npm_config_user_agent = 'yarn/2.2.10';
expect(JsPackageManagerFactory.getPackageManager()).toBeInstanceOf(Yarn2Proxy);
});

it('ONLY YARN 2: when Yarn command is ok, Yarn version is >=2, NPM is ko, PNPM is ko', () => {
spawnSyncMock.mockImplementation((command) => {
// Yarn is ok
if (command === 'yarn') {
Expand Down
Loading

0 comments on commit f091782

Please sign in to comment.