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 20 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
Original file line number Diff line number Diff line change
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 package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 dlx create-jest
```
8 changes: 8 additions & 0 deletions packages/create-jest/bin/create-jest.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../../../tsconfig.test.json",
"include": ["./**/*"],
"references": [{"path": "../../"}]
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type PromptsResults = {
useTypescript: boolean;
clearMocks: boolean;
coverage: boolean;
coverageProvider: boolean;
environment: boolean;
coverageProvider: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the extra diff, I had to fix tslint errors in this package. jest-cli code was ignored, but new package name ends with jest and it matches the condition in tslint script 😄

I decided to do a good thing and fix the errors instead of excluding the package from the script

environment: string;
scripts: boolean;
};
14 changes: 14 additions & 0 deletions packages/create-jest/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "build"
},
"include": ["./src/**/*"],
"exclude": ["./**/__tests__/**/*"],
"references": [
{"path": "../jest-config"},
{"path": "../jest-types"},
{"path": "../jest-util"}
]
}