Skip to content

Commit 9a77155

Browse files
authored
Merge pull request #101 from akd-io/feature/70-turn-questionnaire-into-a-series-of-i
70 Turn questionnaire into a series of inquiries
2 parents e19055e + 0e111f0 commit 9a77155

21 files changed

+414
-243
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Technologies marked as _convenience installs_ are technologies that work out of
6262
| [Yarn](https://yarnpkg.com/) | [CLI Docs](https://yarnpkg.com/cli) - [GitHub repo](https://github.com/yarnpkg/berry) |
6363
| [npm](https://www.npmjs.com/) | [CLI Docs](https://docs.npmjs.com/cli/) |
6464
| [Emotion](https://emotion.sh/docs/introduction) | [Docs](https://emotion.sh/docs/introduction) - [GitHub repo](https://github.com/emotion-js/emotion) |
65-
| [styled-components](https://styled-components.com/) | [Docs](https://styled-components.com/docs) - [GitHub repo](https://github.com/styled-components/styled-components) |
65+
| [Styled Components](https://styled-components.com/) | [Docs](https://styled-components.com/docs) - [GitHub repo](https://github.com/styled-components/styled-components) |
6666
| [CSS Modules](https://github.com/css-modules/css-modules) | [Docs](https://github.com/css-modules/css-modules) - [Next.js-specific docs](https://nextjs.org/docs/basic-features/built-in-css-support#adding-component-level-css) |
6767
| [Sass](https://sass-lang.com/) <img width="14" alt="convenience install icon" src="assets/convenience-icon.png"> | [Docs](https://sass-lang.com/documentation) - [Next.js-specific docs](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support) |
6868
| [React Hook Form](https://react-hook-form.com/) <img width="14" alt="convenience install icon" src="assets/convenience-icon.png"> | [Docs](https://react-hook-form.com/get-started) - [GitHub repo](https://github.com/react-hook-form/react-hook-form) |

src/main/create-next-stack-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { IConfig } from "@oclif/config"
22
import CreateNextStack from "."
33
import { UnknownObject } from "./helpers/is-unknown-object"
4+
import { validateProjectPathInput } from "./helpers/validate-project-path"
45
import { Writable } from "./helpers/writable"
5-
import { validateProjectPathInput } from "./questionnaire/questions/validate-project-path"
66

77
/**
88
* This function is only used to retrieve the ReturnType of a call to `createNextStackInstance.parse(CreateNextStack)`.

src/main/helpers/then-arg.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type ThenArg<T> = T extends PromiseLike<infer U> ? U : T

src/main/helpers/validate-npm-name.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/main/questionnaire/questions/validate-project-path.ts renamed to src/main/helpers/validate-project-path.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { getProjectNameOfPath } from "../../helpers/get-project-name-of-path"
2-
import { validateNpmName } from "../../helpers/validate-npm-name"
3-
import { logError } from "../../logging"
1+
import validateNpmPackageName from "validate-npm-package-name"
2+
import { logError } from "../logging"
3+
import { getProjectNameOfPath } from "./get-project-name-of-path"
44

55
/**
66
* @param this Current Command instance
@@ -30,3 +30,27 @@ export const validateProjectPathInput = (
3030

3131
return true
3232
}
33+
34+
/**
35+
* This code is a slightly modified version of the same code from the Create Next App repository.
36+
* From: https://github.com/vercel/next.js/blob/e8a9bd19967c9f78575faa7d38e90a1270ffa519/packages/create-next-app/helpers/validate-pkg.ts
37+
*/
38+
const validateNpmName = (
39+
name: string
40+
): {
41+
valid: boolean
42+
problems: string[]
43+
} => {
44+
const nameValidation = validateNpmPackageName(name)
45+
if (nameValidation.validForNewPackages) {
46+
return { valid: true, problems: [] }
47+
}
48+
49+
return {
50+
valid: false,
51+
problems: [
52+
...(nameValidation.errors ?? []),
53+
...(nameValidation.warnings ?? []),
54+
],
55+
}
56+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* `withKeyConstraint` is a higher-order [Constrained Identity Function](https://kentcdodds.com/blog/how-to-write-a-constrained-identity-function-in-typescript)
3+
* used to restrict the keys of an object to the exact strings of a specified union type, while still getting the values of the object type accurately inferred from usage.
4+
*
5+
* Note: `withKeyConstraint` returns a function you'll want to call immediately, see example below.
6+
*
7+
* Sample usage:
8+
* ```typescript
9+
* type ExampleKey = "key1" | "key2"
10+
* const example = withKeyConstraint<ExampleKey>()({ // <-- Result of withKeyConstraint is called immediately
11+
* key1: "test",
12+
* key2: 13,
13+
* //key3: () => void // Uncommenting this line would result in a type error, as key3 is not part of ExampleKey
14+
* } as const)
15+
*
16+
* // Correctly inferred values:
17+
* example.key1 // type: "test"
18+
* example.key2 // type: 13
19+
* ```
20+
*/
21+
export const withKeyConstraint = <TRequiredKeys extends string>() => {
22+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
23+
return <
24+
TObject extends {
25+
[Key in TRequiredKeys | keyof TObject]: Key extends TRequiredKeys
26+
? unknown
27+
: never
28+
}
29+
>(
30+
object: TObject
31+
) => object
32+
}

src/main/questionnaire/args-questionnaire.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import {
22
CreateNextStackArgs,
33
ValidCreateNextStackArgs,
44
} from "../create-next-stack-types"
5+
import { validateProjectPathInput } from "../helpers/validate-project-path"
56
import { promptProjectPath } from "./questions/project-name"
6-
import { validateProjectPathInput } from "./questions/validate-project-path"
77

88
export const performArgsQuestionnaire = async (
99
args: CreateNextStackArgs

src/main/questionnaire/flags-questionnaire.ts

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,62 @@
1+
import { ValidCreateNextStackFlags } from "../create-next-stack-types"
2+
import { ThenArg } from "../helpers/then-arg"
3+
import { withKeyConstraint } from "../helpers/with-key-constraint"
4+
import { CategoryValue, promptOptionalCategories } from "./questions/categories"
5+
import { promptAnimation } from "./questions/categories/animation"
6+
import { promptContinuousIntegration } from "./questions/categories/continuous-integration"
7+
import { promptFormStateManagement } from "./questions/categories/form-state-management"
8+
import { promptFormatting } from "./questions/categories/formatting"
19
import {
2-
PackageManagerOption,
3-
StylingOption,
4-
ValidCreateNextStackFlags,
5-
} from "../create-next-stack-types"
6-
import { promptTechnologies } from "./questions/technologies"
10+
MiscellaneousValue,
11+
promptMiscellaneous,
12+
} from "./questions/categories/miscellaneous"
13+
import { promptPackageManager } from "./questions/categories/package-manager"
14+
import { promptStyling } from "./questions/categories/styling"
15+
16+
const categoryToPromptFunction = withKeyConstraint<CategoryValue>()({
17+
formatting: promptFormatting,
18+
formStateManagement: promptFormStateManagement,
19+
animation: promptAnimation,
20+
continuousIntegration: promptContinuousIntegration,
21+
} as const)
22+
23+
type PromptReturnType = ThenArg<
24+
ReturnType<typeof categoryToPromptFunction[CategoryValue]>
25+
>
26+
export type OptionalTechnology = PromptReturnType extends Array<unknown>
27+
? PromptReturnType[number]
28+
: PromptReturnType
729

830
export const performFlagsQuestionnaire =
931
async (): Promise<ValidCreateNextStackFlags> => {
10-
const technologies = await promptTechnologies()
32+
// Mandatory prompts
33+
const packageManager = await promptPackageManager()
34+
const stylingMethod = await promptStyling()
1135

12-
return {
13-
"package-manager": getPackageManager(technologies),
14-
prettier: technologies.includes("prettier"),
15-
styling: getStyling(technologies),
16-
"react-hook-form": technologies.includes("reactHookForm"),
17-
formik: technologies.includes("formik"),
18-
"framer-motion": technologies.includes("framerMotion"),
19-
"github-actions": technologies.includes("githubActions"),
20-
"formatting-pre-commit-hook": technologies.includes("preCommitHook"),
36+
// Optional categories prompt
37+
const optionalCategories = await promptOptionalCategories()
38+
const optionalTechnologies = new Set<OptionalTechnology>()
39+
for (const category of optionalCategories) {
40+
const additionalTechnologies = await categoryToPromptFunction[category]()
41+
additionalTechnologies.forEach((tech) => optionalTechnologies.add(tech))
2142
}
22-
}
23-
24-
type ThenArg<T> = T extends PromiseLike<infer U> ? U : T
2543

26-
const getPackageManager = (
27-
technologies: ThenArg<ReturnType<typeof promptTechnologies>>
28-
): PackageManagerOption => {
29-
// TODO: Strengthen typing. TypeScript throw error here when new package manager is added in promptTechnologies.
30-
if (technologies.includes("yarn")) {
31-
return "yarn"
32-
} else if (technologies.includes("npm")) {
33-
return "npm"
34-
} else {
35-
throw new Error("Package manager not found or not supported.")
36-
}
37-
}
44+
// TODO: Remove prettier-check when promptMiscellaneous adds more options
45+
let miscellaneous: Set<MiscellaneousValue> = new Set()
46+
if (optionalTechnologies.has("prettier")) {
47+
miscellaneous = await promptMiscellaneous(optionalTechnologies)
48+
}
3849

39-
const getStyling = (
40-
technologies: ThenArg<ReturnType<typeof promptTechnologies>>
41-
): StylingOption => {
42-
// TODO: Strengthen typing. TypeScript throw error here when new styling method is added in promptTechnologies.
43-
if (technologies.includes("emotion")) {
44-
return "emotion"
45-
} else if (technologies.includes("styledComponents")) {
46-
return "styled-components"
47-
} else if (technologies.includes("cssModules")) {
48-
return "css-modules"
49-
} else if (technologies.includes("cssModulesWithSass")) {
50-
return "css-modules-with-sass"
51-
} else {
52-
throw new Error("Styling method not found or not supported.")
50+
return {
51+
"package-manager": packageManager,
52+
styling: stylingMethod,
53+
prettier: optionalTechnologies.has("prettier"),
54+
"react-hook-form": optionalTechnologies.has("reactHookForm"),
55+
formik: optionalTechnologies.has("formik"),
56+
"framer-motion": optionalTechnologies.has("framerMotion"),
57+
"github-actions": optionalTechnologies.has("githubActions"),
58+
"formatting-pre-commit-hook": miscellaneous.has(
59+
"formattingPreCommitHook"
60+
),
61+
}
5362
}
54-
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import inquirer from "inquirer"
2+
import { arrayToKeyToKeyMap } from "../../helpers/array-to-key-to-key-map"
3+
4+
const categoryValuesArray = [
5+
"formatting",
6+
"formStateManagement",
7+
"animation",
8+
"continuousIntegration",
9+
] as const
10+
export type CategoryValue = typeof categoryValuesArray[number]
11+
const categoryValues = arrayToKeyToKeyMap(categoryValuesArray)
12+
13+
// TODO: You can strengthen typings by turning the choices array into an object located here, as in technologies.ts
14+
15+
export const promptOptionalCategories = async (): Promise<CategoryValue[]> => {
16+
const answerName = "categories"
17+
type ProjectNameAnswers = {
18+
[answerName]: CategoryValue[]
19+
}
20+
21+
const { categories } = await inquirer.prompt<ProjectNameAnswers>({
22+
name: answerName,
23+
type: "checkbox",
24+
message: "What categories of technologies are you looking to use?",
25+
choices: [
26+
{
27+
value: categoryValues.formatting,
28+
name: "Formatting",
29+
checked: true,
30+
},
31+
{
32+
value: categoryValues.formStateManagement,
33+
name: "Form state management",
34+
checked: true,
35+
},
36+
{
37+
value: categoryValues.animation,
38+
name: "Animation",
39+
checked: true,
40+
},
41+
{
42+
value: categoryValues.continuousIntegration,
43+
name: "Continuous integration",
44+
checked: true,
45+
},
46+
],
47+
})
48+
49+
return categories
50+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import inquirer from "inquirer"
2+
import { arrayToKeyToKeyMap } from "../../../helpers/array-to-key-to-key-map"
3+
4+
const animationValuesArray = ["framerMotion"] as const
5+
type AnimationValue = typeof animationValuesArray[number]
6+
const animationValues = arrayToKeyToKeyMap(animationValuesArray)
7+
8+
const answerName = "animation"
9+
type Answers = {
10+
[answerName]: AnimationValue[]
11+
}
12+
13+
// TODO: Make Framer Motion disabled when Chakra UI has been selected.
14+
15+
export const promptAnimation = async (): Promise<AnimationValue[]> => {
16+
const { animation } = await inquirer.prompt<Answers>({
17+
name: answerName,
18+
type: "checkbox",
19+
message: "Animation:",
20+
choices: [
21+
{
22+
value: animationValues.framerMotion,
23+
name: "Framer Motion",
24+
checked: true,
25+
},
26+
],
27+
})
28+
29+
return animation
30+
}

0 commit comments

Comments
 (0)