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

feat(create-jest): Add npm init / yarn create initialiser #14453

Merged
merged 28 commits into from Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
30e957f
feat(create-jest): Add `npm init` / `yarn create` initialiser
dj-stormtrooper Aug 27, 2023
5b580b1
fix: Updated `yarn.lock`
dj-stormtrooper Aug 27, 2023
e804edb
feat: Add typescript API
dj-stormtrooper Aug 27, 2023
546db25
fix: Prettier error
dj-stormtrooper Aug 27, 2023
e503b28
Merge branch 'main' of github.com:dj-stormtrooper/jest into feat/crea…
dj-stormtrooper Aug 30, 2023
0ffab46
feat: Move config initialisation logic to `create-jest` package
dj-stormtrooper Aug 30, 2023
df988bf
fix: Eslint errors
dj-stormtrooper Aug 30, 2023
92db787
feat: Add `rootDir` to `create-jest` CLI API
dj-stormtrooper Aug 30, 2023
66384ac
fix: Typesafe values operators for `create-jest` package
dj-stormtrooper Aug 30, 2023
e8d2043
fix: Extra diff
dj-stormtrooper Aug 30, 2023
d160788
fix: Remove redundant file
dj-stormtrooper Aug 30, 2023
c9b4ecb
Update packages/create-jest/src/runCreate.ts
dj-stormtrooper Aug 31, 2023
b126c72
fix: Shortcut for nullable values check
dj-stormtrooper Aug 31, 2023
7b366be
chore: Add `create-jest` to typecheck tests configuration
dj-stormtrooper Aug 31, 2023
0e9aed8
fix: Remove unnecessary type declaration
dj-stormtrooper Aug 31, 2023
48b3d1d
fix: Remove unnecessary `local` check
dj-stormtrooper Aug 31, 2023
bbae051
fix: Remove `bin` from exports
dj-stormtrooper Aug 31, 2023
0b4ea0f
fix: Remove extra CLI interface
dj-stormtrooper Aug 31, 2023
8fe5b8d
fix: Restore `bin` export
dj-stormtrooper Aug 31, 2023
810fec8
Merge branch 'feat/create-jest-package' of github.com:dj-stormtrooper…
dj-stormtrooper Aug 31, 2023
513ad24
fix: New package name in error message
dj-stormtrooper Sep 2, 2023
dedea5f
docs: Add `create-jest` usage
dj-stormtrooper Sep 2, 2023
8ae6c55
fix: More generic error message for malformed json in `create-jest`
dj-stormtrooper Sep 2, 2023
8ca2c6a
Merge branch 'main' into feat/create-jest-package
dj-stormtrooper Sep 7, 2023
77adb36
docs: Add `create-jest` to the changelog
dj-stormtrooper Sep 7, 2023
0dcf317
Update packages/create-jest/src/generateConfigFile.ts
SimenB Sep 7, 2023
cc0e430
purdy
SimenB Sep 7, 2023
9e3ddfa
strict
SimenB Sep 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Expand Up @@ -357,7 +357,7 @@ module.exports = {
files: [
'scripts/*',
'packages/*/__benchmarks__/test.js',
'packages/jest-cli/src/init/index.ts',
'packages/create-jest/src/runCreate.ts',
'packages/jest-repl/src/cli/runtime-cli.ts',
],
rules: {
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -131,7 +131,7 @@ If you'd like to learn more about running `jest` through the command line, take
Based on your project, Jest will ask you a few questions and will create a basic configuration file with a short description for each option:

```bash
jest --init
yarn create jest
```

### Using Babel
Expand Down
4 changes: 2 additions & 2 deletions docs/GettingStarted.md
Expand Up @@ -67,8 +67,8 @@ If you'd like to learn more about running `jest` through the command line, take

Based on your project, Jest will ask you a few questions and will create a basic configuration file with a short description for each option:

```bash
jest --init
```bash npm2yarn
npm init jest@latest
```

### Using Babel
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -111,7 +111,7 @@
"test": "yarn lint && yarn jest",
"typecheck": "yarn typecheck:examples && yarn typecheck:tests",
"typecheck:examples": "tsc -p examples/angular --noEmit && tsc -p examples/expect-extend --noEmit && tsc -p examples/typescript --noEmit",
"typecheck:tests": "tsc -b packages/{babel-jest,babel-plugin-jest-hoist,diff-sequences,expect,expect-utils,jest-circus,jest-cli,jest-config,jest-console,jest-snapshot,jest-util,jest-validate,jest-watcher,jest-worker,pretty-format}/**/__tests__",
"typecheck:tests": "tsc -b packages/{babel-jest,babel-plugin-jest-hoist,create-jest,diff-sequences,expect,expect-utils,jest-circus,jest-cli,jest-config,jest-console,jest-snapshot,jest-util,jest-validate,jest-watcher,jest-worker,pretty-format}/**/__tests__",
"verify-old-ts": "node ./scripts/verifyOldTs.mjs",
"verify-pnp": "node ./scripts/verifyPnP.mjs",
"watch": "yarn build:js && node ./scripts/watch.mjs",
Expand Down
8 changes: 8 additions & 0 deletions packages/create-jest/.npmignore
@@ -0,0 +1,8 @@
**/__mocks__/**
**/__tests__/**
__typetests__
src
tsconfig.json
tsconfig.tsbuildinfo
api-extractor.json
.eslintcache
11 changes: 11 additions & 0 deletions packages/create-jest/README.md
@@ -0,0 +1,11 @@
# create-jest

> Getting started with Jest with a single command

```bash
npm init jest@latest
# Or for Yarn
yarn create jest
# Or for pnpm
pnpm create jest
```
8 changes: 8 additions & 0 deletions packages/create-jest/bin/create-jest.js
@@ -0,0 +1,8 @@
#!/usr/bin/env node
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
require('..').runCLI();
42 changes: 42 additions & 0 deletions packages/create-jest/package.json
@@ -0,0 +1,42 @@
{
"name": "create-jest",
"description": "Create a new Jest project",
"version": "29.6.4",
"repository": {
"type": "git",
"url": "https://github.com/jestjs/jest.git",
"directory": "packages/create-jest"
},
"license": "MIT",
"bin": "./bin/create-jest.js",
"main": "./build/index.js",
"types": "./build/index.d.ts",
"exports": {
".": {
"types": "./build/index.d.ts",
"default": "./build/index.js"
},
"./package.json": "./package.json",
"./bin/create-jest": "./bin/create-jest.js"
SimenB marked this conversation as resolved.
Show resolved Hide resolved
},
"dependencies": {
"@jest/types": "workspace:^",
"chalk": "^4.0.0",
"exit": "^0.1.2",
"graceful-fs": "^4.2.9",
"jest-config": "workspace:^",
"jest-util": "workspace:^",
"prompts": "^2.0.1"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/exit": "^0.1.30",
"@types/graceful-fs": "^4.1.3",
"@types/prompts": "^2.0.1"
}
}
Expand Up @@ -10,7 +10,7 @@ import * as path from 'path';
import {writeFileSync} from 'graceful-fs';
import * as prompts from 'prompts';
import {constants} from 'jest-config';
import init from '../';
import {runCreate} from '../runCreate';

const {JEST_CONFIG_EXT_ORDER} = constants;

Expand Down Expand Up @@ -44,7 +44,7 @@ describe('init', () => {
it('should return the default configuration (an empty config)', async () => {
jest.mocked(prompts).mockResolvedValueOnce({});

await init(resolveFromFixture('only-package-json'));
await runCreate(resolveFromFixture('only-package-json'));

const writtenJestConfigFilename =
jest.mocked(writeFileSync).mock.calls[0][0];
Expand All @@ -71,7 +71,7 @@ describe('init', () => {
it('should generate empty config with mjs extension', async () => {
jest.mocked(prompts).mockResolvedValueOnce({});

await init(resolveFromFixture('type-module'));
await runCreate(resolveFromFixture('type-module'));

const writtenJestConfigFilename =
jest.mocked(writeFileSync).mock.calls[0][0];
Expand All @@ -93,7 +93,7 @@ describe('init', () => {
it('should create configuration for {clearMocks: true}', async () => {
jest.mocked(prompts).mockResolvedValueOnce({clearMocks: true});

await init(resolveFromFixture('only-package-json'));
await runCreate(resolveFromFixture('only-package-json'));

const writtenJestConfig = jest.mocked(writeFileSync).mock.calls[0][1];
const evaluatedConfig = eval(writtenJestConfig as string) as Record<
Expand All @@ -107,7 +107,7 @@ describe('init', () => {
it('should create configuration for {coverage: true}', async () => {
jest.mocked(prompts).mockResolvedValueOnce({coverage: true});

await init(resolveFromFixture('only-package-json'));
await runCreate(resolveFromFixture('only-package-json'));

const writtenJestConfig = jest.mocked(writeFileSync).mock.calls[0][1];
const evaluatedConfig = eval(writtenJestConfig as string) as Record<
Expand All @@ -124,7 +124,7 @@ describe('init', () => {
it('should create configuration for {coverageProvider: "babel"}', async () => {
jest.mocked(prompts).mockResolvedValueOnce({coverageProvider: 'babel'});

await init(resolveFromFixture('only-package-json'));
await runCreate(resolveFromFixture('only-package-json'));

const writtenJestConfig = jest.mocked(writeFileSync).mock.calls[0][1];
const evaluatedConfig = eval(writtenJestConfig as string) as Record<
Expand All @@ -138,7 +138,7 @@ describe('init', () => {
it('should create configuration for {coverageProvider: "v8"}', async () => {
jest.mocked(prompts).mockResolvedValueOnce({coverageProvider: 'v8'});

await init(resolveFromFixture('only-package-json'));
await runCreate(resolveFromFixture('only-package-json'));

const writtenJestConfig = jest.mocked(writeFileSync).mock.calls[0][1];
const evaluatedConfig = eval(writtenJestConfig as string) as Record<
Expand All @@ -152,7 +152,7 @@ describe('init', () => {
it('should create configuration for {environment: "jsdom"}', async () => {
jest.mocked(prompts).mockResolvedValueOnce({environment: 'jsdom'});

await init(resolveFromFixture('only-package-json'));
await runCreate(resolveFromFixture('only-package-json'));

const writtenJestConfig = jest.mocked(writeFileSync).mock.calls[0][1];
const evaluatedConfig = eval(writtenJestConfig as string) as Record<
Expand All @@ -165,7 +165,7 @@ describe('init', () => {
it('should create configuration for {environment: "node"}', async () => {
jest.mocked(prompts).mockResolvedValueOnce({environment: 'node'});

await init(resolveFromFixture('only-package-json'));
await runCreate(resolveFromFixture('only-package-json'));

const writtenJestConfig = jest.mocked(writeFileSync).mock.calls[0][1];
const evaluatedConfig = eval(writtenJestConfig as string) as Record<
Expand All @@ -178,7 +178,7 @@ describe('init', () => {
it('should create package.json with configured test command when {scripts: true}', async () => {
jest.mocked(prompts).mockResolvedValueOnce({scripts: true});

await init(resolveFromFixture('only-package-json'));
await runCreate(resolveFromFixture('only-package-json'));

const writtenPackageJson = jest.mocked(writeFileSync).mock.calls[0][1];
const parsedPackageJson = JSON.parse(writtenPackageJson as string) as {
Expand All @@ -196,7 +196,7 @@ describe('init', () => {
expect.assertions(1);

try {
await init(resolveFromFixture('no-package-json'));
await runCreate(resolveFromFixture('no-package-json'));
} catch (error) {
expect((error as Error).message).toMatch(
'Could not find a "package.json" file in',
Expand All @@ -215,7 +215,9 @@ describe('init', () => {
.mockResolvedValueOnce({continue: true})
.mockResolvedValueOnce({});

await init(resolveFromFixture(`has-jest-config-file-${extension}`));
await runCreate(
resolveFromFixture(`has-jest-config-file-${extension}`),
);

expect(jest.mocked(prompts).mock.calls[0][0]).toMatchSnapshot();

Expand All @@ -230,7 +232,9 @@ describe('init', () => {
it('user answered with "No"', async () => {
jest.mocked(prompts).mockResolvedValueOnce({continue: false});

await init(resolveFromFixture(`has-jest-config-file-${extension}`));
await runCreate(
resolveFromFixture(`has-jest-config-file-${extension}`),
);
// return after first prompt
expect(prompts).toHaveBeenCalledTimes(1);
});
Expand All @@ -243,7 +247,7 @@ describe('init', () => {
it('user answered with "Yes"', async () => {
jest.mocked(prompts).mockResolvedValueOnce({useTypescript: true});

await init(resolveFromFixture('test-generated-jest-config-ts'));
await runCreate(resolveFromFixture('test-generated-jest-config-ts'));

expect(jest.mocked(prompts).mock.calls[0][0]).toMatchSnapshot();

Expand All @@ -264,7 +268,7 @@ describe('init', () => {
it('user answered with "No"', async () => {
jest.mocked(prompts).mockResolvedValueOnce({useTypescript: false});

await init(resolveFromFixture('test-generated-jest-config-ts'));
await runCreate(resolveFromFixture('test-generated-jest-config-ts'));

const jestConfigFileName = jest.mocked(writeFileSync).mock.calls[0][0];

Expand All @@ -282,7 +286,7 @@ describe('init', () => {
.mockResolvedValueOnce({continue: true})
.mockResolvedValueOnce({});

await init(resolveFromFixture('has-jest-config-in-package-json'));
await runCreate(resolveFromFixture('has-jest-config-in-package-json'));

expect(jest.mocked(prompts).mock.calls[0][0]).toMatchSnapshot();

Expand All @@ -296,7 +300,7 @@ describe('init', () => {
it('should not ask "test script question"', async () => {
jest.mocked(prompts).mockResolvedValueOnce({});

await init(resolveFromFixture('test-script-configured'));
await runCreate(resolveFromFixture('test-script-configured'));

const questions = jest.mocked(prompts).mock.calls[0][0] as Array<
prompts.PromptObject<string>
Expand Down
5 changes: 5 additions & 0 deletions packages/create-jest/src/__tests__/tsconfig.json
@@ -0,0 +1,5 @@
{
"extends": "../../../../tsconfig.test.json",
"include": ["./**/*"],
"references": [{"path": "../../"}]
}
Expand Up @@ -16,10 +16,7 @@ export class NotFoundPackageJsonError extends Error {

export class MalformedPackageJsonError extends Error {
constructor(packageJsonPath: string) {
super(
`There is malformed json in ${packageJsonPath}\n` +
'Fix it, and then run "jest --init"',
);
super(`There is malformed json in ${packageJsonPath}`);
this.name = '';
// eslint-disable-next-line @typescript-eslint/no-empty-function
Error.captureStackTrace(this, () => {});
Expand Down
Expand Up @@ -7,13 +7,14 @@

import type {Config} from '@jest/types';
import {defaults, descriptions} from 'jest-config';
import type {PromptsResults} from './types';

const stringifyOption = (
option: keyof Config.InitialOptions,
map: Partial<Config.InitialOptions>,
linePrefix = '',
): string => {
const optionDescription = ` // ${descriptions[option]}`;
const optionDescription = ` // ${descriptions[option] ?? ''}`;
SimenB marked this conversation as resolved.
Show resolved Hide resolved
const stringifiedObject = `${option}: ${JSON.stringify(
map[option],
null,
Expand All @@ -27,7 +28,7 @@ const stringifyOption = (
};

const generateConfigFile = (
results: Record<string, unknown>,
results: PromptsResults,
generateEsm = false,
): string => {
const {useTypescript, coverage, coverageProvider, clearMocks, environment} =
Expand Down
8 changes: 8 additions & 0 deletions packages/create-jest/src/index.ts
@@ -0,0 +1,8 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

export {runCreate, runCLI} from './runCreate';
Expand Up @@ -7,10 +7,11 @@

import * as path from 'path';
import chalk = require('chalk');
import exit = require('exit');
import * as fs from 'graceful-fs';
import prompts = require('prompts');
import {constants} from 'jest-config';
import {tryRealpath} from 'jest-util';
import {clearLine, tryRealpath} from 'jest-util';
import {MalformedPackageJsonError, NotFoundPackageJsonError} from './errors';
import generateConfigFile from './generateConfigFile';
import modifyPackageJson from './modifyPackageJson';
Expand All @@ -28,11 +29,28 @@ const {

const getConfigFilename = (ext: string) => JEST_CONFIG_BASE_NAME + ext;

export default async function init(
rootDir: string = tryRealpath(process.cwd()),
): Promise<void> {
export async function runCLI(): Promise<void> {
try {
const rootDir = process.argv[2];
await runCreate(rootDir);
} catch (error: unknown) {
clearLine(process.stderr);
clearLine(process.stdout);
if (error instanceof Error && Boolean(error?.stack)) {
console.error(chalk.red(error.stack));
} else {
console.error(chalk.red(error));
}

exit(1);
throw error;
}
}

export async function runCreate(rootDir = process.cwd()): Promise<void> {
rootDir = tryRealpath(rootDir);
// prerequisite checks
const projectPackageJsonPath: string = path.join(rootDir, PACKAGE_JSON);
const projectPackageJsonPath = path.join(rootDir, PACKAGE_JSON);

if (!fs.existsSync(projectPackageJsonPath)) {
throw new NotFoundPackageJsonError(rootDir);
Expand All @@ -45,7 +63,7 @@ export default async function init(
try {
projectPackageJson = JSON.parse(
fs.readFileSync(projectPackageJsonPath, 'utf-8'),
);
) as ProjectPackageJson;
} catch {
throw new MalformedPackageJsonError(projectPackageJsonPath);
}
Expand All @@ -58,7 +76,7 @@ export default async function init(
fs.existsSync(path.join(rootDir, getConfigFilename(ext))),
);

if (hasJestProperty || existingJestConfigExt) {
if (hasJestProperty || existingJestConfigExt != null) {
const result: {continue: boolean} = await prompts({
initial: true,
message:
Expand Down Expand Up @@ -112,9 +130,10 @@ export default async function init(
: JEST_CONFIG_EXT_JS;

// Determine Jest config path
const jestConfigPath = existingJestConfigExt
? getConfigFilename(existingJestConfigExt)
: path.join(rootDir, getConfigFilename(jestConfigFileExt));
const jestConfigPath =
existingJestConfigExt != null
? getConfigFilename(existingJestConfigExt)
: path.join(rootDir, getConfigFilename(jestConfigFileExt));

const shouldModifyScripts = results.scripts;

Expand Down