Compare commits
2 Commits
@blitzjs/c
...
siddharth/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
828e745fd8 | ||
|
|
368ef1466d |
@@ -7,5 +7,5 @@
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["web", "test-*", "toolkit-*", "@blitzjs/recipe-*"]
|
||||
"ignore": ["web", "test-*", "toolkit-*", "next13"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {BuildConfig} from "unbuild"
|
||||
|
||||
const config: BuildConfig = {
|
||||
entries: ["./src/index-browser", "./src/index-server", "./src/cli/index", "./src/installer"],
|
||||
entries: ["./src/index-browser", "./src/index-server", "./src/cli/index"],
|
||||
externals: ["index-browser.cjs", "index-browser.mjs", "index.cjs", "zod", "react"],
|
||||
declaration: true,
|
||||
rollup: {
|
||||
|
||||
1
packages/blitz/installer.d.ts
vendored
1
packages/blitz/installer.d.ts
vendored
@@ -1 +0,0 @@
|
||||
export * from "./dist/installer"
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require("./dist/installer.cjs")
|
||||
@@ -22,7 +22,6 @@
|
||||
"sideEffects": false,
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"installer.*",
|
||||
"dist/**",
|
||||
"bin/**"
|
||||
],
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
import arg from "arg"
|
||||
import {CliCommand} from "../index"
|
||||
import prompts from "prompts"
|
||||
import {bootstrap} from "global-agent"
|
||||
import {baseLogger, log} from "../../logging"
|
||||
import Debug from "debug"
|
||||
const debug = Debug("blitz:cli")
|
||||
import {join, resolve, dirname} from "path"
|
||||
import {Stream} from "stream"
|
||||
import {promisify} from "util"
|
||||
import {RecipeCLIFlags, RecipeExecutor} from "../../installer"
|
||||
import {setupTsnode} from "../utils/setup-ts-node"
|
||||
import {isInternalBlitzMonorepoDevelopment} from "../utils/helpers"
|
||||
import findUp from "find-up"
|
||||
import resolveFrom from "resolve-from"
|
||||
import {findNodeModulesRoot} from "../utils/find-node-modules"
|
||||
|
||||
interface GlobalAgent {
|
||||
HTTP_PROXY?: string
|
||||
HTTPS_PROXY?: string
|
||||
NO_PROXY?: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
var GLOBAL_AGENT: GlobalAgent
|
||||
}
|
||||
|
||||
const args = arg(
|
||||
{
|
||||
// Types
|
||||
"--help": Boolean,
|
||||
"--env": String,
|
||||
"--yes": Boolean,
|
||||
|
||||
// Aliases
|
||||
"-e": "--env",
|
||||
"-y": "--yes",
|
||||
},
|
||||
{
|
||||
permissive: true,
|
||||
},
|
||||
)
|
||||
|
||||
const pipeline = promisify(Stream.pipeline)
|
||||
|
||||
const got = async (url: string) => {
|
||||
return require("got")(url).catch((e: any) => {
|
||||
if (e.response.statusCode === 403) {
|
||||
baseLogger().error(e.response.body)
|
||||
} else {
|
||||
return e
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const gotJSON = async (url: string) => {
|
||||
debug("[gotJSON] Downloading json from ", url)
|
||||
const res = await got(url)
|
||||
return JSON.parse(res.body)
|
||||
}
|
||||
|
||||
const isUrlValid = async (url: string) => {
|
||||
return (await got(url).catch((e) => e)).statusCode === 200
|
||||
}
|
||||
|
||||
const requireJSON = (file: string) => {
|
||||
return JSON.parse(require("fs-extra").readFileSync(file).toString("utf-8"))
|
||||
}
|
||||
|
||||
const checkLockFileExists = async (filename: string) => {
|
||||
const dotBlitz = join(await findNodeModulesRoot(process.cwd()), ".blitz")
|
||||
return require("fs-extra").existsSync(resolve(join(dotBlitz, "..", "..", filename)))
|
||||
}
|
||||
|
||||
const GH_ROOT = "https://github.com/"
|
||||
const API_ROOT = "https://api.github.com/repos/"
|
||||
const RAW_ROOT = "https://raw.githubusercontent.com/"
|
||||
const CODE_ROOT = "https://codeload.github.com/"
|
||||
|
||||
export enum RecipeLocation {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
interface RecipeMeta {
|
||||
path: string
|
||||
subdirectory?: string
|
||||
location: RecipeLocation
|
||||
}
|
||||
|
||||
interface Tree {
|
||||
path: string
|
||||
mode: string
|
||||
type: string
|
||||
sha: string
|
||||
size: number
|
||||
url: string
|
||||
}
|
||||
|
||||
interface GithubRepoAPITrees {
|
||||
sha: string
|
||||
url: string
|
||||
tree: Tree[]
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
const getOfficialRecipeList = async (): Promise<string[]> => {
|
||||
return await gotJSON(`${API_ROOT}blitz-js/blitz/git/trees/main?recursive=1`).then(
|
||||
(release: GithubRepoAPITrees) =>
|
||||
release.tree.reduce((recipesList: string[], item) => {
|
||||
const filePath = item.path.split("/")
|
||||
const [directory, recipeName] = filePath
|
||||
if (
|
||||
directory === "recipes" &&
|
||||
filePath.length === 2 &&
|
||||
item.type === "tree" &&
|
||||
recipeName
|
||||
) {
|
||||
recipesList.push(recipeName)
|
||||
}
|
||||
return recipesList
|
||||
}, []),
|
||||
)
|
||||
}
|
||||
|
||||
const normalizeRecipePath = (recipeArg: string): RecipeMeta => {
|
||||
const isNativeRecipe = /^([\w\-_]*)$/.test(recipeArg)
|
||||
const isUrlRecipe = recipeArg.startsWith(GH_ROOT)
|
||||
const isGitHubShorthandRecipe = /^([\w-_]*)\/([\w-_]*)$/.test(recipeArg)
|
||||
if (isNativeRecipe || isUrlRecipe || isGitHubShorthandRecipe) {
|
||||
let repoUrl
|
||||
let subdirectory
|
||||
switch (true) {
|
||||
case isUrlRecipe:
|
||||
repoUrl = recipeArg
|
||||
break
|
||||
case isNativeRecipe:
|
||||
repoUrl = `${GH_ROOT}blitz-js/blitz`
|
||||
subdirectory = `recipes/${recipeArg}`
|
||||
break
|
||||
case isGitHubShorthandRecipe:
|
||||
repoUrl = `${GH_ROOT}${recipeArg}`
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
"should be impossible, the 3 cases are the only way to get into this switch",
|
||||
)
|
||||
}
|
||||
return {
|
||||
path: repoUrl,
|
||||
subdirectory,
|
||||
location: RecipeLocation.Remote,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
path: recipeArg,
|
||||
location: RecipeLocation.Local,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cloneRepo = async (
|
||||
repoFullName: string,
|
||||
defaultBranch: string,
|
||||
subdirectory?: string,
|
||||
): Promise<string> => {
|
||||
debug("[cloneRepo] starting...")
|
||||
const dotBlitz = join(await findNodeModulesRoot(process.cwd()), ".blitz")
|
||||
const recipeDir = join(dotBlitz, "..", "..", "recipe-install")
|
||||
// clean up from previous run in case of error
|
||||
require("rimraf").sync(recipeDir)
|
||||
require("fs-extra").mkdirsSync(recipeDir)
|
||||
process.chdir(recipeDir)
|
||||
debug("Extracting recipe to ", recipeDir)
|
||||
|
||||
const repoName = repoFullName.split("/")[1]
|
||||
// `tar` top-level filter is `${repoName}-${defaultBranch}`, and then we want to get our recipe path
|
||||
// within that folder
|
||||
const extractPath = subdirectory ? [`${repoName}-${defaultBranch}/${subdirectory}`] : undefined
|
||||
const depth = subdirectory ? subdirectory.split("/").length + 1 : 1
|
||||
await pipeline(
|
||||
require("got").stream(`${CODE_ROOT}${repoFullName}/tar.gz/${defaultBranch}`),
|
||||
require("tar").extract({strip: depth}, extractPath),
|
||||
)
|
||||
|
||||
return recipeDir
|
||||
}
|
||||
|
||||
const installRecipeAtPath = async (
|
||||
recipePath: string,
|
||||
...runArgs: Parameters<RecipeExecutor<any>["run"]>
|
||||
) => {
|
||||
const recipe = require(recipePath).default as RecipeExecutor<any>
|
||||
|
||||
await recipe.run(...runArgs)
|
||||
}
|
||||
|
||||
const setupProxySupport = async () => {
|
||||
const httpProxy = process.env.http_proxy || process.env.HTTP_PROXY
|
||||
const httpsProxy = process.env.https_proxy || process.env.HTTPS_PROXY
|
||||
const noProxy = process.env.no_proxy || process.env.NO_PROXY
|
||||
|
||||
if (httpProxy || httpsProxy) {
|
||||
globalThis.GLOBAL_AGENT = {
|
||||
HTTP_PROXY: httpProxy,
|
||||
HTTPS_PROXY: httpsProxy,
|
||||
NO_PROXY: noProxy,
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
}
|
||||
}
|
||||
|
||||
const install: CliCommand = async () => {
|
||||
setupTsnode()
|
||||
let selectedRecipe: string | null = args._[1] ? `${args._[1]}` : null
|
||||
await setupProxySupport()
|
||||
|
||||
if (!selectedRecipe) {
|
||||
const officialRecipeList = await getOfficialRecipeList()
|
||||
const res = await prompts({
|
||||
type: "select",
|
||||
name: "recipeName",
|
||||
message: "Select a recipe to install",
|
||||
choices: officialRecipeList.map((r) => {
|
||||
return {title: r, value: r}
|
||||
}),
|
||||
})
|
||||
selectedRecipe = res.recipeName
|
||||
}
|
||||
|
||||
if (selectedRecipe) {
|
||||
const recipeInfo = normalizeRecipePath(selectedRecipe)
|
||||
// Take all the args after the recipe string
|
||||
//
|
||||
// ['material-ui', '--yes', 'prop=true']
|
||||
// --> ['material-ui', 'prop=true']
|
||||
// --> ['prop=true']
|
||||
// --> { prop: 'true' }
|
||||
const cliArgs = args._.filter((arg) => !arg.startsWith("--"))
|
||||
.slice(2)
|
||||
.reduce(
|
||||
(acc, arg) => ({
|
||||
...acc,
|
||||
[`${arg.split("=")[0]}`]: arg.split("=")[1] ? JSON.parse(`"${arg.split("=")[1]}"`) : true, // if no value is provided, assume it's a boolean flag
|
||||
}),
|
||||
{},
|
||||
)
|
||||
|
||||
const cliFlags: RecipeCLIFlags = {
|
||||
yesToAll: args["--yes"] || false,
|
||||
}
|
||||
|
||||
const chalk = (await import("chalk")).default
|
||||
if (recipeInfo.location === RecipeLocation.Remote) {
|
||||
const apiUrl = recipeInfo.path.replace(GH_ROOT, API_ROOT)
|
||||
const rawUrl = recipeInfo.path.replace(GH_ROOT, RAW_ROOT)
|
||||
const repoInfo = await gotJSON(apiUrl)
|
||||
const packageJsonPath = join(
|
||||
`${rawUrl}`,
|
||||
repoInfo.default_branch,
|
||||
recipeInfo.subdirectory ?? "",
|
||||
"package.json",
|
||||
)
|
||||
|
||||
if (!(await isUrlValid(packageJsonPath))) {
|
||||
debug("Url is invalid for ", packageJsonPath)
|
||||
baseLogger().error(`Could not find recipe "${args._[1]}"\n`)
|
||||
console.log(`${chalk.bold("Please provide one of the following:")}
|
||||
|
||||
1. The name of a recipe to install (e.g. "tailwind")
|
||||
${chalk.dim("- Available recipes listed at https://github.com/blitz-js/blitz/tree/main/recipes")}
|
||||
2. The full name of a GitHub repository (e.g. "blitz-js/example-recipe"),
|
||||
3. A full URL to a Github repository (e.g. "https://github.com/blitz-js/example-recipe"), or
|
||||
4. A file path to a locally-written recipe.\n`)
|
||||
process.exit(1)
|
||||
} else {
|
||||
let spinner = log.spinner(`Cloning GitHub repository for ${selectedRecipe} recipe`).start()
|
||||
const recipeRepoPath = await cloneRepo(
|
||||
repoInfo.full_name,
|
||||
repoInfo.default_branch,
|
||||
recipeInfo.subdirectory,
|
||||
)
|
||||
spinner.stop()
|
||||
spinner = log.spinner("Installing package.json dependencies").start()
|
||||
|
||||
let pkgManager = "npm"
|
||||
let installArgs = ["install", "--legacy-peer-deps", "--ignore-scripts"]
|
||||
|
||||
if (await checkLockFileExists("yarn.lock")) {
|
||||
pkgManager = "yarn"
|
||||
installArgs = ["install", "--ignore-scripts"]
|
||||
} else if (await checkLockFileExists("pnpm-lock.yaml")) {
|
||||
pkgManager = "pnpm"
|
||||
installArgs = ["install", "--ignore-scripts"]
|
||||
}
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const installProcess = require("cross-spawn")(pkgManager, installArgs)
|
||||
installProcess.on("exit", resolve)
|
||||
})
|
||||
spinner.stop()
|
||||
|
||||
const recipePackageMain = requireJSON("./package.json").main
|
||||
const recipeEntry = resolve(recipePackageMain)
|
||||
process.chdir(join(process.cwd(), ".."))
|
||||
|
||||
await installRecipeAtPath(recipeEntry, cliArgs, cliFlags)
|
||||
|
||||
require("rimraf").sync(recipeRepoPath)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await installRecipeAtPath(resolve(`${args._[1]}`), cliArgs, cliFlags)
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new Error(err.message)
|
||||
}
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {install}
|
||||
@@ -35,7 +35,6 @@ const commands = {
|
||||
generate: () => import("./commands/generate").then((i) => i.generate),
|
||||
codegen: () => import("./commands/codegen").then((i) => i.codegen),
|
||||
db: () => import("./commands/db").then((i) => i.db),
|
||||
install: () => import("./commands/install").then((i) => i.install),
|
||||
console: () => import("./commands/console").then((i) => i.consoleREPL),
|
||||
routes: () => import("./commands/routes").then((i) => i.routes),
|
||||
}
|
||||
@@ -47,7 +46,6 @@ const aliases: Record<string, keyof typeof commands> = {
|
||||
e: "export",
|
||||
n: "new",
|
||||
g: "generate",
|
||||
i: "install",
|
||||
c: "console",
|
||||
r: "routes",
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import {Text} from "ink"
|
||||
import * as React from "react"
|
||||
import {Newline} from "./newline"
|
||||
|
||||
export const EnterToContinue: React.FC<{message?: string}> = ({
|
||||
message = "Press ENTER to continue",
|
||||
}) => (
|
||||
<>
|
||||
<Newline />
|
||||
<Text bold>{message}</Text>
|
||||
</>
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
import {Box} from "ink"
|
||||
import * as React from "react"
|
||||
|
||||
export const Newline: React.FC<{count?: number}> = ({count = 1}) => {
|
||||
return <Box paddingBottom={count} />
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
import {spawn} from "cross-spawn"
|
||||
import * as fs from "fs-extra"
|
||||
import {Box, Text} from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import * as path from "path"
|
||||
import * as React from "react"
|
||||
import {Newline} from "../components/newline"
|
||||
import {RecipeCLIArgs} from "../types"
|
||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
||||
import {useUserInput} from "../utils/use-user-input"
|
||||
import {IExecutor, executorArgument, ExecutorConfig, getExecutorArgument} from "./executor"
|
||||
|
||||
interface NpmPackage {
|
||||
name: string
|
||||
// defaults to latest published
|
||||
version?: string
|
||||
// defaults to false
|
||||
isDevDep?: boolean
|
||||
}
|
||||
|
||||
export interface Config extends ExecutorConfig {
|
||||
packages: executorArgument<NpmPackage[]>
|
||||
}
|
||||
|
||||
export function isAddDependencyExecutor(executor: ExecutorConfig): executor is Config {
|
||||
return (executor as Config).packages !== undefined
|
||||
}
|
||||
|
||||
export const type = "add-dependency"
|
||||
|
||||
function Package({pkg, loading}: {pkg: NpmPackage; loading: boolean}) {
|
||||
return (
|
||||
<Text>
|
||||
{` `}
|
||||
{loading ? <Spinner /> : "📦"}
|
||||
{` ${pkg.name}@${pkg.version}`}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const DependencyList = ({
|
||||
lede = "Hang tight! Installing dependencies...",
|
||||
depsLoading = false,
|
||||
devDepsLoading = false,
|
||||
packages,
|
||||
}: {
|
||||
lede?: string
|
||||
depsLoading?: boolean
|
||||
devDepsLoading?: boolean
|
||||
packages: NpmPackage[]
|
||||
}) => {
|
||||
const prodPackages = packages.filter((p) => !p.isDevDep)
|
||||
const devPackages = packages.filter((p) => p.isDevDep)
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>{lede}</Text>
|
||||
<Newline />
|
||||
{prodPackages.length ? <Text>Dependencies to be installed:</Text> : null}
|
||||
{prodPackages.map((pkg) => (
|
||||
<Package key={pkg.name} pkg={pkg} loading={depsLoading} />
|
||||
))}
|
||||
<Newline />
|
||||
{devPackages.length ? <Text>Dev Dependencies to be installed:</Text> : null}
|
||||
{devPackages.map((pkg) => (
|
||||
<Package key={pkg.name} pkg={pkg} loading={devDepsLoading} />
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exported for unit testing purposes
|
||||
*/
|
||||
export function getPackageManager() {
|
||||
if (fs.existsSync(path.resolve("yarn.lock"))) {
|
||||
return "yarn"
|
||||
} else if (fs.existsSync(path.resolve("pnpm-lock.yaml"))) {
|
||||
return "pnpm"
|
||||
} else {
|
||||
return "npm"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exported for unit testing purposes
|
||||
*/
|
||||
export async function installPackages(packages: NpmPackage[], isDev = false) {
|
||||
const packageManager = getPackageManager()
|
||||
const isNPM = packageManager === "npm"
|
||||
const pkgInstallArg = isNPM ? "install" : "add"
|
||||
const args: string[] = [pkgInstallArg]
|
||||
|
||||
if (isDev) {
|
||||
args.push(isNPM ? "--save-dev" : "-D")
|
||||
}
|
||||
packages.forEach((pkg) => {
|
||||
pkg.version ? args.push(`${pkg.name}@${pkg.version}`) : args.push(pkg.name)
|
||||
})
|
||||
await new Promise((resolve) => {
|
||||
const cp = spawn(packageManager, args, {
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
})
|
||||
cp.on("exit", resolve)
|
||||
})
|
||||
}
|
||||
|
||||
export const Commit: IExecutor["Commit"] = ({cliArgs, cliFlags, step, onChangeCommitted}) => {
|
||||
const userInput = useUserInput(cliFlags)
|
||||
const [depsInstalled, setDepsInstalled] = React.useState(false)
|
||||
const [devDepsInstalled, setDevDepsInstalled] = React.useState(false)
|
||||
|
||||
const handleChangeCommitted = React.useCallback(() => {
|
||||
const packages = (step as Config).packages
|
||||
const dependencies = packages.length === 1 ? "dependency" : "dependencies"
|
||||
onChangeCommitted(`Installed ${packages.length} ${dependencies}`)
|
||||
}, [onChangeCommitted, step])
|
||||
|
||||
React.useEffect(() => {
|
||||
async function installDeps() {
|
||||
const packagesToInstall = getExecutorArgument((step as Config).packages, cliArgs).filter(
|
||||
(p) => !p.isDevDep,
|
||||
)
|
||||
await installPackages(packagesToInstall)
|
||||
setDepsInstalled(true)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
installDeps()
|
||||
}, [cliArgs, step])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!depsInstalled) return
|
||||
async function installDevDeps() {
|
||||
const packagesToInstall = getExecutorArgument((step as Config).packages, cliArgs).filter(
|
||||
(p) => p.isDevDep,
|
||||
)
|
||||
await installPackages(packagesToInstall, true)
|
||||
setDevDepsInstalled(true)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
installDevDeps()
|
||||
}, [cliArgs, depsInstalled, step])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (depsInstalled && devDepsInstalled) {
|
||||
handleChangeCommitted()
|
||||
}
|
||||
}, [depsInstalled, devDepsInstalled, handleChangeCommitted])
|
||||
|
||||
if (!isAddDependencyExecutor(step)) {
|
||||
onChangeCommitted()
|
||||
return null
|
||||
}
|
||||
|
||||
const childProps: CommitChildProps = {
|
||||
depsInstalled,
|
||||
devDepsInstalled,
|
||||
handleChangeCommitted,
|
||||
step,
|
||||
cliArgs,
|
||||
}
|
||||
|
||||
if (userInput) return <CommitWithInput {...childProps} />
|
||||
else return <CommitWithoutInput {...childProps} />
|
||||
}
|
||||
|
||||
interface CommitChildProps {
|
||||
depsInstalled: boolean
|
||||
devDepsInstalled: boolean
|
||||
handleChangeCommitted: () => void
|
||||
step: Config
|
||||
cliArgs: RecipeCLIArgs
|
||||
}
|
||||
|
||||
const CommitWithInput = ({
|
||||
depsInstalled,
|
||||
devDepsInstalled,
|
||||
handleChangeCommitted,
|
||||
step,
|
||||
cliArgs,
|
||||
}: CommitChildProps) => {
|
||||
useEnterToContinue(handleChangeCommitted, depsInstalled && devDepsInstalled)
|
||||
|
||||
return (
|
||||
<DependencyList
|
||||
depsLoading={!depsInstalled}
|
||||
devDepsLoading={!devDepsInstalled}
|
||||
packages={getExecutorArgument(step.packages, cliArgs)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CommitWithoutInput = ({depsInstalled, devDepsInstalled, step, cliArgs}: CommitChildProps) => (
|
||||
<DependencyList
|
||||
depsLoading={!depsInstalled}
|
||||
devDepsLoading={!devDepsInstalled}
|
||||
packages={getExecutorArgument(step.packages, cliArgs)}
|
||||
/>
|
||||
)
|
||||
@@ -1,71 +0,0 @@
|
||||
import {Box, Text} from "ink"
|
||||
import * as React from "react"
|
||||
import {Newline} from "../components/newline"
|
||||
import {RecipeCLIArgs, RecipeCLIFlags} from "../types"
|
||||
|
||||
export interface ExecutorConfig {
|
||||
successIcon?: string
|
||||
stepId: string | number
|
||||
stepName: string
|
||||
stepType: string
|
||||
// a bit to display to the user to give context to the change
|
||||
explanation: string
|
||||
}
|
||||
|
||||
export interface IExecutor {
|
||||
type: string
|
||||
Propose?: React.FC<{
|
||||
step: ExecutorConfig
|
||||
onProposalAccepted: (data?: any) => void
|
||||
cliArgs: RecipeCLIArgs
|
||||
cliFlags: RecipeCLIFlags
|
||||
}>
|
||||
Commit: React.FC<{
|
||||
step: ExecutorConfig
|
||||
proposalData?: any
|
||||
onChangeCommitted: (data?: any) => void
|
||||
cliArgs: RecipeCLIArgs
|
||||
cliFlags: RecipeCLIFlags
|
||||
}>
|
||||
}
|
||||
|
||||
type dynamicExecutorArgument<T> = (cliArgs: RecipeCLIArgs) => T
|
||||
|
||||
function isDynamicExecutorArgument<T>(
|
||||
input: executorArgument<T>,
|
||||
): input is dynamicExecutorArgument<T> {
|
||||
return typeof (input as dynamicExecutorArgument<T>) === "function"
|
||||
}
|
||||
|
||||
export type executorArgument<T> = T | dynamicExecutorArgument<T>
|
||||
|
||||
export function Frontmatter({executor}: {executor: ExecutorConfig}) {
|
||||
const lineLength = executor.stepName.length + 6
|
||||
const verticalBorder = `+${new Array(lineLength).fill("–").join("")}+`
|
||||
return (
|
||||
<Box flexDirection="column" paddingBottom={1}>
|
||||
<Newline />
|
||||
<Box flexDirection="column">
|
||||
<Text color="#8a3df0" bold>
|
||||
{verticalBorder}
|
||||
</Text>
|
||||
<Text color="#8a3df0" bold>
|
||||
⎪ {executor.stepName} ⎪
|
||||
</Text>
|
||||
<Text color="#8a3df0" bold>
|
||||
{verticalBorder}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text color="gray" italic>
|
||||
{executor.explanation}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function getExecutorArgument<T>(input: executorArgument<T>, cliArgs: RecipeCLIArgs): T {
|
||||
if (isDynamicExecutorArgument(input)) {
|
||||
return input(cliArgs)
|
||||
}
|
||||
return input
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// import { prompt as enquirer } from 'enquirer'
|
||||
import prompts from "prompts"
|
||||
|
||||
enum SearchType {
|
||||
file,
|
||||
directory,
|
||||
}
|
||||
|
||||
interface FilePromptOptions {
|
||||
globFilter?: string
|
||||
getChoices?(context: any): string[]
|
||||
searchType?: SearchType
|
||||
context: any
|
||||
}
|
||||
|
||||
async function getMatchingFiles(filter: string = ""): Promise<string[]> {
|
||||
let {globby} = await import("globby")
|
||||
return globby(filter, {expandDirectories: true})
|
||||
}
|
||||
|
||||
export async function filePrompt(options: FilePromptOptions): Promise<string> {
|
||||
const choices = options.getChoices
|
||||
? options.getChoices(options.context)
|
||||
: await getMatchingFiles(options.globFilter)
|
||||
|
||||
if (choices.length === 1) {
|
||||
return `${choices[0]}`
|
||||
}
|
||||
|
||||
const results: {file: string} = await prompts({
|
||||
type: "autocomplete",
|
||||
name: "file",
|
||||
message: "Select the target file",
|
||||
// @ts-ignore
|
||||
limit: 10,
|
||||
choices: choices.map((choice) => {
|
||||
return {title: choice, value: choice}
|
||||
}),
|
||||
})
|
||||
return results.file
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
import {createPatch} from "diff"
|
||||
import * as fs from "fs-extra"
|
||||
import {Box, Text} from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import * as React from "react"
|
||||
import {EnterToContinue} from "../components/enter-to-continue"
|
||||
import {RecipeCLIArgs} from "../types"
|
||||
import {
|
||||
processFile,
|
||||
stringProcessFile,
|
||||
StringTransformer,
|
||||
transform,
|
||||
Transformer,
|
||||
TransformStatus,
|
||||
} from "../utils/transform"
|
||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
||||
import {useUserInput} from "../utils/use-user-input"
|
||||
import {IExecutor, executorArgument, ExecutorConfig, getExecutorArgument} from "./executor"
|
||||
import {filePrompt} from "./file-prompt"
|
||||
|
||||
export interface Config extends ExecutorConfig {
|
||||
selectTargetFiles?(cliArgs: RecipeCLIArgs): any[]
|
||||
singleFileSearch?: executorArgument<string>
|
||||
transform?: Transformer
|
||||
transformPlain?: StringTransformer
|
||||
}
|
||||
|
||||
export function isFileTransformExecutor(executor: ExecutorConfig): executor is Config {
|
||||
return (
|
||||
(executor as Config).transform !== undefined ||
|
||||
(executor as Config).transformPlain !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
export const type = "file-transform"
|
||||
export const Propose: IExecutor["Propose"] = ({cliArgs, cliFlags, onProposalAccepted, step}) => {
|
||||
const userInput = useUserInput(cliFlags)
|
||||
const [diff, setDiff] = React.useState<string | null>(null)
|
||||
const [error, setError] = React.useState<Error | null>(null)
|
||||
const [filePath, setFilePath] = React.useState("")
|
||||
const [proposalAccepted, setProposalAccepted] = React.useState(false)
|
||||
|
||||
const acceptProposal = React.useCallback(() => {
|
||||
setProposalAccepted(true)
|
||||
onProposalAccepted(filePath)
|
||||
}, [onProposalAccepted, filePath])
|
||||
|
||||
React.useEffect(() => {
|
||||
async function generateDiff() {
|
||||
const fileToTransform: string = await filePrompt({
|
||||
context: cliArgs,
|
||||
globFilter: getExecutorArgument((step as Config).singleFileSearch, cliArgs),
|
||||
getChoices: (step as Config).selectTargetFiles,
|
||||
})
|
||||
|
||||
setFilePath(fileToTransform)
|
||||
const originalFile = fs.readFileSync(fileToTransform).toString("utf-8")
|
||||
|
||||
const newFile = await ((step as Config).transformPlain
|
||||
? stringProcessFile(originalFile, (step as Config).transformPlain!)
|
||||
: processFile(originalFile, (step as Config).transform!))
|
||||
|
||||
return createPatch(fileToTransform, originalFile, newFile)
|
||||
}
|
||||
|
||||
generateDiff().then(setDiff, setError)
|
||||
}, [cliArgs, step])
|
||||
|
||||
// Let the renderer deal with errors from file transformers, otherwise the
|
||||
// process would just hang.
|
||||
if (error) throw error
|
||||
|
||||
if (!diff) {
|
||||
return (
|
||||
<Box>
|
||||
<Text>
|
||||
<Spinner />
|
||||
Generating file diff...
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const childProps: ProposeChildProps = {
|
||||
diff,
|
||||
filePath,
|
||||
proposalAccepted,
|
||||
acceptProposal,
|
||||
}
|
||||
|
||||
if (userInput) return <ProposeWithInput {...childProps} />
|
||||
else return <ProposeWithoutInput {...childProps} />
|
||||
}
|
||||
|
||||
interface ProposeChildProps {
|
||||
diff: string
|
||||
filePath: string
|
||||
proposalAccepted: boolean
|
||||
acceptProposal: () => void
|
||||
}
|
||||
|
||||
const Diff = ({diff}: {diff: string}) => (
|
||||
<>
|
||||
{diff
|
||||
.split("\n")
|
||||
.slice(2)
|
||||
.map((line, idx) => {
|
||||
let styleProps: any = {}
|
||||
if (line.startsWith("-") && !line.startsWith("---")) {
|
||||
styleProps.bold = true
|
||||
styleProps.color = "red"
|
||||
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
||||
styleProps.bold = true
|
||||
styleProps.color = "green"
|
||||
}
|
||||
return (
|
||||
<Text {...styleProps} key={idx}>
|
||||
{line}
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
const ProposeWithInput = ({
|
||||
diff,
|
||||
filePath,
|
||||
proposalAccepted,
|
||||
acceptProposal,
|
||||
}: ProposeChildProps) => {
|
||||
useEnterToContinue(acceptProposal, filePath !== "" && !proposalAccepted)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Diff diff={diff} />
|
||||
<EnterToContinue message="The above changes will be made. Press ENTER to continue" />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const ProposeWithoutInput = ({
|
||||
diff,
|
||||
filePath,
|
||||
proposalAccepted,
|
||||
acceptProposal,
|
||||
}: ProposeChildProps) => {
|
||||
React.useEffect(() => {
|
||||
if (filePath !== "" && !proposalAccepted) {
|
||||
acceptProposal()
|
||||
}
|
||||
}, [acceptProposal, filePath, proposalAccepted])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Diff diff={diff} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export const Commit: IExecutor["Commit"] = ({onChangeCommitted, proposalData: filePath, step}) => {
|
||||
React.useEffect(() => {
|
||||
void (async function () {
|
||||
const results = await transform(
|
||||
async (original) =>
|
||||
await ((step as Config).transformPlain
|
||||
? stringProcessFile(original, (step as Config).transformPlain!)
|
||||
: processFile(original, (step as Config).transform!)),
|
||||
[filePath],
|
||||
)
|
||||
if (results.some((r) => r.status === TransformStatus.Failure)) {
|
||||
console.error(results)
|
||||
}
|
||||
onChangeCommitted(`Modified file: ${filePath}`)
|
||||
})()
|
||||
}, [filePath, onChangeCommitted, step])
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Applying file changes</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import {Generator, GeneratorOptions, SourceRootType} from "@blitzjs/generator"
|
||||
import {Box, Text} from "ink"
|
||||
import {useEffect, useState} from "react"
|
||||
import * as React from "react"
|
||||
import {EnterToContinue} from "../components/enter-to-continue"
|
||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
||||
import {useUserInput} from "../utils/use-user-input"
|
||||
import {IExecutor, executorArgument, ExecutorConfig, getExecutorArgument} from "./executor"
|
||||
|
||||
export interface Config extends ExecutorConfig {
|
||||
targetDirectory?: executorArgument<string>
|
||||
templatePath: executorArgument<string>
|
||||
templateValues: executorArgument<{[key: string]: string}>
|
||||
destinationPathPrompt?: executorArgument<string>
|
||||
}
|
||||
|
||||
export function isNewFileExecutor(executor: ExecutorConfig): executor is Config {
|
||||
return (executor as Config).templatePath !== undefined
|
||||
}
|
||||
|
||||
export const type = "new-file"
|
||||
|
||||
interface TempGeneratorOptions extends GeneratorOptions {
|
||||
targetDirectory?: string
|
||||
templateRoot: string
|
||||
templateValues: any
|
||||
run?: any
|
||||
}
|
||||
|
||||
class TempGenerator extends Generator<TempGeneratorOptions> {
|
||||
sourceRoot: SourceRootType
|
||||
targetDirectory: string
|
||||
templateValues: any
|
||||
returnResults = true
|
||||
|
||||
constructor(options: TempGeneratorOptions) {
|
||||
super(options)
|
||||
this.sourceRoot = {type: "absolute", path: options.templateRoot}
|
||||
this.templateValues = options.templateValues
|
||||
this.targetDirectory = options.targetDirectory || "."
|
||||
}
|
||||
|
||||
getTemplateValues() {
|
||||
return this.templateValues
|
||||
}
|
||||
|
||||
getTargetDirectory() {
|
||||
return this.targetDirectory
|
||||
}
|
||||
}
|
||||
|
||||
export const Commit: IExecutor["Commit"] = ({cliArgs, cliFlags, onChangeCommitted, step}) => {
|
||||
const userInput = useUserInput(cliFlags)
|
||||
const generatorArgs = React.useMemo(
|
||||
() => ({
|
||||
destinationRoot: ".",
|
||||
targetDirectory: getExecutorArgument((step as Config).targetDirectory, cliArgs),
|
||||
templateRoot: getExecutorArgument((step as Config).templatePath, cliArgs),
|
||||
templateValues: getExecutorArgument((step as Config).templateValues, cliArgs),
|
||||
}),
|
||||
[cliArgs, step],
|
||||
)
|
||||
const [fileCreateOutput, setFileCreateOutput] = useState("")
|
||||
const [changeCommited, setChangeCommited] = useState(false)
|
||||
const fileCreateLines = fileCreateOutput.split("\n")
|
||||
const handleChangeCommitted = React.useCallback(() => {
|
||||
setChangeCommited(true)
|
||||
onChangeCommitted(
|
||||
`Successfully created ${fileCreateLines
|
||||
.map((l) => l.split(" ").slice(1).join("").trim())
|
||||
.join(", ")}`,
|
||||
)
|
||||
}, [fileCreateLines, onChangeCommitted])
|
||||
|
||||
useEffect(() => {
|
||||
async function createNewFiles() {
|
||||
if (!fileCreateOutput) {
|
||||
const generator = new TempGenerator(generatorArgs)
|
||||
const results = (await generator.run()) as unknown as string
|
||||
setFileCreateOutput(results)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
createNewFiles()
|
||||
}, [fileCreateOutput, generatorArgs])
|
||||
|
||||
const childProps: CommitChildProps = {
|
||||
changeCommited,
|
||||
fileCreateOutput,
|
||||
handleChangeCommitted,
|
||||
}
|
||||
|
||||
if (userInput) return <CommitWithInput {...childProps} />
|
||||
else return <CommitWithoutInput {...childProps} />
|
||||
}
|
||||
|
||||
interface CommitChildProps {
|
||||
changeCommited: boolean
|
||||
fileCreateOutput: string
|
||||
handleChangeCommitted: () => void
|
||||
}
|
||||
|
||||
const CommitWithInput = ({
|
||||
changeCommited,
|
||||
fileCreateOutput,
|
||||
handleChangeCommitted,
|
||||
}: CommitChildProps) => {
|
||||
useEnterToContinue(handleChangeCommitted, !changeCommited && fileCreateOutput !== "")
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{fileCreateOutput !== "" && (
|
||||
<>
|
||||
<Text>{fileCreateOutput}</Text>
|
||||
<EnterToContinue />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const CommitWithoutInput = ({
|
||||
changeCommited,
|
||||
fileCreateOutput,
|
||||
handleChangeCommitted,
|
||||
}: CommitChildProps) => {
|
||||
React.useEffect(() => {
|
||||
if (!changeCommited && fileCreateOutput !== "") {
|
||||
handleChangeCommitted()
|
||||
}
|
||||
}, [changeCommited, fileCreateOutput, handleChangeCommitted])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">{fileCreateOutput !== "" && <Text>{fileCreateOutput}</Text>}</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import {Box, Text} from "ink"
|
||||
import * as React from "react"
|
||||
import {EnterToContinue} from "../components/enter-to-continue"
|
||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
||||
import {useUserInput} from "../utils/use-user-input"
|
||||
import {IExecutor, executorArgument, ExecutorConfig, getExecutorArgument} from "./executor"
|
||||
|
||||
export interface Config extends ExecutorConfig {
|
||||
message: executorArgument<string>
|
||||
}
|
||||
|
||||
export const type = "print-message"
|
||||
|
||||
export const Commit: IExecutor["Commit"] = ({cliArgs, cliFlags, onChangeCommitted, step}) => {
|
||||
const userInput = useUserInput(cliFlags)
|
||||
const generatorArgs = React.useMemo(
|
||||
() => ({
|
||||
message: getExecutorArgument((step as Config).message, cliArgs),
|
||||
stepName: getExecutorArgument((step as Config).stepName, cliArgs),
|
||||
}),
|
||||
[cliArgs, step],
|
||||
)
|
||||
const [changeCommited, setChangeCommited] = React.useState(false)
|
||||
|
||||
const handleChangeCommitted = React.useCallback(() => {
|
||||
setChangeCommited(true)
|
||||
onChangeCommitted(generatorArgs.stepName)
|
||||
}, [onChangeCommitted, generatorArgs])
|
||||
|
||||
const childProps: CommitChildProps = {
|
||||
changeCommited,
|
||||
generatorArgs,
|
||||
handleChangeCommitted,
|
||||
}
|
||||
|
||||
if (userInput) return <CommitWithInput {...childProps} />
|
||||
else return <CommitWithoutInput {...childProps} />
|
||||
}
|
||||
|
||||
interface CommitChildProps {
|
||||
changeCommited: boolean
|
||||
generatorArgs: {message: string; stepName: string}
|
||||
handleChangeCommitted: () => void
|
||||
}
|
||||
|
||||
const CommitWithInput = ({
|
||||
changeCommited,
|
||||
generatorArgs,
|
||||
handleChangeCommitted,
|
||||
}: CommitChildProps) => {
|
||||
useEnterToContinue(handleChangeCommitted, !changeCommited)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>{generatorArgs.message}</Text>
|
||||
<EnterToContinue />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const CommitWithoutInput = ({
|
||||
changeCommited,
|
||||
generatorArgs,
|
||||
handleChangeCommitted,
|
||||
}: CommitChildProps) => {
|
||||
React.useEffect(() => {
|
||||
if (!changeCommited) {
|
||||
handleChangeCommitted()
|
||||
}
|
||||
}, [changeCommited, handleChangeCommitted])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>{generatorArgs.message}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import {spawn} from "cross-spawn"
|
||||
import {Box, Text} from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import * as React from "react"
|
||||
import {Newline} from "../components/newline"
|
||||
import {RecipeCLIArgs} from "../types"
|
||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
||||
import {useUserInput} from "../utils/use-user-input"
|
||||
import {IExecutor, ExecutorConfig, getExecutorArgument} from "./executor"
|
||||
|
||||
export type CliCommand = string | [string, ...string[]]
|
||||
|
||||
export interface Config extends ExecutorConfig {
|
||||
command: CliCommand
|
||||
}
|
||||
export interface CommitChildProps {
|
||||
commandInstalled: boolean
|
||||
handleChangeCommitted: () => void
|
||||
command: CliCommand
|
||||
cliArgs: RecipeCLIArgs
|
||||
step: Config
|
||||
}
|
||||
|
||||
export const type = "run-command"
|
||||
|
||||
function Command({command, loading}: {command: CliCommand; loading: boolean}) {
|
||||
return (
|
||||
<Text>
|
||||
{` `}
|
||||
{loading ? <Spinner /> : "✅"}
|
||||
{` ${typeof command === "string" ? command : command.join(" ")}`}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandList = ({
|
||||
lede = "Hang tight! Running...",
|
||||
commandLoading = false,
|
||||
step,
|
||||
command,
|
||||
}: {
|
||||
lede?: string
|
||||
commandLoading?: boolean
|
||||
step: Config
|
||||
command: CliCommand
|
||||
}) => {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>{lede}</Text>
|
||||
<Newline />
|
||||
<Command key={step.stepId} command={command} loading={commandLoading} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* INFO: Exported for unit testing purposes
|
||||
*
|
||||
* This function calls the defined command with their optional arguments if defined
|
||||
*
|
||||
* @param {CliCommand} input The Command and arguments
|
||||
* @return Promise<void>
|
||||
*
|
||||
* @example await executeCommand("ls")
|
||||
* @example await executeCommand(["ls"])
|
||||
* @example await executeCommand(["ls", ...["-a", "-l"]])
|
||||
*/
|
||||
export async function executeCommand(input: CliCommand): Promise<void> {
|
||||
// from https://stackoverflow.com/a/43766456/9950655
|
||||
const argsRegex =
|
||||
/("[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'|\/[^/\\]*(?:\\[\S\s][^/\\]*)*\/[gimy]*(?=\s|$)|(?:\\\s|\S)+)/g
|
||||
const command: string[] = Array.isArray(input) ? input : input.match(argsRegex) || []
|
||||
|
||||
if (command.length === 0) {
|
||||
throw new Error(`The command is too short: \`${JSON.stringify(input)}\``)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const cp = spawn(`${command[0]}`, command.slice(1), {
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
})
|
||||
cp.on("exit", resolve)
|
||||
cp.stdout.on("data", () => {})
|
||||
})
|
||||
}
|
||||
|
||||
export const Commit: IExecutor["Commit"] = ({cliArgs, cliFlags, step, onChangeCommitted}) => {
|
||||
const userInput = useUserInput(cliFlags)
|
||||
const [commandInstalled, setCommandInstalled] = React.useState(false)
|
||||
const executorCommand = getExecutorArgument((step as Config).command, cliArgs)
|
||||
|
||||
const handleChangeCommitted = React.useCallback(() => {
|
||||
onChangeCommitted(`Executed command ${executorCommand}`)
|
||||
}, [executorCommand, onChangeCommitted])
|
||||
|
||||
React.useEffect(() => {
|
||||
async function runCommand() {
|
||||
await executeCommand(executorCommand)
|
||||
setCommandInstalled(true)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
runCommand()
|
||||
}, [cliArgs, step, executorCommand])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (commandInstalled) {
|
||||
handleChangeCommitted()
|
||||
}
|
||||
}, [commandInstalled, handleChangeCommitted])
|
||||
|
||||
const childProps: CommitChildProps = {
|
||||
commandInstalled,
|
||||
handleChangeCommitted,
|
||||
command: executorCommand,
|
||||
cliArgs,
|
||||
step: step as Config,
|
||||
}
|
||||
|
||||
if (userInput) return <CommitWithInput {...childProps} />
|
||||
else return <CommitWithoutInput {...childProps} />
|
||||
}
|
||||
|
||||
const CommitWithInput = ({
|
||||
commandInstalled,
|
||||
handleChangeCommitted,
|
||||
command,
|
||||
step,
|
||||
}: CommitChildProps) => {
|
||||
useEnterToContinue(handleChangeCommitted, commandInstalled)
|
||||
|
||||
return <CommandList commandLoading={!commandInstalled} step={step} command={command} />
|
||||
}
|
||||
|
||||
const CommitWithoutInput = ({commandInstalled, command, step}: CommitChildProps) => (
|
||||
<CommandList commandLoading={!commandInstalled} step={step} command={command} />
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
export * from "./recipe-executor"
|
||||
export * from "./recipe-builder"
|
||||
export * from "./executors/executor"
|
||||
export {type as AddDependencyType} from "./executors/add-dependency-executor"
|
||||
export {type as FileTransformType} from "./executors/file-transform-executor"
|
||||
export {type as NewFileType} from "./executors/new-file-executor"
|
||||
export {type as PrintMessageType} from "./executors/print-message-executor"
|
||||
|
||||
export * from "./utils/paths"
|
||||
export * from "./transforms"
|
||||
export {customTsParser} from "./utils/transform"
|
||||
export type {Program, RecipeCLIArgs, RecipeCLIFlags} from "./types"
|
||||
@@ -1,85 +0,0 @@
|
||||
import * as AddDependencyExecutor from "./executors/add-dependency-executor"
|
||||
import * as TransformFileExecutor from "./executors/file-transform-executor"
|
||||
import * as NewFileExecutor from "./executors/new-file-executor"
|
||||
import * as PrintMessageExecutor from "./executors/print-message-executor"
|
||||
import * as RunCommandExecutor from "./executors/run-command-executor"
|
||||
import {ExecutorConfigUnion, RecipeExecutor} from "./recipe-executor"
|
||||
import {RecipeMeta} from "./types"
|
||||
|
||||
export interface IRecipeBuilder {
|
||||
setName(name: string): IRecipeBuilder
|
||||
setDescription(description: string): IRecipeBuilder
|
||||
printMessage(
|
||||
step: Omit<Omit<PrintMessageExecutor.Config, "stepType">, "explanation">,
|
||||
): IRecipeBuilder
|
||||
setOwner(owner: string): IRecipeBuilder
|
||||
setRepoLink(repoLink: string): IRecipeBuilder
|
||||
addAddDependenciesStep(step: Omit<AddDependencyExecutor.Config, "stepType">): IRecipeBuilder
|
||||
addNewFilesStep(step: Omit<NewFileExecutor.Config, "stepType">): IRecipeBuilder
|
||||
addTransformFilesStep(step: Omit<TransformFileExecutor.Config, "stepType">): IRecipeBuilder
|
||||
addRunCommandStep(step: Omit<RunCommandExecutor.Config, "stepType">): IRecipeBuilder
|
||||
|
||||
build(): RecipeExecutor<any>
|
||||
}
|
||||
|
||||
export function RecipeBuilder(): IRecipeBuilder {
|
||||
const steps: ExecutorConfigUnion[] = []
|
||||
const meta: Partial<RecipeMeta> = {}
|
||||
|
||||
return {
|
||||
setName(name: string) {
|
||||
meta.name = name
|
||||
return this
|
||||
},
|
||||
setDescription(description: string) {
|
||||
meta.description = description
|
||||
return this
|
||||
},
|
||||
printMessage(step: Omit<PrintMessageExecutor.Config, "stepType">) {
|
||||
steps.push({
|
||||
stepType: PrintMessageExecutor.type,
|
||||
...step,
|
||||
})
|
||||
return this
|
||||
},
|
||||
setOwner(owner: string) {
|
||||
meta.owner = owner
|
||||
return this
|
||||
},
|
||||
setRepoLink(repoLink: string) {
|
||||
meta.repoLink = repoLink
|
||||
return this
|
||||
},
|
||||
addAddDependenciesStep(step: Omit<AddDependencyExecutor.Config, "stepType">) {
|
||||
steps.push({
|
||||
stepType: AddDependencyExecutor.type,
|
||||
...step,
|
||||
})
|
||||
return this
|
||||
},
|
||||
addNewFilesStep(step: Omit<NewFileExecutor.Config, "stepType">) {
|
||||
steps.push({
|
||||
stepType: NewFileExecutor.type,
|
||||
...step,
|
||||
})
|
||||
return this
|
||||
},
|
||||
addTransformFilesStep(step: Omit<TransformFileExecutor.Config, "stepType">) {
|
||||
steps.push({
|
||||
stepType: TransformFileExecutor.type,
|
||||
...step,
|
||||
})
|
||||
return this
|
||||
},
|
||||
addRunCommandStep(step: Omit<RunCommandExecutor.Config, "stepType">) {
|
||||
steps.push({
|
||||
stepType: RunCommandExecutor.type,
|
||||
...step,
|
||||
})
|
||||
return this
|
||||
},
|
||||
build() {
|
||||
return new RecipeExecutor(meta as RecipeMeta, steps)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import {render} from "ink"
|
||||
import {baseLogger} from "../logging"
|
||||
import React from "react"
|
||||
import * as AddDependencyExecutor from "./executors/add-dependency-executor"
|
||||
import * as FileTransformExecutor from "./executors/file-transform-executor"
|
||||
import * as NewFileExecutor from "./executors/new-file-executor"
|
||||
import * as PrintMessageExecutor from "./executors/print-message-executor"
|
||||
import {RecipeRenderer} from "./recipe-renderer"
|
||||
import {RecipeCLIArgs, RecipeCLIFlags, RecipeMeta} from "./types"
|
||||
// const debug = require('debug')("blitz:installer")
|
||||
|
||||
type ExecutorConfig =
|
||||
| AddDependencyExecutor.Config
|
||||
| FileTransformExecutor.Config
|
||||
| NewFileExecutor.Config
|
||||
| PrintMessageExecutor.Config
|
||||
|
||||
export type {ExecutorConfig as ExecutorConfigUnion}
|
||||
|
||||
export class RecipeExecutor<Options extends RecipeMeta> {
|
||||
private readonly steps: ExecutorConfig[]
|
||||
private readonly options: Options
|
||||
|
||||
constructor(options: Options, steps: ExecutorConfig[]) {
|
||||
this.options = options
|
||||
this.steps = steps
|
||||
}
|
||||
|
||||
async run(
|
||||
cliArgs: RecipeCLIArgs = {},
|
||||
cliFlags: RecipeCLIFlags = {yesToAll: false},
|
||||
): Promise<void> {
|
||||
try {
|
||||
const {waitUntilExit} = render(
|
||||
<RecipeRenderer
|
||||
cliArgs={cliArgs}
|
||||
cliFlags={cliFlags}
|
||||
steps={this.steps}
|
||||
recipeMeta={this.options}
|
||||
/>,
|
||||
{exitOnCtrlC: false},
|
||||
)
|
||||
await waitUntilExit()
|
||||
baseLogger().info(`\n🎉 The ${this.options.name} recipe has been installed!\n`)
|
||||
} catch (e) {
|
||||
baseLogger().error(e as any)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
import {Box, Text, useApp, useInput} from "ink"
|
||||
import React from "react"
|
||||
import {assert} from "../index-server"
|
||||
import {EnterToContinue} from "./components/enter-to-continue"
|
||||
import {Newline} from "./components/newline"
|
||||
import * as AddDependencyExecutor from "./executors/add-dependency-executor"
|
||||
import {ExecutorConfig, Frontmatter, IExecutor} from "./executors/executor"
|
||||
import * as FileTransformExecutor from "./executors/file-transform-executor"
|
||||
import * as NewFileExecutor from "./executors/new-file-executor"
|
||||
import * as PrintMessageExecutor from "./executors/print-message-executor"
|
||||
import * as RunCommandExecutor from "./executors/run-command-executor"
|
||||
import {RecipeCLIArgs, RecipeCLIFlags, RecipeMeta} from "./types"
|
||||
import {useEnterToContinue} from "./utils/use-enter-to-continue"
|
||||
import {useUserInput} from "./utils/use-user-input"
|
||||
|
||||
enum Action {
|
||||
SkipStep,
|
||||
ProposeChange,
|
||||
ApplyChange,
|
||||
CommitApproved,
|
||||
CompleteChange,
|
||||
}
|
||||
|
||||
enum Status {
|
||||
Pending,
|
||||
Proposed,
|
||||
ReadyToCommit,
|
||||
Committing,
|
||||
Committed,
|
||||
}
|
||||
|
||||
const ExecutorMap: {[key: string]: IExecutor} = {
|
||||
[AddDependencyExecutor.type]: AddDependencyExecutor,
|
||||
[NewFileExecutor.type]: NewFileExecutor,
|
||||
[PrintMessageExecutor.type]: PrintMessageExecutor,
|
||||
[FileTransformExecutor.type]: FileTransformExecutor,
|
||||
[RunCommandExecutor.type]: RunCommandExecutor,
|
||||
} as const
|
||||
|
||||
interface State {
|
||||
steps: {
|
||||
executor: ExecutorConfig
|
||||
status: Status
|
||||
proposalData?: any
|
||||
successMsg: string
|
||||
}[]
|
||||
current: number
|
||||
}
|
||||
|
||||
function recipeReducer(state: State, action: {type: Action; data?: any}) {
|
||||
const newState = {...state}
|
||||
const step = newState.steps[newState.current]
|
||||
|
||||
switch (action.type) {
|
||||
case Action.ProposeChange:
|
||||
assert(step, "Step is empty in recipeReducer function")
|
||||
step.status = Status.Proposed
|
||||
break
|
||||
case Action.CommitApproved:
|
||||
assert(step, "Step is empty in recipeReducer function")
|
||||
step.status = Status.ReadyToCommit
|
||||
step.proposalData = action.data
|
||||
break
|
||||
case Action.ApplyChange:
|
||||
assert(step, "Step is empty in recipeReducer function")
|
||||
step.status = Status.Committing
|
||||
break
|
||||
case Action.CompleteChange:
|
||||
assert(step, "Step is empty in recipeReducer function")
|
||||
step.status = Status.Committed
|
||||
step.successMsg = action.data as string
|
||||
newState.current = Math.min(newState.current + 1, newState.steps.length - 1)
|
||||
break
|
||||
case Action.SkipStep:
|
||||
newState.current += 1
|
||||
break
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
interface RecipeProps {
|
||||
cliArgs: RecipeCLIArgs
|
||||
cliFlags: RecipeCLIFlags
|
||||
steps: ExecutorConfig[]
|
||||
recipeMeta: RecipeMeta
|
||||
}
|
||||
|
||||
const DispatchContext = React.createContext<React.Dispatch<{type: Action; data?: any}>>(() => {})
|
||||
|
||||
function WelcomeMessage({
|
||||
recipeMeta,
|
||||
enterToContinue = true,
|
||||
}: {
|
||||
recipeMeta: RecipeMeta
|
||||
enterToContinue?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="#8a3df0" bold>
|
||||
Recipe: {recipeMeta.name}
|
||||
</Text>
|
||||
<Newline />
|
||||
<Text color="gray">
|
||||
<Text italic>{recipeMeta.description}</Text>
|
||||
</Text>
|
||||
<Newline />
|
||||
<Text color="gray">
|
||||
Repo: <Text italic>{recipeMeta.repoLink}</Text>
|
||||
</Text>
|
||||
<Text color="gray">
|
||||
Author: <Text italic>{recipeMeta.owner}</Text>
|
||||
</Text>
|
||||
{enterToContinue && <EnterToContinue />}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function StepMessages({state}: {state: State}) {
|
||||
const messages = state.steps
|
||||
.map((step) => ({
|
||||
msg: step.successMsg,
|
||||
icon: step.executor.successIcon ?? "✅",
|
||||
}))
|
||||
.filter((s) => s.msg)
|
||||
|
||||
return (
|
||||
<>
|
||||
{messages.map(({msg, icon}, index) => (
|
||||
<Text key={msg + index} color="green">
|
||||
{msg === "\n" ? "" : icon} {msg}
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function StepExecutor({
|
||||
cliArgs,
|
||||
cliFlags,
|
||||
proposalData,
|
||||
step,
|
||||
status,
|
||||
}: {
|
||||
step: ExecutorConfig
|
||||
status: Status
|
||||
cliArgs: RecipeCLIArgs
|
||||
cliFlags: RecipeCLIFlags
|
||||
proposalData?: any
|
||||
}) {
|
||||
const executor = ExecutorMap[step.stepType]
|
||||
assert(executor, `Executor not found for ${step.stepType}`)
|
||||
const {Propose, Commit}: IExecutor = executor
|
||||
const dispatch = React.useContext(DispatchContext)
|
||||
|
||||
const handleProposalAccepted = React.useCallback(
|
||||
(msg: any) => {
|
||||
dispatch({type: Action.CommitApproved, data: msg})
|
||||
},
|
||||
[dispatch],
|
||||
)
|
||||
const handleChangeCommitted = React.useCallback(
|
||||
(msg: any) => {
|
||||
dispatch({type: Action.CompleteChange, data: msg})
|
||||
},
|
||||
[dispatch],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (status === Status.Pending) {
|
||||
dispatch({type: Action.ProposeChange})
|
||||
} else if (status === Status.ReadyToCommit) {
|
||||
dispatch({type: Action.ApplyChange})
|
||||
}
|
||||
if (status === Status.Proposed && !Propose) {
|
||||
dispatch({type: Action.CommitApproved})
|
||||
}
|
||||
}, [dispatch, status, Propose])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{status !== Status.Committed ? <Frontmatter executor={step} /> : null}
|
||||
{[Status.Proposed].includes(status) && Propose ? (
|
||||
<Propose
|
||||
cliArgs={cliArgs}
|
||||
cliFlags={cliFlags}
|
||||
step={step}
|
||||
onProposalAccepted={handleProposalAccepted}
|
||||
/>
|
||||
) : null}
|
||||
{[Status.Committing].includes(status) ? (
|
||||
<Commit
|
||||
cliArgs={cliArgs}
|
||||
cliFlags={cliFlags}
|
||||
proposalData={proposalData}
|
||||
step={step}
|
||||
onChangeCommitted={handleChangeCommitted}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function RecipeRenderer({cliArgs, cliFlags, steps, recipeMeta}: RecipeProps) {
|
||||
const userInput = useUserInput(cliFlags)
|
||||
const {exit} = useApp()
|
||||
const mappedSteps = steps.map((e) => ({
|
||||
executor: e,
|
||||
status: Status.Pending,
|
||||
successMsg: "",
|
||||
}))
|
||||
|
||||
if (steps.length === 0) {
|
||||
exit(new Error("This recipe has no steps"))
|
||||
}
|
||||
|
||||
const [state, dispatch] = React.useReducer(recipeReducer, {
|
||||
current: userInput ? -1 : 0,
|
||||
steps: mappedSteps,
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
state.current === state.steps.length - 1 &&
|
||||
state.steps[state.current]?.status === Status.Committed
|
||||
) {
|
||||
exit()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
{userInput ? (
|
||||
<RecipeRendererWithInput
|
||||
cliArgs={cliArgs}
|
||||
cliFlags={cliFlags}
|
||||
state={state}
|
||||
recipeMeta={recipeMeta}
|
||||
/>
|
||||
) : (
|
||||
<RecipeRendererWithoutInput
|
||||
cliArgs={cliArgs}
|
||||
cliFlags={cliFlags}
|
||||
state={state}
|
||||
recipeMeta={recipeMeta}
|
||||
/>
|
||||
)}
|
||||
</DispatchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function RecipeRendererWithInput({
|
||||
cliArgs,
|
||||
cliFlags,
|
||||
recipeMeta,
|
||||
state,
|
||||
}: Omit<RecipeProps, "steps"> & {state: State}) {
|
||||
const {exit} = useApp()
|
||||
const dispatch = React.useContext(DispatchContext)
|
||||
const step = state.steps[state.current]
|
||||
|
||||
useInput((input, key) => {
|
||||
if (input === "c" && key.ctrl) {
|
||||
exit(new Error("You aborted installation"))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
useEnterToContinue(() => dispatch({type: Action.SkipStep}), state.current === -1)
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepMessages state={state} />
|
||||
{state.current === -1 ? (
|
||||
<WelcomeMessage recipeMeta={recipeMeta} />
|
||||
) : step ? (
|
||||
<StepExecutor
|
||||
cliArgs={cliArgs}
|
||||
cliFlags={cliFlags}
|
||||
proposalData={state.steps[state.current]?.proposalData}
|
||||
step={step.executor}
|
||||
status={step.status}
|
||||
/>
|
||||
) : (
|
||||
new Error("Step not found in RecipeRendererWithInput")
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function RecipeRendererWithoutInput({
|
||||
cliArgs,
|
||||
cliFlags,
|
||||
recipeMeta,
|
||||
state,
|
||||
}: Omit<RecipeProps, "steps"> & {state: State}) {
|
||||
return (
|
||||
<>
|
||||
<WelcomeMessage recipeMeta={recipeMeta} enterToContinue={false} />
|
||||
<StepMessages state={state} />
|
||||
<StepExecutor
|
||||
cliArgs={cliArgs}
|
||||
cliFlags={cliFlags}
|
||||
proposalData={state.steps[state.current]?.proposalData}
|
||||
step={state.steps[state.current]?.executor as ExecutorConfig}
|
||||
status={state.steps[state.current]?.status as Status}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type {ExpressionKind} from "ast-types/gen/kinds"
|
||||
import j from "jscodeshift"
|
||||
import {Program} from "../types"
|
||||
import {addImport} from "./add-import"
|
||||
|
||||
export const addBlitzMiddleware = (program: Program, middleware: ExpressionKind): Program => {
|
||||
const pluginArray = program.find(j.Identifier, (node) => node.name === "plugins")
|
||||
|
||||
pluginArray.get().parentPath.value.value.elements = [
|
||||
...pluginArray.get().parentPath.value.value.elements,
|
||||
j.template.expression`BlitzServerMiddleware(${middleware})`,
|
||||
]
|
||||
const blitzServerMiddleWare = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("BlitzServerMiddleware"))],
|
||||
j.literal("blitz"),
|
||||
)
|
||||
addImport(program, blitzServerMiddleWare)
|
||||
|
||||
return program
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import j from "jscodeshift"
|
||||
import {Program} from "../types"
|
||||
|
||||
export function addImport(program: Program, importToAdd: j.ImportDeclaration): Program {
|
||||
const importStatementCount = program.find(j.ImportDeclaration).length
|
||||
if (importStatementCount === 0) {
|
||||
program.find(j.Statement).at(0).insertBefore(importToAdd)
|
||||
return program
|
||||
}
|
||||
program.find<j.ImportDeclaration>(j.ImportDeclaration).forEach((stmt, idx) => {
|
||||
const node = stmt.node
|
||||
if (idx === importStatementCount - 1) {
|
||||
stmt.replace(node, importToAdd)
|
||||
}
|
||||
})
|
||||
return program
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import j from "jscodeshift"
|
||||
import {Program} from "../types"
|
||||
|
||||
export const findModuleExportsExpressions = (program: Program) =>
|
||||
program.find<j.AssignmentExpression>(j.AssignmentExpression).filter((path) => {
|
||||
const {left, right} = path.value
|
||||
|
||||
return (
|
||||
left.type === "MemberExpression" &&
|
||||
left.object.type === "Identifier" &&
|
||||
left.property.type === "Identifier" &&
|
||||
left.property.name === "exports" &&
|
||||
right.type === "ObjectExpression"
|
||||
)
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
export * from "./add-import"
|
||||
export * from "./add-blitz-middleware"
|
||||
export * from "./find-module-exports-expressions"
|
||||
export * from "./prisma"
|
||||
export * from "./transform-next-config"
|
||||
export * from "./with-utilities"
|
||||
export * from "./wrap-blitz-config"
|
||||
export * from "./update-babel-config"
|
||||
export * from "./wrap-app-return-statement"
|
||||
@@ -1,27 +0,0 @@
|
||||
import {Enum} from "@mrleebo/prisma-ast"
|
||||
import {produceSchema} from "./produce-schema"
|
||||
|
||||
/**
|
||||
* Adds an enum to your schema.prisma data model.
|
||||
*
|
||||
* @param source - schema.prisma source file contents
|
||||
* @param enumProps - the enum to add
|
||||
* @returns The modified schema.prisma source
|
||||
* @example Usage
|
||||
* ```
|
||||
* addPrismaEnum(source, {
|
||||
type: "enum",
|
||||
name: "Role",
|
||||
enumerators: [
|
||||
{type: "enumerator", name: "USER"},
|
||||
{type: "enumerator", name: "ADMIN"},
|
||||
],
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
export function addPrismaEnum(source: string, enumProps: Enum): Promise<string> {
|
||||
return produceSchema(source, (schema) => {
|
||||
const existing = schema.list.find((x) => x.type === "enum" && x.name === enumProps.name)
|
||||
existing ? Object.assign(existing, enumProps) : schema.list.push(enumProps)
|
||||
})
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import {Field, Model} from "@mrleebo/prisma-ast"
|
||||
import {produceSchema} from "./produce-schema"
|
||||
|
||||
/**
|
||||
* Adds a field to a model in your schema.prisma data model.
|
||||
*
|
||||
* @param source - schema.prisma source file contents
|
||||
* @param modelName - name of the model to add a field to
|
||||
* @param fieldProps - the field to add
|
||||
* @returns The modified schema.prisma source
|
||||
* @example Usage
|
||||
* ```
|
||||
* addPrismaField(source, "Project", {
|
||||
type: "field",
|
||||
name: "name",
|
||||
fieldType: "String",
|
||||
optional: false,
|
||||
attributes: [{type: "attribute", kind: "field", name: "unique"}],
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
export function addPrismaField(
|
||||
source: string,
|
||||
modelName: string,
|
||||
fieldProps: Field,
|
||||
): Promise<string> {
|
||||
return produceSchema(source, (schema) => {
|
||||
const model = schema.list.find((x) => x.type === "model" && x.name === modelName) as Model
|
||||
if (!model) return
|
||||
|
||||
const existing = model.properties.find((x) => x.type === "field" && x.name === fieldProps.name)
|
||||
existing ? Object.assign(existing, fieldProps) : model.properties.push(fieldProps)
|
||||
})
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import {Generator} from "@mrleebo/prisma-ast"
|
||||
import {produceSchema} from "./produce-schema"
|
||||
|
||||
/**
|
||||
* Adds a generator to your schema.prisma data model.
|
||||
*
|
||||
* @param source - schema.prisma source file contents
|
||||
* @param generatorProps - the generator to add
|
||||
* @returns The modified schema.prisma source
|
||||
* @example Usage
|
||||
* ```
|
||||
* addPrismaGenerator(source, {
|
||||
type: "generator",
|
||||
name: "nexusPrisma",
|
||||
assignments: [{type: "assignment", key: "provider", value: '"nexus-prisma"'}],
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
export function addPrismaGenerator(source: string, generatorProps: Generator): Promise<string> {
|
||||
return produceSchema(source, (schema) => {
|
||||
const existing = schema.list.find(
|
||||
(x) => x.type === "generator" && x.name === generatorProps.name,
|
||||
) as Generator
|
||||
existing ? Object.assign(existing, generatorProps) : schema.list.push(generatorProps)
|
||||
})
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import {Model, ModelAttribute} from "@mrleebo/prisma-ast"
|
||||
import {produceSchema} from "./produce-schema"
|
||||
|
||||
/**
|
||||
* Adds a field to a model in your schema.prisma data model.
|
||||
*
|
||||
* @remarks Not ready for actual use
|
||||
* @param source - schema.prisma source file contents
|
||||
* @param modelName - name of the model to add a field to
|
||||
* @param attributeProps - the model attribute (such as an index) to add
|
||||
* @returns The modified schema.prisma source
|
||||
* @example Usage
|
||||
* ```
|
||||
* addPrismaModelAttribute(source, "Project", {
|
||||
* type: "attribute",
|
||||
* kind: "model",
|
||||
* name: "index",
|
||||
* args: [{ type: "attributeArgument", value: { type: "array", args: ["name"] } }]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function addPrismaModelAttribute(
|
||||
source: string,
|
||||
modelName: string,
|
||||
attributeProps: ModelAttribute,
|
||||
): Promise<string> {
|
||||
return produceSchema(source, (schema) => {
|
||||
const model = schema.list.find((x) => x.type === "model" && x.name === modelName) as Model
|
||||
if (!model) return
|
||||
|
||||
const existing = model.properties.find(
|
||||
(x) => x.type === "attribute" && x.name === attributeProps.name,
|
||||
)
|
||||
|
||||
existing ? Object.assign(existing, attributeProps) : model.properties.push(attributeProps)
|
||||
})
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import {Model} from "@mrleebo/prisma-ast"
|
||||
import {produceSchema} from "./produce-schema"
|
||||
|
||||
/**
|
||||
* Adds an enum to your schema.prisma data model.
|
||||
*
|
||||
* @param source - schema.prisma source file contents
|
||||
* @param modelProps - the model to add
|
||||
* @returns The modified schema.prisma source
|
||||
* @example Usage
|
||||
* ```
|
||||
* addPrismaModel(source, {
|
||||
type: "model",
|
||||
name: "Project",
|
||||
properties: [{type: "field", name: "id", fieldType: "String"}],
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
export function addPrismaModel(source: string, modelProps: Model): Promise<string> {
|
||||
return produceSchema(source, (schema) => {
|
||||
const existing = schema.list.find((x) => x.type === "model" && x.name === modelProps.name)
|
||||
existing ? Object.assign(existing, modelProps) : schema.list.push(modelProps)
|
||||
})
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export * from "./add-prisma-enum"
|
||||
export * from "./add-prisma-field"
|
||||
export * from "./add-prisma-generator"
|
||||
export * from "./add-prisma-model-attribute"
|
||||
export * from "./add-prisma-model"
|
||||
export * from "./produce-schema"
|
||||
export * from "./set-prisma-data-source"
|
||||
@@ -1,14 +0,0 @@
|
||||
import {printSchema as printer, Schema} from "@mrleebo/prisma-ast"
|
||||
|
||||
/**
|
||||
* Takes the schema.prisma document parsed from @mrleebo/prisma-ast and
|
||||
* serializes it back to a schema.prisma source string. To ensure consistent
|
||||
* formatting and prettify the document, we also execute the
|
||||
* IntrospectionEngine from @prisma/sdk.
|
||||
*
|
||||
* @param schema - the parsed prisma schema
|
||||
* @returns the schema.prisma source string
|
||||
*/
|
||||
export function printSchema(schema: Schema): string {
|
||||
return printer(schema)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import {getSchema, printSchema, Schema} from "@mrleebo/prisma-ast"
|
||||
|
||||
/**
|
||||
* A file transformer that parses a schema.prisma string, offers you a callback
|
||||
* of the parsed document object, then takes your changes to the document and
|
||||
* writes out a new schema.prisma string with the changes applied.
|
||||
*
|
||||
* @param source - schema.prisma source file contents
|
||||
* @param producer - a callback function that can mutate the parsed data model
|
||||
* @returns The modified schema.prisma source
|
||||
*/
|
||||
export async function produceSchema(
|
||||
source: string,
|
||||
producer: (schema: Schema) => void,
|
||||
): Promise<string> {
|
||||
const schema = await getSchema(source)
|
||||
producer(schema)
|
||||
return printSchema(schema)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import {Datasource} from "@mrleebo/prisma-ast"
|
||||
import {produceSchema} from "./produce-schema"
|
||||
|
||||
/**
|
||||
* Modify the prisma datasource metadata to use the provider and url specified.
|
||||
*
|
||||
* @param source - schema.prisma source file contents
|
||||
* @param datasourceProps - datasource object to assign to the schema
|
||||
* @returns The modified schema.prisma source
|
||||
* @example Usage
|
||||
* ```
|
||||
* setPrismaDataSource(source, {
|
||||
type: "datasource",
|
||||
name: "db",
|
||||
assignments: [
|
||||
{type: "assignment", key: "provider", value: '"postgresql"'},
|
||||
{
|
||||
type: "assignment",
|
||||
key: "url",
|
||||
value: {type: "function", name: "env", params: ['"DATABASE_URL"']},
|
||||
},
|
||||
],
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
export function setPrismaDataSource(source: string, datasourceProps: Datasource): Promise<string> {
|
||||
return produceSchema(source, (schema) => {
|
||||
const existing = schema.list.find((x) => x.type === "datasource")
|
||||
existing ? Object.assign(existing, datasourceProps) : schema.list.push(datasourceProps)
|
||||
})
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import type {ExpressionKind} from "ast-types/gen/kinds"
|
||||
import j from "jscodeshift"
|
||||
import {Program} from "../types"
|
||||
|
||||
function recursiveConfigSearch(
|
||||
program: Program,
|
||||
obj: ExpressionKind,
|
||||
): j.ObjectExpression | undefined {
|
||||
// Identifier being a variable name
|
||||
if (obj.type === "Identifier") {
|
||||
const {node} = j(obj).get()
|
||||
|
||||
// Get the definition of the variable
|
||||
const identifier: j.ASTPath<j.VariableDeclarator> = program
|
||||
.find(j.VariableDeclarator, {
|
||||
id: {name: node.name},
|
||||
})
|
||||
.get()
|
||||
|
||||
// Return what is after the `=`
|
||||
return identifier.value.init ? recursiveConfigSearch(program, identifier.value.init) : undefined
|
||||
} else if (obj.type === "CallExpression") {
|
||||
// If it's an function call (like `withBundleAnalyzer`), get the first argument
|
||||
if (obj.arguments.length === 0) {
|
||||
// If it has no arguments, create an empty object: `{}`
|
||||
let config = j.objectExpression([])
|
||||
obj.arguments.push(config)
|
||||
return config
|
||||
} else {
|
||||
const arg = obj.arguments[0]
|
||||
if (arg) {
|
||||
if (arg.type === "SpreadElement") return undefined
|
||||
else return recursiveConfigSearch(program, arg)
|
||||
}
|
||||
}
|
||||
} else if (obj.type === "ObjectExpression") {
|
||||
// If it's an object, return it
|
||||
return obj
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export type TransformBlitzConfigCallback = (config: j.ObjectExpression) => j.ObjectExpression
|
||||
|
||||
export function transformBlitzConfig(
|
||||
program: Program,
|
||||
transform: TransformBlitzConfigCallback,
|
||||
): Program {
|
||||
let moduleExportsExpressions = program.find(j.AssignmentExpression, {
|
||||
operator: "=",
|
||||
left: {object: {name: "module"}, property: {name: "exports"}},
|
||||
right: {},
|
||||
})
|
||||
|
||||
// If there isn't any `module.exports = ...`, create one
|
||||
if (moduleExportsExpressions.length === 0) {
|
||||
let config = j.objectExpression([])
|
||||
|
||||
config = transform(config)
|
||||
|
||||
let moduleExportExpression = j.expressionStatement(
|
||||
j.assignmentExpression(
|
||||
"=",
|
||||
j.memberExpression(j.identifier("module"), j.identifier("exports")),
|
||||
config,
|
||||
),
|
||||
)
|
||||
|
||||
program.get().node.program.body.push(moduleExportExpression)
|
||||
} else if (moduleExportsExpressions.length === 1) {
|
||||
let moduleExportsExpression: j.ASTPath<j.AssignmentExpression> = moduleExportsExpressions.get()
|
||||
|
||||
let config: j.ObjectExpression | undefined = recursiveConfigSearch(
|
||||
program,
|
||||
moduleExportsExpression.value.right,
|
||||
)
|
||||
|
||||
if (config) {
|
||||
config = transform(config)
|
||||
} else {
|
||||
console.warn(
|
||||
"The configuration couldn't be found, but there is a 'module.exports' inside `blitz.config.js`",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.warn("There are multiple 'module.exports' inside 'blitz.config.js'")
|
||||
}
|
||||
|
||||
return program
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import j from "jscodeshift"
|
||||
import {assert} from "../../index-server"
|
||||
import {Program} from "../types"
|
||||
|
||||
export function transformNextConfig(program: Program): {
|
||||
program: Program
|
||||
configObj: []
|
||||
pushToConfig: (property: j.ObjectProperty) => void
|
||||
wrapConfig: (func: string | j.CallExpression) => {
|
||||
withBlitz: j.Identifier | j.CallExpression
|
||||
}
|
||||
addRequireStatement: (identifier: string, packageName: string) => void
|
||||
} {
|
||||
const configObj = program
|
||||
.find<j.VariableDeclarator>(j.VariableDeclarator, (filter) => filter.id.name === "config")
|
||||
.get().parentPath.value[0].init.properties
|
||||
|
||||
assert(configObj, "Config object not found")
|
||||
|
||||
const pushToConfig = (property: j.ObjectProperty) => {
|
||||
configObj.push(property)
|
||||
}
|
||||
|
||||
const wrapConfig = (
|
||||
func: string | j.CallExpression,
|
||||
): {
|
||||
withBlitz: j.Identifier | j.CallExpression
|
||||
} => {
|
||||
const withBlitz = program
|
||||
.find(j.CallExpression, (filter) => filter.callee.name === "withBlitz")
|
||||
.get().value.arguments
|
||||
|
||||
assert(withBlitz, "withBlitz wrapper not found")
|
||||
|
||||
if (typeof func === "string") {
|
||||
withBlitz.push(j.template.expression`${func}(${withBlitz})`)
|
||||
withBlitz.splice(0, 1)
|
||||
} else {
|
||||
withBlitz.push(func)
|
||||
withBlitz.splice(0, 1)
|
||||
}
|
||||
|
||||
return {
|
||||
withBlitz,
|
||||
}
|
||||
}
|
||||
|
||||
const addRequireStatement = (identifier: string, packageName: string) => {
|
||||
program
|
||||
.get()
|
||||
.value.program.body.unshift(
|
||||
j.expressionStatement(
|
||||
j.assignmentExpression(
|
||||
"=",
|
||||
j.identifier(identifier),
|
||||
j.callExpression(j.identifier("require"), [j.identifier(`"${packageName}"`)]),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
program,
|
||||
configObj,
|
||||
pushToConfig,
|
||||
wrapConfig,
|
||||
addRequireStatement,
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import type {ExpressionKind} from "ast-types/gen/kinds"
|
||||
import {namedTypes} from "ast-types"
|
||||
import j, {ASTPath} from "jscodeshift"
|
||||
import {JsonObject, JsonValue} from "../types"
|
||||
import {Program} from "../types"
|
||||
import {findModuleExportsExpressions} from "./find-module-exports-expressions"
|
||||
|
||||
type AddBabelItemDefinition = string | [name: string, options: JsonObject]
|
||||
|
||||
const jsonValueToExpression = (value: JsonValue): ExpressionKind =>
|
||||
typeof value === "string"
|
||||
? j.stringLiteral(value)
|
||||
: typeof value === "number"
|
||||
? j.numericLiteral(value)
|
||||
: typeof value === "boolean"
|
||||
? j.booleanLiteral(value)
|
||||
: value === null
|
||||
? j.nullLiteral()
|
||||
: Array.isArray(value)
|
||||
? j.arrayExpression(value.map(jsonValueToExpression))
|
||||
: j.objectExpression(
|
||||
Object.entries(value)
|
||||
.filter((entry): entry is [string, JsonValue] => entry[1] !== undefined)
|
||||
.map(([key, value]) =>
|
||||
j.objectProperty(j.stringLiteral(key), jsonValueToExpression(value)),
|
||||
),
|
||||
)
|
||||
|
||||
function updateBabelConfig(program: Program, item: AddBabelItemDefinition, key: string): Program {
|
||||
findModuleExportsExpressions(program).forEach((moduleExportsExpression) => {
|
||||
const foundExpression: Program = j(moduleExportsExpression)
|
||||
foundExpression
|
||||
.find<j.ObjectProperty>(j.ObjectProperty, {key: {name: key}})
|
||||
.forEach((items) => {
|
||||
// Don't add it again if it already exists,
|
||||
// that what this code does. For simplicity,
|
||||
// all the examples will be with key = 'presets'
|
||||
|
||||
const itemName = Array.isArray(item) ? item[0] : item
|
||||
|
||||
if (items.node.value.type === "Literal" || items.node.value.type === "StringLiteral") {
|
||||
// {
|
||||
// presets: "this-preset"
|
||||
// }
|
||||
if (itemName !== items.node.value.value) {
|
||||
items.node.value = j.arrayExpression([items.node.value, jsonValueToExpression(item)])
|
||||
}
|
||||
} else if (items.node.value.type === "ArrayExpression") {
|
||||
// {
|
||||
// presets: ["this-preset", "maybe-another", ...]
|
||||
// }
|
||||
// Here, it will return if it find the preset inside the
|
||||
// array, so the last line doesn't push a duplicated preset
|
||||
for (const [i, element] of items.node.value.elements.entries()) {
|
||||
if (!element) continue
|
||||
|
||||
if (element.type === "Literal" || element.type === "StringLiteral") {
|
||||
// {
|
||||
// presets: [..., "this-preset", ...]
|
||||
// }
|
||||
if (element.value === itemName) return
|
||||
} else if (element.type === "ArrayExpression") {
|
||||
// {
|
||||
// presets: [..., ["this-preset"], ...]
|
||||
// }
|
||||
if (
|
||||
(element.elements[0]?.type === "Literal" ||
|
||||
element.elements[0]?.type === "StringLiteral") &&
|
||||
element.elements[0].value === itemName
|
||||
) {
|
||||
if (
|
||||
element.elements[1]?.type === "ObjectExpression" &&
|
||||
element.elements[1].properties.length > 0
|
||||
) {
|
||||
// The preset has a config.
|
||||
// ["this-preset", {...}]
|
||||
if (Array.isArray(item)) {
|
||||
// If it has an adittional config, add the new keys
|
||||
// (don't matter if they already exists, let the user handle it later by themself)
|
||||
let obj = element.elements[1]
|
||||
|
||||
for (const key in item[1]) {
|
||||
const value = item[1][key]
|
||||
if (value === undefined) continue
|
||||
obj.properties.push(
|
||||
j.objectProperty(j.stringLiteral(key), jsonValueToExpression(value)),
|
||||
)
|
||||
}
|
||||
|
||||
items.node.value.elements[i] = obj
|
||||
}
|
||||
} else {
|
||||
// The preset has no config.
|
||||
// Its ["this-preset"]
|
||||
items.node.value.elements[i] = jsonValueToExpression(item)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
items.node.value.elements.push(jsonValueToExpression(item))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return program
|
||||
}
|
||||
|
||||
export const addBabelPreset = (program: Program, preset: AddBabelItemDefinition): Program =>
|
||||
updateBabelConfig(program, preset, "presets")
|
||||
export const addBabelPlugin = (program: Program, plugin: AddBabelItemDefinition): Program =>
|
||||
updateBabelConfig(program, plugin, "plugins")
|
||||
@@ -1,20 +0,0 @@
|
||||
import {CommentKind, TypeAnnotationKind, TSTypeAnnotationKind} from "ast-types/gen/kinds"
|
||||
import j from "jscodeshift"
|
||||
|
||||
export function withComments<
|
||||
Node extends {
|
||||
comments?: CommentKind[] | null
|
||||
},
|
||||
>(node: Node, comments: CommentKind[]): Node {
|
||||
node.comments = comments
|
||||
return node
|
||||
}
|
||||
|
||||
export function withTypeAnnotation<
|
||||
Node extends {
|
||||
typeAnnotation?: TypeAnnotationKind | TSTypeAnnotationKind | null
|
||||
},
|
||||
>(node: Node, type: Parameters<typeof j.tsTypeAnnotation>[0]): Node {
|
||||
node.typeAnnotation = j.tsTypeAnnotation(type)
|
||||
return node
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import {NodePath} from "ast-types/lib/node-path"
|
||||
import j, {JSXAttribute} from "jscodeshift"
|
||||
import {assert} from "../../index-server"
|
||||
import {Program} from "../types"
|
||||
|
||||
export function wrapAppWithProvider(
|
||||
program: Program,
|
||||
element: string,
|
||||
attributes?: string[],
|
||||
): Program {
|
||||
const findMyApp = program.find(j.FunctionDeclaration, (node) => node.id.name === "MyApp")
|
||||
assert(findMyApp.length, "MyApp function not found")
|
||||
|
||||
findMyApp.forEach((path: NodePath) => {
|
||||
const statement = path.value.body.body.filter(
|
||||
(b: j.ReturnStatement) => b.type === "ReturnStatement",
|
||||
)[0]
|
||||
const argument = statement.argument
|
||||
|
||||
let attrs: JSXAttribute[] = []
|
||||
if (attributes) {
|
||||
attrs = attributes.map((i) => j.jsxAttribute(j.jsxIdentifier(i)))
|
||||
}
|
||||
|
||||
statement.argument = j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier(element), attrs),
|
||||
j.jsxClosingElement(j.jsxIdentifier(element)),
|
||||
[j.jsxText("\n"), argument, j.jsxText("\n")],
|
||||
)
|
||||
})
|
||||
|
||||
return program
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import j from "jscodeshift"
|
||||
import {Program} from "../types"
|
||||
|
||||
export function wrapBlitzConfig(program: Program, functionName: string): Program {
|
||||
let moduleExportsExpressions = program.find(j.AssignmentExpression, {
|
||||
operator: "=",
|
||||
left: {object: {name: "module"}, property: {name: "exports"}},
|
||||
right: {},
|
||||
})
|
||||
|
||||
// If there isn't any `module.exports = ...`, create one
|
||||
if (moduleExportsExpressions.length === 0) {
|
||||
let moduleExportExpression = j.expressionStatement(
|
||||
j.assignmentExpression(
|
||||
"=",
|
||||
j.memberExpression(j.identifier("module"), j.identifier("exports")),
|
||||
j.callExpression(j.identifier(functionName), [j.objectExpression([])]),
|
||||
),
|
||||
)
|
||||
|
||||
program.get().node.program.body.push(moduleExportExpression)
|
||||
} else if (moduleExportsExpressions.length === 1) {
|
||||
let moduleExportsExpression: j.ASTPath<j.AssignmentExpression> = moduleExportsExpressions.get()
|
||||
|
||||
moduleExportsExpression.value.right = j.callExpression(j.identifier(functionName), [
|
||||
moduleExportsExpression.value.right,
|
||||
])
|
||||
} else {
|
||||
console.warn("There are multiple 'module.exports' inside 'blitz.config.js'")
|
||||
}
|
||||
|
||||
return program
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import type * as j from "jscodeshift"
|
||||
|
||||
export interface RecipeMeta {
|
||||
name: string
|
||||
description: string
|
||||
owner: string
|
||||
repoLink: string
|
||||
}
|
||||
|
||||
export type RecipeCLIArgs = {[Key in string]?: string | true}
|
||||
|
||||
export interface RecipeCLIFlags {
|
||||
yesToAll: boolean
|
||||
}
|
||||
|
||||
export type Program = j.Collection<j.Program>
|
||||
|
||||
/**
|
||||
Matches a JSON object.
|
||||
This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. Don't use this as a direct return type as the user would have to double-cast it: `jsonObject as unknown as CustomResponse`. Instead, you could extend your CustomResponse type from it to ensure your type only uses JSON-compatible types: `interface CustomResponse extends JsonObject { … }`.
|
||||
@see https://github.com/sindresorhus/type-fest
|
||||
*/
|
||||
export type JsonObject = {[Key in string]?: JsonValue}
|
||||
|
||||
/**
|
||||
Matches a JSON array.
|
||||
@see https://github.com/sindresorhus/type-fest
|
||||
*/
|
||||
export type JsonArray = JsonValue[]
|
||||
|
||||
/**
|
||||
Matches any valid JSON primitive value.
|
||||
@see https://github.com/sindresorhus/type-fest
|
||||
*/
|
||||
export type JsonPrimitive = string | number | boolean | null
|
||||
|
||||
/**
|
||||
Matches any valid JSON value.
|
||||
@see https://github.com/sindresorhus/type-fest
|
||||
*/
|
||||
export type JsonValue = JsonPrimitive | JsonObject | JsonArray
|
||||
@@ -1,77 +0,0 @@
|
||||
import * as fs from "fs-extra"
|
||||
import * as path from "path"
|
||||
|
||||
function ext(jsx = false) {
|
||||
return fs.existsSync(path.resolve("tsconfig.json")) ? (jsx ? ".tsx" : ".ts") : ".js"
|
||||
}
|
||||
|
||||
function getBlitzPath(type: string) {
|
||||
const appPath = `app/blitz-${type}${ext(false)}`
|
||||
const srcPath = `src/blitz-${type}${ext(false)}`
|
||||
const appDir = fs.existsSync(path.resolve(appPath))
|
||||
const srcDir = fs.existsSync(path.resolve(srcPath))
|
||||
|
||||
if (appDir) {
|
||||
return appPath
|
||||
} else if (srcDir) {
|
||||
return srcPath
|
||||
}
|
||||
}
|
||||
|
||||
function getAppSourceDir() {
|
||||
const srcPath = "src/pages"
|
||||
const srcDir = fs.existsSync(path.resolve(srcPath))
|
||||
|
||||
if (srcDir) {
|
||||
return "src"
|
||||
} else {
|
||||
return "app"
|
||||
}
|
||||
}
|
||||
|
||||
function findPageDir() {
|
||||
const srcPagePath = `src/pages`
|
||||
const srcPage = getAppSourceDir()
|
||||
|
||||
switch (srcPage) {
|
||||
case "src": {
|
||||
return srcPagePath
|
||||
}
|
||||
default: {
|
||||
return `pages`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const paths = {
|
||||
document() {
|
||||
return `${findPageDir()}/_document${ext(true)}`
|
||||
},
|
||||
app() {
|
||||
return `${findPageDir()}/_app${ext(true)}`
|
||||
},
|
||||
appSrcDirectory() {
|
||||
return getAppSourceDir()
|
||||
},
|
||||
blitzServer() {
|
||||
return getBlitzPath("server")
|
||||
},
|
||||
blitzClient() {
|
||||
return getBlitzPath("client")
|
||||
},
|
||||
entry() {
|
||||
return `${findPageDir()}/index${ext(true)}`
|
||||
},
|
||||
nextConfig() {
|
||||
return `next.config.js`
|
||||
},
|
||||
babelConfig() {
|
||||
return `babel.config.js`
|
||||
},
|
||||
packageJson() {
|
||||
return "package.json"
|
||||
},
|
||||
prismaSchema() {
|
||||
return "db/schema.prisma"
|
||||
},
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import * as fs from "fs-extra"
|
||||
import j from "jscodeshift"
|
||||
import getBabelOptions, {Overrides} from "recast/parsers/_babel_options"
|
||||
import * as babel from "recast/parsers/babel"
|
||||
import {Program} from "../types"
|
||||
|
||||
export const customTsParser: {} = {
|
||||
parse(source: string, options?: Overrides) {
|
||||
const babelOptions = getBabelOptions(options)
|
||||
babelOptions.plugins.push("typescript")
|
||||
babelOptions.plugins.push("jsx")
|
||||
|
||||
return babel.parser.parse(source, babelOptions)
|
||||
},
|
||||
}
|
||||
|
||||
export enum TransformStatus {
|
||||
Success = "success",
|
||||
Failure = "failure",
|
||||
}
|
||||
export interface TransformResult {
|
||||
status: TransformStatus
|
||||
filename: string
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export type StringTransformer = (program: string) => string | Promise<string>
|
||||
export type Transformer = (program: Program) => Program | Promise<Program>
|
||||
|
||||
export function stringProcessFile(
|
||||
original: string,
|
||||
transformerFn: StringTransformer,
|
||||
): string | Promise<string> {
|
||||
return transformerFn(original)
|
||||
}
|
||||
|
||||
export async function processFile(original: string, transformerFn: Transformer): Promise<string> {
|
||||
const program = j(original, {parser: customTsParser})
|
||||
return (await transformerFn(program)).toSource()
|
||||
}
|
||||
|
||||
export async function transform(
|
||||
processFile: (original: string) => Promise<string>,
|
||||
targetFilePaths: string[],
|
||||
): Promise<TransformResult[]> {
|
||||
const results: TransformResult[] = []
|
||||
for (const filePath of targetFilePaths) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
results.push({
|
||||
status: TransformStatus.Failure,
|
||||
filename: filePath,
|
||||
error: new Error(`Error: ${filePath} not found`),
|
||||
})
|
||||
}
|
||||
try {
|
||||
const fileBuffer = fs.readFileSync(filePath)
|
||||
const fileSource = fileBuffer.toString("utf-8")
|
||||
const transformedCode = await processFile(fileSource)
|
||||
fs.writeFileSync(filePath, transformedCode)
|
||||
results.push({
|
||||
status: TransformStatus.Success,
|
||||
filename: filePath,
|
||||
})
|
||||
} catch (err) {
|
||||
results.push({
|
||||
status: TransformStatus.Failure,
|
||||
filename: filePath,
|
||||
error: err as any,
|
||||
})
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import {useInput} from "ink"
|
||||
|
||||
export function useEnterToContinue(cb: Function, additionalCondition: boolean = true) {
|
||||
useInput((_input, key) => {
|
||||
if (additionalCondition && key.return) {
|
||||
cb()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import {useStdin} from "ink"
|
||||
import {RecipeCLIFlags} from "../types"
|
||||
|
||||
export function useUserInput(cliFlags: RecipeCLIFlags) {
|
||||
const {isRawModeSupported} = useStdin()
|
||||
return isRawModeSupported && !cliFlags.yesToAll
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# @blitzjs/recipe-base-web
|
||||
|
||||
## 2.0.0-beta.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1476a577]
|
||||
- blitz@2.0.0-beta.11
|
||||
|
||||
## 2.0.0-beta.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9db6c885]
|
||||
- Updated dependencies [d98e4bac]
|
||||
- Updated dependencies [9fe0cc54]
|
||||
- Updated dependencies [af58e2b2]
|
||||
- Updated dependencies [2ade7268]
|
||||
- Updated dependencies [0edeaa37]
|
||||
- Updated dependencies [430f6ec7]
|
||||
- Updated dependencies [15d22af2]
|
||||
- Updated dependencies [aa34661f]
|
||||
- Updated dependencies [8e0c9d76]
|
||||
- Updated dependencies [e2c18895]
|
||||
- blitz@2.0.0-beta.5
|
||||
@@ -1,12 +0,0 @@
|
||||
## base-web
|
||||
|
||||
The Blitz Recipe for installing Base Web
|
||||
|
||||
## more information
|
||||
|
||||
- [Base Web Homepage](https://baseweb.design/)
|
||||
- [Github page](https://github.com/uber/baseweb)
|
||||
|
||||
## contributors
|
||||
|
||||
- Konrad Kalemba <konrad@kale.mba>
|
||||
@@ -1,355 +0,0 @@
|
||||
import {addImport, paths, RecipeBuilder} from "blitz/installer"
|
||||
import j from "jscodeshift"
|
||||
import {join} from "path"
|
||||
|
||||
export default RecipeBuilder()
|
||||
.setName("Base Web")
|
||||
.setDescription(`This will install all necessary dependencies and configure Base Web for use.`)
|
||||
.setOwner("Konrad Kalemba <konrad@kale.mba>")
|
||||
.setRepoLink("https://github.com/blitz-js/blitz/")
|
||||
.addAddDependenciesStep({
|
||||
stepId: "addDeps",
|
||||
stepName: "Add dependencies",
|
||||
explanation: `Add 'baseui' and Styletron as a dependency too -- it's a toolkit for CSS in JS styling which Base Web relies on.`,
|
||||
packages: [
|
||||
{name: "baseui", version: "^10.5.0"},
|
||||
{name: "styletron-engine-atomic", version: "^1.4.8"},
|
||||
{name: "styletron-react", version: "^6.0.2"},
|
||||
{name: "@types/styletron-engine-atomic", version: "^1.1.1"},
|
||||
{name: "@types/styletron-react", version: "^5.0.3"},
|
||||
],
|
||||
})
|
||||
.addNewFilesStep({
|
||||
stepId: "addStyletronUtil",
|
||||
stepName: "Add Styletron util file",
|
||||
explanation: `Next, we need to add a util file that will help us to make Styletron work both client- and server-side.`,
|
||||
targetDirectory: "./utils",
|
||||
templatePath: join(__dirname, "templates", "utils"),
|
||||
templateValues: {},
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "addStyletronAndBaseProvidersToApp",
|
||||
stepName: "Import required providers and wrap the root of the app with them",
|
||||
explanation: `Additionally we supply StyletronProvider with 'value' and 'debug' props. BaseProvider requires a 'theme' prop we set with default Base Web's light theme.`,
|
||||
singleFileSearch: paths.app(),
|
||||
transform(program) {
|
||||
const styletronProviderImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("Provider"), j.identifier("StyletronProvider"))],
|
||||
j.literal("styletron-react"),
|
||||
)
|
||||
|
||||
const styletronAndDebugImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("styletron")), j.importSpecifier(j.identifier("debug"))],
|
||||
j.literal("utils/styletron"),
|
||||
)
|
||||
|
||||
const themeAndBaseProviderImport = j.importDeclaration(
|
||||
[
|
||||
j.importSpecifier(j.identifier("LightTheme")),
|
||||
j.importSpecifier(j.identifier("BaseProvider")),
|
||||
],
|
||||
j.literal("baseui"),
|
||||
)
|
||||
|
||||
addImport(program, styletronProviderImport)
|
||||
addImport(program, styletronAndDebugImport)
|
||||
addImport(program, themeAndBaseProviderImport)
|
||||
|
||||
program
|
||||
.find(j.FunctionDeclaration, (node) => node.id.name === "MyApp")
|
||||
.forEach((path) => {
|
||||
const statement = path.value.body.body.filter(
|
||||
(b) => b.type === "ReturnStatement",
|
||||
)[0] as j.ReturnStatement
|
||||
const argument = statement?.argument as j.JSXElement
|
||||
|
||||
statement.argument = j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("StyletronProvider"), [
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("value"),
|
||||
j.jsxExpressionContainer(j.identifier("styletron")),
|
||||
),
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("debug"),
|
||||
j.jsxExpressionContainer(j.identifier("debug")),
|
||||
),
|
||||
j.jsxAttribute(j.jsxIdentifier("debugAfterHydration")),
|
||||
]),
|
||||
j.jsxClosingElement(j.jsxIdentifier("StyletronProvider")),
|
||||
[
|
||||
j.literal("\n"),
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("BaseProvider"), [
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("theme"),
|
||||
j.jsxExpressionContainer(j.identifier("LightTheme")),
|
||||
),
|
||||
]),
|
||||
j.jsxClosingElement(j.jsxIdentifier("BaseProvider")),
|
||||
[j.literal("\n"), argument, j.literal("\n")],
|
||||
),
|
||||
j.literal("\n"),
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
return program
|
||||
},
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "modifyGetInitialPropsAndAddStylesheetsToDocument",
|
||||
stepName: "Modify getInitialProps method and add stylesheets to Document",
|
||||
explanation: `To make Styletron work server-side we need to modify getInitialProps method of custom Document class. We also have to put Styletron's generated stylesheets in DocumentHead.`,
|
||||
singleFileSearch: paths.document(),
|
||||
transform(program) {
|
||||
const styletronProviderImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("Provider"), j.identifier("StyletronProvider"))],
|
||||
j.literal("styletron-react"),
|
||||
)
|
||||
|
||||
const styletronServerAndSheetImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("Server")), j.importSpecifier(j.identifier("Sheet"))],
|
||||
j.literal("styletron-engine-atomic"),
|
||||
)
|
||||
|
||||
const styletronImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("styletron"))],
|
||||
j.literal("utils/styletron"),
|
||||
)
|
||||
|
||||
addImport(program, styletronProviderImport)
|
||||
addImport(program, styletronServerAndSheetImport)
|
||||
addImport(program, styletronImport)
|
||||
|
||||
program
|
||||
.find(j.ImportDeclaration, {source: {value: "next/document"}})
|
||||
.forEach((nextDocumentImportPath) => {
|
||||
let specifiers = nextDocumentImportPath.value.specifiers || []
|
||||
if (
|
||||
!specifiers
|
||||
.filter((spec) => j.ImportSpecifier.check(spec))
|
||||
.some((node) => (node as j.ImportSpecifier)?.imported?.name === "DocumentContext")
|
||||
) {
|
||||
specifiers.push(j.importSpecifier(j.identifier("DocumentContext")))
|
||||
}
|
||||
})
|
||||
|
||||
program.find(j.ClassDeclaration).forEach((path) => {
|
||||
const props = j.typeAlias(
|
||||
j.identifier("MyDocumentProps"),
|
||||
null,
|
||||
j.objectTypeAnnotation([
|
||||
j.objectTypeProperty(
|
||||
j.identifier("stylesheets"),
|
||||
j.arrayTypeAnnotation(j.genericTypeAnnotation(j.identifier("Sheet"), null)),
|
||||
false,
|
||||
),
|
||||
]),
|
||||
)
|
||||
|
||||
path.insertBefore(props)
|
||||
|
||||
path.value.superTypeParameters = j.typeParameterInstantiation([
|
||||
j.genericTypeAnnotation(j.identifier("MyDocumentProps"), null),
|
||||
])
|
||||
})
|
||||
|
||||
program.find(j.ClassBody).forEach((path) => {
|
||||
const {node} = path
|
||||
|
||||
const ctxParam = j.identifier("ctx")
|
||||
ctxParam.typeAnnotation = j.tsTypeAnnotation(
|
||||
j.tsTypeReference(j.identifier("DocumentContext")),
|
||||
)
|
||||
|
||||
const stylesheetsObjectProperty = j.objectProperty(
|
||||
j.identifier("stylesheets"),
|
||||
j.identifier("stylesheets"),
|
||||
)
|
||||
stylesheetsObjectProperty.shorthand = true
|
||||
|
||||
const getInitialPropsBody = j.blockStatement([
|
||||
j.variableDeclaration("const", [
|
||||
j.variableDeclarator(
|
||||
j.identifier("originalRenderPage"),
|
||||
j.memberExpression(j.identifier("ctx"), j.identifier("renderPage")),
|
||||
),
|
||||
]),
|
||||
j.expressionStatement(
|
||||
j.assignmentExpression(
|
||||
"=",
|
||||
j.memberExpression(j.identifier("ctx"), j.identifier("renderPage")),
|
||||
j.arrowFunctionExpression(
|
||||
[],
|
||||
j.callExpression(j.identifier("originalRenderPage"), [
|
||||
j.objectExpression([
|
||||
j.objectProperty(
|
||||
j.identifier("enhanceApp"),
|
||||
j.arrowFunctionExpression(
|
||||
[j.identifier("App")],
|
||||
j.arrowFunctionExpression(
|
||||
[j.identifier("props")],
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("StyletronProvider"), [
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("value"),
|
||||
j.jsxExpressionContainer(j.identifier("styletron")),
|
||||
),
|
||||
]),
|
||||
j.jsxClosingElement(j.jsxIdentifier("StyletronProvider")),
|
||||
[
|
||||
j.literal("\n"),
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(
|
||||
j.jsxIdentifier("App"),
|
||||
[j.jsxSpreadAttribute(j.identifier("props"))],
|
||||
true,
|
||||
),
|
||||
),
|
||||
j.literal("\n"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
j.variableDeclaration("const", [
|
||||
j.variableDeclarator(
|
||||
j.identifier("initialProps"),
|
||||
j.awaitExpression(
|
||||
j.callExpression(
|
||||
j.memberExpression(j.identifier("Document"), j.identifier("getInitialProps")),
|
||||
[j.identifier("ctx")],
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
j.variableDeclaration("const", [
|
||||
j.variableDeclarator(
|
||||
j.identifier("stylesheets"),
|
||||
j.logicalExpression(
|
||||
"||",
|
||||
j.callExpression(
|
||||
j.memberExpression(
|
||||
j.parenthesizedExpression(
|
||||
j.tsAsExpression(
|
||||
j.identifier("styletron"),
|
||||
j.tsTypeReference(j.identifier("Server")),
|
||||
),
|
||||
),
|
||||
j.identifier("getStylesheets"),
|
||||
),
|
||||
[],
|
||||
),
|
||||
j.arrayExpression([]),
|
||||
),
|
||||
),
|
||||
]),
|
||||
j.returnStatement(
|
||||
j.objectExpression([
|
||||
j.spreadElement(j.identifier("initialProps")),
|
||||
stylesheetsObjectProperty,
|
||||
]),
|
||||
),
|
||||
])
|
||||
|
||||
const getInitialPropsMethod = j.classMethod(
|
||||
"method",
|
||||
j.identifier("getInitialProps"),
|
||||
[ctxParam],
|
||||
getInitialPropsBody,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
getInitialPropsMethod.async = true
|
||||
|
||||
// TODO: better way will be to check if the method already exists and modify it or else add it
|
||||
// currently it gets added assuming it did not exist before
|
||||
node.body.splice(0, 0, getInitialPropsMethod)
|
||||
})
|
||||
|
||||
program.find(j.JSXElement, {openingElement: {name: {name: "Head"}}}).forEach((path) => {
|
||||
const {node} = path
|
||||
path.replace(
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("Head")),
|
||||
j.jsxClosingElement(j.jsxIdentifier("Head")),
|
||||
[
|
||||
...(node.children || []),
|
||||
j.literal("\n"),
|
||||
j.jsxExpressionContainer(
|
||||
j.callExpression(
|
||||
j.memberExpression(
|
||||
j.memberExpression(
|
||||
j.memberExpression(j.thisExpression(), j.identifier("props")),
|
||||
j.identifier("stylesheets"),
|
||||
),
|
||||
j.identifier("map"),
|
||||
),
|
||||
[
|
||||
j.arrowFunctionExpression(
|
||||
[j.identifier("sheet"), j.identifier("i")],
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(
|
||||
j.jsxIdentifier("style"),
|
||||
[
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("className"),
|
||||
j.literal("_styletron_hydrate_"),
|
||||
),
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("dangerouslySetInnerHTML"),
|
||||
j.jsxExpressionContainer(
|
||||
j.objectExpression([
|
||||
j.objectProperty(
|
||||
j.identifier("__html"),
|
||||
j.memberExpression(j.identifier("sheet"), j.identifier("css")),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("media"),
|
||||
j.jsxExpressionContainer(
|
||||
j.memberExpression(
|
||||
j.memberExpression(j.identifier("sheet"), j.identifier("attrs")),
|
||||
j.identifier("media"),
|
||||
),
|
||||
),
|
||||
),
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("data-hydrate"),
|
||||
j.jsxExpressionContainer(
|
||||
j.memberExpression(
|
||||
j.memberExpression(j.identifier("sheet"), j.identifier("attrs")),
|
||||
j.stringLiteral("data-hydrate"),
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
j.jsxAttribute(
|
||||
j.jsxIdentifier("key"),
|
||||
j.jsxExpressionContainer(j.jsxIdentifier("i")),
|
||||
),
|
||||
],
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
j.literal("\n"),
|
||||
],
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return program
|
||||
},
|
||||
})
|
||||
.build()
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@blitzjs/recipe-base-web",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.11",
|
||||
"description": "The Blitz Recipe for installing Base Web",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/blitz-js/blitz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blitz",
|
||||
"blitzjs",
|
||||
"base-web"
|
||||
],
|
||||
"author": "Konrad Kalemba <konrad@kale.mba>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/blitz-js/blitz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"dependencies": {
|
||||
"blitz": "2.2.0",
|
||||
"jscodeshift": "0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jscodeshift": "0.11.2"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import {Client, Server} from "styletron-engine-atomic"
|
||||
import {DebugEngine} from "styletron-react"
|
||||
|
||||
const getHydrateClass = () =>
|
||||
document.getElementsByClassName("_styletron_hydrate_") as HTMLCollectionOf<HTMLStyleElement>
|
||||
|
||||
export const styletron =
|
||||
typeof window === "undefined"
|
||||
? new Server()
|
||||
: new Client({
|
||||
hydrate: getHydrateClass(),
|
||||
})
|
||||
|
||||
export const debug = process.env.NODE_ENV === "production" ? void 0 : new DebugEngine()
|
||||
@@ -1,25 +0,0 @@
|
||||
# @blitzjs/recipe-bulma
|
||||
|
||||
## 2.0.0-beta.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1476a577]
|
||||
- blitz@2.0.0-beta.11
|
||||
|
||||
## 2.0.0-beta.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9db6c885]
|
||||
- Updated dependencies [d98e4bac]
|
||||
- Updated dependencies [9fe0cc54]
|
||||
- Updated dependencies [af58e2b2]
|
||||
- Updated dependencies [2ade7268]
|
||||
- Updated dependencies [0edeaa37]
|
||||
- Updated dependencies [430f6ec7]
|
||||
- Updated dependencies [15d22af2]
|
||||
- Updated dependencies [aa34661f]
|
||||
- Updated dependencies [8e0c9d76]
|
||||
- Updated dependencies [e2c18895]
|
||||
- blitz@2.0.0-beta.5
|
||||
@@ -1,17 +0,0 @@
|
||||
## bulma
|
||||
|
||||
The Blitz Recipe for installing Bulma CSS
|
||||
|
||||
```
|
||||
blitz install bulma
|
||||
```
|
||||
|
||||
## More information
|
||||
|
||||
- [How to use receipes in Blitz](https://blitzjs.com/docs/using-recipes)
|
||||
- [Learn about Bulma CSS](https://bulma.io/)
|
||||
- [Bulma CSS's Github](https://github.com/jgthms/bulma)
|
||||
|
||||
## Contributors
|
||||
|
||||
- vivek <vivek7405@hey.com>
|
||||
@@ -1,37 +0,0 @@
|
||||
import {addImport, paths, RecipeBuilder} from "blitz/installer"
|
||||
import j from "jscodeshift"
|
||||
import {join} from "path"
|
||||
|
||||
export default RecipeBuilder()
|
||||
.setName("Bulma CSS")
|
||||
.setDescription(`This will install all necessary dependencies and configure Bulma for use.`)
|
||||
.setOwner("vivek7405@hey.com")
|
||||
.setRepoLink("https://github.com/blitz-js/blitz/")
|
||||
.addAddDependenciesStep({
|
||||
stepId: "addDeps",
|
||||
stepName: "npm dependencies",
|
||||
explanation: `Bulma CSS requires a couple of dependencies including sass for converting sass and scss to css`,
|
||||
packages: [
|
||||
{name: "bulma", version: "0.9.x", isDevDep: true},
|
||||
{name: "sass", version: "1.43.x", isDevDep: true},
|
||||
],
|
||||
})
|
||||
.addNewFilesStep({
|
||||
stepId: "addStyles",
|
||||
stepName: "Stylesheet",
|
||||
explanation: `Adds a root CSS stylesheet where Bulma is imported and where you can add global styles`,
|
||||
targetDirectory: "./app/core/styles",
|
||||
templatePath: join(__dirname, "templates", "styles"),
|
||||
templateValues: {},
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "importStyles",
|
||||
stepName: "Import stylesheets",
|
||||
explanation: `Imports the stylesheet we just added into your app`,
|
||||
singleFileSearch: paths.app(),
|
||||
transform(program) {
|
||||
const stylesImport = j.importDeclaration([], j.literal("app/core/styles/index.scss"))
|
||||
return addImport(program, stylesImport)
|
||||
},
|
||||
})
|
||||
.build()
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@blitzjs/recipe-bulma",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.11",
|
||||
"description": "The Blitz Recipe for installing Bulma CSS",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/blitz-js/blitz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blitz",
|
||||
"blitzjs"
|
||||
],
|
||||
"author": "vivek <vivek7405@hey.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/blitz-js/blitz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"dependencies": {
|
||||
"blitz": "2.2.0",
|
||||
"jscodeshift": "0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jscodeshift": "0.11.2"
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
@charset "utf-8";
|
||||
|
||||
// Customization
|
||||
|
||||
// You can easily customize Bulma with your own variables.
|
||||
// Just uncomment the following block to see the result.
|
||||
|
||||
/*
|
||||
1. Import the initial variables
|
||||
@import "node_modules/bulma/sass/utilities/initial-variables";
|
||||
|
||||
2. Set your own initial variables
|
||||
Update the blue shade, used for links
|
||||
$blue: #06bcef;
|
||||
Add pink and its invert
|
||||
$pink: #ff8080;
|
||||
$pink-invert: #fff;
|
||||
Update the sans-serif font family
|
||||
$family-sans-serif: "Helvetica", "Arial", sans-serif;
|
||||
|
||||
3. Set the derived variables
|
||||
Use the new pink as the primary color
|
||||
$primary: $pink;
|
||||
$primary-invert: $pink-invert;
|
||||
|
||||
4. Import the rest of Bulma
|
||||
*/
|
||||
|
||||
@import "node_modules/bulma/bulma.sass";
|
||||
@@ -1,25 +0,0 @@
|
||||
# @blitzjs/recipe-bumbag-ui
|
||||
|
||||
## 2.0.0-beta.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1476a577]
|
||||
- blitz@2.0.0-beta.11
|
||||
|
||||
## 2.0.0-beta.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9db6c885]
|
||||
- Updated dependencies [d98e4bac]
|
||||
- Updated dependencies [9fe0cc54]
|
||||
- Updated dependencies [af58e2b2]
|
||||
- Updated dependencies [2ade7268]
|
||||
- Updated dependencies [0edeaa37]
|
||||
- Updated dependencies [430f6ec7]
|
||||
- Updated dependencies [15d22af2]
|
||||
- Updated dependencies [aa34661f]
|
||||
- Updated dependencies [8e0c9d76]
|
||||
- Updated dependencies [e2c18895]
|
||||
- blitz@2.0.0-beta.5
|
||||
@@ -1,12 +0,0 @@
|
||||
## bumbag-ui
|
||||
|
||||
The Blitz Recipe for installing Bumbag UI
|
||||
|
||||
## more information
|
||||
|
||||
- [Bumbag UI Homepage](https://www.bumbag.style/)
|
||||
- [Github page](https://github.com/jxom/bumbag-ui)
|
||||
|
||||
## contributors
|
||||
|
||||
- Agusti Fernandez Pardo <agusti@cruceritis.com>
|
||||
@@ -1,121 +0,0 @@
|
||||
import {addImport, paths, Program, RecipeBuilder} from "blitz/installer"
|
||||
import j from "jscodeshift"
|
||||
|
||||
function wrapComponentWithBumbagProvider(program: Program) {
|
||||
program
|
||||
.find(j.FunctionDeclaration, (node) => node.id.name === "MyApp")
|
||||
.forEach((path) => {
|
||||
const statement = path.value.body.body.filter(
|
||||
(b) => b.type === "ReturnStatement",
|
||||
)[0] as j.ReturnStatement
|
||||
const argument = statement?.argument as j.JSXElement
|
||||
|
||||
try {
|
||||
statement.argument = j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("BumbagProvider isSSR")),
|
||||
j.jsxClosingElement(j.jsxIdentifier("BumbagProvider")),
|
||||
[j.jsxText("\n"), argument, j.jsxText("\n")],
|
||||
)
|
||||
} catch {
|
||||
console.error("Already installed recipe")
|
||||
}
|
||||
})
|
||||
return program
|
||||
}
|
||||
|
||||
function injectInitializeColorModeAndExtractCritical(program: Program) {
|
||||
// Finds body element and injects InitializeColorMode before it.
|
||||
program.find(j.JSXElement, {openingElement: {name: {name: "body"}}}).forEach((path) => {
|
||||
const {node} = path
|
||||
path.replace(
|
||||
j.jsxElement(
|
||||
j.jsxOpeningElement(j.jsxIdentifier("body")),
|
||||
j.jsxClosingElement(j.jsxIdentifier("body")),
|
||||
[
|
||||
j.literal("\n"),
|
||||
j.jsxElement(j.jsxOpeningElement(j.jsxIdentifier("InitializeColorMode"), [], true)),
|
||||
...(node.children || []),
|
||||
],
|
||||
),
|
||||
)
|
||||
})
|
||||
// Find ClassDeclaration and insert extractCritical on getInitialProps
|
||||
program
|
||||
.find(j.ClassDeclaration)
|
||||
.at(0)
|
||||
.get()
|
||||
.insertAfter(
|
||||
`
|
||||
MyDocument.getInitialProps = async (ctx) => {
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
const styles = extractCritical(initialProps.html)
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
<style
|
||||
data-emotion-css={styles.ids.join(' ')}
|
||||
dangerouslySetInnerHTML={{ __html: styles.css }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}
|
||||
}
|
||||
`,
|
||||
)
|
||||
return program
|
||||
}
|
||||
|
||||
export default RecipeBuilder()
|
||||
.setName("Bumbag UI")
|
||||
.setDescription(
|
||||
`This will install all necessary dependencies and configure BumbagProvider in your _app and _document`,
|
||||
)
|
||||
.setOwner("me@agusti.me")
|
||||
.setRepoLink("https://github.com/blitz-js/blitz/")
|
||||
.addAddDependenciesStep({
|
||||
stepId: "addDeps",
|
||||
stepName: "npm dependencies",
|
||||
explanation: `Bumbag UI requires both "bumbag" and "bumbag-server" (SSR) npm packages`,
|
||||
packages: [
|
||||
{name: "bumbag", version: "2.x"},
|
||||
{name: "bumbag-server", version: "2.x"},
|
||||
],
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "importBmumbagProvider",
|
||||
stepName: "Import BumbagProvider",
|
||||
explanation: `Import bumbag Provider as BumbagProvider into _app`,
|
||||
singleFileSearch: paths.app(),
|
||||
transform(program) {
|
||||
const stylesImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("Provider as BumbagProvider"))],
|
||||
j.literal("bumbag"),
|
||||
)
|
||||
|
||||
addImport(program, stylesImport)
|
||||
return wrapComponentWithBumbagProvider(program)
|
||||
},
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "ImportExtractCriticalAndInitializeColorMode",
|
||||
stepName: "ImportExtractCritical & initializeColorMode",
|
||||
explanation: `Import InitializeColorMode from bumbag, and extractCritical into _document`,
|
||||
singleFileSearch: paths.document(),
|
||||
transform(program) {
|
||||
const initializeColorModeImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("InitializeColorMode"))],
|
||||
j.literal("bumbag"),
|
||||
)
|
||||
const exctractCriticalImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("extractCritical"))],
|
||||
j.literal("bumbag-server"),
|
||||
)
|
||||
addImport(program, initializeColorModeImport)
|
||||
addImport(program, exctractCriticalImport)
|
||||
|
||||
return injectInitializeColorModeAndExtractCritical(program)
|
||||
},
|
||||
})
|
||||
.build()
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "@blitzjs/recipe-bumbag-ui",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.11",
|
||||
"description": "The Blitz Recipe for installing Bumbag UI",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/blitz-js/blitz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blitz",
|
||||
"blitzjs",
|
||||
"chakra",
|
||||
"bumbag-ui"
|
||||
],
|
||||
"author": "Agusti Fernandez Pardo <agusti@cruceritis.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/blitz-js/blitz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"dependencies": {
|
||||
"blitz": "2.2.0",
|
||||
"jscodeshift": "0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jscodeshift": "0.11.2",
|
||||
"ast-types": "0.14.2"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# @blitzjs/recipe-chakra-ui
|
||||
|
||||
## 2.0.0-beta.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1476a577]
|
||||
- blitz@2.0.0-beta.11
|
||||
|
||||
## 2.0.0-beta.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9db6c885]
|
||||
- Updated dependencies [d98e4bac]
|
||||
- Updated dependencies [9fe0cc54]
|
||||
- Updated dependencies [af58e2b2]
|
||||
- Updated dependencies [2ade7268]
|
||||
- Updated dependencies [0edeaa37]
|
||||
- Updated dependencies [430f6ec7]
|
||||
- Updated dependencies [15d22af2]
|
||||
- Updated dependencies [aa34661f]
|
||||
- Updated dependencies [8e0c9d76]
|
||||
- Updated dependencies [e2c18895]
|
||||
- blitz@2.0.0-beta.5
|
||||
@@ -1,12 +0,0 @@
|
||||
## chakra-ui
|
||||
|
||||
Easily styling Blitz app using Chakra UI
|
||||
|
||||
## more information
|
||||
|
||||
- [Chakra UI homepage](https://chakra-ui.com)
|
||||
- [Github page](https://github.com/chrisbull/blitz-app-with-chakra-ui-template)
|
||||
|
||||
## contributors
|
||||
|
||||
- zeko369
|
||||
@@ -1,192 +0,0 @@
|
||||
import {addImport, paths, Program, RecipeBuilder, wrapAppWithProvider} from "blitz/installer"
|
||||
import j, {JSXIdentifier} from "jscodeshift"
|
||||
|
||||
function updateLabeledTextFieldWithInputComponent(program: Program) {
|
||||
program
|
||||
.find(j.TSInterfaceDeclaration)
|
||||
.find(j.TSExpressionWithTypeArguments)
|
||||
.forEach((path: j.ASTPath<j.TSExpressionWithTypeArguments>) => {
|
||||
path.replace(
|
||||
j.tsExpressionWithTypeArguments(
|
||||
j.identifier("ComponentPropsWithoutRef"),
|
||||
j.tsTypeParameterInstantiation([j.tsTypeQuery(j.identifier("Input"))]),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return program
|
||||
}
|
||||
|
||||
function replaceOuterDivWithFormControl(program: Program) {
|
||||
program
|
||||
.find(j.JSXElement)
|
||||
.filter((path) => {
|
||||
const {node} = path
|
||||
const openingElementNameNode = node?.openingElement?.name as JSXIdentifier
|
||||
|
||||
// This will not include JSX elements within curly braces
|
||||
const countOfChildrenJSXElements =
|
||||
path.node.children?.filter((childNode) => childNode.type === "JSXElement").length || 0
|
||||
|
||||
return (
|
||||
openingElementNameNode?.name === "div" &&
|
||||
node?.openingElement?.selfClosing === false &&
|
||||
countOfChildrenJSXElements === 1
|
||||
)
|
||||
})
|
||||
.forEach((path) => {
|
||||
path.node.openingElement = j.jsxOpeningElement(
|
||||
j.jsxIdentifier("FormControl"),
|
||||
path.node.openingElement.attributes,
|
||||
)
|
||||
path.node.closingElement = j.jsxClosingElement(j.jsxIdentifier("FormControl"))
|
||||
})
|
||||
|
||||
return program
|
||||
}
|
||||
|
||||
function replaceInputWithChakraInput(program: Program) {
|
||||
program
|
||||
.find(j.JSXElement)
|
||||
.filter((path) => {
|
||||
const {node} = path
|
||||
const openingElementNameNode = node?.openingElement?.name as JSXIdentifier
|
||||
|
||||
return openingElementNameNode?.name === "input" && node?.openingElement?.selfClosing === true
|
||||
})
|
||||
.forEach((path) => {
|
||||
const {node} = path
|
||||
node.openingElement = j.jsxOpeningElement(
|
||||
j.jsxIdentifier("Input"),
|
||||
node.openingElement.attributes,
|
||||
node?.openingElement?.selfClosing,
|
||||
)
|
||||
})
|
||||
|
||||
return program
|
||||
}
|
||||
|
||||
function replaceLabelWithChakraLabel(program: Program) {
|
||||
program
|
||||
.find(j.JSXElement)
|
||||
.filter((path) => {
|
||||
const {node} = path
|
||||
const openingElementNameNode = node?.openingElement?.name as JSXIdentifier
|
||||
|
||||
return openingElementNameNode?.name === "label" && node?.openingElement?.selfClosing === false
|
||||
})
|
||||
.forEach((path) => {
|
||||
path.node.openingElement = j.jsxOpeningElement(
|
||||
j.jsxIdentifier("FormLabel"),
|
||||
path.node.openingElement.attributes,
|
||||
)
|
||||
path.node.closingElement = j.jsxClosingElement(j.jsxIdentifier("FormLabel"))
|
||||
})
|
||||
|
||||
return program
|
||||
}
|
||||
|
||||
function removeDefaultStyleElement(program: Program) {
|
||||
program
|
||||
.find(j.JSXElement)
|
||||
.filter((path) => {
|
||||
const {node} = path
|
||||
const openingElementNameNode = node?.openingElement?.name as JSXIdentifier
|
||||
|
||||
// Assumes there's one style element at the point the user runs the recipe.
|
||||
return openingElementNameNode?.name === "style" && node?.openingElement?.selfClosing === false
|
||||
})
|
||||
.forEach((path) => {
|
||||
// Removes the node.
|
||||
path.replace()
|
||||
})
|
||||
|
||||
return program
|
||||
}
|
||||
|
||||
export default RecipeBuilder()
|
||||
.setName("Chakra UI")
|
||||
.setDescription(`This will install all necessary dependencies and configure Chakra UI for use.`)
|
||||
.setOwner("zekan.fran369@gmail.com")
|
||||
.setRepoLink("https://github.com/blitz-js/blitz/")
|
||||
.addAddDependenciesStep({
|
||||
stepId: "addDeps",
|
||||
stepName: "npm dependencies",
|
||||
explanation: `Chakra UI requires some other dependencies like emotion to work`,
|
||||
packages: [
|
||||
{name: "@chakra-ui/react", version: "1.x"},
|
||||
{name: "@emotion/react", version: "11.x"},
|
||||
{name: "@emotion/styled", version: "11.x"},
|
||||
{name: "framer-motion", version: "5.x"},
|
||||
],
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "importProviderAndReset",
|
||||
stepName: "Import ChakraProvider component",
|
||||
explanation: `Import the chakra-ui provider into _app, so it is accessible in the whole app`,
|
||||
singleFileSearch: paths.app(),
|
||||
transform(program) {
|
||||
const stylesImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("ChakraProvider"))],
|
||||
j.literal("@chakra-ui/react"),
|
||||
)
|
||||
|
||||
addImport(program, stylesImport)
|
||||
return wrapAppWithProvider(program, "ChakraProvider")
|
||||
},
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "updateLabeledTextField",
|
||||
stepName: "Update the `LabeledTextField` with Chakra UI's `Input` component",
|
||||
explanation: `The LabeledTextField component uses Chakra UI's input component`,
|
||||
singleFileSearch: `${paths.appSrcDirectory()}/core/components/LabeledTextField.tsx`,
|
||||
transform(program) {
|
||||
// Add ComponentPropsWithoutRef import
|
||||
program.find(j.ImportDeclaration, {source: {value: "react"}}).forEach((path) => {
|
||||
let specifiers = path.value.specifiers || []
|
||||
if (
|
||||
!specifiers.some(
|
||||
(node) => (node as j.ImportSpecifier)?.imported?.name === "ComponentPropsWithoutRef",
|
||||
)
|
||||
) {
|
||||
specifiers.push(j.importSpecifier(j.identifier("ComponentPropsWithoutRef")))
|
||||
}
|
||||
})
|
||||
|
||||
const chakraInputImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("Input"))],
|
||||
j.literal("@chakra-ui/input"),
|
||||
)
|
||||
|
||||
const chakraFormControlImport = j.importDeclaration(
|
||||
[
|
||||
j.importSpecifier(j.identifier("FormControl")),
|
||||
j.importSpecifier(j.identifier("FormLabel")),
|
||||
],
|
||||
j.literal("@chakra-ui/form-control"),
|
||||
)
|
||||
|
||||
addImport(program, chakraInputImport)
|
||||
addImport(program, chakraFormControlImport)
|
||||
|
||||
// Imperative steps to describe transformations
|
||||
|
||||
// 1. Update the type of `LabeledTextField`
|
||||
updateLabeledTextFieldWithInputComponent(program)
|
||||
|
||||
// 2. Remove the default <style jsx> styling
|
||||
removeDefaultStyleElement(program)
|
||||
|
||||
// 3. Replace outer div with `FormControl`
|
||||
replaceOuterDivWithFormControl(program)
|
||||
|
||||
// 4. Replace `input` with `ChakraInput`
|
||||
replaceInputWithChakraInput(program)
|
||||
|
||||
// 5. Replace `label` with `ChakraLabel`
|
||||
replaceLabelWithChakraLabel(program)
|
||||
|
||||
return program
|
||||
},
|
||||
})
|
||||
.build()
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "@blitzjs/recipe-chakra-ui",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.11",
|
||||
"description": "The Blitz Recipe for installing Chakra UI",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/blitz-js/blitz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blitz",
|
||||
"blitzjs",
|
||||
"chakra",
|
||||
"chakra-ui"
|
||||
],
|
||||
"author": "zeko369 <zekan.fran369@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/blitz-js/blitz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"dependencies": {
|
||||
"blitz": "2.2.0",
|
||||
"jscodeshift": "0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jscodeshift": "0.11.2",
|
||||
"ast-types": "0.14.2"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# @blitzjs/recipe-emotion
|
||||
|
||||
## 2.0.0-beta.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1476a577]
|
||||
- blitz@2.0.0-beta.11
|
||||
|
||||
## 2.0.0-beta.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9db6c885]
|
||||
- Updated dependencies [d98e4bac]
|
||||
- Updated dependencies [9fe0cc54]
|
||||
- Updated dependencies [af58e2b2]
|
||||
- Updated dependencies [2ade7268]
|
||||
- Updated dependencies [0edeaa37]
|
||||
- Updated dependencies [430f6ec7]
|
||||
- Updated dependencies [15d22af2]
|
||||
- Updated dependencies [aa34661f]
|
||||
- Updated dependencies [8e0c9d76]
|
||||
- Updated dependencies [e2c18895]
|
||||
- blitz@2.0.0-beta.5
|
||||
@@ -1,12 +0,0 @@
|
||||
## emotion
|
||||
|
||||
The Blitz Recipe for installing Emotion
|
||||
|
||||
## more information
|
||||
|
||||
- [Emotion homepage](https://emotion.sh/docs/introduction)
|
||||
- [Github page](https://github.com/emotion-js/emotion)
|
||||
|
||||
## contributors
|
||||
|
||||
- Justin Hall <justin.r.hall@gmail.com>
|
||||
@@ -1,90 +0,0 @@
|
||||
import {
|
||||
addBabelPlugin,
|
||||
addBabelPreset,
|
||||
addImport,
|
||||
paths,
|
||||
Program,
|
||||
RecipeBuilder,
|
||||
} from "blitz/installer"
|
||||
import j from "jscodeshift"
|
||||
import {join} from "path"
|
||||
|
||||
function applyGlobalStyles(program: Program) {
|
||||
program
|
||||
.find(j.JSXElement, {openingElement: {name: {name: "ErrorBoundary"}}})
|
||||
.forEach((elementPath) => {
|
||||
if (Array.isArray(elementPath.node.children)) {
|
||||
elementPath.node.children.splice(0, 0, j.literal("\n"))
|
||||
elementPath.node.children.splice(
|
||||
1,
|
||||
0,
|
||||
j.jsxExpressionContainer(j.identifier("globalStyles")),
|
||||
)
|
||||
}
|
||||
})
|
||||
.get().value.extra.parenthesized = false
|
||||
|
||||
return program
|
||||
}
|
||||
|
||||
export default RecipeBuilder()
|
||||
.setName("Emotion")
|
||||
.setDescription(`This will install all necessary dependencies and configure Emotion for use.`)
|
||||
.setOwner("justin.r.hall+blitz@gmail.com")
|
||||
.setRepoLink("https://github.com/blitz-js/blitz/")
|
||||
.addAddDependenciesStep({
|
||||
stepId: "addDeps",
|
||||
stepName: "npm dependencies",
|
||||
explanation: `We'll install @emotion/react and @emotion/styled for general usage, and @emotion/babel-plugin to enable some advanced features.`,
|
||||
packages: [
|
||||
{name: "@emotion/react", version: "11.x"},
|
||||
{name: "@emotion/styled", version: "11.x"},
|
||||
{name: "@emotion/babel-plugin", version: "11.x"},
|
||||
],
|
||||
})
|
||||
.addNewFilesStep({
|
||||
stepId: "createGlobalStyles",
|
||||
stepName: "Create global styles",
|
||||
explanation: `Adding some default global styles, but feel free to customize or even remove them as you see fit.`,
|
||||
targetDirectory: "./app/core",
|
||||
templatePath: join(__dirname, "templates", "styles"),
|
||||
templateValues: {},
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "addGlobalStyles",
|
||||
stepName: "Import global styles",
|
||||
explanation: `Next, we'll import and render the global styles.`,
|
||||
singleFileSearch: paths.app(),
|
||||
transform(program) {
|
||||
const stylesImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("globalStyles"))],
|
||||
j.literal("app/core/styles"),
|
||||
)
|
||||
|
||||
addImport(program, stylesImport)
|
||||
return applyGlobalStyles(program)
|
||||
},
|
||||
})
|
||||
.addNewFilesStep({
|
||||
stepId: "create babel file",
|
||||
stepName: "Create babel file",
|
||||
explanation: `Adding default babel file.`,
|
||||
targetDirectory: "./babel.config.js",
|
||||
templatePath: join(__dirname, "templates", "babel.config.js"),
|
||||
templateValues: {},
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "updateBabelConfig",
|
||||
stepName: "Add Babel plugin and preset",
|
||||
explanation: `Update the Babel configuration to use Emotion's plugin and preset to enable some advanced features.`,
|
||||
singleFileSearch: paths.babelConfig(),
|
||||
transform(program) {
|
||||
program = addBabelPlugin(program, "@emotion")
|
||||
program = addBabelPreset(program, [
|
||||
"preset-react",
|
||||
{runtime: "automatic", importSource: "@emotion/react"},
|
||||
])
|
||||
return program
|
||||
},
|
||||
})
|
||||
.build()
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@blitzjs/recipe-emotion",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.11",
|
||||
"description": "The Blitz Recipe for installing Emotion",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/blitz-js/blitz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blitz",
|
||||
"blitzjs"
|
||||
],
|
||||
"author": "Justin Hall <justin.r.hall@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/blitz-js/blitz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"dependencies": {
|
||||
"blitz": "2.2.0",
|
||||
"jscodeshift": "0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jscodeshift": "0.11.2"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ["next/babel"],
|
||||
plugins: [],
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import {css, Global} from "@emotion/react"
|
||||
|
||||
export const globalStyles = (
|
||||
<Global
|
||||
styles={css`
|
||||
html,
|
||||
body {
|
||||
background-color: papayawhip;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
# @blitzjs/recipe-gh-action-yarn-mariadb
|
||||
|
||||
## 2.0.0-beta.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1476a577]
|
||||
- blitz@2.0.0-beta.11
|
||||
|
||||
## 2.0.0-beta.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9db6c885]
|
||||
- Updated dependencies [d98e4bac]
|
||||
- Updated dependencies [9fe0cc54]
|
||||
- Updated dependencies [af58e2b2]
|
||||
- Updated dependencies [2ade7268]
|
||||
- Updated dependencies [0edeaa37]
|
||||
- Updated dependencies [430f6ec7]
|
||||
- Updated dependencies [15d22af2]
|
||||
- Updated dependencies [aa34661f]
|
||||
- Updated dependencies [8e0c9d76]
|
||||
- Updated dependencies [e2c18895]
|
||||
- blitz@2.0.0-beta.5
|
||||
@@ -1,13 +0,0 @@
|
||||
## gh-action-yarn-mariadb
|
||||
|
||||
The Blitz Recipe for adding github actions workflow with MariaDB
|
||||
|
||||
## more information
|
||||
|
||||
- [Github Actions Documentation](https://docs.github.com/en/actions)
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- [MariaDB](https://hub.docker.com/_/mariadb)
|
||||
|
||||
## contributors
|
||||
|
||||
- Kevin Langley Jr. <me@kevinlangleyjr.com>
|
||||
@@ -1,17 +0,0 @@
|
||||
import {RecipeBuilder} from "blitz/installer"
|
||||
import {join} from "path"
|
||||
|
||||
export default RecipeBuilder()
|
||||
.setName("Github Action Workflow For Yarn & MariaDB")
|
||||
.setDescription("This Github Action config will build and test your blitz app on each push")
|
||||
.setOwner("me@kevinlangleyjr.com")
|
||||
.setRepoLink("https://github.com/blitz-js/blitz/")
|
||||
.addNewFilesStep({
|
||||
stepId: "addWorkflow",
|
||||
stepName: "Add .github/workflows/main.yml",
|
||||
explanation: `NOTE: Your app must be configured to use MariaDB for this`,
|
||||
targetDirectory: ".github/workflows/",
|
||||
templatePath: join(__dirname, "templates"),
|
||||
templateValues: {},
|
||||
})
|
||||
.build()
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "@blitzjs/recipe-gh-action-yarn-mariadb",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.11",
|
||||
"description": "The Blitz Recipe for adding github actions workflow with MariaDB",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/blitz-js/blitz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blitz",
|
||||
"blitzjs",
|
||||
"github",
|
||||
"actions"
|
||||
],
|
||||
"author": "Kevin Langley Jr. <me@kevinlangleyjr.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/blitz-js/blitz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"dependencies": {
|
||||
"blitz": "2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
###############
|
||||
# Lint & Test
|
||||
###############
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: mysql://root:password@127.0.0.1:3306/test
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:latest
|
||||
ports:
|
||||
- 3306:3306
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping"
|
||||
--health-interval=5s
|
||||
--health-timeout=2s
|
||||
--health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Node
|
||||
- name: Read .node-version
|
||||
id: node_version
|
||||
run: echo ::set-output name=NODE_VERSION::$(cat .node-version || echo "14")
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2-beta
|
||||
with:
|
||||
node-version: ${{ steps.node_version.outputs.NODE_VERSION }}
|
||||
|
||||
# Yarn cache/install
|
||||
- name: Find yarn cache location
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: JS package cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ steps.yarn-cache.outputs.dir }}
|
||||
**/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
C:\Users\runneradmin\AppData\Local\Cypress\Cache
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install packages
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
# Lint Code
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
# Migrate DB & generate prisma client
|
||||
- name: Migrate DB & generate prisma client
|
||||
run: yarn blitz prisma migrate dev
|
||||
|
||||
# Jest Tests (API/Frontend)
|
||||
- name: Run Jest Tests
|
||||
run: yarn test
|
||||
|
||||
# E2E Tests via Cypress
|
||||
# - name: Run Cypress E2E Tests
|
||||
# uses: cypress-io/github-action@v2
|
||||
# with:
|
||||
# # we have already installed all dependencies above
|
||||
# install: false
|
||||
# start: yarn test:server --production
|
||||
# wait-on: "http://localhost:3099"
|
||||
# wait-on-timeout: 300
|
||||
# env:
|
||||
# NODE_ENV: test
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# - name: Upload Cypress Screenshots
|
||||
# uses: actions/upload-artifact@v1
|
||||
# # Only capture images on failure
|
||||
# if: failure()
|
||||
# with:
|
||||
# name: cypress-screenshots
|
||||
# path: cypress/screenshots
|
||||
# retention-days: 3
|
||||
# - name: Upload Cypress Videos
|
||||
# uses: actions/upload-artifact@v1
|
||||
# # Test run video was always captured, so this action uses "always()" condition
|
||||
# if: always()
|
||||
# with:
|
||||
# name: cypress-videos
|
||||
# path: cypress/videos
|
||||
# retention-days: 3
|
||||
@@ -1,25 +0,0 @@
|
||||
# @blitzjs/recipe-gh-action-yarn-postgres
|
||||
|
||||
## 2.0.0-beta.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1476a577]
|
||||
- blitz@2.0.0-beta.11
|
||||
|
||||
## 2.0.0-beta.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9db6c885]
|
||||
- Updated dependencies [d98e4bac]
|
||||
- Updated dependencies [9fe0cc54]
|
||||
- Updated dependencies [af58e2b2]
|
||||
- Updated dependencies [2ade7268]
|
||||
- Updated dependencies [0edeaa37]
|
||||
- Updated dependencies [430f6ec7]
|
||||
- Updated dependencies [15d22af2]
|
||||
- Updated dependencies [aa34661f]
|
||||
- Updated dependencies [8e0c9d76]
|
||||
- Updated dependencies [e2c18895]
|
||||
- blitz@2.0.0-beta.5
|
||||
@@ -1,13 +0,0 @@
|
||||
## gh-action-yarn-postgres
|
||||
|
||||
The Blitz Recipe for adding github actions workflow with postgres
|
||||
|
||||
## more information
|
||||
|
||||
- [Github Actions documentation](https://docs.github.com/en/actions)
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- [Postgres Documentation](https://www.postgresql.org/docs/)
|
||||
|
||||
## contributors
|
||||
|
||||
- Brandon Bayer <b@bayer.ws>
|
||||
@@ -1,17 +0,0 @@
|
||||
import {RecipeBuilder} from "blitz/installer"
|
||||
import {join} from "path"
|
||||
|
||||
export default RecipeBuilder()
|
||||
.setName("Github Action Workflow For Yarn & Postgres")
|
||||
.setDescription("This Github Action config will build and test your blitz app on each push")
|
||||
.setOwner("b@bayer.ws")
|
||||
.setRepoLink("https://github.com/blitz-js/blitz/")
|
||||
.addNewFilesStep({
|
||||
stepId: "addWorkflow",
|
||||
stepName: "Add .github/workflows/main.yml",
|
||||
explanation: `NOTE: Your app must be configured to use Postgres for this`,
|
||||
targetDirectory: ".github/workflows/",
|
||||
templatePath: join(__dirname, "templates"),
|
||||
templateValues: {},
|
||||
})
|
||||
.build()
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "@blitzjs/recipe-gh-action-yarn-postgres",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.11",
|
||||
"description": "The Blitz Recipe for adding github actions workflow with postgres",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/blitz-js/blitz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blitz",
|
||||
"blitzjs",
|
||||
"github",
|
||||
"actions"
|
||||
],
|
||||
"author": "Brandon Bayer <b@bayer.ws>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/blitz-js/blitz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"dependencies": {
|
||||
"blitz": "2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
###############
|
||||
# Lint & Test
|
||||
###############
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost/test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12
|
||||
ports: ["5432:5432"]
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
# Make sure the database is ready before we use it
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Node
|
||||
- name: Read .node-version
|
||||
id: node_version
|
||||
run: echo ::set-output name=NODE_VERSION::$(cat .node-version || echo "14")
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2-beta
|
||||
with:
|
||||
node-version: ${{ steps.node_version.outputs.NODE_VERSION }}
|
||||
|
||||
# Yarn cache/install
|
||||
- name: Find yarn cache location
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: JS package cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ steps.yarn-cache.outputs.dir }}
|
||||
**/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
C:\Users\runneradmin\AppData\Local\Cypress\Cache
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install packages
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
# Lint Code
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
# Migrate DB & generate prisma client
|
||||
- name: Migrate DB & generate prisma client
|
||||
run: yarn blitz prisma migrate dev
|
||||
|
||||
# Jest Tests (API/Frontend)
|
||||
- name: Run Jest Tests
|
||||
run: yarn test
|
||||
|
||||
# E2E Tests via Cypress
|
||||
# - name: Run Cypress E2E Tests
|
||||
# uses: cypress-io/github-action@v2
|
||||
# with:
|
||||
# # we have already installed all dependencies above
|
||||
# install: false
|
||||
# start: yarn test:server --production
|
||||
# wait-on: "http://localhost:3099"
|
||||
# wait-on-timeout: 300
|
||||
# env:
|
||||
# NODE_ENV: test
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# - name: Upload Cypress Screenshots
|
||||
# uses: actions/upload-artifact@v1
|
||||
# # Only capture images on failure
|
||||
# if: failure()
|
||||
# with:
|
||||
# name: cypress-screenshots
|
||||
# path: cypress/screenshots
|
||||
# retention-days: 3
|
||||
# - name: Upload Cypress Videos
|
||||
# uses: actions/upload-artifact@v1
|
||||
# # Test run video was always captured, so this action uses "always()" condition
|
||||
# if: always()
|
||||
# with:
|
||||
# name: cypress-videos
|
||||
# path: cypress/videos
|
||||
# retention-days: 3
|
||||
@@ -1,25 +0,0 @@
|
||||
# @blitzjs/recipe-ghost
|
||||
|
||||
## 2.0.0-beta.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1476a577]
|
||||
- blitz@2.0.0-beta.11
|
||||
|
||||
## 2.0.0-beta.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9db6c885]
|
||||
- Updated dependencies [d98e4bac]
|
||||
- Updated dependencies [9fe0cc54]
|
||||
- Updated dependencies [af58e2b2]
|
||||
- Updated dependencies [2ade7268]
|
||||
- Updated dependencies [0edeaa37]
|
||||
- Updated dependencies [430f6ec7]
|
||||
- Updated dependencies [15d22af2]
|
||||
- Updated dependencies [aa34661f]
|
||||
- Updated dependencies [8e0c9d76]
|
||||
- Updated dependencies [e2c18895]
|
||||
- blitz@2.0.0-beta.5
|
||||
@@ -1,12 +0,0 @@
|
||||
## ghost
|
||||
|
||||
Use Ghost in blitz
|
||||
|
||||
## more information
|
||||
|
||||
- [Ghost](https://ghost.org/)
|
||||
- [Github](https://github.com/TryGhost/Ghost)
|
||||
|
||||
## contributors
|
||||
|
||||
- Mark Hughes <m@rkhugh.es>
|
||||
@@ -1,95 +0,0 @@
|
||||
import {addBlitzMiddleware, addImport, paths, RecipeBuilder} from "blitz/installer"
|
||||
import j from "jscodeshift"
|
||||
import path from "path"
|
||||
|
||||
export default RecipeBuilder()
|
||||
.setName("Ghost")
|
||||
.setDescription("Access your Ghost CMS directly in blitz via the Ghost SDK.")
|
||||
.setOwner("Mark Hughes <m@rkhugh.es>")
|
||||
.setRepoLink("https://github.com/blitz-js/blitz/")
|
||||
.addAddDependenciesStep({
|
||||
stepId: "addDeps",
|
||||
stepName: "Add npm dependencies",
|
||||
explanation: `@tryghost/content-api needs to be installed`,
|
||||
packages: [
|
||||
{name: "@tryghost/content-api", version: "1.x"},
|
||||
{name: "@types/tryghost__content-api", version: "1.x", isDevDep: true},
|
||||
],
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "ghostEnv",
|
||||
stepName: 'Add default environment variables to blitz"',
|
||||
explanation: "Add your real variables into your .env.local file.",
|
||||
singleFileSearch: ".env",
|
||||
transformPlain(env: string) {
|
||||
return (
|
||||
env +
|
||||
"\n# Ghost environment variables, add your real variables into your .env.local file\n" +
|
||||
"GHOST_URL=" +
|
||||
"\n" +
|
||||
"GHOST_KEY=" +
|
||||
"\n"
|
||||
)
|
||||
},
|
||||
})
|
||||
.addNewFilesStep({
|
||||
stepId: "addIntegration",
|
||||
stepName: "Add ghost integration file",
|
||||
explanation: "Add integration file for ghost.",
|
||||
targetDirectory: "integrations",
|
||||
templatePath: path.join(__dirname, "templates", "integrations"),
|
||||
templateValues: {},
|
||||
})
|
||||
.addNewFilesStep({
|
||||
stepId: "addDefaultFiles",
|
||||
stepName: "Add default files",
|
||||
explanation: "Create default files to show usage of ghost in blitz.",
|
||||
targetDirectory: "app",
|
||||
templatePath: path.join(__dirname, "templates", "app"),
|
||||
templateValues: {},
|
||||
})
|
||||
.addNewFilesStep({
|
||||
stepId: "addDefaultPages",
|
||||
stepName: "Add default pages",
|
||||
explanation: "Create default pages to show usage of ghost in blitz.",
|
||||
targetDirectory: "pages",
|
||||
templatePath: path.join(__dirname, "templates", "pages"),
|
||||
templateValues: {},
|
||||
})
|
||||
.addTransformFilesStep({
|
||||
stepId: "ghostMiddleware",
|
||||
stepName: "Add default middleware to expose ghost",
|
||||
explanation: "Adds ghostapi to middleware so we can expose it in queries and mutations.",
|
||||
singleFileSearch: paths.blitzServer(),
|
||||
transform(program) {
|
||||
// // import ghostApi from integrations/ghost
|
||||
const cssBaselineImport = j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier("ghostApi"))],
|
||||
j.literal("integrations/ghost"),
|
||||
)
|
||||
|
||||
addImport(program, cssBaselineImport)
|
||||
|
||||
// This is the middleware we want to add
|
||||
const ghostApiMiddleware = j.arrowFunctionExpression(
|
||||
[j.identifier("_"), j.identifier("res"), j.identifier("next")],
|
||||
j.blockStatement([
|
||||
j.expressionStatement(
|
||||
j.assignmentExpression(
|
||||
"=",
|
||||
j.memberExpression(j.identifier("res"), j.identifier("blitzCtx.ghostApi")),
|
||||
j.identifier("ghostApi"),
|
||||
),
|
||||
),
|
||||
j.returnStatement(j.callExpression(j.identifier("next"), [])),
|
||||
]),
|
||||
)
|
||||
|
||||
// Add our middleware
|
||||
addBlitzMiddleware(program, ghostApiMiddleware)
|
||||
|
||||
// and.. return!
|
||||
return program
|
||||
},
|
||||
})
|
||||
.build()
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@blitzjs/recipe-ghost",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.11",
|
||||
"description": "Use Ghost in blitz",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/blitz-js/blitz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blitz",
|
||||
"blitzjs",
|
||||
"ghost"
|
||||
],
|
||||
"author": "Mark Hughes <m@rkhugh.es>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/blitz-js/blitz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"dependencies": {
|
||||
"blitz": "2.2.0",
|
||||
"jscodeshift": "0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jscodeshift": "0.11.2"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import {PostsOrPages} from "@tryghost/content-api"
|
||||
import {useQuery} from "@blitzjs/rpc"
|
||||
import getPosts from "../queries/getPosts"
|
||||
|
||||
const usePostsPage = (page: number, limit: number): [PostsOrPages, boolean] => {
|
||||
const [posts] = useQuery(
|
||||
getPosts,
|
||||
{
|
||||
page,
|
||||
limit,
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
)
|
||||
|
||||
const [nextPosts] = useQuery(
|
||||
getPosts,
|
||||
{
|
||||
page: page + 1,
|
||||
limit,
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
)
|
||||
|
||||
const hasNext = nextPosts.length > 0
|
||||
|
||||
return [posts, hasNext]
|
||||
}
|
||||
|
||||
export default usePostsPage
|
||||
@@ -1,101 +0,0 @@
|
||||
import {ReactNode, Suspense} from "react"
|
||||
import Head from "next/head"
|
||||
import {useQuery} from "@blitzjs/rpc"
|
||||
import getSettings from "../queries/getSettings"
|
||||
|
||||
type LayoutProps = {
|
||||
title?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/*
|
||||
* This layout is supplied to show how to make use of SEO options provided by Ghost.
|
||||
*/
|
||||
const GhostLayout = ({title, children}: LayoutProps) => {
|
||||
// NOTE: If you want to use settings from ghost, move this to getInitialProps for better SEO
|
||||
const [settings] = useQuery(getSettings, {}, {suspense: false})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{settings?.title} - {title || "my ghost blog"}
|
||||
</title>
|
||||
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={settings?.og_title ?? ""} />
|
||||
<meta property="og:description" content={settings?.og_description ?? ""} />
|
||||
<meta property="og:image" content={settings?.og_title ?? ""} />
|
||||
|
||||
<meta property="article:publisher" content={settings?.facebook ?? ""} />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={settings?.twitter_title ?? ""} />
|
||||
<meta name="twitter:description" content={settings?.twitter_description ?? ""} />
|
||||
<meta name="twitter:url" content={settings?.url ?? ""} />
|
||||
<meta name="twitter:image" content={settings?.twitter_image ?? ""} />
|
||||
<meta name="twitter:site" content={settings?.twitter ?? ""} />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: settings?.og_title,
|
||||
url: settings?.url,
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: settings?.og_title,
|
||||
},
|
||||
},
|
||||
url: settings?.url,
|
||||
mainEntityOfPage: {
|
||||
"@type": "WebPage",
|
||||
"@id": settings?.url,
|
||||
},
|
||||
description: settings?.og_description,
|
||||
})}
|
||||
</script>
|
||||
</Head>
|
||||
|
||||
<h1>
|
||||
{settings?.title} - {title || "blitz-ghost"}
|
||||
</h1>
|
||||
|
||||
<br />
|
||||
|
||||
<Suspense fallback={() => <>Loading...</>}>{children}</Suspense>
|
||||
|
||||
<style jsx global>{`
|
||||
@import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;700&display=swap");
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Libre Franklin", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
.image-card {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.image-card img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GhostLayout
|
||||
@@ -1,21 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
const GetAuthorById = z.object({
|
||||
id: z.string(),
|
||||
options: BrowseParams.optional(),
|
||||
})
|
||||
|
||||
export default async function getAuthorById(
|
||||
{id, options}: z.infer<typeof GetAuthorById>,
|
||||
ctx: Ctx,
|
||||
) {
|
||||
const author = await ctx.ghost.authors.read(
|
||||
{
|
||||
id,
|
||||
},
|
||||
options,
|
||||
)
|
||||
return author
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
const GetAuthorBySlug = z.object({
|
||||
slug: z.string(),
|
||||
options: BrowseParams.optional(),
|
||||
})
|
||||
|
||||
export default async function getAuthorBySlug(
|
||||
{slug, options}: z.infer<typeof GetAuthorBySlug>,
|
||||
ctx: Ctx,
|
||||
) {
|
||||
const author = await ctx.ghost.authors.read(
|
||||
{
|
||||
slug,
|
||||
},
|
||||
options,
|
||||
)
|
||||
return author
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
export default async function getAuthors(params: z.infer<typeof BrowseParams>, ctx: Ctx) {
|
||||
const authors = await ctx.ghost.posts.browse(params)
|
||||
return authors
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
const GetPageById = z.object({
|
||||
id: z.string(),
|
||||
options: BrowseParams.optional(),
|
||||
})
|
||||
|
||||
export default async function getPageById({id, options}: z.infer<typeof GetPageById>, ctx: Ctx) {
|
||||
const page = await ctx.ghost.pages.read(
|
||||
{
|
||||
id,
|
||||
},
|
||||
options,
|
||||
)
|
||||
return page
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
const GetPageBySlug = z.object({
|
||||
slug: z.string(),
|
||||
options: BrowseParams.optional(),
|
||||
})
|
||||
|
||||
export default async function getPageBySlug(
|
||||
{slug, options}: z.infer<typeof GetPageBySlug>,
|
||||
ctx: Ctx,
|
||||
) {
|
||||
const page = await ctx.ghost.pages.read(
|
||||
{
|
||||
slug,
|
||||
},
|
||||
options,
|
||||
)
|
||||
return page
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
const GetPostById = z.object({
|
||||
id: z.string(),
|
||||
options: BrowseParams.optional(),
|
||||
})
|
||||
|
||||
export default async function getPostById({id, options}: z.infer<typeof GetPostById>, ctx: Ctx) {
|
||||
const post = await ctx.ghost.posts.read(
|
||||
{
|
||||
id,
|
||||
},
|
||||
options,
|
||||
)
|
||||
return post
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
const GetPostBySlug = z.object({
|
||||
slug: z.string(),
|
||||
options: BrowseParams.optional(),
|
||||
})
|
||||
|
||||
export default async function getPostBySlug(
|
||||
{slug, options}: z.infer<typeof GetPostBySlug>,
|
||||
ctx: Ctx,
|
||||
) {
|
||||
const post = await ctx.ghost.posts.read(
|
||||
{
|
||||
slug,
|
||||
},
|
||||
options,
|
||||
)
|
||||
return post
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
export default async function getPosts(options: z.infer<typeof BrowseParams>, ctx: Ctx) {
|
||||
const posts = await ctx.ghost.posts.browse(options)
|
||||
return posts
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
export default async function getSettings(options: z.infer<typeof BrowseParams>, ctx: Ctx) {
|
||||
const settings = await ctx.ghost.settings.browse(options)
|
||||
return settings
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
const GetTagById = z.object({
|
||||
id: z.string(),
|
||||
options: BrowseParams.optional(),
|
||||
})
|
||||
|
||||
export default async function getTagById({id, options}: z.infer<typeof GetTagById>, ctx: Ctx) {
|
||||
const tag = await ctx.ghost.tags.read(
|
||||
{
|
||||
id,
|
||||
},
|
||||
options,
|
||||
)
|
||||
return tag
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
const GetTagBySlug = z.object({
|
||||
slug: z.string(),
|
||||
options: BrowseParams.optional(),
|
||||
})
|
||||
|
||||
export default async function getTagBySlug(
|
||||
{slug, options}: z.infer<typeof GetTagBySlug>,
|
||||
ctx: Ctx,
|
||||
) {
|
||||
const tag = await ctx.ghost.tags.read(
|
||||
{
|
||||
slug,
|
||||
},
|
||||
options,
|
||||
)
|
||||
return tag
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import {Ctx} from "blitz"
|
||||
import * as z from "zod"
|
||||
import {BrowseParams} from "../validations"
|
||||
|
||||
export default async function getTags(options: z.infer<typeof BrowseParams>, ctx: Ctx) {
|
||||
const tags = await ctx.ghost.tags.browse(options)
|
||||
return tags
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as z from "zod"
|
||||
|
||||
const IncludeParam = z.enum(["authors", "tags", "count.posts"])
|
||||
const FieldsParam = z.enum(["authors", "tags", "count.posts"])
|
||||
const FilterParam = z.string()
|
||||
const LimitParam = z.number()
|
||||
const PageParam = z.number()
|
||||
const OrderParam = z.string()
|
||||
|
||||
export const BrowseParams = z.object({
|
||||
include: z.union([IncludeParam, z.array(IncludeParam)]).optional(),
|
||||
fields: z.union([FieldsParam, z.array(FieldsParam)]).optional(),
|
||||
format: z.union([IncludeParam, z.array(IncludeParam)]).optional(),
|
||||
filter: z.union([FilterParam, z.array(FilterParam)]).optional(),
|
||||
limit: z.union([LimitParam, z.array(LimitParam)]).optional(),
|
||||
page: z.union([PageParam, z.array(PageParam)]).optional(),
|
||||
order: z.union([OrderParam, z.array(OrderParam)]).optional(),
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import GhostContentAPI from "@tryghost/content-api"
|
||||
|
||||
export const ghostApi = new GhostContentAPI({
|
||||
url: process.env.GHOST_URL as string,
|
||||
version: "v3",
|
||||
key: process.env.GHOST_KEY as string,
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
import {BlitzPage, Routes} from "@blitzjs/next"
|
||||
import Link from "next/link"
|
||||
import GhostLayout from "app/ghost/layouts/GhostLayout"
|
||||
import usePostsPage from "app/ghost/hooks/usePostsPage"
|
||||
|
||||
/*
|
||||
* This file is just for a very basic demonstration of using ghost with blitz.
|
||||
*/
|
||||
const GhostIndex: BlitzPage = () => {
|
||||
const [posts] = usePostsPage(1, 5)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{posts.map(
|
||||
({id, slug, feature_image: featureImage, title, custom_excerpt = undefined, excerpt}) => (
|
||||
<div key={id}>
|
||||
{featureImage && (
|
||||
<div className="image-card">
|
||||
<Link prefetch={true} href={Routes.GhostPostPage({slug})}>
|
||||
<img loading="lazy" src={featureImage} />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<b>{title}</b>
|
||||
<p>{custom_excerpt ?? excerpt}</p>
|
||||
<Link prefetch={true} href={Routes.GhostPostPage({slug})}>
|
||||
Read More...
|
||||
</Link>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
<footer>
|
||||
<a
|
||||
href="https://blitzjs.com?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by <a href="https://blitzjs.com/">Blitz.js</a> &{" "}
|
||||
<a href="https://github.com/tryghost/ghost">Ghost</a>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
GhostIndex.suppressFirstRenderFlicker = true
|
||||
GhostIndex.getLayout = (page) => <GhostLayout title="Home">{page}</GhostLayout>
|
||||
|
||||
export default GhostIndex
|
||||
@@ -1,37 +0,0 @@
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import {useMutation, useQuery} from "@blitzjs/rpc"
|
||||
import {Routes, BlitzPage} from "@blitzjs/next"
|
||||
import {useRouter} from "next/router"
|
||||
import GhostLayout from "app/ghost/layouts/GhostLayout"
|
||||
import getPostBySlug from "app/ghost/queries/getPostBySlug"
|
||||
|
||||
/*
|
||||
* This file is just for a very basic demonstration of using ghost with blitz.
|
||||
*/
|
||||
const GhostPostPage: BlitzPage = (args) => {
|
||||
const router = useRouter()
|
||||
|
||||
const {slug} = router.params
|
||||
|
||||
const [post] = useQuery(getPostBySlug, {slug})
|
||||
return (
|
||||
<div>
|
||||
{post.feature_image && (
|
||||
<img src={post.feature_image} style={{maxWidth: "90%", maxHeight: "400px"}} />
|
||||
)}
|
||||
|
||||
<h1>{post.title}</h1>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: post?.html ?? "",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
GhostPostPage.suppressFirstRenderFlicker = true
|
||||
GhostPostPage.getLayout = (page) => <GhostLayout title="Read">{page}</GhostLayout>
|
||||
|
||||
export default GhostPostPage
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user