committed by
GitHub
parent
d05f00c0a8
commit
5f1c6a4571
28
.github/workflows/main.yml
vendored
28
.github/workflows/main.yml
vendored
@@ -110,6 +110,33 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
|
||||||
|
testNextPackages:
|
||||||
|
name: Next - Test Packages
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: nextjs
|
||||||
|
needs: build-linux
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
BLITZ_TELEMETRY_DISABLED: 1
|
||||||
|
steps:
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
id: restore-build
|
||||||
|
with:
|
||||||
|
path: ./*
|
||||||
|
key: ${{ runner.os }}-${{ github.sha }}
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "14"
|
||||||
|
- name: Setup kernel to increase watchers
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
|
||||||
|
- name: Test Next Packages
|
||||||
|
run: yarn testonly:packages
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
testBlitzExamples:
|
testBlitzExamples:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
name: Blitz - Test Example Apps (ubuntu-latest)
|
name: Blitz - Test Example Apps (ubuntu-latest)
|
||||||
@@ -378,6 +405,7 @@ jobs:
|
|||||||
testIntegrationBlitzWin,
|
testIntegrationBlitzWin,
|
||||||
testUnit,
|
testUnit,
|
||||||
testBlitzPackages,
|
testBlitzPackages,
|
||||||
|
testNextPackages,
|
||||||
testBlitzExamples,
|
testBlitzExamples,
|
||||||
testBlitzExamplesWin,
|
testBlitzExamplesWin,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"dev": "lerna run dev --stream --parallel",
|
"dev": "lerna run dev --stream --parallel",
|
||||||
"dev2": "while true; do yarn --check-files && yarn dev; done",
|
"dev2": "while true; do yarn --check-files && yarn dev; done",
|
||||||
"testonly": "jest --runInBand",
|
"testonly": "jest --runInBand",
|
||||||
|
"testonly:packages": "ultra -r --filter \"packages/*\" --concurrency 15 test",
|
||||||
"testheadless": "cross-env HEADLESS=true yarn testonly",
|
"testheadless": "cross-env HEADLESS=true yarn testonly",
|
||||||
"testsafari": "cross-env BROWSER_NAME=safari yarn testonly",
|
"testsafari": "cross-env BROWSER_NAME=safari yarn testonly",
|
||||||
"testfirefox": "cross-env BROWSER_NAME=firefox yarn testonly",
|
"testfirefox": "cross-env BROWSER_NAME=firefox yarn testonly",
|
||||||
@@ -145,6 +146,7 @@
|
|||||||
"taskr": "1.1.0",
|
"taskr": "1.1.0",
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"typescript": "4.5.2",
|
"typescript": "4.5.2",
|
||||||
|
"ultra-runner": "3.10.5",
|
||||||
"wait-port": "0.2.2",
|
"wait-port": "0.2.2",
|
||||||
"web-streams-polyfill": "2.1.1",
|
"web-streams-polyfill": "2.1.1",
|
||||||
"webpack-bundle-analyzer": "4.3.0",
|
"webpack-bundle-analyzer": "4.3.0",
|
||||||
|
|||||||
3
nextjs/packages/installer/jest.config.js
Normal file
3
nextjs/packages/installer/jest.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: '../../../jest-unit.config.js',
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
6
nextjs/packages/installer/src/components/newline.tsx
Normal file
6
nextjs/packages/installer/src/components/newline.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Box } from 'ink'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export const Newline: React.FC<{ count?: number }> = ({ count = 1 }) => {
|
||||||
|
return <Box paddingBottom={count} />
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
import {spawn} from "cross-spawn"
|
import { spawn } from 'cross-spawn'
|
||||||
import * as fs from "fs-extra"
|
import * as fs from 'fs-extra'
|
||||||
import {Box, Text} from "ink"
|
import { Box, Text } from 'ink'
|
||||||
import Spinner from "ink-spinner"
|
import Spinner from 'ink-spinner'
|
||||||
import * as path from "path"
|
import * as path from 'path'
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import {Newline} from "../components/newline"
|
import { Newline } from '../components/newline'
|
||||||
import {RecipeCLIArgs} from "../types"
|
import { RecipeCLIArgs } from '../types'
|
||||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
import { useEnterToContinue } from '../utils/use-enter-to-continue'
|
||||||
import {useUserInput} from "../utils/use-user-input"
|
import { useUserInput } from '../utils/use-user-input'
|
||||||
import {Executor, executorArgument, ExecutorConfig, getExecutorArgument} from "./executor"
|
import {
|
||||||
|
Executor,
|
||||||
|
executorArgument,
|
||||||
|
ExecutorConfig,
|
||||||
|
getExecutorArgument,
|
||||||
|
} from './executor'
|
||||||
|
|
||||||
interface NpmPackage {
|
interface NpmPackage {
|
||||||
name: string
|
name: string
|
||||||
@@ -22,24 +27,26 @@ export interface Config extends ExecutorConfig {
|
|||||||
packages: executorArgument<NpmPackage[]>
|
packages: executorArgument<NpmPackage[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAddDependencyExecutor(executor: ExecutorConfig): executor is Config {
|
export function isAddDependencyExecutor(
|
||||||
|
executor: ExecutorConfig
|
||||||
|
): executor is Config {
|
||||||
return (executor as Config).packages !== undefined
|
return (executor as Config).packages !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const type = "add-dependency"
|
export const type = 'add-dependency'
|
||||||
|
|
||||||
function Package({pkg, loading}: {pkg: NpmPackage; loading: boolean}) {
|
function Package({ pkg, loading }: { pkg: NpmPackage; loading: boolean }) {
|
||||||
return (
|
return (
|
||||||
<Text>
|
<Text>
|
||||||
{` `}
|
{` `}
|
||||||
{loading ? <Spinner /> : "📦"}
|
{loading ? <Spinner /> : '📦'}
|
||||||
{` ${pkg.name}@${pkg.version}`}
|
{` ${pkg.name}@${pkg.version}`}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DependencyList = ({
|
const DependencyList = ({
|
||||||
lede = "Hang tight! Installing dependencies...",
|
lede = 'Hang tight! Installing dependencies...',
|
||||||
depsLoading = false,
|
depsLoading = false,
|
||||||
devDepsLoading = false,
|
devDepsLoading = false,
|
||||||
packages,
|
packages,
|
||||||
@@ -60,7 +67,9 @@ const DependencyList = ({
|
|||||||
<Package key={pkg.name} pkg={pkg} loading={depsLoading} />
|
<Package key={pkg.name} pkg={pkg} loading={depsLoading} />
|
||||||
))}
|
))}
|
||||||
<Newline />
|
<Newline />
|
||||||
{devPackages.length ? <Text>Dev Dependencies to be installed:</Text> : null}
|
{devPackages.length ? (
|
||||||
|
<Text>Dev Dependencies to be installed:</Text>
|
||||||
|
) : null}
|
||||||
{devPackages.map((pkg) => (
|
{devPackages.map((pkg) => (
|
||||||
<Package key={pkg.name} pkg={pkg} loading={devDepsLoading} />
|
<Package key={pkg.name} pkg={pkg} loading={devDepsLoading} />
|
||||||
))}
|
))}
|
||||||
@@ -72,10 +81,10 @@ const DependencyList = ({
|
|||||||
* Exported for unit testing purposes
|
* Exported for unit testing purposes
|
||||||
*/
|
*/
|
||||||
export function getPackageManager() {
|
export function getPackageManager() {
|
||||||
if (fs.existsSync(path.resolve("yarn.lock"))) {
|
if (fs.existsSync(path.resolve('yarn.lock'))) {
|
||||||
return "yarn"
|
return 'yarn'
|
||||||
}
|
}
|
||||||
return "npm"
|
return 'npm'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,40 +92,46 @@ export function getPackageManager() {
|
|||||||
*/
|
*/
|
||||||
export async function installPackages(packages: NpmPackage[], isDev = false) {
|
export async function installPackages(packages: NpmPackage[], isDev = false) {
|
||||||
const packageManager = getPackageManager()
|
const packageManager = getPackageManager()
|
||||||
const isNPM = packageManager === "npm"
|
const isNPM = packageManager === 'npm'
|
||||||
const pkgInstallArg = isNPM ? "install" : "add"
|
const pkgInstallArg = isNPM ? 'install' : 'add'
|
||||||
const args: string[] = [pkgInstallArg]
|
const args: string[] = [pkgInstallArg]
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
args.push(isNPM ? "--save-dev" : "-D")
|
args.push(isNPM ? '--save-dev' : '-D')
|
||||||
}
|
}
|
||||||
packages.forEach((pkg) => {
|
packages.forEach((pkg) => {
|
||||||
pkg.version ? args.push(`${pkg.name}@${pkg.version}`) : args.push(pkg.name)
|
pkg.version ? args.push(`${pkg.name}@${pkg.version}`) : args.push(pkg.name)
|
||||||
})
|
})
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
const cp = spawn(packageManager, args, {
|
const cp = spawn(packageManager, args, {
|
||||||
stdio: ["inherit", "pipe", "pipe"],
|
stdio: ['inherit', 'pipe', 'pipe'],
|
||||||
})
|
})
|
||||||
cp.on("exit", resolve)
|
cp.on('exit', resolve)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Commit: Executor["Commit"] = ({cliArgs, cliFlags, step, onChangeCommitted}) => {
|
export const Commit: Executor['Commit'] = ({
|
||||||
|
cliArgs,
|
||||||
|
cliFlags,
|
||||||
|
step,
|
||||||
|
onChangeCommitted,
|
||||||
|
}) => {
|
||||||
const userInput = useUserInput(cliFlags)
|
const userInput = useUserInput(cliFlags)
|
||||||
const [depsInstalled, setDepsInstalled] = React.useState(false)
|
const [depsInstalled, setDepsInstalled] = React.useState(false)
|
||||||
const [devDepsInstalled, setDevDepsInstalled] = React.useState(false)
|
const [devDepsInstalled, setDevDepsInstalled] = React.useState(false)
|
||||||
|
|
||||||
const handleChangeCommitted = React.useCallback(() => {
|
const handleChangeCommitted = React.useCallback(() => {
|
||||||
const packages = (step as Config).packages
|
const packages = (step as Config).packages
|
||||||
const dependencies = packages.length === 1 ? "dependency" : "dependencies"
|
const dependencies = packages.length === 1 ? 'dependency' : 'dependencies'
|
||||||
onChangeCommitted(`Installed ${packages.length} ${dependencies}`)
|
onChangeCommitted(`Installed ${packages.length} ${dependencies}`)
|
||||||
}, [onChangeCommitted, step])
|
}, [onChangeCommitted, step])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function installDeps() {
|
async function installDeps() {
|
||||||
const packagesToInstall = getExecutorArgument((step as Config).packages, cliArgs).filter(
|
const packagesToInstall = getExecutorArgument(
|
||||||
(p) => !p.isDevDep,
|
(step as Config).packages,
|
||||||
)
|
cliArgs
|
||||||
|
).filter((p) => !p.isDevDep)
|
||||||
await installPackages(packagesToInstall)
|
await installPackages(packagesToInstall)
|
||||||
setDepsInstalled(true)
|
setDepsInstalled(true)
|
||||||
}
|
}
|
||||||
@@ -127,9 +142,10 @@ export const Commit: Executor["Commit"] = ({cliArgs, cliFlags, step, onChangeCom
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!depsInstalled) return
|
if (!depsInstalled) return
|
||||||
async function installDevDeps() {
|
async function installDevDeps() {
|
||||||
const packagesToInstall = getExecutorArgument((step as Config).packages, cliArgs).filter(
|
const packagesToInstall = getExecutorArgument(
|
||||||
(p) => p.isDevDep,
|
(step as Config).packages,
|
||||||
)
|
cliArgs
|
||||||
|
).filter((p) => p.isDevDep)
|
||||||
await installPackages(packagesToInstall, true)
|
await installPackages(packagesToInstall, true)
|
||||||
setDevDepsInstalled(true)
|
setDevDepsInstalled(true)
|
||||||
}
|
}
|
||||||
@@ -186,7 +202,12 @@ const CommitWithInput = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommitWithoutInput = ({depsInstalled, devDepsInstalled, step, cliArgs}: CommitChildProps) => (
|
const CommitWithoutInput = ({
|
||||||
|
depsInstalled,
|
||||||
|
devDepsInstalled,
|
||||||
|
step,
|
||||||
|
cliArgs,
|
||||||
|
}: CommitChildProps) => (
|
||||||
<DependencyList
|
<DependencyList
|
||||||
depsLoading={!depsInstalled}
|
depsLoading={!depsInstalled}
|
||||||
devDepsLoading={!devDepsInstalled}
|
devDepsLoading={!devDepsInstalled}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Box, Text} from "ink"
|
import { Box, Text } from 'ink'
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import {Newline} from "../components/newline"
|
import { Newline } from '../components/newline'
|
||||||
import {RecipeCLIArgs, RecipeCLIFlags} from "../types"
|
import { RecipeCLIArgs, RecipeCLIFlags } from '../types'
|
||||||
|
|
||||||
export interface ExecutorConfig {
|
export interface ExecutorConfig {
|
||||||
successIcon?: string
|
successIcon?: string
|
||||||
@@ -32,16 +32,16 @@ export interface Executor {
|
|||||||
type dynamicExecutorArgument<T> = (cliArgs: RecipeCLIArgs) => T
|
type dynamicExecutorArgument<T> = (cliArgs: RecipeCLIArgs) => T
|
||||||
|
|
||||||
function isDynamicExecutorArgument<T>(
|
function isDynamicExecutorArgument<T>(
|
||||||
input: executorArgument<T>,
|
input: executorArgument<T>
|
||||||
): input is dynamicExecutorArgument<T> {
|
): input is dynamicExecutorArgument<T> {
|
||||||
return typeof (input as dynamicExecutorArgument<T>) === "function"
|
return typeof (input as dynamicExecutorArgument<T>) === 'function'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type executorArgument<T> = T | dynamicExecutorArgument<T>
|
export type executorArgument<T> = T | dynamicExecutorArgument<T>
|
||||||
|
|
||||||
export function Frontmatter({executor}: {executor: ExecutorConfig}) {
|
export function Frontmatter({ executor }: { executor: ExecutorConfig }) {
|
||||||
const lineLength = executor.stepName.length + 6
|
const lineLength = executor.stepName.length + 6
|
||||||
const verticalBorder = `+${new Array(lineLength).fill("–").join("")}+`
|
const verticalBorder = `+${new Array(lineLength).fill('–').join('')}+`
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" paddingBottom={1}>
|
<Box flexDirection="column" paddingBottom={1}>
|
||||||
<Newline />
|
<Newline />
|
||||||
@@ -63,7 +63,10 @@ export function Frontmatter({executor}: {executor: ExecutorConfig}) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExecutorArgument<T>(input: executorArgument<T>, cliArgs: RecipeCLIArgs): T {
|
export function getExecutorArgument<T>(
|
||||||
|
input: executorArgument<T>,
|
||||||
|
cliArgs: RecipeCLIArgs
|
||||||
|
): T {
|
||||||
if (isDynamicExecutorArgument(input)) {
|
if (isDynamicExecutorArgument(input)) {
|
||||||
return input(cliArgs)
|
return input(cliArgs)
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {prompt as enquirer} from "enquirer"
|
import { prompt as enquirer } from 'enquirer'
|
||||||
import globby from "globby"
|
import globby from 'globby'
|
||||||
|
|
||||||
enum SearchType {
|
enum SearchType {
|
||||||
file,
|
file,
|
||||||
@@ -13,8 +13,8 @@ interface FilePromptOptions {
|
|||||||
context: any
|
context: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMatchingFiles(filter: string = ""): Promise<string[]> {
|
function getMatchingFiles(filter: string = ''): Promise<string[]> {
|
||||||
return globby(filter, {expandDirectories: true})
|
return globby(filter, { expandDirectories: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function filePrompt(options: FilePromptOptions): Promise<string> {
|
export async function filePrompt(options: FilePromptOptions): Promise<string> {
|
||||||
@@ -24,10 +24,10 @@ export async function filePrompt(options: FilePromptOptions): Promise<string> {
|
|||||||
if (choices.length === 1) {
|
if (choices.length === 1) {
|
||||||
return choices[0]
|
return choices[0]
|
||||||
}
|
}
|
||||||
const results: {file: string} = await enquirer({
|
const results: { file: string } = await enquirer({
|
||||||
type: "autocomplete",
|
type: 'autocomplete',
|
||||||
name: "file",
|
name: 'file',
|
||||||
message: "Select the target file",
|
message: 'Select the target file',
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
limit: 10,
|
limit: 10,
|
||||||
choices,
|
choices,
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import {createPatch} from "diff"
|
import { createPatch } from 'diff'
|
||||||
import * as fs from "fs-extra"
|
import * as fs from 'fs-extra'
|
||||||
import {Box, Text} from "ink"
|
import { Box, Text } from 'ink'
|
||||||
import Spinner from "ink-spinner"
|
import Spinner from 'ink-spinner'
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import {EnterToContinue} from "../components/enter-to-continue"
|
import { EnterToContinue } from '../components/enter-to-continue'
|
||||||
import {RecipeCLIArgs} from "../types"
|
import { RecipeCLIArgs } from '../types'
|
||||||
import {
|
import {
|
||||||
processFile,
|
processFile,
|
||||||
stringProcessFile,
|
stringProcessFile,
|
||||||
@@ -12,11 +12,16 @@ import {
|
|||||||
transform,
|
transform,
|
||||||
Transformer,
|
Transformer,
|
||||||
TransformStatus,
|
TransformStatus,
|
||||||
} from "../utils/transform"
|
} from '../utils/transform'
|
||||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
import { useEnterToContinue } from '../utils/use-enter-to-continue'
|
||||||
import {useUserInput} from "../utils/use-user-input"
|
import { useUserInput } from '../utils/use-user-input'
|
||||||
import {Executor, executorArgument, ExecutorConfig, getExecutorArgument} from "./executor"
|
import {
|
||||||
import {filePrompt} from "./file-prompt"
|
Executor,
|
||||||
|
executorArgument,
|
||||||
|
ExecutorConfig,
|
||||||
|
getExecutorArgument,
|
||||||
|
} from './executor'
|
||||||
|
import { filePrompt } from './file-prompt'
|
||||||
|
|
||||||
export interface Config extends ExecutorConfig {
|
export interface Config extends ExecutorConfig {
|
||||||
selectTargetFiles?(cliArgs: RecipeCLIArgs): any[]
|
selectTargetFiles?(cliArgs: RecipeCLIArgs): any[]
|
||||||
@@ -25,19 +30,26 @@ export interface Config extends ExecutorConfig {
|
|||||||
transformPlain?: StringTransformer
|
transformPlain?: StringTransformer
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFileTransformExecutor(executor: ExecutorConfig): executor is Config {
|
export function isFileTransformExecutor(
|
||||||
|
executor: ExecutorConfig
|
||||||
|
): executor is Config {
|
||||||
return (
|
return (
|
||||||
(executor as Config).transform !== undefined ||
|
(executor as Config).transform !== undefined ||
|
||||||
(executor as Config).transformPlain !== undefined
|
(executor as Config).transformPlain !== undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const type = "file-transform"
|
export const type = 'file-transform'
|
||||||
export const Propose: Executor["Propose"] = ({cliArgs, cliFlags, onProposalAccepted, step}) => {
|
export const Propose: Executor['Propose'] = ({
|
||||||
|
cliArgs,
|
||||||
|
cliFlags,
|
||||||
|
onProposalAccepted,
|
||||||
|
step,
|
||||||
|
}) => {
|
||||||
const userInput = useUserInput(cliFlags)
|
const userInput = useUserInput(cliFlags)
|
||||||
const [diff, setDiff] = React.useState<string | null>(null)
|
const [diff, setDiff] = React.useState<string | null>(null)
|
||||||
const [error, setError] = React.useState<Error | null>(null)
|
const [error, setError] = React.useState<Error | null>(null)
|
||||||
const [filePath, setFilePath] = React.useState("")
|
const [filePath, setFilePath] = React.useState('')
|
||||||
const [proposalAccepted, setProposalAccepted] = React.useState(false)
|
const [proposalAccepted, setProposalAccepted] = React.useState(false)
|
||||||
|
|
||||||
const acceptProposal = React.useCallback(() => {
|
const acceptProposal = React.useCallback(() => {
|
||||||
@@ -49,11 +61,14 @@ export const Propose: Executor["Propose"] = ({cliArgs, cliFlags, onProposalAccep
|
|||||||
async function generateDiff() {
|
async function generateDiff() {
|
||||||
const fileToTransform: string = await filePrompt({
|
const fileToTransform: string = await filePrompt({
|
||||||
context: cliArgs,
|
context: cliArgs,
|
||||||
globFilter: getExecutorArgument((step as Config).singleFileSearch, cliArgs),
|
globFilter: getExecutorArgument(
|
||||||
|
(step as Config).singleFileSearch,
|
||||||
|
cliArgs
|
||||||
|
),
|
||||||
getChoices: (step as Config).selectTargetFiles,
|
getChoices: (step as Config).selectTargetFiles,
|
||||||
})
|
})
|
||||||
setFilePath(fileToTransform)
|
setFilePath(fileToTransform)
|
||||||
const originalFile = fs.readFileSync(fileToTransform).toString("utf-8")
|
const originalFile = fs.readFileSync(fileToTransform).toString('utf-8')
|
||||||
const newFile = await ((step as Config).transformPlain
|
const newFile = await ((step as Config).transformPlain
|
||||||
? stringProcessFile(originalFile, (step as Config).transformPlain!)
|
? stringProcessFile(originalFile, (step as Config).transformPlain!)
|
||||||
: processFile(originalFile, (step as Config).transform!))
|
: processFile(originalFile, (step as Config).transform!))
|
||||||
@@ -96,19 +111,19 @@ interface ProposeChildProps {
|
|||||||
acceptProposal: () => void
|
acceptProposal: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Diff = ({diff}: {diff: string}) => (
|
const Diff = ({ diff }: { diff: string }) => (
|
||||||
<>
|
<>
|
||||||
{diff
|
{diff
|
||||||
.split("\n")
|
.split('\n')
|
||||||
.slice(2)
|
.slice(2)
|
||||||
.map((line, idx) => {
|
.map((line, idx) => {
|
||||||
let styleProps: any = {}
|
let styleProps: any = {}
|
||||||
if (line.startsWith("-") && !line.startsWith("---")) {
|
if (line.startsWith('-') && !line.startsWith('---')) {
|
||||||
styleProps.bold = true
|
styleProps.bold = true
|
||||||
styleProps.color = "red"
|
styleProps.color = 'red'
|
||||||
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
||||||
styleProps.bold = true
|
styleProps.bold = true
|
||||||
styleProps.color = "green"
|
styleProps.color = 'green'
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Text {...styleProps} key={idx}>
|
<Text {...styleProps} key={idx}>
|
||||||
@@ -125,7 +140,7 @@ const ProposeWithInput = ({
|
|||||||
proposalAccepted,
|
proposalAccepted,
|
||||||
acceptProposal,
|
acceptProposal,
|
||||||
}: ProposeChildProps) => {
|
}: ProposeChildProps) => {
|
||||||
useEnterToContinue(acceptProposal, filePath !== "" && !proposalAccepted)
|
useEnterToContinue(acceptProposal, filePath !== '' && !proposalAccepted)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
@@ -142,7 +157,7 @@ const ProposeWithoutInput = ({
|
|||||||
acceptProposal,
|
acceptProposal,
|
||||||
}: ProposeChildProps) => {
|
}: ProposeChildProps) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (filePath !== "" && !proposalAccepted) {
|
if (filePath !== '' && !proposalAccepted) {
|
||||||
acceptProposal()
|
acceptProposal()
|
||||||
}
|
}
|
||||||
}, [acceptProposal, filePath, proposalAccepted])
|
}, [acceptProposal, filePath, proposalAccepted])
|
||||||
@@ -154,7 +169,11 @@ const ProposeWithoutInput = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Commit: Executor["Commit"] = ({onChangeCommitted, proposalData: filePath, step}) => {
|
export const Commit: Executor['Commit'] = ({
|
||||||
|
onChangeCommitted,
|
||||||
|
proposalData: filePath,
|
||||||
|
step,
|
||||||
|
}) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void (async function () {
|
void (async function () {
|
||||||
const results = await transform(
|
const results = await transform(
|
||||||
@@ -162,7 +181,7 @@ export const Commit: Executor["Commit"] = ({onChangeCommitted, proposalData: fil
|
|||||||
await ((step as Config).transformPlain
|
await ((step as Config).transformPlain
|
||||||
? stringProcessFile(original, (step as Config).transformPlain!)
|
? stringProcessFile(original, (step as Config).transformPlain!)
|
||||||
: processFile(original, (step as Config).transform!)),
|
: processFile(original, (step as Config).transform!)),
|
||||||
[filePath],
|
[filePath]
|
||||||
)
|
)
|
||||||
if (results.some((r) => r.status === TransformStatus.Failure)) {
|
if (results.some((r) => r.status === TransformStatus.Failure)) {
|
||||||
console.error(results)
|
console.error(results)
|
||||||
@@ -1,24 +1,31 @@
|
|||||||
import {Generator, GeneratorOptions, SourceRootType} from "@blitzjs/generator"
|
import { Generator, GeneratorOptions, SourceRootType } from '@blitzjs/generator'
|
||||||
import {Box, Text} from "ink"
|
import { Box, Text } from 'ink'
|
||||||
import {useEffect, useState} from "react"
|
import { useEffect, useState } from 'react'
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import {EnterToContinue} from "../components/enter-to-continue"
|
import { EnterToContinue } from '../components/enter-to-continue'
|
||||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
import { useEnterToContinue } from '../utils/use-enter-to-continue'
|
||||||
import {useUserInput} from "../utils/use-user-input"
|
import { useUserInput } from '../utils/use-user-input'
|
||||||
import {Executor, executorArgument, ExecutorConfig, getExecutorArgument} from "./executor"
|
import {
|
||||||
|
Executor,
|
||||||
|
executorArgument,
|
||||||
|
ExecutorConfig,
|
||||||
|
getExecutorArgument,
|
||||||
|
} from './executor'
|
||||||
|
|
||||||
export interface Config extends ExecutorConfig {
|
export interface Config extends ExecutorConfig {
|
||||||
targetDirectory?: executorArgument<string>
|
targetDirectory?: executorArgument<string>
|
||||||
templatePath: executorArgument<string>
|
templatePath: executorArgument<string>
|
||||||
templateValues: executorArgument<{[key: string]: string}>
|
templateValues: executorArgument<{ [key: string]: string }>
|
||||||
destinationPathPrompt?: executorArgument<string>
|
destinationPathPrompt?: executorArgument<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isNewFileExecutor(executor: ExecutorConfig): executor is Config {
|
export function isNewFileExecutor(
|
||||||
|
executor: ExecutorConfig
|
||||||
|
): executor is Config {
|
||||||
return (executor as Config).templatePath !== undefined
|
return (executor as Config).templatePath !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const type = "new-file"
|
export const type = 'new-file'
|
||||||
|
|
||||||
interface TempGeneratorOptions extends GeneratorOptions {
|
interface TempGeneratorOptions extends GeneratorOptions {
|
||||||
targetDirectory?: string
|
targetDirectory?: string
|
||||||
@@ -34,9 +41,9 @@ class TempGenerator extends Generator<TempGeneratorOptions> {
|
|||||||
|
|
||||||
constructor(options: TempGeneratorOptions) {
|
constructor(options: TempGeneratorOptions) {
|
||||||
super(options)
|
super(options)
|
||||||
this.sourceRoot = {type: "absolute", path: options.templateRoot}
|
this.sourceRoot = { type: 'absolute', path: options.templateRoot }
|
||||||
this.templateValues = options.templateValues
|
this.templateValues = options.templateValues
|
||||||
this.targetDirectory = options.targetDirectory || "."
|
this.targetDirectory = options.targetDirectory || '.'
|
||||||
}
|
}
|
||||||
|
|
||||||
getTemplateValues() {
|
getTemplateValues() {
|
||||||
@@ -48,26 +55,37 @@ class TempGenerator extends Generator<TempGeneratorOptions> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Commit: Executor["Commit"] = ({cliArgs, cliFlags, onChangeCommitted, step}) => {
|
export const Commit: Executor['Commit'] = ({
|
||||||
|
cliArgs,
|
||||||
|
cliFlags,
|
||||||
|
onChangeCommitted,
|
||||||
|
step,
|
||||||
|
}) => {
|
||||||
const userInput = useUserInput(cliFlags)
|
const userInput = useUserInput(cliFlags)
|
||||||
const generatorArgs = React.useMemo(
|
const generatorArgs = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
destinationRoot: ".",
|
destinationRoot: '.',
|
||||||
targetDirectory: getExecutorArgument((step as Config).targetDirectory, cliArgs),
|
targetDirectory: getExecutorArgument(
|
||||||
|
(step as Config).targetDirectory,
|
||||||
|
cliArgs
|
||||||
|
),
|
||||||
templateRoot: getExecutorArgument((step as Config).templatePath, cliArgs),
|
templateRoot: getExecutorArgument((step as Config).templatePath, cliArgs),
|
||||||
templateValues: getExecutorArgument((step as Config).templateValues, cliArgs),
|
templateValues: getExecutorArgument(
|
||||||
|
(step as Config).templateValues,
|
||||||
|
cliArgs
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
[cliArgs, step],
|
[cliArgs, step]
|
||||||
)
|
)
|
||||||
const [fileCreateOutput, setFileCreateOutput] = useState("")
|
const [fileCreateOutput, setFileCreateOutput] = useState('')
|
||||||
const [changeCommited, setChangeCommited] = useState(false)
|
const [changeCommited, setChangeCommited] = useState(false)
|
||||||
const fileCreateLines = fileCreateOutput.split("\n")
|
const fileCreateLines = fileCreateOutput.split('\n')
|
||||||
const handleChangeCommitted = React.useCallback(() => {
|
const handleChangeCommitted = React.useCallback(() => {
|
||||||
setChangeCommited(true)
|
setChangeCommited(true)
|
||||||
onChangeCommitted(
|
onChangeCommitted(
|
||||||
`Successfully created ${fileCreateLines
|
`Successfully created ${fileCreateLines
|
||||||
.map((l) => l.split(" ").slice(1).join("").trim())
|
.map((l) => l.split(' ').slice(1).join('').trim())
|
||||||
.join(", ")}`,
|
.join(', ')}`
|
||||||
)
|
)
|
||||||
}, [fileCreateLines, onChangeCommitted])
|
}, [fileCreateLines, onChangeCommitted])
|
||||||
|
|
||||||
@@ -104,11 +122,14 @@ const CommitWithInput = ({
|
|||||||
fileCreateOutput,
|
fileCreateOutput,
|
||||||
handleChangeCommitted,
|
handleChangeCommitted,
|
||||||
}: CommitChildProps) => {
|
}: CommitChildProps) => {
|
||||||
useEnterToContinue(handleChangeCommitted, !changeCommited && fileCreateOutput !== "")
|
useEnterToContinue(
|
||||||
|
handleChangeCommitted,
|
||||||
|
!changeCommited && fileCreateOutput !== ''
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{fileCreateOutput !== "" && (
|
{fileCreateOutput !== '' && (
|
||||||
<>
|
<>
|
||||||
<Text>{fileCreateOutput}</Text>
|
<Text>{fileCreateOutput}</Text>
|
||||||
<EnterToContinue />
|
<EnterToContinue />
|
||||||
@@ -124,12 +145,14 @@ const CommitWithoutInput = ({
|
|||||||
handleChangeCommitted,
|
handleChangeCommitted,
|
||||||
}: CommitChildProps) => {
|
}: CommitChildProps) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!changeCommited && fileCreateOutput !== "") {
|
if (!changeCommited && fileCreateOutput !== '') {
|
||||||
handleChangeCommitted()
|
handleChangeCommitted()
|
||||||
}
|
}
|
||||||
}, [changeCommited, fileCreateOutput, handleChangeCommitted])
|
}, [changeCommited, fileCreateOutput, handleChangeCommitted])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">{fileCreateOutput !== "" && <Text>{fileCreateOutput}</Text>}</Box>
|
<Box flexDirection="column">
|
||||||
|
{fileCreateOutput !== '' && <Text>{fileCreateOutput}</Text>}
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,34 @@
|
|||||||
import {Box, Text} from "ink"
|
import { Box, Text } from 'ink'
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import {EnterToContinue} from "../components/enter-to-continue"
|
import { EnterToContinue } from '../components/enter-to-continue'
|
||||||
import {useEnterToContinue} from "../utils/use-enter-to-continue"
|
import { useEnterToContinue } from '../utils/use-enter-to-continue'
|
||||||
import {useUserInput} from "../utils/use-user-input"
|
import { useUserInput } from '../utils/use-user-input'
|
||||||
import {Executor, executorArgument, ExecutorConfig, getExecutorArgument} from "./executor"
|
import {
|
||||||
|
Executor,
|
||||||
|
executorArgument,
|
||||||
|
ExecutorConfig,
|
||||||
|
getExecutorArgument,
|
||||||
|
} from './executor'
|
||||||
|
|
||||||
export interface Config extends ExecutorConfig {
|
export interface Config extends ExecutorConfig {
|
||||||
message: executorArgument<string>
|
message: executorArgument<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const type = "print-message"
|
export const type = 'print-message'
|
||||||
|
|
||||||
export const Commit: Executor["Commit"] = ({cliArgs, cliFlags, onChangeCommitted, step}) => {
|
export const Commit: Executor['Commit'] = ({
|
||||||
|
cliArgs,
|
||||||
|
cliFlags,
|
||||||
|
onChangeCommitted,
|
||||||
|
step,
|
||||||
|
}) => {
|
||||||
const userInput = useUserInput(cliFlags)
|
const userInput = useUserInput(cliFlags)
|
||||||
const generatorArgs = React.useMemo(
|
const generatorArgs = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
message: getExecutorArgument((step as Config).message, cliArgs),
|
message: getExecutorArgument((step as Config).message, cliArgs),
|
||||||
stepName: getExecutorArgument((step as Config).stepName, cliArgs),
|
stepName: getExecutorArgument((step as Config).stepName, cliArgs),
|
||||||
}),
|
}),
|
||||||
[cliArgs, step],
|
[cliArgs, step]
|
||||||
)
|
)
|
||||||
const [changeCommited, setChangeCommited] = React.useState(false)
|
const [changeCommited, setChangeCommited] = React.useState(false)
|
||||||
|
|
||||||
@@ -39,7 +49,7 @@ export const Commit: Executor["Commit"] = ({cliArgs, cliFlags, onChangeCommitted
|
|||||||
|
|
||||||
interface CommitChildProps {
|
interface CommitChildProps {
|
||||||
changeCommited: boolean
|
changeCommited: boolean
|
||||||
generatorArgs: {message: string; stepName: string}
|
generatorArgs: { message: string; stepName: string }
|
||||||
handleChangeCommitted: () => void
|
handleChangeCommitted: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
12
nextjs/packages/installer/src/index.ts
Normal file
12
nextjs/packages/installer/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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,21 +1,27 @@
|
|||||||
import * as AddDependencyExecutor from "./executors/add-dependency-executor"
|
import * as AddDependencyExecutor from './executors/add-dependency-executor'
|
||||||
import * as TransformFileExecutor from "./executors/file-transform-executor"
|
import * as TransformFileExecutor from './executors/file-transform-executor'
|
||||||
import * as NewFileExecutor from "./executors/new-file-executor"
|
import * as NewFileExecutor from './executors/new-file-executor'
|
||||||
import * as PrintMessageExecutor from "./executors/print-message-executor"
|
import * as PrintMessageExecutor from './executors/print-message-executor'
|
||||||
import {ExecutorConfigUnion, RecipeExecutor} from "./recipe-executor"
|
import { ExecutorConfigUnion, RecipeExecutor } from './recipe-executor'
|
||||||
import {RecipeMeta} from "./types"
|
import { RecipeMeta } from './types'
|
||||||
|
|
||||||
export interface IRecipeBuilder {
|
export interface IRecipeBuilder {
|
||||||
setName(name: string): IRecipeBuilder
|
setName(name: string): IRecipeBuilder
|
||||||
setDescription(description: string): IRecipeBuilder
|
setDescription(description: string): IRecipeBuilder
|
||||||
printMessage(
|
printMessage(
|
||||||
step: Omit<Omit<PrintMessageExecutor.Config, "stepType">, "explanation">,
|
step: Omit<Omit<PrintMessageExecutor.Config, 'stepType'>, 'explanation'>
|
||||||
): IRecipeBuilder
|
): IRecipeBuilder
|
||||||
setOwner(owner: string): IRecipeBuilder
|
setOwner(owner: string): IRecipeBuilder
|
||||||
setRepoLink(repoLink: string): IRecipeBuilder
|
setRepoLink(repoLink: string): IRecipeBuilder
|
||||||
addAddDependenciesStep(step: Omit<AddDependencyExecutor.Config, "stepType">): IRecipeBuilder
|
addAddDependenciesStep(
|
||||||
addNewFilesStep(step: Omit<NewFileExecutor.Config, "stepType">): IRecipeBuilder
|
step: Omit<AddDependencyExecutor.Config, 'stepType'>
|
||||||
addTransformFilesStep(step: Omit<TransformFileExecutor.Config, "stepType">): IRecipeBuilder
|
): IRecipeBuilder
|
||||||
|
addNewFilesStep(
|
||||||
|
step: Omit<NewFileExecutor.Config, 'stepType'>
|
||||||
|
): IRecipeBuilder
|
||||||
|
addTransformFilesStep(
|
||||||
|
step: Omit<TransformFileExecutor.Config, 'stepType'>
|
||||||
|
): IRecipeBuilder
|
||||||
build(): RecipeExecutor<any>
|
build(): RecipeExecutor<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +38,7 @@ export function RecipeBuilder(): IRecipeBuilder {
|
|||||||
meta.description = description
|
meta.description = description
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
printMessage(step: Omit<PrintMessageExecutor.Config, "stepType">) {
|
printMessage(step: Omit<PrintMessageExecutor.Config, 'stepType'>) {
|
||||||
steps.push({
|
steps.push({
|
||||||
stepType: PrintMessageExecutor.type,
|
stepType: PrintMessageExecutor.type,
|
||||||
...step,
|
...step,
|
||||||
@@ -47,21 +53,25 @@ export function RecipeBuilder(): IRecipeBuilder {
|
|||||||
meta.repoLink = repoLink
|
meta.repoLink = repoLink
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
addAddDependenciesStep(step: Omit<AddDependencyExecutor.Config, "stepType">) {
|
addAddDependenciesStep(
|
||||||
|
step: Omit<AddDependencyExecutor.Config, 'stepType'>
|
||||||
|
) {
|
||||||
steps.push({
|
steps.push({
|
||||||
stepType: AddDependencyExecutor.type,
|
stepType: AddDependencyExecutor.type,
|
||||||
...step,
|
...step,
|
||||||
})
|
})
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
addNewFilesStep(step: Omit<NewFileExecutor.Config, "stepType">) {
|
addNewFilesStep(step: Omit<NewFileExecutor.Config, 'stepType'>) {
|
||||||
steps.push({
|
steps.push({
|
||||||
stepType: NewFileExecutor.type,
|
stepType: NewFileExecutor.type,
|
||||||
...step,
|
...step,
|
||||||
})
|
})
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
addTransformFilesStep(step: Omit<TransformFileExecutor.Config, "stepType">) {
|
addTransformFilesStep(
|
||||||
|
step: Omit<TransformFileExecutor.Config, 'stepType'>
|
||||||
|
) {
|
||||||
steps.push({
|
steps.push({
|
||||||
stepType: TransformFileExecutor.type,
|
stepType: TransformFileExecutor.type,
|
||||||
...step,
|
...step,
|
||||||
52
nextjs/packages/installer/src/recipe-executor.tsx
Normal file
52
nextjs/packages/installer/src/recipe-executor.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { render } from 'ink'
|
||||||
|
import { baseLogger } from 'next/dist/server/lib/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({ displayDateTime: false, displayLogLevel: false }).info(
|
||||||
|
`\n🎉 The ${this.options.name} recipe has been installed!\n`
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
baseLogger({ displayDateTime: false }).error(e as any)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import {Box, Text, useApp, useInput} from "ink"
|
import { Box, Text, useApp, useInput } from 'ink'
|
||||||
import React from "react"
|
import React from 'react'
|
||||||
import {EnterToContinue} from "./components/enter-to-continue"
|
import { EnterToContinue } from './components/enter-to-continue'
|
||||||
import {Newline} from "./components/newline"
|
import { Newline } from './components/newline'
|
||||||
import * as AddDependencyExecutor from "./executors/add-dependency-executor"
|
import * as AddDependencyExecutor from './executors/add-dependency-executor'
|
||||||
import {Executor, ExecutorConfig, Frontmatter} from "./executors/executor"
|
import { Executor, ExecutorConfig, Frontmatter } from './executors/executor'
|
||||||
import * as FileTransformExecutor from "./executors/file-transform-executor"
|
import * as FileTransformExecutor from './executors/file-transform-executor'
|
||||||
import * as NewFileExecutor from "./executors/new-file-executor"
|
import * as NewFileExecutor from './executors/new-file-executor'
|
||||||
import * as PrintMessageExecutor from "./executors/print-message-executor"
|
import * as PrintMessageExecutor from './executors/print-message-executor'
|
||||||
import {RecipeCLIArgs, RecipeCLIFlags, RecipeMeta} from "./types"
|
import { RecipeCLIArgs, RecipeCLIFlags, RecipeMeta } from './types'
|
||||||
import {useEnterToContinue} from "./utils/use-enter-to-continue"
|
import { useEnterToContinue } from './utils/use-enter-to-continue'
|
||||||
import {useUserInput} from "./utils/use-user-input"
|
import { useUserInput } from './utils/use-user-input'
|
||||||
|
|
||||||
enum Action {
|
enum Action {
|
||||||
SkipStep,
|
SkipStep,
|
||||||
@@ -27,7 +27,7 @@ enum Status {
|
|||||||
Committed,
|
Committed,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExecutorMap: {[key: string]: Executor} = {
|
const ExecutorMap: { [key: string]: Executor } = {
|
||||||
[AddDependencyExecutor.type]: AddDependencyExecutor,
|
[AddDependencyExecutor.type]: AddDependencyExecutor,
|
||||||
[NewFileExecutor.type]: NewFileExecutor,
|
[NewFileExecutor.type]: NewFileExecutor,
|
||||||
[PrintMessageExecutor.type]: PrintMessageExecutor,
|
[PrintMessageExecutor.type]: PrintMessageExecutor,
|
||||||
@@ -35,12 +35,17 @@ const ExecutorMap: {[key: string]: Executor} = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
steps: {executor: ExecutorConfig; status: Status; proposalData?: any; successMsg: string}[]
|
steps: {
|
||||||
|
executor: ExecutorConfig
|
||||||
|
status: Status
|
||||||
|
proposalData?: any
|
||||||
|
successMsg: string
|
||||||
|
}[]
|
||||||
current: number
|
current: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeReducer(state: State, action: {type: Action; data?: any}) {
|
function recipeReducer(state: State, action: { type: Action; data?: any }) {
|
||||||
const newState = {...state}
|
const newState = { ...state }
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case Action.ProposeChange:
|
case Action.ProposeChange:
|
||||||
newState.steps[newState.current].status = Status.Proposed
|
newState.steps[newState.current].status = Status.Proposed
|
||||||
@@ -55,7 +60,10 @@ function recipeReducer(state: State, action: {type: Action; data?: any}) {
|
|||||||
case Action.CompleteChange:
|
case Action.CompleteChange:
|
||||||
newState.steps[newState.current].status = Status.Committed
|
newState.steps[newState.current].status = Status.Committed
|
||||||
newState.steps[newState.current].successMsg = action.data as string
|
newState.steps[newState.current].successMsg = action.data as string
|
||||||
newState.current = Math.min(newState.current + 1, newState.steps.length - 1)
|
newState.current = Math.min(
|
||||||
|
newState.current + 1,
|
||||||
|
newState.steps.length - 1
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case Action.SkipStep:
|
case Action.SkipStep:
|
||||||
newState.current += 1
|
newState.current += 1
|
||||||
@@ -71,7 +79,9 @@ interface RecipeProps {
|
|||||||
recipeMeta: RecipeMeta
|
recipeMeta: RecipeMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
const DispatchContext = React.createContext<React.Dispatch<{type: Action; data?: any}>>(() => {})
|
const DispatchContext = React.createContext<
|
||||||
|
React.Dispatch<{ type: Action; data?: any }>
|
||||||
|
>(() => {})
|
||||||
|
|
||||||
function WelcomeMessage({
|
function WelcomeMessage({
|
||||||
recipeMeta,
|
recipeMeta,
|
||||||
@@ -101,16 +111,19 @@ function WelcomeMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StepMessages({state}: {state: State}) {
|
function StepMessages({ state }: { state: State }) {
|
||||||
const messages = state.steps
|
const messages = state.steps
|
||||||
.map((step) => ({msg: step.successMsg, icon: step.executor.successIcon ?? "✅"}))
|
.map((step) => ({
|
||||||
|
msg: step.successMsg,
|
||||||
|
icon: step.executor.successIcon ?? '✅',
|
||||||
|
}))
|
||||||
.filter((s) => s.msg)
|
.filter((s) => s.msg)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{messages.map(({msg, icon}, index) => (
|
{messages.map(({ msg, icon }, index) => (
|
||||||
<Text key={msg + index} color="green">
|
<Text key={msg + index} color="green">
|
||||||
{msg === "\n" ? "" : icon} {msg}
|
{msg === '\n' ? '' : icon} {msg}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -130,30 +143,30 @@ function StepExecutor({
|
|||||||
cliFlags: RecipeCLIFlags
|
cliFlags: RecipeCLIFlags
|
||||||
proposalData?: any
|
proposalData?: any
|
||||||
}) {
|
}) {
|
||||||
const {Propose, Commit}: Executor = ExecutorMap[step.stepType]
|
const { Propose, Commit }: Executor = ExecutorMap[step.stepType]
|
||||||
const dispatch = React.useContext(DispatchContext)
|
const dispatch = React.useContext(DispatchContext)
|
||||||
|
|
||||||
const handleProposalAccepted = React.useCallback(
|
const handleProposalAccepted = React.useCallback(
|
||||||
(msg) => {
|
(msg) => {
|
||||||
dispatch({type: Action.CommitApproved, data: msg})
|
dispatch({ type: Action.CommitApproved, data: msg })
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch]
|
||||||
)
|
)
|
||||||
const handleChangeCommitted = React.useCallback(
|
const handleChangeCommitted = React.useCallback(
|
||||||
(msg) => {
|
(msg) => {
|
||||||
dispatch({type: Action.CompleteChange, data: msg})
|
dispatch({ type: Action.CompleteChange, data: msg })
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch]
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (status === Status.Pending) {
|
if (status === Status.Pending) {
|
||||||
dispatch({type: Action.ProposeChange})
|
dispatch({ type: Action.ProposeChange })
|
||||||
} else if (status === Status.ReadyToCommit) {
|
} else if (status === Status.ReadyToCommit) {
|
||||||
dispatch({type: Action.ApplyChange})
|
dispatch({ type: Action.ApplyChange })
|
||||||
}
|
}
|
||||||
if (status === Status.Proposed && !Propose) {
|
if (status === Status.Proposed && !Propose) {
|
||||||
dispatch({type: Action.CommitApproved})
|
dispatch({ type: Action.CommitApproved })
|
||||||
}
|
}
|
||||||
}, [dispatch, status, Propose])
|
}, [dispatch, status, Propose])
|
||||||
|
|
||||||
@@ -181,16 +194,25 @@ function StepExecutor({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RecipeRenderer({cliArgs, cliFlags, steps, recipeMeta}: RecipeProps) {
|
export function RecipeRenderer({
|
||||||
|
cliArgs,
|
||||||
|
cliFlags,
|
||||||
|
steps,
|
||||||
|
recipeMeta,
|
||||||
|
}: RecipeProps) {
|
||||||
const userInput = useUserInput(cliFlags)
|
const userInput = useUserInput(cliFlags)
|
||||||
const {exit} = useApp()
|
const { exit } = useApp()
|
||||||
const [state, dispatch] = React.useReducer(recipeReducer, {
|
const [state, dispatch] = React.useReducer(recipeReducer, {
|
||||||
current: userInput ? -1 : 0,
|
current: userInput ? -1 : 0,
|
||||||
steps: steps.map((e) => ({executor: e, status: Status.Pending, successMsg: ""})),
|
steps: steps.map((e) => ({
|
||||||
|
executor: e,
|
||||||
|
status: Status.Pending,
|
||||||
|
successMsg: '',
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (steps.length === 0) {
|
if (steps.length === 0) {
|
||||||
exit(new Error("This recipe has no steps"))
|
exit(new Error('This recipe has no steps'))
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -228,18 +250,21 @@ function RecipeRendererWithInput({
|
|||||||
cliFlags,
|
cliFlags,
|
||||||
recipeMeta,
|
recipeMeta,
|
||||||
state,
|
state,
|
||||||
}: Omit<RecipeProps, "steps"> & {state: State}) {
|
}: Omit<RecipeProps, 'steps'> & { state: State }) {
|
||||||
const {exit} = useApp()
|
const { exit } = useApp()
|
||||||
const dispatch = React.useContext(DispatchContext)
|
const dispatch = React.useContext(DispatchContext)
|
||||||
|
|
||||||
useInput((input, key) => {
|
useInput((input, key) => {
|
||||||
if (input === "c" && key.ctrl) {
|
if (input === 'c' && key.ctrl) {
|
||||||
exit(new Error("You aborted installation"))
|
exit(new Error('You aborted installation'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useEnterToContinue(() => dispatch({type: Action.SkipStep}), state.current === -1)
|
useEnterToContinue(
|
||||||
|
() => dispatch({ type: Action.SkipStep }),
|
||||||
|
state.current === -1
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -264,7 +289,7 @@ function RecipeRendererWithoutInput({
|
|||||||
cliFlags,
|
cliFlags,
|
||||||
recipeMeta,
|
recipeMeta,
|
||||||
state,
|
state,
|
||||||
}: Omit<RecipeProps, "steps"> & {state: State}) {
|
}: Omit<RecipeProps, 'steps'> & { state: State }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WelcomeMessage recipeMeta={recipeMeta} enterToContinue={false} />
|
<WelcomeMessage recipeMeta={recipeMeta} enterToContinue={false} />
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { ExpressionKind } from 'ast-types/gen/kinds'
|
||||||
|
import j from 'jscodeshift'
|
||||||
|
import { Program } from '../types'
|
||||||
|
import { transformBlitzConfig } from '.'
|
||||||
|
|
||||||
|
export const addBlitzMiddleware = (
|
||||||
|
program: Program,
|
||||||
|
middleware: ExpressionKind
|
||||||
|
): Program =>
|
||||||
|
transformBlitzConfig(program, (config) => {
|
||||||
|
// Locate the middleware property
|
||||||
|
const middlewareProp = config.properties.find(
|
||||||
|
(value) =>
|
||||||
|
value.type === 'ObjectProperty' &&
|
||||||
|
value.key.type === 'Identifier' &&
|
||||||
|
value.key.name === 'middleware'
|
||||||
|
) as j.ObjectProperty | undefined
|
||||||
|
|
||||||
|
if (middlewareProp && middlewareProp.value.type === 'ArrayExpression') {
|
||||||
|
// We found it, pop on our middleware.
|
||||||
|
middlewareProp.value.elements.push(middleware)
|
||||||
|
} else {
|
||||||
|
// No middleware prop, add our own.
|
||||||
|
config.properties.push(
|
||||||
|
j.property('init', j.identifier('middleware'), {
|
||||||
|
type: 'ArrayExpression',
|
||||||
|
elements: [middleware],
|
||||||
|
loc: null,
|
||||||
|
comments: null,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
})
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import j from "jscodeshift"
|
import j from 'jscodeshift'
|
||||||
import {Program} from "../types"
|
import { Program } from '../types'
|
||||||
|
|
||||||
export function addImport(program: Program, importToAdd: j.ImportDeclaration): Program {
|
export function addImport(
|
||||||
|
program: Program,
|
||||||
|
importToAdd: j.ImportDeclaration
|
||||||
|
): Program {
|
||||||
const importStatementCount = program.find(j.ImportDeclaration).length
|
const importStatementCount = program.find(j.ImportDeclaration).length
|
||||||
if (importStatementCount === 0) {
|
if (importStatementCount === 0) {
|
||||||
program.find(j.Statement).at(0).insertBefore(importToAdd)
|
program.find(j.Statement).at(0).insertBefore(importToAdd)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import j from 'jscodeshift'
|
||||||
|
import { Program } from '../types'
|
||||||
|
|
||||||
|
export const findModuleExportsExpressions = (program: Program) =>
|
||||||
|
program.find(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'
|
||||||
|
)
|
||||||
|
})
|
||||||
7
nextjs/packages/installer/src/transforms/index.ts
Normal file
7
nextjs/packages/installer/src/transforms/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './add-import'
|
||||||
|
export * from './add-blitz-middleware'
|
||||||
|
export * from './find-module-exports-expressions'
|
||||||
|
export * from './prisma'
|
||||||
|
export * from './transform-blitz-config'
|
||||||
|
export * from './update-babel-config'
|
||||||
|
export * from './wrap-blitz-config'
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Enum} from "@mrleebo/prisma-ast"
|
import { Enum } from '@mrleebo/prisma-ast'
|
||||||
import {produceSchema} from "./produce-schema"
|
import { produceSchema } from './produce-schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an enum to your schema.prisma data model.
|
* Adds an enum to your schema.prisma data model.
|
||||||
@@ -19,9 +19,14 @@ import {produceSchema} from "./produce-schema"
|
|||||||
})
|
})
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function addPrismaEnum(source: string, enumProps: Enum): Promise<string> {
|
export function addPrismaEnum(
|
||||||
|
source: string,
|
||||||
|
enumProps: Enum
|
||||||
|
): Promise<string> {
|
||||||
return produceSchema(source, (schema) => {
|
return produceSchema(source, (schema) => {
|
||||||
const existing = schema.list.find((x) => x.type === "enum" && x.name === enumProps.name)
|
const existing = schema.list.find(
|
||||||
|
(x) => x.type === 'enum' && x.name === enumProps.name
|
||||||
|
)
|
||||||
existing ? Object.assign(existing, enumProps) : schema.list.push(enumProps)
|
existing ? Object.assign(existing, enumProps) : schema.list.push(enumProps)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Field, Model} from "@mrleebo/prisma-ast"
|
import { Field, Model } from '@mrleebo/prisma-ast'
|
||||||
import {produceSchema} from "./produce-schema"
|
import { produceSchema } from './produce-schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a field to a model in your schema.prisma data model.
|
* Adds a field to a model in your schema.prisma data model.
|
||||||
@@ -22,13 +22,19 @@ import {produceSchema} from "./produce-schema"
|
|||||||
export function addPrismaField(
|
export function addPrismaField(
|
||||||
source: string,
|
source: string,
|
||||||
modelName: string,
|
modelName: string,
|
||||||
fieldProps: Field,
|
fieldProps: Field
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return produceSchema(source, (schema) => {
|
return produceSchema(source, (schema) => {
|
||||||
const model = schema.list.find((x) => x.type === "model" && x.name === modelName) as Model
|
const model = schema.list.find(
|
||||||
|
(x) => x.type === 'model' && x.name === modelName
|
||||||
|
) as Model
|
||||||
if (!model) return
|
if (!model) return
|
||||||
|
|
||||||
const existing = model.properties.find((x) => x.type === "field" && x.name === fieldProps.name)
|
const existing = model.properties.find(
|
||||||
existing ? Object.assign(existing, fieldProps) : model.properties.push(fieldProps)
|
(x) => x.type === 'field' && x.name === fieldProps.name
|
||||||
|
)
|
||||||
|
existing
|
||||||
|
? Object.assign(existing, fieldProps)
|
||||||
|
: model.properties.push(fieldProps)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Generator} from "@mrleebo/prisma-ast"
|
import { Generator } from '@mrleebo/prisma-ast'
|
||||||
import {produceSchema} from "./produce-schema"
|
import { produceSchema } from './produce-schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a generator to your schema.prisma data model.
|
* Adds a generator to your schema.prisma data model.
|
||||||
@@ -16,11 +16,16 @@ import {produceSchema} from "./produce-schema"
|
|||||||
})
|
})
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function addPrismaGenerator(source: string, generatorProps: Generator): Promise<string> {
|
export function addPrismaGenerator(
|
||||||
|
source: string,
|
||||||
|
generatorProps: Generator
|
||||||
|
): Promise<string> {
|
||||||
return produceSchema(source, (schema) => {
|
return produceSchema(source, (schema) => {
|
||||||
const existing = schema.list.find(
|
const existing = schema.list.find(
|
||||||
(x) => x.type === "generator" && x.name === generatorProps.name,
|
(x) => x.type === 'generator' && x.name === generatorProps.name
|
||||||
) as Generator
|
) as Generator
|
||||||
existing ? Object.assign(existing, generatorProps) : schema.list.push(generatorProps)
|
existing
|
||||||
|
? Object.assign(existing, generatorProps)
|
||||||
|
: schema.list.push(generatorProps)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Model, ModelAttribute} from "@mrleebo/prisma-ast"
|
import { Model, ModelAttribute } from '@mrleebo/prisma-ast'
|
||||||
import {produceSchema} from "./produce-schema"
|
import { produceSchema } from './produce-schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a field to a model in your schema.prisma data model.
|
* Adds a field to a model in your schema.prisma data model.
|
||||||
@@ -22,16 +22,20 @@ import {produceSchema} from "./produce-schema"
|
|||||||
export function addPrismaModelAttribute(
|
export function addPrismaModelAttribute(
|
||||||
source: string,
|
source: string,
|
||||||
modelName: string,
|
modelName: string,
|
||||||
attributeProps: ModelAttribute,
|
attributeProps: ModelAttribute
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return produceSchema(source, (schema) => {
|
return produceSchema(source, (schema) => {
|
||||||
const model = schema.list.find((x) => x.type === "model" && x.name === modelName) as Model
|
const model = schema.list.find(
|
||||||
|
(x) => x.type === 'model' && x.name === modelName
|
||||||
|
) as Model
|
||||||
if (!model) return
|
if (!model) return
|
||||||
|
|
||||||
const existing = model.properties.find(
|
const existing = model.properties.find(
|
||||||
(x) => x.type === "attribute" && x.name === attributeProps.name,
|
(x) => x.type === 'attribute' && x.name === attributeProps.name
|
||||||
)
|
)
|
||||||
|
|
||||||
existing ? Object.assign(existing, attributeProps) : model.properties.push(attributeProps)
|
existing
|
||||||
|
? Object.assign(existing, attributeProps)
|
||||||
|
: model.properties.push(attributeProps)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Model} from "@mrleebo/prisma-ast"
|
import { Model } from '@mrleebo/prisma-ast'
|
||||||
import {produceSchema} from "./produce-schema"
|
import { produceSchema } from './produce-schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an enum to your schema.prisma data model.
|
* Adds an enum to your schema.prisma data model.
|
||||||
@@ -16,9 +16,16 @@ import {produceSchema} from "./produce-schema"
|
|||||||
})
|
})
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function addPrismaModel(source: string, modelProps: Model): Promise<string> {
|
export function addPrismaModel(
|
||||||
|
source: string,
|
||||||
|
modelProps: Model
|
||||||
|
): Promise<string> {
|
||||||
return produceSchema(source, (schema) => {
|
return produceSchema(source, (schema) => {
|
||||||
const existing = schema.list.find((x) => x.type === "model" && x.name === modelProps.name)
|
const existing = schema.list.find(
|
||||||
existing ? Object.assign(existing, modelProps) : schema.list.push(modelProps)
|
(x) => x.type === 'model' && x.name === modelProps.name
|
||||||
|
)
|
||||||
|
existing
|
||||||
|
? Object.assign(existing, modelProps)
|
||||||
|
: schema.list.push(modelProps)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
7
nextjs/packages/installer/src/transforms/prisma/index.ts
Normal file
7
nextjs/packages/installer/src/transforms/prisma/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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,4 +1,4 @@
|
|||||||
import {printSchema as printer, Schema} from "@mrleebo/prisma-ast"
|
import { printSchema as printer, Schema } from '@mrleebo/prisma-ast'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes the schema.prisma document parsed from @mrleebo/prisma-ast and
|
* Takes the schema.prisma document parsed from @mrleebo/prisma-ast and
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {getSchema, printSchema, Schema} from "@mrleebo/prisma-ast"
|
import { getSchema, printSchema, Schema } from '@mrleebo/prisma-ast'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A file transformer that parses a schema.prisma string, offers you a callback
|
* A file transformer that parses a schema.prisma string, offers you a callback
|
||||||
@@ -11,7 +11,7 @@ import {getSchema, printSchema, Schema} from "@mrleebo/prisma-ast"
|
|||||||
*/
|
*/
|
||||||
export async function produceSchema(
|
export async function produceSchema(
|
||||||
source: string,
|
source: string,
|
||||||
producer: (schema: Schema) => void,
|
producer: (schema: Schema) => void
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const schema = await getSchema(source)
|
const schema = await getSchema(source)
|
||||||
producer(schema)
|
producer(schema)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Datasource} from "@mrleebo/prisma-ast"
|
import { Datasource } from '@mrleebo/prisma-ast'
|
||||||
import {produceSchema} from "./produce-schema"
|
import { produceSchema } from './produce-schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify the prisma datasource metadata to use the provider and url specified.
|
* Modify the prisma datasource metadata to use the provider and url specified.
|
||||||
@@ -23,9 +23,14 @@ import {produceSchema} from "./produce-schema"
|
|||||||
})
|
})
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function setPrismaDataSource(source: string, datasourceProps: Datasource): Promise<string> {
|
export function setPrismaDataSource(
|
||||||
|
source: string,
|
||||||
|
datasourceProps: Datasource
|
||||||
|
): Promise<string> {
|
||||||
return produceSchema(source, (schema) => {
|
return produceSchema(source, (schema) => {
|
||||||
const existing = schema.list.find((x) => x.type === "datasource")
|
const existing = schema.list.find((x) => x.type === 'datasource')
|
||||||
existing ? Object.assign(existing, datasourceProps) : schema.list.push(datasourceProps)
|
existing
|
||||||
|
? Object.assign(existing, datasourceProps)
|
||||||
|
: schema.list.push(datasourceProps)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,27 @@
|
|||||||
import type {ExpressionKind} from "ast-types/gen/kinds"
|
import type { ExpressionKind } from 'ast-types/gen/kinds'
|
||||||
import j from "jscodeshift"
|
import j from 'jscodeshift'
|
||||||
import {Program} from "../types"
|
import { Program } from '../types'
|
||||||
|
|
||||||
function recursiveConfigSearch(
|
function recursiveConfigSearch(
|
||||||
program: Program,
|
program: Program,
|
||||||
obj: ExpressionKind,
|
obj: ExpressionKind
|
||||||
): j.ObjectExpression | undefined {
|
): j.ObjectExpression | undefined {
|
||||||
// Identifier being a variable name
|
// Identifier being a variable name
|
||||||
if (obj.type === "Identifier") {
|
if (obj.type === 'Identifier') {
|
||||||
const {node} = j(obj).get()
|
const { node } = j(obj).get()
|
||||||
|
|
||||||
// Get the definition of the variable
|
// Get the definition of the variable
|
||||||
const identifier: j.ASTPath<j.VariableDeclarator> = program
|
const identifier: j.ASTPath<j.VariableDeclarator> = program
|
||||||
.find(j.VariableDeclarator, {
|
.find(j.VariableDeclarator, {
|
||||||
id: {name: node.name},
|
id: { name: node.name },
|
||||||
})
|
})
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
// Return what is after the `=`
|
// Return what is after the `=`
|
||||||
return identifier.value.init ? recursiveConfigSearch(program, identifier.value.init) : undefined
|
return identifier.value.init
|
||||||
} else if (obj.type === "CallExpression") {
|
? recursiveConfigSearch(program, identifier.value.init)
|
||||||
|
: undefined
|
||||||
|
} else if (obj.type === 'CallExpression') {
|
||||||
// If it's an function call (like `withBundleAnalyzer`), get the first argument
|
// If it's an function call (like `withBundleAnalyzer`), get the first argument
|
||||||
if (obj.arguments.length === 0) {
|
if (obj.arguments.length === 0) {
|
||||||
// If it has no arguments, create an empty object: `{}`
|
// If it has no arguments, create an empty object: `{}`
|
||||||
@@ -28,10 +30,10 @@ function recursiveConfigSearch(
|
|||||||
return config
|
return config
|
||||||
} else {
|
} else {
|
||||||
const arg = obj.arguments[0]
|
const arg = obj.arguments[0]
|
||||||
if (arg.type === "SpreadElement") return undefined
|
if (arg.type === 'SpreadElement') return undefined
|
||||||
else return recursiveConfigSearch(program, arg)
|
else return recursiveConfigSearch(program, arg)
|
||||||
}
|
}
|
||||||
} else if (obj.type === "ObjectExpression") {
|
} else if (obj.type === 'ObjectExpression') {
|
||||||
// If it's an object, return it
|
// If it's an object, return it
|
||||||
return obj
|
return obj
|
||||||
} else {
|
} else {
|
||||||
@@ -39,15 +41,17 @@ function recursiveConfigSearch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TransformBlitzConfigCallback = (config: j.ObjectExpression) => j.ObjectExpression
|
export type TransformBlitzConfigCallback = (
|
||||||
|
config: j.ObjectExpression
|
||||||
|
) => j.ObjectExpression
|
||||||
|
|
||||||
export function transformBlitzConfig(
|
export function transformBlitzConfig(
|
||||||
program: Program,
|
program: Program,
|
||||||
transform: TransformBlitzConfigCallback,
|
transform: TransformBlitzConfigCallback
|
||||||
): Program {
|
): Program {
|
||||||
let moduleExportsExpressions = program.find(j.AssignmentExpression, {
|
let moduleExportsExpressions = program.find(j.AssignmentExpression, {
|
||||||
operator: "=",
|
operator: '=',
|
||||||
left: {object: {name: "module"}, property: {name: "exports"}},
|
left: { object: { name: 'module' }, property: { name: 'exports' } },
|
||||||
right: {},
|
right: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -59,10 +63,10 @@ export function transformBlitzConfig(
|
|||||||
|
|
||||||
let moduleExportExpression = j.expressionStatement(
|
let moduleExportExpression = j.expressionStatement(
|
||||||
j.assignmentExpression(
|
j.assignmentExpression(
|
||||||
"=",
|
'=',
|
||||||
j.memberExpression(j.identifier("module"), j.identifier("exports")),
|
j.memberExpression(j.identifier('module'), j.identifier('exports')),
|
||||||
config,
|
config
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
program.get().node.program.body.push(moduleExportExpression)
|
program.get().node.program.body.push(moduleExportExpression)
|
||||||
@@ -71,14 +75,14 @@ export function transformBlitzConfig(
|
|||||||
|
|
||||||
let config: j.ObjectExpression | undefined = recursiveConfigSearch(
|
let config: j.ObjectExpression | undefined = recursiveConfigSearch(
|
||||||
program,
|
program,
|
||||||
moduleExportsExpression.value.right,
|
moduleExportsExpression.value.right
|
||||||
)
|
)
|
||||||
|
|
||||||
if (config) {
|
if (config) {
|
||||||
config = transform(config)
|
config = transform(config)
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
"The configuration couldn't be found, but there is a 'module.exports' inside `blitz.config.js`",
|
"The configuration couldn't be found, but there is a 'module.exports' inside `blitz.config.js`"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import type {ExpressionKind} from "ast-types/gen/kinds"
|
import type { ExpressionKind } from 'ast-types/gen/kinds'
|
||||||
import j from "jscodeshift"
|
import j from 'jscodeshift'
|
||||||
import {JsonObject, JsonValue} from "../types"
|
import { JsonObject, JsonValue } from '../types'
|
||||||
import {Program} from "../types"
|
import { Program } from '../types'
|
||||||
import {findModuleExportsExpressions} from "./find-module-exports-expressions"
|
import { findModuleExportsExpressions } from './find-module-exports-expressions'
|
||||||
|
|
||||||
type AddBabelItemDefinition = string | [name: string, options: JsonObject]
|
type AddBabelItemDefinition = string | [name: string, options: JsonObject]
|
||||||
|
|
||||||
const jsonValueToExpression = (value: JsonValue): ExpressionKind =>
|
const jsonValueToExpression = (value: JsonValue): ExpressionKind =>
|
||||||
typeof value === "string"
|
typeof value === 'string'
|
||||||
? j.stringLiteral(value)
|
? j.stringLiteral(value)
|
||||||
: typeof value === "number"
|
: typeof value === 'number'
|
||||||
? j.numericLiteral(value)
|
? j.numericLiteral(value)
|
||||||
: typeof value === "boolean"
|
: typeof value === 'boolean'
|
||||||
? j.booleanLiteral(value)
|
? j.booleanLiteral(value)
|
||||||
: value === null
|
: value === null
|
||||||
? j.nullLiteral()
|
? j.nullLiteral()
|
||||||
@@ -19,16 +19,22 @@ const jsonValueToExpression = (value: JsonValue): ExpressionKind =>
|
|||||||
? j.arrayExpression(value.map(jsonValueToExpression))
|
? j.arrayExpression(value.map(jsonValueToExpression))
|
||||||
: j.objectExpression(
|
: j.objectExpression(
|
||||||
Object.entries(value)
|
Object.entries(value)
|
||||||
.filter((entry): entry is [string, JsonValue] => entry[1] !== undefined)
|
.filter(
|
||||||
|
(entry): entry is [string, JsonValue] => entry[1] !== undefined
|
||||||
|
)
|
||||||
.map(([key, value]) =>
|
.map(([key, value]) =>
|
||||||
j.objectProperty(j.stringLiteral(key), jsonValueToExpression(value)),
|
j.objectProperty(j.stringLiteral(key), jsonValueToExpression(value))
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
function updateBabelConfig(program: Program, item: AddBabelItemDefinition, key: string): Program {
|
function updateBabelConfig(
|
||||||
|
program: Program,
|
||||||
|
item: AddBabelItemDefinition,
|
||||||
|
key: string
|
||||||
|
): Program {
|
||||||
findModuleExportsExpressions(program).forEach((moduleExportsExpression) => {
|
findModuleExportsExpressions(program).forEach((moduleExportsExpression) => {
|
||||||
j(moduleExportsExpression)
|
j(moduleExportsExpression)
|
||||||
.find(j.ObjectProperty, {key: {name: key}})
|
.find(j.ObjectProperty, { key: { name: key } })
|
||||||
.forEach((items) => {
|
.forEach((items) => {
|
||||||
// Don't add it again if it already exists,
|
// Don't add it again if it already exists,
|
||||||
// that what this code does. For simplicity,
|
// that what this code does. For simplicity,
|
||||||
@@ -36,14 +42,20 @@ function updateBabelConfig(program: Program, item: AddBabelItemDefinition, key:
|
|||||||
|
|
||||||
const itemName = Array.isArray(item) ? item[0] : item
|
const itemName = Array.isArray(item) ? item[0] : item
|
||||||
|
|
||||||
if (items.node.value.type === "Literal" || items.node.value.type === "StringLiteral") {
|
if (
|
||||||
|
items.node.value.type === 'Literal' ||
|
||||||
|
items.node.value.type === 'StringLiteral'
|
||||||
|
) {
|
||||||
// {
|
// {
|
||||||
// presets: "this-preset"
|
// presets: "this-preset"
|
||||||
// }
|
// }
|
||||||
if (itemName !== items.node.value.value) {
|
if (itemName !== items.node.value.value) {
|
||||||
items.node.value = j.arrayExpression([items.node.value, jsonValueToExpression(item)])
|
items.node.value = j.arrayExpression([
|
||||||
|
items.node.value,
|
||||||
|
jsonValueToExpression(item),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
} else if (items.node.value.type === "ArrayExpression") {
|
} else if (items.node.value.type === 'ArrayExpression') {
|
||||||
// {
|
// {
|
||||||
// presets: ["this-preset", "maybe-another", ...]
|
// presets: ["this-preset", "maybe-another", ...]
|
||||||
// }
|
// }
|
||||||
@@ -52,22 +64,25 @@ function updateBabelConfig(program: Program, item: AddBabelItemDefinition, key:
|
|||||||
for (const [i, element] of items.node.value.elements.entries()) {
|
for (const [i, element] of items.node.value.elements.entries()) {
|
||||||
if (!element) continue
|
if (!element) continue
|
||||||
|
|
||||||
if (element.type === "Literal" || element.type === "StringLiteral") {
|
if (
|
||||||
|
element.type === 'Literal' ||
|
||||||
|
element.type === 'StringLiteral'
|
||||||
|
) {
|
||||||
// {
|
// {
|
||||||
// presets: [..., "this-preset", ...]
|
// presets: [..., "this-preset", ...]
|
||||||
// }
|
// }
|
||||||
if (element.value === itemName) return
|
if (element.value === itemName) return
|
||||||
} else if (element.type === "ArrayExpression") {
|
} else if (element.type === 'ArrayExpression') {
|
||||||
// {
|
// {
|
||||||
// presets: [..., ["this-preset"], ...]
|
// presets: [..., ["this-preset"], ...]
|
||||||
// }
|
// }
|
||||||
if (
|
if (
|
||||||
(element.elements[0]?.type === "Literal" ||
|
(element.elements[0]?.type === 'Literal' ||
|
||||||
element.elements[0]?.type === "StringLiteral") &&
|
element.elements[0]?.type === 'StringLiteral') &&
|
||||||
element.elements[0].value === itemName
|
element.elements[0].value === itemName
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
element.elements[1]?.type === "ObjectExpression" &&
|
element.elements[1]?.type === 'ObjectExpression' &&
|
||||||
element.elements[1].properties.length > 0
|
element.elements[1].properties.length > 0
|
||||||
) {
|
) {
|
||||||
// The preset has a config.
|
// The preset has a config.
|
||||||
@@ -81,7 +96,10 @@ function updateBabelConfig(program: Program, item: AddBabelItemDefinition, key:
|
|||||||
const value = item[1][key]
|
const value = item[1][key]
|
||||||
if (value === undefined) continue
|
if (value === undefined) continue
|
||||||
obj.properties.push(
|
obj.properties.push(
|
||||||
j.objectProperty(j.stringLiteral(key), jsonValueToExpression(value)),
|
j.objectProperty(
|
||||||
|
j.stringLiteral(key),
|
||||||
|
jsonValueToExpression(value)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +123,11 @@ function updateBabelConfig(program: Program, item: AddBabelItemDefinition, key:
|
|||||||
return program
|
return program
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addBabelPreset = (program: Program, preset: AddBabelItemDefinition): Program =>
|
export const addBabelPreset = (
|
||||||
updateBabelConfig(program, preset, "presets")
|
program: Program,
|
||||||
export const addBabelPlugin = (program: Program, plugin: AddBabelItemDefinition): Program =>
|
preset: AddBabelItemDefinition
|
||||||
updateBabelConfig(program, plugin, "plugins")
|
): Program => updateBabelConfig(program, preset, 'presets')
|
||||||
|
export const addBabelPlugin = (
|
||||||
|
program: Program,
|
||||||
|
plugin: AddBabelItemDefinition
|
||||||
|
): Program => updateBabelConfig(program, plugin, 'plugins')
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import j from "jscodeshift"
|
import j from 'jscodeshift'
|
||||||
import {Program} from "../types"
|
import { Program } from '../types'
|
||||||
|
|
||||||
export function wrapBlitzConfig(program: Program, functionName: string): Program {
|
export function wrapBlitzConfig(
|
||||||
|
program: Program,
|
||||||
|
functionName: string
|
||||||
|
): Program {
|
||||||
let moduleExportsExpressions = program.find(j.AssignmentExpression, {
|
let moduleExportsExpressions = program.find(j.AssignmentExpression, {
|
||||||
operator: "=",
|
operator: '=',
|
||||||
left: {object: {name: "module"}, property: {name: "exports"}},
|
left: { object: { name: 'module' }, property: { name: 'exports' } },
|
||||||
right: {},
|
right: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -12,19 +15,20 @@ export function wrapBlitzConfig(program: Program, functionName: string): Program
|
|||||||
if (moduleExportsExpressions.length === 0) {
|
if (moduleExportsExpressions.length === 0) {
|
||||||
let moduleExportExpression = j.expressionStatement(
|
let moduleExportExpression = j.expressionStatement(
|
||||||
j.assignmentExpression(
|
j.assignmentExpression(
|
||||||
"=",
|
'=',
|
||||||
j.memberExpression(j.identifier("module"), j.identifier("exports")),
|
j.memberExpression(j.identifier('module'), j.identifier('exports')),
|
||||||
j.callExpression(j.identifier(functionName), [j.objectExpression([])]),
|
j.callExpression(j.identifier(functionName), [j.objectExpression([])])
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
program.get().node.program.body.push(moduleExportExpression)
|
program.get().node.program.body.push(moduleExportExpression)
|
||||||
} else if (moduleExportsExpressions.length === 1) {
|
} else if (moduleExportsExpressions.length === 1) {
|
||||||
let moduleExportsExpression: j.ASTPath<j.AssignmentExpression> = moduleExportsExpressions.get()
|
let moduleExportsExpression: j.ASTPath<j.AssignmentExpression> = moduleExportsExpressions.get()
|
||||||
|
|
||||||
moduleExportsExpression.value.right = j.callExpression(j.identifier(functionName), [
|
moduleExportsExpression.value.right = j.callExpression(
|
||||||
moduleExportsExpression.value.right,
|
j.identifier(functionName),
|
||||||
])
|
[moduleExportsExpression.value.right]
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
console.warn("There are multiple 'module.exports' inside 'blitz.config.js'")
|
console.warn("There are multiple 'module.exports' inside 'blitz.config.js'")
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type * as j from "jscodeshift"
|
import type * as j from 'jscodeshift'
|
||||||
|
|
||||||
export interface RecipeMeta {
|
export interface RecipeMeta {
|
||||||
name: string
|
name: string
|
||||||
@@ -7,7 +7,7 @@ export interface RecipeMeta {
|
|||||||
repoLink: string
|
repoLink: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RecipeCLIArgs = {[Key in string]?: string | true}
|
export type RecipeCLIArgs = { [Key in string]?: string | true }
|
||||||
|
|
||||||
export interface RecipeCLIFlags {
|
export interface RecipeCLIFlags {
|
||||||
yesToAll: boolean
|
yesToAll: boolean
|
||||||
@@ -20,7 +20,7 @@ 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 { … }`.
|
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
|
@see https://github.com/sindresorhus/type-fest
|
||||||
*/
|
*/
|
||||||
export type JsonObject = {[Key in string]?: JsonValue}
|
export type JsonObject = { [Key in string]?: JsonValue }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Matches a JSON array.
|
Matches a JSON array.
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import * as fs from "fs-extra"
|
import * as fs from 'fs-extra'
|
||||||
import * as path from "path"
|
import * as path from 'path'
|
||||||
|
|
||||||
function ext(jsx = false) {
|
function ext(jsx = false) {
|
||||||
return fs.existsSync(path.resolve("tsconfig.json")) ? (jsx ? ".tsx" : ".ts") : ".js"
|
return fs.existsSync(path.resolve('tsconfig.json'))
|
||||||
|
? jsx
|
||||||
|
? '.tsx'
|
||||||
|
: '.ts'
|
||||||
|
: '.js'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const paths = {
|
export const paths = {
|
||||||
@@ -16,15 +20,15 @@ export const paths = {
|
|||||||
return `app/pages/index${ext(true)}`
|
return `app/pages/index${ext(true)}`
|
||||||
},
|
},
|
||||||
babelConfig() {
|
babelConfig() {
|
||||||
return "babel.config.js"
|
return 'babel.config.js'
|
||||||
},
|
},
|
||||||
blitzConfig() {
|
blitzConfig() {
|
||||||
return `blitz.config${ext()}`
|
return `blitz.config${ext()}`
|
||||||
},
|
},
|
||||||
packageJson() {
|
packageJson() {
|
||||||
return "package.json"
|
return 'package.json'
|
||||||
},
|
},
|
||||||
prismaSchema() {
|
prismaSchema() {
|
||||||
return "db/schema.prisma"
|
return 'db/schema.prisma'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import * as fs from "fs-extra"
|
import * as fs from 'fs-extra'
|
||||||
import j from "jscodeshift"
|
import j from 'jscodeshift'
|
||||||
import getBabelOptions, {Overrides} from "recast/parsers/_babel_options"
|
import getBabelOptions, { Overrides } from 'recast/parsers/_babel_options'
|
||||||
import * as babel from "recast/parsers/babel"
|
import * as babel from 'recast/parsers/babel'
|
||||||
import {Program} from "../types"
|
import { Program } from '../types'
|
||||||
|
|
||||||
export const customTsParser = {
|
export const customTsParser = {
|
||||||
parse(source: string, options?: Overrides) {
|
parse(source: string, options?: Overrides) {
|
||||||
const babelOptions = getBabelOptions(options)
|
const babelOptions = getBabelOptions(options)
|
||||||
babelOptions.plugins.push("typescript")
|
babelOptions.plugins.push('typescript')
|
||||||
babelOptions.plugins.push("jsx")
|
babelOptions.plugins.push('jsx')
|
||||||
return babel.parser.parse(source, babelOptions)
|
return babel.parser.parse(source, babelOptions)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TransformStatus {
|
export enum TransformStatus {
|
||||||
Success = "success",
|
Success = 'success',
|
||||||
Failure = "failure",
|
Failure = 'failure',
|
||||||
}
|
}
|
||||||
export interface TransformResult {
|
export interface TransformResult {
|
||||||
status: TransformStatus
|
status: TransformStatus
|
||||||
@@ -28,19 +28,22 @@ export type Transformer = (program: Program) => Program | Promise<Program>
|
|||||||
|
|
||||||
export function stringProcessFile(
|
export function stringProcessFile(
|
||||||
original: string,
|
original: string,
|
||||||
transformerFn: StringTransformer,
|
transformerFn: StringTransformer
|
||||||
): string | Promise<string> {
|
): string | Promise<string> {
|
||||||
return transformerFn(original)
|
return transformerFn(original)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processFile(original: string, transformerFn: Transformer): Promise<string> {
|
export async function processFile(
|
||||||
const program = j(original, {parser: customTsParser})
|
original: string,
|
||||||
|
transformerFn: Transformer
|
||||||
|
): Promise<string> {
|
||||||
|
const program = j(original, { parser: customTsParser })
|
||||||
return (await transformerFn(program)).toSource()
|
return (await transformerFn(program)).toSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function transform(
|
export async function transform(
|
||||||
processFile: (original: string) => Promise<string>,
|
processFile: (original: string) => Promise<string>,
|
||||||
targetFilePaths: string[],
|
targetFilePaths: string[]
|
||||||
): Promise<TransformResult[]> {
|
): Promise<TransformResult[]> {
|
||||||
const results: TransformResult[] = []
|
const results: TransformResult[] = []
|
||||||
for (const filePath of targetFilePaths) {
|
for (const filePath of targetFilePaths) {
|
||||||
@@ -53,7 +56,7 @@ export async function transform(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const fileBuffer = fs.readFileSync(filePath)
|
const fileBuffer = fs.readFileSync(filePath)
|
||||||
const fileSource = fileBuffer.toString("utf-8")
|
const fileSource = fileBuffer.toString('utf-8')
|
||||||
const transformedCode = await processFile(fileSource)
|
const transformedCode = await processFile(fileSource)
|
||||||
fs.writeFileSync(filePath, transformedCode)
|
fs.writeFileSync(filePath, transformedCode)
|
||||||
results.push({
|
results.push({
|
||||||
12
nextjs/packages/installer/src/utils/use-enter-to-continue.ts
Normal file
12
nextjs/packages/installer/src/utils/use-enter-to-continue.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useInput } from 'ink'
|
||||||
|
|
||||||
|
export function useEnterToContinue(
|
||||||
|
cb: Function,
|
||||||
|
additionalCondition: boolean = true
|
||||||
|
) {
|
||||||
|
useInput((_input, key) => {
|
||||||
|
if (additionalCondition && key.return) {
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
7
nextjs/packages/installer/src/utils/use-user-input.ts
Normal file
7
nextjs/packages/installer/src/utils/use-user-input.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { useStdin } from 'ink'
|
||||||
|
import { RecipeCLIFlags } from '../types'
|
||||||
|
|
||||||
|
export function useUserInput(cliFlags: RecipeCLIFlags) {
|
||||||
|
const { isRawModeSupported } = useStdin()
|
||||||
|
return isRawModeSupported && !cliFlags.yesToAll
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { spawn } from 'cross-spawn'
|
||||||
|
import { existsSync } from 'fs-extra'
|
||||||
|
import { mocked } from 'ts-jest/utils'
|
||||||
|
import * as AddDependencyExecutor from '../../src/executors/add-dependency-executor'
|
||||||
|
|
||||||
|
jest.mock('fs-extra')
|
||||||
|
jest.mock('cross-spawn')
|
||||||
|
|
||||||
|
describe('add dependency executor', () => {
|
||||||
|
const testConfiguration = {
|
||||||
|
stepId: 'addDependencies',
|
||||||
|
stepName: 'Add dependencies',
|
||||||
|
stepType: 'add-dependency',
|
||||||
|
explanation: 'This step will add some dependencies for testing purposes',
|
||||||
|
packages: [{ name: 'typescript', version: '4' }, { name: 'ts-node' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should properly identify executor', () => {
|
||||||
|
const wrongConfiguration = {
|
||||||
|
stepId: 'wrongStep',
|
||||||
|
stepName: 'Wrong Step',
|
||||||
|
stepType: 'wrong-type',
|
||||||
|
explanation: 'This step is wrong',
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
AddDependencyExecutor.isAddDependencyExecutor(wrongConfiguration)
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
AddDependencyExecutor.isAddDependencyExecutor(testConfiguration)
|
||||||
|
).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should choose proper package manager according to lock file', () => {
|
||||||
|
mocked(existsSync).mockReturnValueOnce(true)
|
||||||
|
expect(AddDependencyExecutor.getPackageManager()).toEqual('yarn')
|
||||||
|
expect(AddDependencyExecutor.getPackageManager()).toEqual('npm')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should issue proper commands according to the specified packages', async () => {
|
||||||
|
const mockedSpawn = mockSpawn()
|
||||||
|
mocked(spawn).mockImplementation(mockedSpawn.spawn as any)
|
||||||
|
|
||||||
|
// NPM
|
||||||
|
mocked(existsSync).mockReturnValue(false)
|
||||||
|
await AddDependencyExecutor.installPackages(
|
||||||
|
testConfiguration.packages,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
await AddDependencyExecutor.installPackages(
|
||||||
|
testConfiguration.packages,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Yarn
|
||||||
|
mocked(existsSync).mockReturnValue(true)
|
||||||
|
await AddDependencyExecutor.installPackages(
|
||||||
|
testConfiguration.packages,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
await AddDependencyExecutor.installPackages(
|
||||||
|
testConfiguration.packages,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedSpawn.calls.length).toEqual(4)
|
||||||
|
expect(mockedSpawn.calls[0]).toEqual(
|
||||||
|
'npm install --save-dev typescript@4 ts-node'
|
||||||
|
)
|
||||||
|
expect(mockedSpawn.calls[1]).toEqual('npm install typescript@4 ts-node')
|
||||||
|
expect(mockedSpawn.calls[2]).toEqual('yarn add -D typescript@4 ts-node')
|
||||||
|
expect(mockedSpawn.calls[3]).toEqual('yarn add typescript@4 ts-node')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primitive mock of spawn function
|
||||||
|
*/
|
||||||
|
const mockSpawn = () => {
|
||||||
|
let calls: string[] = []
|
||||||
|
|
||||||
|
return {
|
||||||
|
spawn: (command: string, args: string[], _: unknown = {}) => {
|
||||||
|
calls.push(`${command} ${args.join(' ')}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
on: (_: string, resolve: () => void) => resolve(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calls,
|
||||||
|
}
|
||||||
|
}
|
||||||
25
nextjs/packages/installer/test/executors/executor.test.tsx
Normal file
25
nextjs/packages/installer/test/executors/executor.test.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { render } from 'ink-testing-library'
|
||||||
|
import React from 'react'
|
||||||
|
import stripAnsi from 'strip-ansi'
|
||||||
|
import { Frontmatter } from '../../src/executors/executor'
|
||||||
|
|
||||||
|
describe('Executor', () => {
|
||||||
|
const executorConfig = {
|
||||||
|
stepId: 'newFile',
|
||||||
|
stepName: 'New File',
|
||||||
|
stepType: 'new-file',
|
||||||
|
explanation: 'Testing text for a new file',
|
||||||
|
}
|
||||||
|
it('should render Frontmatter', () => {
|
||||||
|
const { lastFrame } = render(<Frontmatter executor={executorConfig} />)
|
||||||
|
|
||||||
|
expect(stripAnsi(lastFrame())).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should contain a step name and explanation', () => {
|
||||||
|
const { frames } = render(<Frontmatter executor={executorConfig} />)
|
||||||
|
|
||||||
|
expect(frames[0].includes('New File')).toBeTruthy()
|
||||||
|
expect(frames[0].includes('Testing text for a new file')).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { render } from 'ink-testing-library'
|
||||||
|
import React from 'react'
|
||||||
|
import stripAnsi from 'strip-ansi'
|
||||||
|
import { Commit as PrintMessageExecutor } from '../../src/executors/print-message-executor'
|
||||||
|
|
||||||
|
describe('Executor', () => {
|
||||||
|
const executorConfig = {
|
||||||
|
stepId: 'printMessage',
|
||||||
|
stepName: 'Print message',
|
||||||
|
stepType: 'print-message',
|
||||||
|
explanation: 'Testing text for a print message',
|
||||||
|
message: 'My message',
|
||||||
|
}
|
||||||
|
it('should render PrintMessageExecutor', () => {
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<PrintMessageExecutor
|
||||||
|
cliArgs={null}
|
||||||
|
cliFlags={{ yesToAll: false }}
|
||||||
|
onChangeCommitted={() => {}}
|
||||||
|
step={executorConfig}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stripAnsi(lastFrame())).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should contain a step name and explanation', () => {
|
||||||
|
const { frames } = render(
|
||||||
|
<PrintMessageExecutor
|
||||||
|
cliArgs={null}
|
||||||
|
cliFlags={{ yesToAll: false }}
|
||||||
|
onChangeCommitted={() => {}}
|
||||||
|
step={executorConfig}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(frames[0].includes('My message')).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
36
nextjs/packages/installer/test/transforms/add-import.test.ts
Normal file
36
nextjs/packages/installer/test/transforms/add-import.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { addImport, customTsParser } from '@blitzjs/installer'
|
||||||
|
import j from 'jscodeshift'
|
||||||
|
|
||||||
|
function executeImport(
|
||||||
|
fileStr: string,
|
||||||
|
importStatement: j.ImportDeclaration
|
||||||
|
): string {
|
||||||
|
return addImport(
|
||||||
|
j(fileStr, { parser: customTsParser }),
|
||||||
|
importStatement
|
||||||
|
).toSource({ tabWidth: 60 })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('addImport transform', () => {
|
||||||
|
it('adds import at start of file with no imports present', () => {
|
||||||
|
const file = `export const truth = () => 42`
|
||||||
|
const importStatement = j.importDeclaration(
|
||||||
|
[j.importDefaultSpecifier(j.identifier('React'))],
|
||||||
|
j.literal('react')
|
||||||
|
)
|
||||||
|
expect(executeImport(file, importStatement)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds import at the end of all imports if imports are present', () => {
|
||||||
|
const file = `import React from 'react'
|
||||||
|
|
||||||
|
export default function Comp() {
|
||||||
|
return <div>hello world!</div>
|
||||||
|
}`
|
||||||
|
const importStatement = j.importDeclaration(
|
||||||
|
[],
|
||||||
|
j.literal('app/styles/app.css')
|
||||||
|
)
|
||||||
|
expect(executeImport(file, importStatement)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import {addPrismaEnum} from "@blitzjs/installer"
|
import { addPrismaEnum } from '@blitzjs/installer'
|
||||||
|
|
||||||
describe("addPrismaEnum", () => {
|
describe('addPrismaEnum', () => {
|
||||||
const subject = (source: string) =>
|
const subject = (source: string) =>
|
||||||
addPrismaEnum(source, {
|
addPrismaEnum(source, {
|
||||||
type: "enum",
|
type: 'enum',
|
||||||
name: "Role",
|
name: 'Role',
|
||||||
enumerators: [
|
enumerators: [
|
||||||
{type: "enumerator", name: "USER"},
|
{ type: 'enumerator', name: 'USER' },
|
||||||
{type: "enumerator", name: "ADMIN"},
|
{ type: 'enumerator', name: 'ADMIN' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates enum", async () => {
|
it('creates enum', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import {addPrismaField} from "@blitzjs/installer"
|
import { addPrismaField } from '@blitzjs/installer'
|
||||||
|
|
||||||
describe("addPrismaField", () => {
|
describe('addPrismaField', () => {
|
||||||
const subject = (source: string) =>
|
const subject = (source: string) =>
|
||||||
addPrismaField(source, "Project", {
|
addPrismaField(source, 'Project', {
|
||||||
type: "field",
|
type: 'field',
|
||||||
name: "name",
|
name: 'name',
|
||||||
fieldType: "String",
|
fieldType: 'String',
|
||||||
optional: false,
|
optional: false,
|
||||||
attributes: [{type: "attribute", kind: "field", name: "unique"}],
|
attributes: [{ type: 'attribute', kind: 'field', name: 'unique' }],
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates field", async () => {
|
it('creates field', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
@@ -24,7 +24,7 @@ model Project {
|
|||||||
expect(await subject(source)).toMatchSnapshot()
|
expect(await subject(source)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("skips if model is missing", async () => {
|
it('skips if model is missing', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import {addPrismaGenerator} from "@blitzjs/installer"
|
import { addPrismaGenerator } from '@blitzjs/installer'
|
||||||
|
|
||||||
describe("addPrismaGenerator", () => {
|
describe('addPrismaGenerator', () => {
|
||||||
const subject = (source: string) =>
|
const subject = (source: string) =>
|
||||||
addPrismaGenerator(source, {
|
addPrismaGenerator(source, {
|
||||||
type: "generator",
|
type: 'generator',
|
||||||
name: "nexusPrisma",
|
name: 'nexusPrisma',
|
||||||
assignments: [{type: "assignment", key: "provider", value: '"nexus-prisma"'}],
|
assignments: [
|
||||||
|
{ type: 'assignment', key: 'provider', value: '"nexus-prisma"' },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
it("adds generator and keeps existing generator", async () => {
|
it('adds generator and keeps existing generator', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
@@ -21,7 +23,7 @@ generator client {
|
|||||||
|
|
||||||
expect(await subject(source)).toMatchSnapshot()
|
expect(await subject(source)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
it("adds generator to file", async () => {
|
it('adds generator to file', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
@@ -31,7 +33,7 @@ datasource db {
|
|||||||
expect(await subject(source)).toMatchSnapshot()
|
expect(await subject(source)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("overwrites same generator", async () => {
|
it('overwrites same generator', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import {addPrismaModelAttribute} from "@blitzjs/installer"
|
import { addPrismaModelAttribute } from '@blitzjs/installer'
|
||||||
|
|
||||||
describe("addPrismaModelAttribute", () => {
|
describe('addPrismaModelAttribute', () => {
|
||||||
const subject = (source: string) =>
|
const subject = (source: string) =>
|
||||||
addPrismaModelAttribute(source, "Project", {
|
addPrismaModelAttribute(source, 'Project', {
|
||||||
type: "attribute",
|
type: 'attribute',
|
||||||
kind: "model",
|
kind: 'model',
|
||||||
name: "index",
|
name: 'index',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
type: "attributeArgument",
|
type: 'attributeArgument',
|
||||||
value: {
|
value: {
|
||||||
type: "array",
|
type: 'array',
|
||||||
args: ["name"],
|
args: ['name'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates index", async () => {
|
it('creates index', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
@@ -32,7 +32,7 @@ model Project {
|
|||||||
expect(await subject(source)).toMatchSnapshot()
|
expect(await subject(source)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("skips if model is missing", async () => {
|
it('skips if model is missing', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { addPrismaModel } from '@blitzjs/installer'
|
||||||
|
|
||||||
|
describe('addPrismaModel', () => {
|
||||||
|
const subject = (source: string) =>
|
||||||
|
addPrismaModel(source, {
|
||||||
|
type: 'model',
|
||||||
|
name: 'Project',
|
||||||
|
properties: [{ type: 'field', name: 'id', fieldType: 'String' }],
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates model', async () => {
|
||||||
|
const source = `
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = "file:./db.sqlite"
|
||||||
|
}`.trim()
|
||||||
|
|
||||||
|
expect(await subject(source)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
import {produceSchema} from "@blitzjs/installer"
|
import { produceSchema } from '@blitzjs/installer'
|
||||||
import fs from "fs"
|
import fs from 'fs'
|
||||||
import path from "path"
|
import path from 'path'
|
||||||
import {promisify} from "util"
|
import { promisify } from 'util'
|
||||||
const readFile = promisify(fs.readFile)
|
const readFile = promisify(fs.readFile)
|
||||||
|
|
||||||
describe("produceSchema", () => {
|
describe('produceSchema', () => {
|
||||||
const subject = (source: string) => produceSchema(source, () => {})
|
const subject = (source: string) => produceSchema(source, () => {})
|
||||||
|
|
||||||
let originalDatabaseUrl
|
let originalDatabaseUrl
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
originalDatabaseUrl = process.env.DATABASE_URL
|
originalDatabaseUrl = process.env.DATABASE_URL
|
||||||
process.env.DATABASE_URL ||= "file:./db.sqlite"
|
process.env.DATABASE_URL ||= 'file:./db.sqlite'
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
process.env.DATABASE_URL = originalDatabaseUrl
|
process.env.DATABASE_URL = originalDatabaseUrl
|
||||||
})
|
})
|
||||||
|
|
||||||
const fixturesDir = path.resolve(__dirname, "./fixtures")
|
const fixturesDir = path.resolve(__dirname, './fixtures')
|
||||||
fs.readdirSync(fixturesDir).forEach((file) => {
|
fs.readdirSync(fixturesDir).forEach((file) => {
|
||||||
it(`cleanly parses and serializes schema: [${file}]`, async () => {
|
it(`cleanly parses and serializes schema: [${file}]`, async () => {
|
||||||
const source = await readFile(path.resolve(fixturesDir, file), {encoding: "utf-8"})
|
const source = await readFile(path.resolve(fixturesDir, file), {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
expect(await subject(source)).toMatchSnapshot()
|
expect(await subject(source)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import {setPrismaDataSource} from "@blitzjs/installer"
|
import { setPrismaDataSource } from '@blitzjs/installer'
|
||||||
|
|
||||||
describe("setPrismaDataSource", () => {
|
describe('setPrismaDataSource', () => {
|
||||||
const subject = (source: string) =>
|
const subject = (source: string) =>
|
||||||
setPrismaDataSource(source, {
|
setPrismaDataSource(source, {
|
||||||
type: "datasource",
|
type: 'datasource',
|
||||||
name: "db",
|
name: 'db',
|
||||||
assignments: [
|
assignments: [
|
||||||
{type: "assignment", key: "provider", value: '"postgresql"'},
|
{ type: 'assignment', key: 'provider', value: '"postgresql"' },
|
||||||
{
|
{
|
||||||
type: "assignment",
|
type: 'assignment',
|
||||||
key: "url",
|
key: 'url',
|
||||||
value: {type: "function", name: "env", params: ['"DATABASE_URL"']},
|
value: { type: 'function', name: 'env', params: ['"DATABASE_URL"'] },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
it("sets datasource", async () => {
|
it('sets datasource', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
// comment up here
|
// comment up here
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ datasource db {
|
|||||||
expect(await subject(source)).toMatchSnapshot()
|
expect(await subject(source)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("adds datasource if missing", async () => {
|
it('adds datasource if missing', async () => {
|
||||||
const source = `
|
const source = `
|
||||||
// wow there is no datasource here
|
// wow there is no datasource here
|
||||||
`.trim()
|
`.trim()
|
||||||
@@ -2,21 +2,23 @@ import {
|
|||||||
customTsParser,
|
customTsParser,
|
||||||
transformBlitzConfig,
|
transformBlitzConfig,
|
||||||
TransformBlitzConfigCallback,
|
TransformBlitzConfigCallback,
|
||||||
} from "@blitzjs/installer"
|
} from '@blitzjs/installer'
|
||||||
import j from "jscodeshift"
|
import j from 'jscodeshift'
|
||||||
import type {Options as RecastOptions} from "recast"
|
import type { Options as RecastOptions } from 'recast'
|
||||||
|
|
||||||
const recastOptions: RecastOptions = {
|
const recastOptions: RecastOptions = {
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
arrayBracketSpacing: false,
|
arrayBracketSpacing: false,
|
||||||
objectCurlySpacing: false,
|
objectCurlySpacing: false,
|
||||||
quote: "single",
|
quote: 'single',
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeTransform = (fileStr: string, transform: TransformBlitzConfigCallback) =>
|
const executeTransform = (
|
||||||
transformBlitzConfig(j(fileStr, {parser: customTsParser}), transform)
|
fileStr: string,
|
||||||
|
transform: TransformBlitzConfigCallback
|
||||||
|
) => transformBlitzConfig(j(fileStr, { parser: customTsParser }), transform)
|
||||||
|
|
||||||
describe("transformBlitzConfig finds config", () => {
|
describe('transformBlitzConfig finds config', () => {
|
||||||
const CONFIG = `{testProp: 'found'}`
|
const CONFIG = `{testProp: 'found'}`
|
||||||
|
|
||||||
function findConfig(fileStr: string, equalTo = CONFIG): boolean {
|
function findConfig(fileStr: string, equalTo = CONFIG): boolean {
|
||||||
@@ -32,28 +34,28 @@ describe("transformBlitzConfig finds config", () => {
|
|||||||
return configStr === equalTo
|
return configStr === equalTo
|
||||||
}
|
}
|
||||||
|
|
||||||
it("simple module.exports", () => {
|
it('simple module.exports', () => {
|
||||||
const file = `
|
const file = `
|
||||||
module.exports = ${CONFIG}
|
module.exports = ${CONFIG}
|
||||||
`
|
`
|
||||||
expect(findConfig(file)).toBe(true)
|
expect(findConfig(file)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("different config object as a control", () => {
|
it('different config object as a control', () => {
|
||||||
const file = `
|
const file = `
|
||||||
module.exports = {other: false}
|
module.exports = {other: false}
|
||||||
`
|
`
|
||||||
expect(findConfig(file)).toBe(false)
|
expect(findConfig(file)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("simple module.exports", () => {
|
it('simple module.exports', () => {
|
||||||
const file = `
|
const file = `
|
||||||
module.exports = ${CONFIG}
|
module.exports = ${CONFIG}
|
||||||
`
|
`
|
||||||
expect(findConfig(file)).toBe(true)
|
expect(findConfig(file)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("inside a variable", () => {
|
it('inside a variable', () => {
|
||||||
const file = `
|
const file = `
|
||||||
const config = ${CONFIG}
|
const config = ${CONFIG}
|
||||||
|
|
||||||
@@ -62,21 +64,21 @@ describe("transformBlitzConfig finds config", () => {
|
|||||||
expect(findConfig(file)).toBe(true)
|
expect(findConfig(file)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("with a wrapper", () => {
|
it('with a wrapper', () => {
|
||||||
const file = `
|
const file = `
|
||||||
module.exports = withBundleAnalyzer(${CONFIG})
|
module.exports = withBundleAnalyzer(${CONFIG})
|
||||||
`
|
`
|
||||||
expect(findConfig(file)).toBe(true)
|
expect(findConfig(file)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("with an empty wrapper", () => {
|
it('with an empty wrapper', () => {
|
||||||
const file = `
|
const file = `
|
||||||
module.exports = withBundleAnalyzer()
|
module.exports = withBundleAnalyzer()
|
||||||
`
|
`
|
||||||
expect(findConfig(file)).toBe(false)
|
expect(findConfig(file)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("as a variable inside a wrapper", () => {
|
it('as a variable inside a wrapper', () => {
|
||||||
const file = `
|
const file = `
|
||||||
const config = ${CONFIG}
|
const config = ${CONFIG}
|
||||||
module.exports = withBundleAnalyzer(config)
|
module.exports = withBundleAnalyzer(config)
|
||||||
@@ -84,14 +86,14 @@ describe("transformBlitzConfig finds config", () => {
|
|||||||
expect(findConfig(file)).toBe(true)
|
expect(findConfig(file)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("nested wrapper", () => {
|
it('nested wrapper', () => {
|
||||||
const file = `
|
const file = `
|
||||||
module.exports = wrapper(wrapper(wrapper(${CONFIG})))
|
module.exports = wrapper(wrapper(wrapper(${CONFIG})))
|
||||||
`
|
`
|
||||||
expect(findConfig(file)).toBe(true)
|
expect(findConfig(file)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("wrapper inside a variable", () => {
|
it('wrapper inside a variable', () => {
|
||||||
const file = `
|
const file = `
|
||||||
const config = wrapper(${CONFIG})
|
const config = wrapper(${CONFIG})
|
||||||
module.exports = config
|
module.exports = config
|
||||||
@@ -99,7 +101,7 @@ describe("transformBlitzConfig finds config", () => {
|
|||||||
expect(findConfig(file)).toBe(true)
|
expect(findConfig(file)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("the very worst case", () => {
|
it('the very worst case', () => {
|
||||||
const file = `
|
const file = `
|
||||||
const config1 = wrapper(
|
const config1 = wrapper(
|
||||||
wrapper(${CONFIG}),
|
wrapper(${CONFIG}),
|
||||||
@@ -112,110 +114,123 @@ describe("transformBlitzConfig finds config", () => {
|
|||||||
expect(findConfig(file)).toBe(true)
|
expect(findConfig(file)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("create empty object on empty function", () => {
|
it('create empty object on empty function', () => {
|
||||||
const file = `
|
const file = `
|
||||||
module.exports = withBundleAnalyzer()
|
module.exports = withBundleAnalyzer()
|
||||||
`
|
`
|
||||||
expect(findConfig(file, "{}")).toBe(true)
|
expect(findConfig(file, '{}')).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("transformBlitzConfig transform", () => {
|
describe('transformBlitzConfig transform', () => {
|
||||||
it("module.exports", () => {
|
it('module.exports', () => {
|
||||||
const file = `module.exports = {}`
|
const file = `module.exports = {}`
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
executeTransform(file, (config) => {
|
executeTransform(file, (config) => {
|
||||||
config.properties.push(j.objectProperty(j.identifier("test"), j.booleanLiteral(true)))
|
config.properties.push(
|
||||||
|
j.objectProperty(j.identifier('test'), j.booleanLiteral(true))
|
||||||
|
)
|
||||||
return config
|
return config
|
||||||
}).toSource(recastOptions),
|
}).toSource(recastOptions)
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("empty file", () => {
|
it('empty file', () => {
|
||||||
const file = ""
|
const file = ''
|
||||||
|
|
||||||
expect(executeTransform(file, (config) => config).toSource(recastOptions)).toMatchSnapshot()
|
expect(
|
||||||
|
executeTransform(file, (config) => config).toSource(recastOptions)
|
||||||
|
).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("with wrapper", () => {
|
it('with wrapper', () => {
|
||||||
const file = `module.exports = withBundleAnalyzer({})`
|
const file = `module.exports = withBundleAnalyzer({})`
|
||||||
|
|
||||||
expect(executeTransform(file, (config) => config).toSource(recastOptions)).toMatchSnapshot()
|
expect(
|
||||||
|
executeTransform(file, (config) => config).toSource(recastOptions)
|
||||||
|
).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("with empty wrapper", () => {
|
it('with empty wrapper', () => {
|
||||||
const file = `module.exports = withBundleAnalyzer()`
|
const file = `module.exports = withBundleAnalyzer()`
|
||||||
|
|
||||||
expect(executeTransform(file, (config) => config).toSource(recastOptions)).toMatchSnapshot()
|
expect(
|
||||||
|
executeTransform(file, (config) => config).toSource(recastOptions)
|
||||||
|
).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("the config file from examples/auth", () => {
|
it('the config file from examples/auth', () => {
|
||||||
const file = [
|
const file = [
|
||||||
'import {sessionMiddleware, simpleRolesIsAuthorized} from "blitz"',
|
'import {sessionMiddleware, simpleRolesIsAuthorized} from "blitz"',
|
||||||
'import db from "db"',
|
'import db from "db"',
|
||||||
'const withBundleAnalyzer = require("@next/bundle-analyzer")({',
|
'const withBundleAnalyzer = require("@next/bundle-analyzer")({',
|
||||||
' enabled: process.env.ANALYZE === "true",',
|
' enabled: process.env.ANALYZE === "true",',
|
||||||
"})",
|
'})',
|
||||||
"",
|
'',
|
||||||
"module.exports = withBundleAnalyzer({",
|
'module.exports = withBundleAnalyzer({',
|
||||||
" middleware: [",
|
' middleware: [',
|
||||||
" sessionMiddleware({",
|
' sessionMiddleware({',
|
||||||
' cookiePrefix: "blitz-auth-example",',
|
' cookiePrefix: "blitz-auth-example",',
|
||||||
" isAuthorized: simpleRolesIsAuthorized,",
|
' isAuthorized: simpleRolesIsAuthorized,',
|
||||||
" // sessionExpiryMinutes: 4,",
|
' // sessionExpiryMinutes: 4,',
|
||||||
" getSession: (handle) => db.session.findFirst({where: {handle}}),",
|
' getSession: (handle) => db.session.findFirst({where: {handle}}),',
|
||||||
" }),",
|
' }),',
|
||||||
" ],",
|
' ],',
|
||||||
" cli: {",
|
' cli: {',
|
||||||
" clearConsoleOnBlitzDev: false,",
|
' clearConsoleOnBlitzDev: false,',
|
||||||
" },",
|
' },',
|
||||||
" log: {",
|
' log: {',
|
||||||
' // level: "trace",',
|
' // level: "trace",',
|
||||||
" },",
|
' },',
|
||||||
" experimental: {",
|
' experimental: {',
|
||||||
" initServer() {",
|
' initServer() {',
|
||||||
' console.log("Hello world from initServer")',
|
' console.log("Hello world from initServer")',
|
||||||
" },",
|
' },',
|
||||||
" },",
|
' },',
|
||||||
" /*",
|
' /*',
|
||||||
" webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => {",
|
' webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => {',
|
||||||
" // Note: we provide webpack above so you should not `require` it",
|
' // Note: we provide webpack above so you should not `require` it',
|
||||||
" // Perform customizations to webpack config",
|
' // Perform customizations to webpack config',
|
||||||
" // Important: return the modified config",
|
' // Important: return the modified config',
|
||||||
" return config",
|
' return config',
|
||||||
" },",
|
' },',
|
||||||
" webpackDevMiddleware: (config) => {",
|
' webpackDevMiddleware: (config) => {',
|
||||||
" // Perform customizations to webpack dev middleware config",
|
' // Perform customizations to webpack dev middleware config',
|
||||||
" // Important: return the modified config",
|
' // Important: return the modified config',
|
||||||
" return config",
|
' return config',
|
||||||
" },",
|
' },',
|
||||||
" */",
|
' */',
|
||||||
"})",
|
'})',
|
||||||
].join("\n")
|
].join('\n')
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
executeTransform(file, (config) => {
|
executeTransform(file, (config) => {
|
||||||
const cliValue = j.objectExpression([
|
const cliValue = j.objectExpression([
|
||||||
j.objectProperty(j.identifier("clearConsoleOnBlitzDev"), j.booleanLiteral(true)),
|
j.objectProperty(
|
||||||
|
j.identifier('clearConsoleOnBlitzDev'),
|
||||||
|
j.booleanLiteral(true)
|
||||||
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
const cliProp = config.properties.find(
|
const cliProp = config.properties.find(
|
||||||
(value) =>
|
(value) =>
|
||||||
value.type === "ObjectProperty" &&
|
value.type === 'ObjectProperty' &&
|
||||||
value.key.type === "Identifier" &&
|
value.key.type === 'Identifier' &&
|
||||||
value.key.name === "cli",
|
value.key.name === 'cli'
|
||||||
) as j.ObjectProperty | undefined
|
) as j.ObjectProperty | undefined
|
||||||
|
|
||||||
if (!cliProp) {
|
if (!cliProp) {
|
||||||
config.properties.push(j.objectProperty(j.identifier("cli"), cliValue))
|
config.properties.push(
|
||||||
|
j.objectProperty(j.identifier('cli'), cliValue)
|
||||||
|
)
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
cliProp.value = cliValue
|
cliProp.value = cliValue
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}).toSource(recastOptions),
|
}).toSource(recastOptions)
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
addBabelPlugin,
|
||||||
|
addBabelPreset,
|
||||||
|
customTsParser,
|
||||||
|
} from '@blitzjs/installer'
|
||||||
|
import j from 'jscodeshift'
|
||||||
|
|
||||||
|
function executeBabelPlugin(
|
||||||
|
fileStr: string,
|
||||||
|
plugin: string | [string, Object]
|
||||||
|
): string {
|
||||||
|
return addBabelPlugin(
|
||||||
|
j(fileStr, { parser: customTsParser }),
|
||||||
|
plugin
|
||||||
|
).toSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeBabelPreset(
|
||||||
|
fileStr: string,
|
||||||
|
plugin: string | [string, Object]
|
||||||
|
): string {
|
||||||
|
return addBabelPreset(
|
||||||
|
j(fileStr, { parser: customTsParser }),
|
||||||
|
plugin
|
||||||
|
).toSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('addBabelPlugin transform', () => {
|
||||||
|
it('adds babel plugin literal', () => {
|
||||||
|
const source = `module.exports = {
|
||||||
|
presets: ["@babel/preset-typescript"],
|
||||||
|
plugins: [],
|
||||||
|
}`
|
||||||
|
|
||||||
|
expect(executeBabelPlugin(source, '@emotion')).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds babel plugin array', () => {
|
||||||
|
const source = `module.exports = {
|
||||||
|
presets: ["@babel/preset-typescript"],
|
||||||
|
plugins: [],
|
||||||
|
}`
|
||||||
|
|
||||||
|
expect(
|
||||||
|
executeBabelPlugin(source, [
|
||||||
|
'@babel/plugin-proposal-decorators',
|
||||||
|
{ legacy: true },
|
||||||
|
])
|
||||||
|
).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('avoid duplicated', () => {
|
||||||
|
const source = `module.exports = {
|
||||||
|
presets: ["@babel/preset-typescript"],
|
||||||
|
plugins: ["@babel/plugin-proposal-decorators"],
|
||||||
|
}`
|
||||||
|
|
||||||
|
expect(
|
||||||
|
executeBabelPlugin(source, [
|
||||||
|
'@babel/plugin-proposal-decorators',
|
||||||
|
{ legacy: true },
|
||||||
|
])
|
||||||
|
).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addBabelPreset transform', () => {
|
||||||
|
it('adds babel preset literal', () => {
|
||||||
|
const source = `module.exports = {
|
||||||
|
presets: ["@babel/preset-typescript"],
|
||||||
|
plugins: [],
|
||||||
|
}`
|
||||||
|
|
||||||
|
expect(executeBabelPreset(source, 'blitz/babel')).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds babel preset array', () => {
|
||||||
|
const source = `module.exports = {
|
||||||
|
presets: ["@babel/preset-typescript"],
|
||||||
|
plugins: [],
|
||||||
|
}`
|
||||||
|
|
||||||
|
expect(
|
||||||
|
executeBabelPreset(source, ['blitz/babel', { legacy: true }])
|
||||||
|
).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('avoid duplicated', () => {
|
||||||
|
const source = `module.exports = {
|
||||||
|
presets: [["blitz/babel", {legacy: true}]],
|
||||||
|
plugins: [],
|
||||||
|
}`
|
||||||
|
|
||||||
|
expect(executeBabelPreset(source, 'blitz/babel')).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
||||||
28
nextjs/packages/installer/test/utils/paths.test.ts
Normal file
28
nextjs/packages/installer/test/utils/paths.test.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { paths } from '@blitzjs/installer'
|
||||||
|
import * as fs from 'fs-extra'
|
||||||
|
|
||||||
|
jest.mock('fs-extra')
|
||||||
|
|
||||||
|
const testIfNotWindows = process.platform === 'win32' ? test.skip : test
|
||||||
|
|
||||||
|
describe('path utils', () => {
|
||||||
|
it('returns proper file paths in a TS project', () => {
|
||||||
|
fs.existsSync.mockReturnValue(true)
|
||||||
|
expect(paths.document()).toBe('app/pages/_document.tsx')
|
||||||
|
expect(paths.app()).toBe('app/pages/_app.tsx')
|
||||||
|
expect(paths.entry()).toBe('app/pages/index.tsx')
|
||||||
|
// Blitz and Babel configs are always JS, we shouldn't transform this extension
|
||||||
|
expect(paths.blitzConfig()).toBe('blitz.config.ts')
|
||||||
|
expect(paths.babelConfig()).toBe('babel.config.js')
|
||||||
|
})
|
||||||
|
|
||||||
|
// SKIP test because the fs mock is failing on windows
|
||||||
|
testIfNotWindows('returns proper file paths in a JS project', () => {
|
||||||
|
fs.existsSync.mockReturnValue(false)
|
||||||
|
expect(paths.document()).toBe('app/pages/_document.js')
|
||||||
|
expect(paths.app()).toBe('app/pages/_app.js')
|
||||||
|
expect(paths.entry()).toBe('app/pages/index.js')
|
||||||
|
expect(paths.blitzConfig()).toBe('blitz.config.js')
|
||||||
|
expect(paths.babelConfig()).toBe('babel.config.js')
|
||||||
|
})
|
||||||
|
})
|
||||||
3
nextjs/packages/next-codemod/jest.config.js
Normal file
3
nextjs/packages/next-codemod/jest.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: '../../../jest-unit.config.js',
|
||||||
|
}
|
||||||
0
nextjs/packages/next-codemod/jest.setup.js
Normal file
0
nextjs/packages/next-codemod/jest.setup.js
Normal file
@@ -1,5 +1,5 @@
|
|||||||
export default (class extends React.Component {
|
export default class extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const test = this.props.url
|
const test = this.props.url
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|||||||
1719
nextjs/yarn.lock
1719
nextjs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
|||||||
"nextjs",
|
"nextjs",
|
||||||
"nextjs/packages/next",
|
"nextjs/packages/next",
|
||||||
"nextjs/packages/next-mdx",
|
"nextjs/packages/next-mdx",
|
||||||
|
"nextjs/packages/installer",
|
||||||
"nextjs/packages/eslint-config-next",
|
"nextjs/packages/eslint-config-next",
|
||||||
"nextjs/packages/eslint-plugin-next",
|
"nextjs/packages/eslint-plugin-next",
|
||||||
"nextjs/packages/next-env",
|
"nextjs/packages/next-env",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"preconstruct": {
|
"preconstruct": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
"nextjs/packages/installer",
|
||||||
"!packages/cli",
|
"!packages/cli",
|
||||||
"!packages/eslint-config"
|
"!packages/eslint-config"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
preset: "../../jest-unit.config.js",
|
|
||||||
}
|
|
||||||
@@ -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,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,52 +0,0 @@
|
|||||||
import {render} from "ink"
|
|
||||||
import {baseLogger} from "next/dist/server/lib/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({displayDateTime: false, displayLogLevel: false}).silly(
|
|
||||||
`\n🎉 The ${this.options.name} recipe has been installed!\n`,
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
baseLogger({displayDateTime: false}).error(e as any)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import type {ExpressionKind} from "ast-types/gen/kinds"
|
|
||||||
import j from "jscodeshift"
|
|
||||||
import {Program} from "../types"
|
|
||||||
import {transformBlitzConfig} from "."
|
|
||||||
|
|
||||||
export const addBlitzMiddleware = (program: Program, middleware: ExpressionKind): Program =>
|
|
||||||
transformBlitzConfig(program, (config) => {
|
|
||||||
// Locate the middleware property
|
|
||||||
const middlewareProp = config.properties.find(
|
|
||||||
(value) =>
|
|
||||||
value.type === "ObjectProperty" &&
|
|
||||||
value.key.type === "Identifier" &&
|
|
||||||
value.key.name === "middleware",
|
|
||||||
) as j.ObjectProperty | undefined
|
|
||||||
|
|
||||||
if (middlewareProp && middlewareProp.value.type === "ArrayExpression") {
|
|
||||||
// We found it, pop on our middleware.
|
|
||||||
middlewareProp.value.elements.push(middleware)
|
|
||||||
} else {
|
|
||||||
// No middleware prop, add our own.
|
|
||||||
config.properties.push(
|
|
||||||
j.property("init", j.identifier("middleware"), {
|
|
||||||
type: "ArrayExpression",
|
|
||||||
elements: [middleware],
|
|
||||||
loc: null,
|
|
||||||
comments: null,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
})
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import j from "jscodeshift"
|
|
||||||
import {Program} from "../types"
|
|
||||||
|
|
||||||
export const findModuleExportsExpressions = (program: Program) =>
|
|
||||||
program.find(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,7 +0,0 @@
|
|||||||
export * from "./add-import"
|
|
||||||
export * from "./add-blitz-middleware"
|
|
||||||
export * from "./find-module-exports-expressions"
|
|
||||||
export * from "./prisma"
|
|
||||||
export * from "./transform-blitz-config"
|
|
||||||
export * from "./update-babel-config"
|
|
||||||
export * from "./wrap-blitz-config"
|
|
||||||
@@ -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,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,73 +0,0 @@
|
|||||||
import {spawn} from "cross-spawn"
|
|
||||||
import {existsSync} from "fs-extra"
|
|
||||||
import {mocked} from "ts-jest/utils"
|
|
||||||
import * as AddDependencyExecutor from "../../src/executors/add-dependency-executor"
|
|
||||||
|
|
||||||
jest.mock("fs-extra")
|
|
||||||
jest.mock("cross-spawn")
|
|
||||||
|
|
||||||
describe("add dependency executor", () => {
|
|
||||||
const testConfiguration = {
|
|
||||||
stepId: "addDependencies",
|
|
||||||
stepName: "Add dependencies",
|
|
||||||
stepType: "add-dependency",
|
|
||||||
explanation: "This step will add some dependencies for testing purposes",
|
|
||||||
packages: [{name: "typescript", version: "4"}, {name: "ts-node"}],
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should properly identify executor", () => {
|
|
||||||
const wrongConfiguration = {
|
|
||||||
stepId: "wrongStep",
|
|
||||||
stepName: "Wrong Step",
|
|
||||||
stepType: "wrong-type",
|
|
||||||
explanation: "This step is wrong",
|
|
||||||
}
|
|
||||||
expect(AddDependencyExecutor.isAddDependencyExecutor(wrongConfiguration)).toBeFalsy()
|
|
||||||
expect(AddDependencyExecutor.isAddDependencyExecutor(testConfiguration)).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should choose proper package manager according to lock file", () => {
|
|
||||||
mocked(existsSync).mockReturnValueOnce(true)
|
|
||||||
expect(AddDependencyExecutor.getPackageManager()).toEqual("yarn")
|
|
||||||
expect(AddDependencyExecutor.getPackageManager()).toEqual("npm")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should issue proper commands according to the specified packages", async () => {
|
|
||||||
const mockedSpawn = mockSpawn()
|
|
||||||
mocked(spawn).mockImplementation(mockedSpawn.spawn as any)
|
|
||||||
|
|
||||||
// NPM
|
|
||||||
mocked(existsSync).mockReturnValue(false)
|
|
||||||
await AddDependencyExecutor.installPackages(testConfiguration.packages, true)
|
|
||||||
await AddDependencyExecutor.installPackages(testConfiguration.packages, false)
|
|
||||||
|
|
||||||
// Yarn
|
|
||||||
mocked(existsSync).mockReturnValue(true)
|
|
||||||
await AddDependencyExecutor.installPackages(testConfiguration.packages, true)
|
|
||||||
await AddDependencyExecutor.installPackages(testConfiguration.packages, false)
|
|
||||||
|
|
||||||
expect(mockedSpawn.calls.length).toEqual(4)
|
|
||||||
expect(mockedSpawn.calls[0]).toEqual("npm install --save-dev typescript@4 ts-node")
|
|
||||||
expect(mockedSpawn.calls[1]).toEqual("npm install typescript@4 ts-node")
|
|
||||||
expect(mockedSpawn.calls[2]).toEqual("yarn add -D typescript@4 ts-node")
|
|
||||||
expect(mockedSpawn.calls[3]).toEqual("yarn add typescript@4 ts-node")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Primitive mock of spawn function
|
|
||||||
*/
|
|
||||||
const mockSpawn = () => {
|
|
||||||
let calls: string[] = []
|
|
||||||
|
|
||||||
return {
|
|
||||||
spawn: (command: string, args: string[], _: unknown = {}) => {
|
|
||||||
calls.push(`${command} ${args.join(" ")}`)
|
|
||||||
|
|
||||||
return {
|
|
||||||
on: (_: string, resolve: () => void) => resolve(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calls,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import {render} from "ink-testing-library"
|
|
||||||
import React from "react"
|
|
||||||
import stripAnsi from "strip-ansi"
|
|
||||||
import {Frontmatter} from "../../src/executors/executor"
|
|
||||||
|
|
||||||
describe("Executor", () => {
|
|
||||||
const executorConfig = {
|
|
||||||
stepId: "newFile",
|
|
||||||
stepName: "New File",
|
|
||||||
stepType: "new-file",
|
|
||||||
explanation: "Testing text for a new file",
|
|
||||||
}
|
|
||||||
it("should render Frontmatter", () => {
|
|
||||||
const {lastFrame} = render(<Frontmatter executor={executorConfig} />)
|
|
||||||
|
|
||||||
expect(stripAnsi(lastFrame())).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should contain a step name and explanation", () => {
|
|
||||||
const {frames} = render(<Frontmatter executor={executorConfig} />)
|
|
||||||
|
|
||||||
expect(frames[0].includes("New File")).toBeTruthy()
|
|
||||||
expect(frames[0].includes("Testing text for a new file")).toBeTruthy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import {render} from "ink-testing-library"
|
|
||||||
import React from "react"
|
|
||||||
import stripAnsi from "strip-ansi"
|
|
||||||
import {Commit as PrintMessageExecutor} from "../../src/executors/print-message-executor"
|
|
||||||
|
|
||||||
describe("Executor", () => {
|
|
||||||
const executorConfig = {
|
|
||||||
stepId: "printMessage",
|
|
||||||
stepName: "Print message",
|
|
||||||
stepType: "print-message",
|
|
||||||
explanation: "Testing text for a print message",
|
|
||||||
message: "My message",
|
|
||||||
}
|
|
||||||
it("should render PrintMessageExecutor", () => {
|
|
||||||
const {lastFrame} = render(
|
|
||||||
<PrintMessageExecutor
|
|
||||||
cliArgs={null}
|
|
||||||
cliFlags={{yesToAll: false}}
|
|
||||||
onChangeCommitted={() => {}}
|
|
||||||
step={executorConfig}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(stripAnsi(lastFrame())).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should contain a step name and explanation", () => {
|
|
||||||
const {frames} = render(
|
|
||||||
<PrintMessageExecutor
|
|
||||||
cliArgs={null}
|
|
||||||
cliFlags={{yesToAll: false}}
|
|
||||||
onChangeCommitted={() => {}}
|
|
||||||
step={executorConfig}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(frames[0].includes("My message")).toBeTruthy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {addImport, customTsParser} from "@blitzjs/installer"
|
|
||||||
import j from "jscodeshift"
|
|
||||||
|
|
||||||
function executeImport(fileStr: string, importStatement: j.ImportDeclaration): string {
|
|
||||||
return addImport(j(fileStr, {parser: customTsParser}), importStatement).toSource({tabWidth: 60})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("addImport transform", () => {
|
|
||||||
it("adds import at start of file with no imports present", () => {
|
|
||||||
const file = `export const truth = () => 42`
|
|
||||||
const importStatement = j.importDeclaration(
|
|
||||||
[j.importDefaultSpecifier(j.identifier("React"))],
|
|
||||||
j.literal("react"),
|
|
||||||
)
|
|
||||||
expect(executeImport(file, importStatement)).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("adds import at the end of all imports if imports are present", () => {
|
|
||||||
const file = `import React from 'react'
|
|
||||||
|
|
||||||
export default function Comp() {
|
|
||||||
return <div>hello world!</div>
|
|
||||||
}`
|
|
||||||
const importStatement = j.importDeclaration([], j.literal("app/styles/app.css"))
|
|
||||||
expect(executeImport(file, importStatement)).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import {addPrismaModel} from "@blitzjs/installer"
|
|
||||||
|
|
||||||
describe("addPrismaModel", () => {
|
|
||||||
const subject = (source: string) =>
|
|
||||||
addPrismaModel(source, {
|
|
||||||
type: "model",
|
|
||||||
name: "Project",
|
|
||||||
properties: [{type: "field", name: "id", fieldType: "String"}],
|
|
||||||
})
|
|
||||||
|
|
||||||
it("creates model", async () => {
|
|
||||||
const source = `
|
|
||||||
datasource db {
|
|
||||||
provider = "sqlite"
|
|
||||||
url = "file:./db.sqlite"
|
|
||||||
}`.trim()
|
|
||||||
|
|
||||||
expect(await subject(source)).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import {addBabelPlugin, addBabelPreset, customTsParser} from "@blitzjs/installer"
|
|
||||||
import j from "jscodeshift"
|
|
||||||
|
|
||||||
function executeBabelPlugin(fileStr: string, plugin: string | [string, Object]): string {
|
|
||||||
return addBabelPlugin(j(fileStr, {parser: customTsParser}), plugin).toSource()
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeBabelPreset(fileStr: string, plugin: string | [string, Object]): string {
|
|
||||||
return addBabelPreset(j(fileStr, {parser: customTsParser}), plugin).toSource()
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("addBabelPlugin transform", () => {
|
|
||||||
it("adds babel plugin literal", () => {
|
|
||||||
const source = `module.exports = {
|
|
||||||
presets: ["@babel/preset-typescript"],
|
|
||||||
plugins: [],
|
|
||||||
}`
|
|
||||||
|
|
||||||
expect(executeBabelPlugin(source, "@emotion")).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("adds babel plugin array", () => {
|
|
||||||
const source = `module.exports = {
|
|
||||||
presets: ["@babel/preset-typescript"],
|
|
||||||
plugins: [],
|
|
||||||
}`
|
|
||||||
|
|
||||||
expect(
|
|
||||||
executeBabelPlugin(source, ["@babel/plugin-proposal-decorators", {legacy: true}]),
|
|
||||||
).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("avoid duplicated", () => {
|
|
||||||
const source = `module.exports = {
|
|
||||||
presets: ["@babel/preset-typescript"],
|
|
||||||
plugins: ["@babel/plugin-proposal-decorators"],
|
|
||||||
}`
|
|
||||||
|
|
||||||
expect(
|
|
||||||
executeBabelPlugin(source, ["@babel/plugin-proposal-decorators", {legacy: true}]),
|
|
||||||
).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("addBabelPreset transform", () => {
|
|
||||||
it("adds babel preset literal", () => {
|
|
||||||
const source = `module.exports = {
|
|
||||||
presets: ["@babel/preset-typescript"],
|
|
||||||
plugins: [],
|
|
||||||
}`
|
|
||||||
|
|
||||||
expect(executeBabelPreset(source, "blitz/babel")).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("adds babel preset array", () => {
|
|
||||||
const source = `module.exports = {
|
|
||||||
presets: ["@babel/preset-typescript"],
|
|
||||||
plugins: [],
|
|
||||||
}`
|
|
||||||
|
|
||||||
expect(executeBabelPreset(source, ["blitz/babel", {legacy: true}])).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("avoid duplicated", () => {
|
|
||||||
const source = `module.exports = {
|
|
||||||
presets: [["blitz/babel", {legacy: true}]],
|
|
||||||
plugins: [],
|
|
||||||
}`
|
|
||||||
|
|
||||||
expect(executeBabelPreset(source, "blitz/babel")).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import {paths} from "@blitzjs/installer"
|
|
||||||
import * as fs from "fs-extra"
|
|
||||||
|
|
||||||
jest.mock("fs-extra")
|
|
||||||
|
|
||||||
const testIfNotWindows = process.platform === "win32" ? test.skip : test
|
|
||||||
|
|
||||||
describe("path utils", () => {
|
|
||||||
it("returns proper file paths in a TS project", () => {
|
|
||||||
fs.existsSync.mockReturnValue(true)
|
|
||||||
expect(paths.document()).toBe("app/pages/_document.tsx")
|
|
||||||
expect(paths.app()).toBe("app/pages/_app.tsx")
|
|
||||||
expect(paths.entry()).toBe("app/pages/index.tsx")
|
|
||||||
// Blitz and Babel configs are always JS, we shouldn't transform this extension
|
|
||||||
expect(paths.blitzConfig()).toBe("blitz.config.ts")
|
|
||||||
expect(paths.babelConfig()).toBe("babel.config.js")
|
|
||||||
})
|
|
||||||
|
|
||||||
// SKIP test because the fs mock is failing on windows
|
|
||||||
testIfNotWindows("returns proper file paths in a JS project", () => {
|
|
||||||
fs.existsSync.mockReturnValue(false)
|
|
||||||
expect(paths.document()).toBe("app/pages/_document.js")
|
|
||||||
expect(paths.app()).toBe("app/pages/_app.js")
|
|
||||||
expect(paths.entry()).toBe("app/pages/index.js")
|
|
||||||
expect(paths.blitzConfig()).toBe("blitz.config.js")
|
|
||||||
expect(paths.babelConfig()).toBe("babel.config.js")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
"packages/blitz/src/**/*",
|
"packages/blitz/src/**/*",
|
||||||
"packages/display/src/**/*",
|
"packages/display/src/**/*",
|
||||||
"packages/generator/src/**/*",
|
"packages/generator/src/**/*",
|
||||||
"packages/installer/src/**/*",
|
|
||||||
"packages/repl/src/**/*",
|
"packages/repl/src/**/*",
|
||||||
"packages/server/src/**/*",
|
"packages/server/src/**/*",
|
||||||
"recipes/*"
|
"recipes/*",
|
||||||
|
"nextjs/packages/installer/src/**/*"
|
||||||
],
|
],
|
||||||
"exclude": ["*.test.ts", "*.test.tsx"]
|
"exclude": ["*.test.ts", "*.test.tsx"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user