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

Add support for Prisma #258

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
26 changes: 11 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,17 @@ Make sure you are set up locally by following the [Getting Started](#getting-sta

## Writing a Plugin

Plugins aren't too scary. A Create Next Stack plugin consists of a simple TypeScript file that calls a `createPlugin()` function with JSON object.
Plugins aren't too scary. A Create Next Stack plugin consists of a simple TypeScript file that exports an object of type [`Plugin`](./packages/create-next-stack/src/main/plugin.ts).

See the [Framer Motion plugin](packages/create-next-stack/src/main/plugins/framer-motion.ts) for example. This plugin adds the `framer-motion` npm dependency to the generated Next.js project, as well as adding some documentation about the technology.

```typescript
export const framerMotionPlugin = createPlugin({
export const framerMotionPlugin: Plugin = {
id: "framer-motion",
name: "Framer Motion",
description: "Adds support for Framer Motion",
active: ({ flags }) => Boolean(flags["framer-motion"]),
dependencies: {
"framer-motion": {
name: "framer-motion",
version: "^9.0.0",
},
},
dependencies: [{ name: "framer-motion", version: "^9.0.0" }],
technologies: [
{
id: "framerMotion",
Expand All @@ -135,19 +130,20 @@ export const framerMotionPlugin = createPlugin({
],
},
],
} as const)
} as const
```

Below is a breakdown of the `createPlugin()` function's JSON object:
Below is a small breakdown of the properties of the above plugin object:

- The `id` property is a unique identifier for the plugin.
- The `name` property is the name of the plugin.
- The `description` property is a short description of the plugin.
- The `active` property is a function that returns a boolean indicating whether the plugin should be active. This function is passed the `flags` object, which contains all the flags passed to the `create-next-stack` command.
- The `dependencies` property is an object containing the npm dependencies that should be added to the generated Next.js project. The key and `name` property is the name of the dependency, and the `version` property is version of the dependency.
- The `technologies` property is an array of objects containing information about the technology. The `name` property is the name of the technology. The `description` property is a short description of the technology. The `links` property is an array of objects containing links to the technology's website, documentation, and GitHub repository.
- The `active` property is a function that returns a boolean indicating whether the plugin should be active. This function is passed the flags and arguments passed by users to the `create-next-stack` command.
- The `dependencies` property is an array of npm dependencies that should be added to the generated Next.js project.
- The `technologies` property is an array of objects containing documentation about the technologies supported by the plugin.

Some of these properties are optional, and some are required. Some properties are used by the CLI, some are used by the website, and some both. It's not too important to know everywhere these properties are used. As long as we specify as many properties as possible, the CLI and website is going to find out how to use it.
Some of these properties are optional, and some are required. Some properties are used by the CLI, some are used by the website, and some both. It's not too important to know exactly where these properties are used. As long as we specify all relevant properties, the CLI and website is going to find out how to use it.

For a complete list of properties that can be passed to the `createPlugin()` function, their explanations, and usage, see the [`Plugin` type definition](packages/create-next-stack/src/main/plugin.ts). You should find all the documentation you need there. If not, please [open an issue](https://github.com/akd-io/create-next-stack/issues/new).
For a complete list of properties of the `Plugin` type, their explanations, and usage, see the [`Plugin` type definition](packages/create-next-stack/src/main/plugin.ts). You should find all the documentation you need there. If not, please [open an issue](https://github.com/akd-io/create-next-stack/issues/new).

For more examples, please take a look at the [existing plugins](packages/create-next-stack/src/main/plugins).
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type OptionKey =
| "plausible"
| "vercel"
| "netlify"
| "prisma"

const options = {
pnpm: { key: "pnpm", value: "pnpm", label: "pnpm" },
Expand Down Expand Up @@ -124,6 +125,11 @@ const options = {
value: "netlify",
label: "Netlify",
},
prisma: {
key: "prisma",
value: "prisma",
label: "Prisma",
},
} satisfies {
[Key in OptionKey]: {
key: Key
Expand Down Expand Up @@ -173,6 +179,7 @@ const deploymentOptionKeys = [
optionKeys.vercel,
optionKeys.netlify,
] satisfies OptionKey[]
const ormOptionKeys = [optionKeys.prisma] satisfies OptionKey[]

type ProjectName = string
type PackageManager = (typeof packageManagerOptionKeys)[number]
Expand All @@ -187,6 +194,7 @@ type ServerStateManagementLibrary =
(typeof serverStateManagementLibraryOptionKeys)[number]
type Analytics = (typeof analyticsOptionKeys)[number]
type Deployment = (typeof deploymentOptionKeys)[number]
type ORM = (typeof ormOptionKeys)[number]

type TechnologiesFormData = {
projectName: ProjectName
Expand All @@ -201,6 +209,7 @@ type TechnologiesFormData = {
serverStateManagementLibraries: ServerStateManagementLibrary[]
analytics: Analytics[]
deployment: Deployment[]
orm: ORM[]
}
const defaultFormData: TechnologiesFormData = {
projectName: "my-app",
Expand All @@ -215,6 +224,7 @@ const defaultFormData: TechnologiesFormData = {
serverStateManagementLibraries: [optionKeys.reactQuery],
analytics: [],
deployment: [optionKeys.vercel],
orm: [],
}
const formDataKeys = objectToKeyToKeyMap(defaultFormData)

Expand All @@ -233,6 +243,7 @@ const categoryLabels = {
serverStateManagementLibraries: "Server State Management",
analytics: "Analytics",
deployment: "Deployment",
orm: "ORMs",
} as const

export const TechnologiesForm: React.FC = () => {
Expand Down Expand Up @@ -271,6 +282,7 @@ export const TechnologiesForm: React.FC = () => {
pushArgs(formData.serverStateManagementLibraries)
pushArgs(formData.analytics)
pushArgs(formData.deployment)
pushArgs(formData.orm)

const projectNameSegments = formData.projectName.split("/")
const lastPartOfProjectName = projectNameSegments.pop()!
Expand All @@ -293,7 +305,8 @@ export const TechnologiesForm: React.FC = () => {
| "continuousIntegration"
| "serverStateManagementLibraries"
| "analytics"
| "deployment",
| "deployment"
| "orm",
optionKeys: Array<keyof typeof options>,
validators?: {
[key in keyof typeof options]?: Array<{
Expand Down Expand Up @@ -455,6 +468,13 @@ export const TechnologiesForm: React.FC = () => {
analyticsOptionKeys
)}
</Flex>

<Flex direction="column" gap="4">
<Heading as="h3" size="md">
{categoryLabels.orm}
</Heading>
{CheckboxesOfOptionKeys(formDataKeys.orm, ormOptionKeys)}
</Flex>
</Flex>

<Flex direction="column" gap="8" flexBasis="100%">
Expand Down
2 changes: 2 additions & 0 deletions packages/create-next-stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ The table below provides an overview of the technologies currently supported by
| next-plausible | [Website](https://next-plausible.vercel.app/) - [GitHub](https://github.com/4lejandrito/next-plausible) |
| Vercel | [Website](https://vercel.com/) - [Docs](https://vercel.com/docs) - [CLI Docs](https://vercel.com/docs/cli) |
| Netlify | [Website](https://www.netlify.com/) - [Docs](https://docs.netlify.com/) - [CLI Docs](https://cli.netlify.com/) |
| Prisma | [Website](https://www.prisma.io/) - [Docs](https://www.prisma.io/docs) - [GitHub](https://github.com/prisma/prisma) |

<!-- CNS-END-OF-TECHNOLOGIES-TABLE -->

Expand Down Expand Up @@ -104,6 +105,7 @@ FLAGS
<options: pnpm|yarn|npm>
--plausible Adds Plausible. (Analytics)
--prettier Adds Prettier. (Code formatting)
--prisma Adds Prisma. (ORM)
--react-hook-form Adds React Hook Form. (Form library)
--react-icons Adds React Icons. (Icon library)
--react-query Adds React Query. (Server state management
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ export default class CreateNextStack extends Command {
vercel: Flags.boolean({
description: "Adds Vercel. (Hosting)",
}),

// ORMs
prisma: Flags.boolean({
description: "Adds Prisma. (ORM)",
}),
}

async run(): Promise<void> {
Expand Down
11 changes: 0 additions & 11 deletions packages/create-next-stack/src/main/helpers/deeply-readonly.ts

This file was deleted.

7 changes: 7 additions & 0 deletions packages/create-next-stack/src/main/helpers/filterAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const filterAsync = async <T>(
array: T[],
predicate: (item: T) => Promise<boolean>
): Promise<T[]> => {
const verdicts = await Promise.all(array.map(predicate))
return array.filter((_, index) => verdicts[index])
}
6 changes: 5 additions & 1 deletion packages/create-next-stack/src/main/helpers/io.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { existsSync } from "fs"
import fs from "fs/promises"
import path from "path"
import { logDebug, logError } from "../logging"
import { isUnknownArray } from "./is-unknown-array"
import { isUnknownObject } from "./is-unknown-object"
Expand All @@ -13,7 +14,10 @@ export const makeDirectory = async (path: string): Promise<void> => {
}

export const writeFile: typeof fs.writeFile = async (file, data, options) => {
logDebug("Writing file:", file.toString())
const fileString = file.toString()
const directory = path.dirname(fileString)
await makeDirectory(directory)
logDebug("Writing file:", fileString)
return fs.writeFile(file, data, options)
}

Expand Down
126 changes: 36 additions & 90 deletions packages/create-next-stack/src/main/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
import { NextConfig } from "next"
import { ValidCNSInputs } from "./create-next-stack-types"
import { DeeplyReadonly } from "./helpers/deeply-readonly"

type PluginConfig = DeeplyReadonly<{
export type Plugin = {
/** ID that uniquely identifies the plugin */
id: string
/** Name of the plugin */
name: string
/** Description of the plugin */
description: string
/** Whether the plugin is active or not. This determines if dependencies are installed, technologies and scripts added, steps run, and more. */
active: boolean | ((inputs: ValidCNSInputs) => boolean)
active: boolean | ((inputs: ValidCNSInputs) => boolean | Promise<boolean>)
/** Dependencies that are added to the package.json file. */
dependencies?: Record<string, Package>
dependencies?: Package[]
/** Dev dependencies that are added to the package.json file. */
devDependencies?: Record<string, Package>
devDependencies?: Package[]
/** Temporary dependencies uninstalled when Create Next Stack is done. */
tmpDependencies?: Record<string, Package>
tmpDependencies?: Package[]
/** Descriptions of the technologies supported by the plugin. */
technologies?: Technology[]
/** Scripts that are added to the package.json file. */
scripts?: Script[]
/** A series of functions that are run by Create Next Stack. */
steps?: Record<string, RawStep>
steps?: Step[]
/**
* Environment variables needed by the plugin.
* These variables are added to the generated .env and README.md files.
Expand All @@ -40,6 +39,20 @@ type PluginConfig = DeeplyReadonly<{
* The list will be added to the generated landing page, the README.md file and written to the console.
*/
todos?: string[]
/** Files to be added by the plugin. */
addFiles?: Array<{
/** Destination of the file to add. */
destination: string
/** Content of the file. */
content: string | ((inputs: ValidCNSInputs) => string | Promise<string>)
/**
* Condition to determine if the file should be added.
*
*/
condition?:
| boolean
| ((inputs: ValidCNSInputs) => boolean | Promise<boolean>)
}>
/** Slots to fill in the generated files. */
slots?: {
/** Slots to fill in the _app.tsx file. The file is generated using the following template:
Expand Down Expand Up @@ -150,7 +163,7 @@ type PluginConfig = DeeplyReadonly<{
wrappersEnd?: string
}
}
}>
}

export type Package = {
/** Name of the package. */
Expand Down Expand Up @@ -196,97 +209,30 @@ type Script = {
command: string
}

type RawStep = {
export type Step = {
/** ID that uniquely identified the technology across all plugins' steps. */
id: string

/**
* `description` should be written in present continuous tense, without punctuation, and with a lowercase first letter unless the description starts with a name or similar.
*
* Eg. "setting up Prettier" or "adding ESLint"
*/
/** A description of the step. It should be written in present continuous tense, without punctuation, and with a lowercase first letter unless the description starts with a name or similar. */
description: string

/**
* A boolean or function that determines whether the custom run function should run.
*
* Default is true
*/
/** A boolean or function that determines whether the custom run function should run. Default is true. */
shouldRun?: boolean | ((inputs: ValidCNSInputs) => Promise<boolean> | boolean)

/** Custom run function. */
run: (inputs: ValidCNSInputs) => Promise<void>
}

export const createPlugin = <TPluginConfig extends PluginConfig>(
pluginConfig: TPluginConfig
): Plugin<TPluginConfig> => {
const plugin = {
...pluginConfig,
}
const enhancements = {
steps:
pluginConfig.steps != null
? Object.entries(pluginConfig.steps).reduce(
(acc, [key, value]) => ({
...acc,
[key]: createStep(value, plugin as Plugin<TPluginConfig>),
}),
{} as Record<string, Step>
)
: undefined,
}
for (const [key, value] of Object.entries(enhancements)) {
Object.defineProperty(plugin, key, {
value,
enumerable: true,
})
}
return plugin as Plugin<TPluginConfig>
}

export const createStep = <TRawStep extends RawStep = RawStep>(
step: TRawStep,
plugin: Plugin
): Step<TRawStep> => {
return {
// defaults
shouldRun: true,

// TODO: Consider memoizing shouldRun, as it is sometimes called multiple times. See the lint-staged setup step.

// step
...step,

// enhancements
plugin,
}
}

export type Plugin<TPluginConfig extends PluginConfig = PluginConfig> =
TPluginConfig & {
steps?: {
[key in keyof TPluginConfig["steps"]]: Step<RawStep> // TODO: Fix type. This should be Step<TPluginConfig["steps"][key]>, but that doesn't work.
}
}

export type Step<TStep extends RawStep = RawStep> = TStep & {
shouldRun: NonNullable<RawStep["shouldRun"]>
plugin: Plugin
}

export const evalActive = (
active: PluginConfig["active"],
export const evalProperty = async <T extends boolean | string>(
value: T | ((inputs: ValidCNSInputs) => T | Promise<T>),
inputs: ValidCNSInputs
): boolean => {
if (typeof active === "function") return active(inputs)
return active
): Promise<T> => {
if (typeof value === "function") return await value(inputs)
return value
}

export const evalShouldRun = async (
shouldRun: Step["shouldRun"],
inputs: ValidCNSInputs
): Promise<boolean> => {
if (typeof shouldRun === "function") return await shouldRun(inputs)
return shouldRun
export const evalOptionalProperty = async <T extends boolean | string>(
value: T | ((inputs: ValidCNSInputs) => T | Promise<T>) | undefined,
inputs: ValidCNSInputs,
defaultValue: Exclude<T, undefined>
): Promise<T> => {
if (typeof value === "undefined") return defaultValue
return await evalProperty(value, inputs)
}
Loading