Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Generate a new project on init in empty directory #24997

Merged
merged 70 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
68983e8
Create function to execute creation script
Nov 27, 2023
07f08a3
Create flow to scaffold a new project
Nov 27, 2023
8817642
Plug scaffold flow into storybook init
Nov 27, 2023
ee01b44
Don't use app router
Nov 28, 2023
96b624e
Add empty project error
Nov 28, 2023
607c523
Remove prefers offline
Nov 28, 2023
6145b5d
Use execa
Nov 28, 2023
1c62a97
Remove left right margins from boxen
Nov 28, 2023
7114d54
Update docs to remove empty project callout
Nov 28, 2023
43f5be5
Add env var to reselect new project
Nov 28, 2023
33fca51
Add new option to types
Nov 28, 2023
8186fa7
fix linting warning
Nov 28, 2023
156ec8a
Infer package manager from user agent
Nov 28, 2023
de3dd24
fix merge conflicts
Nov 29, 2023
e329fb8
Don't expose testing flag; Only use Env var
Nov 29, 2023
097a9c6
Don't install deps on create for pnpm
Nov 29, 2023
85cd3ef
Add empty dir sandboxes
Nov 29, 2023
b2daaec
Add optional env vars to templates
Nov 29, 2023
0032ff2
Skip all empty sandboxes
Nov 29, 2023
07790cb
Start writing CircleCI script to test empty init
Nov 29, 2023
3b8cdc4
Test out empty init circle job
Nov 29, 2023
8e20a75
oops
Nov 29, 2023
f115839
try again
Nov 29, 2023
4eb0c2b
one more time
Nov 29, 2023
acfc94c
Updated config.yml
ShaunEvening Nov 29, 2023
0b011b9
Actually get to the right file for the cli bin
Nov 29, 2023
cd20c52
fix package manager flag
Nov 29, 2023
826ab3d
Make sure empty projects are init in the right dir
Nov 29, 2023
b4250b3
add --yes
Nov 29, 2023
1cfd09e
Try npm instead
Nov 29, 2023
6c12cad
Merge branch 'next' into cli/empty-project-init
Nov 30, 2023
27252cb
try adding --yes back
Nov 30, 2023
d66e4bc
Try echoing out env var
Nov 30, 2023
c81c38e
Print the WD
Nov 30, 2023
95c7889
Try setting yarn to berry
Nov 30, 2023
7195595
Test npm instead
Nov 30, 2023
a7ff9e7
Set in sandbox to true
Nov 30, 2023
d36324c
run all empty templates
Nov 30, 2023
5175507
Add conditional logic
Nov 30, 2023
24b947b
Updated config.yml
ShaunEvening Nov 30, 2023
2956a01
Add yarn and pnpm
Nov 30, 2023
5cd8fbf
Only run tests for NPM for now
Nov 30, 2023
4114f70
Re-enable daily flow
Nov 30, 2023
96e5271
Re-enable daily workflow
Nov 30, 2023
7b9b269
Updated config.yml
ShaunEvening Nov 30, 2023
392f1b5
Merge branch 'next' into cli/empty-project-init
Nov 30, 2023
2eb8a9a
ignore package.json in pnpm dlx scenario
Nov 30, 2023
c414978
Merge branch 'next' into cli/empty-project-init
Nov 30, 2023
0046573
ignore .cache
Nov 30, 2023
e0256df
Merge branch 'next' into cli/empty-project-init
Dec 1, 2023
db79ce0
Try running verdaccio in CI
Dec 1, 2023
2d4f494
Update other package managers to use verdaccio
Dec 1, 2023
a6379b0
Try using npx in circleCI
Dec 1, 2023
8b2bdf9
Merge branch 'next' into cli/empty-project-init
Dec 4, 2023
4ed6f99
Resolve version issue for CLI
Dec 4, 2023
ed9cb67
Add debug env var
Dec 4, 2023
a9d54ca
Create project in temp dir
Dec 5, 2023
f3766dc
Delete cache folder
Dec 5, 2023
ffbff85
Cleanup
Dec 5, 2023
98d1598
Comment out empty templates
Dec 5, 2023
41d0803
Use env vars in script
Dec 6, 2023
c876156
Merge branch 'next' into cli/empty-project-init
Dec 6, 2023
b70226b
Remove unused templates
Dec 7, 2023
0a46302
Move inferPackageManager into JsPackageManager
Dec 7, 2023
f8bf4ef
Fix packageManager inference
Dec 7, 2023
cedc6fb
Remove unused imports
Dec 7, 2023
df1f00f
Merge branch 'next' into cli/empty-project-init
Dec 7, 2023
8fbd375
Break the cache
shilman Dec 8, 2023
8a2baff
Merge branch 'next' into cli/empty-project-init
Dec 8, 2023
cc81dad
Add telemetry for successful scaffolding
Dec 8, 2023
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
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: 31
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
35 changes: 35 additions & 0 deletions code/lib/cli/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,39 @@ describe('Helpers', () => {
}).toThrowError(`Could not coerce ${invalidSemverString} into a semver.`);
});
});
describe('inferPackageManagerFromUserAgent', () => {
afterEach(() => {
delete process.env.npm_config_user_agent;
});

it('should return npm for invalid user agent', () => {
process.env.npm_config_user_agent = '';
expect(helpers.inferPackageManagerFromUserAgent()).toBe('npm');
});

it('should infer pnpm from user agent', () => {
process.env.npm_config_user_agent = 'pnpm/7.4.0';
expect(helpers.inferPackageManagerFromUserAgent()).toBe('pnpm');
});

it('should infer npm from user agent', () => {
process.env.npm_config_user_agent = 'npm/7.24.0';
expect(helpers.inferPackageManagerFromUserAgent()).toBe('npm');
});

it('should infer yarn 1 from user agent', () => {
process.env.npm_config_user_agent = 'yarn/1.22.11';
expect(helpers.inferPackageManagerFromUserAgent()).toBe('yarn1');
});

it('should infer yarn 2 from user agent', () => {
process.env.npm_config_user_agent = 'yarn/2.4.0';
expect(helpers.inferPackageManagerFromUserAgent()).toBe('yarn2');
});

it('should return npm for unknown package manager', () => {
process.env.npm_config_user_agent = 'unknown';
expect(helpers.inferPackageManagerFromUserAgent()).toBe('npm');
});
});
});
27 changes: 27 additions & 0 deletions code/lib/cli/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
JsPackageManager,
PackageJson,
PackageJsonWithDepsAndDevDeps,
PackageManagerName,
} from './js-package-manager';
import type { SupportedFrameworks, SupportedRenderers } from './project_types';
import { SupportedLanguage } from './project_types';
Expand Down Expand Up @@ -314,3 +315,29 @@ export function coerceSemver(version: string) {
invariant(coercedSemver != null, `Could not coerce ${version} into a semver.`);
return coercedSemver;
}

/**
* Infer the package manager based on the command the user is running.
* Each package manager sets the `npm_config_user_agent` environment variable with its name and version e.g. "npm/7.24.0"
* Which is really useful when invoking commands via npx/pnpx/yarn create/etc.
*/
export function inferPackageManagerFromUserAgent(): PackageManagerName {
const userAgent = process.env.npm_config_user_agent;
if (!userAgent) return 'npm';
const packageSpec = userAgent.split(' ')[0];
const [pkgMgrName, pkgMgrVersion] = packageSpec.split('/');

if (pkgMgrName === 'pnpm') {
return 'pnpm';
}

if (pkgMgrName === 'npm') {
return 'npm';
}

if (pkgMgrName === 'yarn') {
return `yarn${pkgMgrVersion?.startsWith('1.') ? '1' : '2'}`;
}

return 'npm';
}
73 changes: 18 additions & 55 deletions code/lib/cli/src/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ 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';
import { commandLog, codeLog, paddedLog } from './helpers';
import { commandLog, codeLog, paddedLog, inferPackageManagerFromUserAgent } from './helpers';
import angularGenerator from './generators/ANGULAR';
import emberGenerator from './generators/EMBER';
import reactGenerator from './generators/REACT';
Expand All @@ -31,11 +30,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 @@ -234,45 +235,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 @@ -292,11 +254,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 || inferPackageManagerFromUserAgent(),
ShaunEvening marked this conversation as resolved.
Show resolved Hide resolved
});

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 @@ -307,15 +268,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);

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 @@ -398,6 +360,7 @@ async function doInitiate(
projectType === ProjectType.ANGULAR
? `ng run ${installResult.projectName}:storybook`
: packageManager.getRunStorybookCommand();

logger.log(
boxen(
dedent`
Expand Down
Loading