1
0
mirror of synced 2026-02-03 18:01:02 -05:00

Compare commits

...

4 Commits

Author SHA1 Message Date
Dillon Raphael
377fd5c0e8 init route manifest generator 2022-04-06 17:08:32 -04:00
Dillon Raphael
5c7fc7d1f2 WIP: migrate legacy new cli command 2022-04-04 21:44:13 -04:00
Dillon Raphael
8b8a36ab5d remove pnpm global link artifacts 2022-04-04 15:38:23 -04:00
Dillon Raphael
c1dd5cd38f WIP: init cli + init env utils 2022-04-04 14:54:05 -04:00
24 changed files with 1337 additions and 19 deletions

View File

@@ -4,4 +4,7 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
module.exports = withBundleAnalyzer({
reactStrictMode: true,
experimental: {
esmExternals: "loose",
},
})

View File

@@ -1,4 +1,4 @@
const PageWithRedirect = () => {
const PageWithAuthRedirect = () => {
return (
<div>
{JSON.stringify(
@@ -13,6 +13,6 @@ const PageWithRedirect = () => {
)
}
PageWithRedirect.redirectAuthenticatedTo = "/"
PageWithAuthRedirect.redirectAuthenticatedTo = "/"
export default PageWithRedirect
export default PageWithAuthRedirect

View File

@@ -14,8 +14,8 @@ export const getStaticProps = gSP(async ({ctx}) => {
}
})
function Page({data}) {
function PageWithGsp({data}) {
return <div>{JSON.stringify(data, null, 2)}</div>
}
export default Page
export default PageWithGsp

View File

@@ -16,8 +16,8 @@ export const getServerSideProps = gSSP<Props>(async ({ctx}) => {
}
})
function Page(props: Props) {
function PageWithGssp(props: Props) {
return <div>{JSON.stringify(props, null, 2)}</div>
}
export default Page
export default PageWithGssp

View File

@@ -1,6 +1,6 @@
import {useAuthenticatedSession} from "../src/client-setup"
export default function PgaeWithUseAuthorizeIf() {
export default function PageWithUseAuthSession() {
useAuthenticatedSession()
return <div>This page is using useAuthenticatedSession</div>
}

View File

@@ -1,6 +1,6 @@
import {useAuthorizeIf} from "../src/client-setup"
export default function PgaeWithUseAuthorizeIf() {
export default function PageWithUseAuthorizeIf() {
useAuthorizeIf(Math.random() > 0.5)
return <div>This page is using useAuthorizeIf</div>
}

View File

@@ -1,6 +1,6 @@
import {useRedirectAuthenticated} from "../src/client-setup"
export default function PgaeWithUseAuthorizeIf() {
export default function PageWithUseRedirectAuth() {
useRedirectAuthenticated("/")
return <div>This page is using useRedirectAuthenticated</div>
}

View File

@@ -1,7 +1,7 @@
const PageWithRedirect = ({data}) => {
const PageWithoutFlicker = ({data}) => {
return <div>{JSON.stringify(data)}</div>
}
PageWithRedirect.suppressFirstRenderFlicker = true
PageWithoutFlicker.suppressFirstRenderFlicker = true
export default PageWithRedirect
export default PageWithoutFlicker

View File

@@ -21,6 +21,7 @@
],
"dependencies": {
"debug": "4.3.3",
"fs-extra": "^9.1.0",
"react-query": "3.34.12"
},
"devDependencies": {

View File

@@ -102,6 +102,7 @@ const setupClient = <TPlugins extends readonly ClientPlugin<object>[]>({
// todo: finish this
// Used to build BlitzPage type
const types = {} as {plugins: typeof plugins}
/*@ts-ignore */
return {
types,

View File

@@ -73,3 +73,5 @@ export const setupBlitz = ({plugins}: SetupBlitzOptions) => {
return {gSSP, gSP, api}
}
export * from "./routes-manifest"

View File

@@ -0,0 +1,514 @@
import {join} from "path"
import os from "os"
import {promises} from "fs"
const readFile = promises.readFile
import {outputFile} from "fs-extra"
export const CONFIG_FILE = ".blitz.config.compiled.js"
export const NEXT_CONFIG_FILE = "next.config.js"
export const PHASE_PRODUCTION_SERVER = "phase-production-server"
// Because on Windows absolute paths in the generated code can break because of numbers, eg 1 in the path,
// we have to use a private alias
export const PAGES_DIR_ALIAS = "private-next-pages"
/* Fetch next.js config */
export const VALID_LOADERS = ["default", "imgix", "cloudinary", "akamai", "custom"] as const
export type LoaderValue = typeof VALID_LOADERS[number]
export type ImageConfig = {
deviceSizes: number[]
imageSizes: number[]
loader: LoaderValue
path: string
domains?: string[]
disableStaticImages?: boolean
minimumCacheTTL?: number
}
export const imageConfigDefault: ImageConfig = {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
path: "/_next/image",
loader: "default",
domains: [],
disableStaticImages: false,
minimumCacheTTL: 60,
}
export const defaultConfig: any = {
env: {},
webpack: null,
webpackDevMiddleware: null,
distDir: ".next",
cleanDistDir: true,
assetPrefix: "",
configOrigin: "default",
useFileSystemPublicRoutes: true,
generateBuildId: () => null,
generateEtags: true,
pageExtensions: ["tsx", "ts", "jsx", "js"],
target: "server",
poweredByHeader: true,
compress: true,
analyticsId: process.env.VERCEL_ANALYTICS_ID || "",
images: imageConfigDefault,
devIndicators: {
buildActivity: true,
},
onDemandEntries: {
maxInactiveAge: 60 * 1000,
pagesBufferLength: 2,
},
amp: {
canonicalBase: "",
},
basePath: "",
sassOptions: {},
trailingSlash: false,
i18n: null,
productionBrowserSourceMaps: false,
optimizeFonts: true,
log: {
level: "info",
},
webpack5: Number(process.env.NEXT_PRIVATE_TEST_WEBPACK4_MODE) > 0 ? false : undefined,
excludeDefaultMomentLocales: true,
serverRuntimeConfig: {},
publicRuntimeConfig: {},
reactStrictMode: false,
httpAgentOptions: {
keepAlive: true,
},
experimental: {
swcLoader: false,
swcMinify: false,
cpus: Math.max(
1,
(Number(process.env.CIRCLE_NODE_TOTAL) || (os.cpus() || {length: 1}).length) - 1,
),
plugins: false,
profiling: false,
isrFlushToDisk: true,
workerThreads: false,
pageEnv: false,
optimizeImages: false,
optimizeCss: false,
scrollRestoration: false,
stats: false,
externalDir: false,
disableOptimizedLoading: false,
gzipSize: true,
craCompat: false,
esmExternals: false,
staticPageGenerationTimeout: 60,
pageDataCollectionTimeout: 60,
// default to 50MB limit
isrMemoryCacheSize: 50 * 1024 * 1024,
concurrentFeatures: false,
},
future: {
strictPostcssConfiguration: false,
},
}
export function assignDefaultsBase(userConfig: {[key: string]: any}) {
const config = Object.keys(userConfig).reduce<{[key: string]: any}>((currentConfig, key) => {
const value = userConfig[key]
if (value === undefined || value === null) {
return currentConfig
}
// Copied from assignDefaults in server/config.ts
if (!!value && value.constructor === Object) {
currentConfig[key] = {
...defaultConfig[key],
...Object.keys(value).reduce<any>((c, k) => {
const v = value[k]
if (v !== undefined && v !== null) {
c[k] = v
}
return c
}, {}),
}
} else {
currentConfig[key] = value
}
return currentConfig
}, {})
const result = {...defaultConfig, ...config}
return result
}
const normalizeConfig = (phase: string, config: any) => {
if (typeof config === "function") {
config = config(phase, {defaultConfig})
if (typeof config.then === "function") {
throw new Error(
"> Promise returned in blitz config. https://nextjs.org/docs/messages/promise-in-next-config",
)
}
}
return config
}
const loadConfig = (pagesDir: string) => {
let userConfigModule
try {
const path = join(pagesDir, NEXT_CONFIG_FILE)
// eslint-disable-next-line no-eval -- block webpack from following this module path
userConfigModule = eval("require")(path)
} catch {
console.log("Did not find custom config file")
// In case user does not have custom config
userConfigModule = {}
}
let userConfig = normalizeConfig(
PHASE_PRODUCTION_SERVER,
userConfigModule.default || userConfigModule,
)
return assignDefaultsBase(userConfig) as any
}
/* Find Routes */
export const topLevelFoldersThatMayContainPages = ["pages", "src", "app", "integrations"]
export function getIsRpcFile(filePathFromAppRoot: string) {
return (
/[\\/]queries[\\/]/.test(filePathFromAppRoot) || /[\\/]mutations[\\/]/.test(filePathFromAppRoot)
)
}
export function getIsPageFile(filePathFromAppRoot: string) {
return (
/[\\/]pages[\\/]/.test(filePathFromAppRoot) ||
/[\\/]api[\\/]/.test(filePathFromAppRoot) ||
getIsRpcFile(filePathFromAppRoot)
)
}
export async function recursiveFindPages(
dir: string,
filter: RegExp,
ignore?: RegExp,
arr: string[] = [],
rootDir: string = dir,
): Promise<string[]> {
let folders = await promises.readdir(dir)
if (dir === rootDir) {
folders = folders.filter((folder) => topLevelFoldersThatMayContainPages.includes(folder))
}
await Promise.all(
folders.map(async (part: string) => {
const absolutePath = join(dir, part)
if (ignore && ignore.test(part)) return
const pathStat = await promises.stat(absolutePath)
if (pathStat.isDirectory()) {
await recursiveFindPages(absolutePath, filter, ignore, arr, rootDir)
return
}
if (!filter.test(part)) {
return
}
const relativeFromRoot = absolutePath.replace(rootDir, "")
if (getIsPageFile(relativeFromRoot)) {
arr.push(relativeFromRoot)
return
}
}),
)
return arr.sort()
}
export function buildPageExtensionRegex(pageExtensions: string[]) {
return new RegExp(`(?<!\\.test|\\.spec)\\.(?:${pageExtensions.join("|")})$`)
}
type PagesMapping = {
[page: string]: string
}
export function convertPageFilePathToRoutePath(filePath: string, pageExtensions: string[]) {
return filePath
.replace(/^.*?[\\/]pages[\\/]/, "/")
.replace(/^.*?[\\/]api[\\/]/, "/api/")
.replace(/^.*?[\\/]queries[\\/]/, "/api/rpc/")
.replace(/^.*?[\\/]mutations[\\/]/, "/api/rpc/")
.replace(new RegExp(`\\.+(${pageExtensions.join("|")})$`), "")
}
export function createPagesMapping(pagePaths: string[], pageExtensions: string[]) {
const previousPages: PagesMapping = {}
const pages = pagePaths.reduce((result: PagesMapping, pagePath): PagesMapping => {
let page = `${convertPageFilePathToRoutePath(pagePath, pageExtensions).replace(
/\\/g,
"/",
)}`.replace(/\/index$/, "")
let pageKey = page === "" ? "/" : page
if (pageKey in result) {
console.warn(
`Duplicate page detected. ${previousPages[pageKey]} and ${pagePath} both resolve to ${pageKey}.`,
)
} else {
previousPages[pageKey] = pagePath
}
result[pageKey] = join(PAGES_DIR_ALIAS, pagePath).replace(/\\/g, "/")
return result
}, {})
pages["/_app"] = pages["/_app"] || "next/dist/pages/_app"
pages["/_error"] = pages["/_error"] || "next/dist/pages/_error"
pages["/_document"] = pages["/_document"] || "next/dist/pages/_document"
return pages
}
export function collectPages(directory: string, pageExtensions: string[]): Promise<string[]> {
return recursiveFindPages(directory, buildPageExtensionRegex(pageExtensions))
}
function getVerb(type: string) {
switch (type) {
case "api":
return "*"
case "rpc":
return "post"
default:
return "get"
}
}
const apiPathRegex = /([\\/]api[\\/])/
export async function collectAllRoutes(directory: string, config: any) {
const routeFiles = await collectPages(directory, config.pageExtensions!)
const rawRouteMappings = createPagesMapping(routeFiles, config.pageExtensions!)
const routes = []
for (const [route, filePath] of Object.entries(rawRouteMappings)) {
if (["/_app", "/_document", "/_error"].includes(route)) continue
let type
if (getIsRpcFile(filePath)) {
type = "rpc"
} else if (apiPathRegex.test(filePath)) {
type = "api"
} else {
type = "page"
}
routes.push({
filePath: filePath.replace("private-next-pages/", ""),
route,
type,
verb: getVerb(type),
})
}
return routes
}
function dedupeBy<T>(arr: [string, T][], by: (v: [string, T]) => string): [string, T][] {
const allKeys = arr.map(by)
const countKeys = allKeys.reduce(
(obj, key) => ({...obj, [key]: (obj[key] || 0) + 1}),
{} as {[key: string]: number},
)
const duplicateKeys = Object.keys(countKeys).filter((key) => countKeys[key]! > 1)
if (duplicateKeys.length) {
duplicateKeys.forEach((key) => {
let errorMessage = `The page component is named "${key}" on the following routes:\n\n`
arr
.filter((v) => by(v) === key)
.forEach(([route]) => {
errorMessage += `\t${route}\n`
})
console.error(errorMessage)
})
console.error(
"The page component must have a unique name across all routes, so change the component names so they are all unique.\n",
)
// Don't throw error in internal monorepo development because existing nextjs
// integration tests all have duplicate page names
if (process.env.NODE_ENV === "production") {
const error = Error("Duplicate Page Name")
delete error.stack
throw error
}
}
return arr.filter((v) => !duplicateKeys.includes(by(v)))
}
type Parameter = {
name: string
optional: boolean
}
interface RouteManifestEntry {
name: string
parameters: Parameter[]
multipleParameters: Parameter[]
mdx?: boolean
}
export function setupManifest(routes: Record<string, RouteManifestEntry>): {
implementation: string
declaration: string
} {
const routesWithoutDuplicates = dedupeBy(Object.entries(routes), ([_path, {name}]) => name)
const implementationLines = routesWithoutDuplicates.map(
([path, {name}]) => `${name}: (query) => ({ pathname: "${path}", query })`,
)
const declarationLines = routesWithoutDuplicates.map(
([_path, {name, parameters, multipleParameters}]) => {
if (parameters.length === 0 && multipleParameters.length === 0) {
return `${name}(query?: ParsedUrlQueryInput): RouteUrlObject`
}
return `${name}(query: { ${[
...parameters.map(
(param) => param.name + (param.optional ? "?" : "") + ": string | number",
),
...multipleParameters.map(
(param) => param.name + (param.optional ? "?" : "") + ": (string | number)[]",
),
].join("; ")} } & ParsedUrlQueryInput): RouteUrlObject`
},
)
const declarationEnding = declarationLines.length > 0 ? ";" : ""
return {
implementation:
"exports.Routes = {\n" + implementationLines.map((line) => " " + line).join(",\n") + "\n}",
declaration: `
import type { ParsedUrlQueryInput } from "querystring"
import type { RouteUrlObject } from "@blitzjs/auth"
export const Routes: {
${declarationLines.map((line) => " " + line).join(";\n") + declarationEnding}
}`.trim(),
}
}
function removeSquareBracketsFromSegments(value: string): string
function removeSquareBracketsFromSegments(value: string[]): string[]
function removeSquareBracketsFromSegments(value: string | string[]): string | string[] {
if (typeof value === "string") {
return value.replace("[", "").replace("]", "")
}
return value.map((val) => val.replace("[", "").replace("]", ""))
}
function partition(arr: any[], predicate: (value: any) => boolean) {
if (!Array.isArray(arr)) {
throw new Error("expected first argument to be an array")
}
if (typeof predicate != "function") {
throw new Error("expected second argument to be a function")
}
var first = []
var second = []
var length = arr.length
for (var i = 0; i < length; i++) {
var nextValue = arr[i]
if (predicate(nextValue)) {
first.push(nextValue)
} else {
second.push(nextValue)
}
}
return [first, second]
}
const squareBracketsRegex = /\[\[.*?\]\]|\[.*?\]/g
export function parseParametersFromRoute(
path: string,
): Pick<RouteManifestEntry, "parameters" | "multipleParameters"> {
const parameteredSegments = path.match(squareBracketsRegex) ?? []
const withoutBrackets = removeSquareBracketsFromSegments(parameteredSegments)
const [multipleParameters, parameters] = partition(withoutBrackets, (p) => p.includes("..."))
return {
parameters: parameters!.map((value) => {
const containsSquareBrackets = squareBracketsRegex.test(value)
if (containsSquareBrackets) {
return {
name: removeSquareBracketsFromSegments(value),
optional: true,
}
}
return {
name: value,
optional: false,
}
}),
multipleParameters: multipleParameters!.map((param) => {
const withoutEllipsis = param.replace("...", "")
const containsSquareBrackets = squareBracketsRegex.test(withoutEllipsis)
if (containsSquareBrackets) {
return {
name: removeSquareBracketsFromSegments(withoutEllipsis),
optional: true,
}
}
return {
name: withoutEllipsis,
optional: false,
}
}),
}
}
const pascalCase = (value: string): string => {
const val = value.replace(/[-_\s/.]+(.)?/g, (_match, chr) => (chr ? chr.toUpperCase() : ""))
return val.substr(0, 1).toUpperCase() + val.substr(1)
}
export function parseDefaultExportName(contents: string): string | null {
const result = contents.match(/export\s+default(?:\s+(?:const|let|class|var|function))?\s+(\w+)/)
if (!result) {
return null
}
return result[1] ?? null
}
export async function generateManifest() {
const config = await loadConfig(process.cwd())
const allRoutes = await collectAllRoutes(process.cwd(), config)
const routes: Record<string, RouteManifestEntry> = {}
for (let {filePath, route, type} of allRoutes) {
if (type === "api" || type === "rpc") continue
if (/\.mdx$/.test(filePath)) {
routes[route] = {
...parseParametersFromRoute(route),
name: route === "/" ? "Index" : pascalCase(route),
mdx: true,
}
} else {
const fileContents = await readFile(join(process.cwd(), filePath), {
encoding: "utf-8",
})
const defaultExportName = parseDefaultExportName(fileContents)
if (!defaultExportName) continue
routes[route] = {
...parseParametersFromRoute(route),
name: defaultExportName,
}
}
}
const {declaration, implementation} = setupManifest(routes)
const dotBlitz = join(process.cwd(), ".blitz")
await outputFile(join(dotBlitz, "index.js"), implementation, {
encoding: "utf-8",
})
await outputFile(join(dotBlitz, "index-browser.js"), implementation, {
encoding: "utf-8",
})
await outputFile(join(dotBlitz, "index.d.ts"), declaration, {
encoding: "utf-8",
})
}
export const findBlitzConfigDirectory = async () => {
let blitzDir = await promises.readdir(join(process.cwd(), ".blitz"))
console.log(blitzDir)
return blitzDir
}

View File

@@ -1,7 +1,7 @@
import {BuildConfig} from "unbuild"
const config: BuildConfig = {
entries: ["./src/index-browser", "./src/index-server"],
entries: ["./src/index-browser", "./src/index-server", "./src/cli/index"],
externals: [
"index-browser.cjs",
"index-browser.mjs",

View File

@@ -18,12 +18,22 @@
"files": [
"dist/**"
],
"bin": {
"blitz": "dist/index.cjs"
},
"dependencies": {
"@blitzjs/generator": "workspace:*",
"@blitzjs/next": "workspace:*",
"arg": "5.0.1",
"chalk": "^4.1.0",
"console-table-printer": "2.10.0",
"cross-spawn": "7.0.3",
"dotenv": "16.0.0",
"dotenv-expand": "8.0.3",
"hasbin": "1.2.3",
"npm-which": "3.0.1",
"ora": "5.3.0",
"prompts": "2.4.2",
"superjson": "1.8.0",
"tslog": "3.3.1"
},
@@ -32,15 +42,20 @@
"@types/cookie": "0.4.1",
"@types/cross-spawn": "6.0.2",
"@types/express": "4.17.13",
"@types/hasbin": "1.2.0",
"@types/node-fetch": "2.6.1",
"@types/npm-which": "3.0.1",
"@types/prompts": "2.0.14",
"@types/react": "17.0.43",
"@types/react-dom": "17.0.14",
"@types/test-listen": "1.1.0",
"express": "4.17.3",
"http": "0.0.1-security",
"node-fetch": "3.2.3",
"p-event": "5.0.1",
"pkg-dir": "6.0.1",
"react": "18.0.0",
"resolve-cwd": "3.0.0",
"test-listen": "1.1.0",
"typescript": "^4.5.3",
"unbuild": "0.6.9",

View File

@@ -0,0 +1,9 @@
import {cliCommand} from "../index"
/* @ts-ignore */
import {generateManifest} from "@blitzjs/next"
const codegen: cliCommand = async () => {
await generateManifest()
}
export {codegen}

View File

@@ -0,0 +1,7 @@
import {cliCommand} from "../index"
const dev: cliCommand = (argv) => {
console.log("dev hit")
}
export {dev}

View File

@@ -0,0 +1,302 @@
import {loadEnvConfig} from "../../env-utils"
import prompts from "prompts"
import path from "path"
import chalk from "chalk"
import hasbin from "hasbin"
import {cliCommand} from "../index"
import arg from "arg"
import {AppGenerator, AppGeneratorOptions, getLatestVersion} from "@blitzjs/generator"
import {runPrisma} from "../../prisma-utils"
enum TForms {
"react-final-form" = "React Final Form",
"react-hook-form" = "React Hook Form",
"formik" = "Formik",
}
enum TLanguage {
"typescript" = "TypeScript",
"javascript" = "Javascript",
}
type TPkgManager = "npm" | "yarn" | "pnpm"
enum TTemplate {
"full" = "full",
"minimal" = "minimal",
}
const templates: {[key in TTemplate]: AppGeneratorOptions["template"]} = {
full: {
path: "app",
},
minimal: {
path: "minimalapp",
skipForms: true,
skipDatabase: true,
},
}
const IS_YARN_INSTALLED = hasbin.sync("yarn")
const IS_PNPM_INSTALLED = hasbin.sync("pnpm")
const PREFERABLE_PKG_MANAGER: TPkgManager = IS_PNPM_INSTALLED
? "pnpm"
: IS_YARN_INSTALLED
? "yarn"
: "npm"
const args = arg(
{
// Types
"--name": String,
"--npm": Boolean,
"--yarn": Boolean,
"--pnpm": Boolean,
"--form": String,
"--language": String,
"--template": String,
"--skip-install": Boolean,
"--dry-run": Boolean,
"--no-git": Boolean,
"--skip-upgrade": Boolean,
},
{
permissive: true,
},
)
let projectName: string = ""
let projectPath: string = ""
let projectLanguage: string | TLanguage = ""
let projectFormLib: AppGeneratorOptions["form"] = undefined
let projectTemplate: AppGeneratorOptions["template"] = templates.full
let projectPkgManger: TPkgManager = PREFERABLE_PKG_MANAGER
let shouldInstallDeps: boolean = true
const determineProjectName = async () => {
if (!args["--name"]) {
const res = await prompts({
type: "text",
name: "name",
message: "What would you like to name your project?",
initial: "blitz-app",
})
projectName = res.name.trim().replaceAll(" ", "-")
projectPath = path.resolve(projectName)
} else {
projectName = args["--name"]
projectPath = path.resolve(projectName)
}
}
const determineLanguage = async () => {
// Check if language from flag is valid
if (
!args["--language"] ||
(args["--language"] && !Object.keys(TLanguage).includes(args["--language"].toLowerCase()))
) {
const res = await prompts({
type: "select",
name: "language",
message: "Pick which language you'd like to use for your new blitz project",
initial: 0,
choices: Object.entries(TLanguage).map((c) => {
return {title: c[1], value: c[1]}
}),
})
projectLanguage = res.language
} else {
projectLanguage = args["--language"]
}
}
const determineFormLib = async () => {
// Check if form from flag is valid
if (!args["--form"] || (args["--form"] && !Object.keys(TForms).includes(args["--form"]))) {
const res = await prompts({
type: "select",
name: "form",
message: "Pick which form you'd like to use for your new blitz project",
initial: 0,
choices: Object.entries(TForms).map((c) => {
return {title: c[1], value: c[1]}
}),
})
projectFormLib = res.form
} else {
switch (args["--form"]) {
case "react-final-form":
projectFormLib = TForms["react-final-form"]
case "react-hook-form":
projectFormLib = TForms["react-hook-form"]
case "formik":
projectFormLib = TForms["formik"]
}
}
}
const determineTemplate = async () => {
// Check if template from flag is valid
if (
!args["--template"] ||
(args["--template"] && !Object.keys(TTemplate).includes(args["--template"].toLowerCase()))
) {
const res = await prompts({
type: "select",
name: "template",
message: "Pick which template you'd like to use for your new blitz project",
initial: 0,
choices: Object.entries(TTemplate).map((c) => {
return {title: c[1], value: c[1]}
}),
})
projectTemplate = templates[res.template as TTemplate]
} else {
projectTemplate = templates[args["--template"] as TTemplate]
}
}
const determinePkgManagerToInstallDeps = async () => {
if (args["--skip-install"]) {
shouldInstallDeps = false
return
}
const isPkgManagerSpecifiedAsFlag = args["--npm"] || args["--yarn"] || args["--pnpm"]
if (isPkgManagerSpecifiedAsFlag) {
if (args["--npm"]) {
projectPkgManger = "npm"
} else if (args["--pnpm"]) {
if (IS_PNPM_INSTALLED) {
projectPkgManger = "pnpm"
} else {
console.warn(`Pnpm is not installed. Fallback to ${projectPkgManger}`)
}
} else if (args["--yarn"]) {
if (IS_YARN_INSTALLED) {
projectPkgManger = "yarn"
} else {
console.warn(`Yarn is not installed. Fallback to ${projectPkgManger}`)
}
}
} else {
const hasPkgManagerChoice = IS_YARN_INSTALLED || IS_PNPM_INSTALLED
if (hasPkgManagerChoice) {
const res = await prompts({
type: "select",
name: "pkgManager",
message: "Install dependencies?",
initial: 0,
choices: [
{title: "npm"},
{title: "yarn", disabled: !IS_YARN_INSTALLED},
{title: "pnpm", disabled: !IS_PNPM_INSTALLED},
{title: "skip"},
],
})
if (res.pkgManager === "skip") {
shouldInstallDeps = false
} else {
shouldInstallDeps = res.pkgManager
}
} else {
const res = await prompts({
type: "confirm",
name: "installDeps",
message: "Install dependencies?",
initial: true,
})
shouldInstallDeps = res.installDeps
}
}
}
const newApp: cliCommand = async (argv) => {
const shouldUpgrade = !args["--skip-upgrade"]
if (shouldUpgrade) {
//TODO: Handle checking for updates
}
await determineProjectName()
await determineLanguage()
await determineTemplate()
await determinePkgManagerToInstallDeps()
if (!projectTemplate.skipForms) {
await determineFormLib()
}
try {
const latestBlitzVersion = (await getLatestVersion("blitz")).value
const requireManualInstall = args["--dry-run"] || !shouldInstallDeps
const postInstallSteps = args["--name"] === "." ? [] : [`cd ${projectName}`]
const generatorOpts: AppGeneratorOptions = {
template: projectTemplate,
destinationRoot: projectPath,
appName: projectName,
useTs: projectLanguage === "TypeScript",
yarn: projectPkgManger === "yarn",
pnpm: projectPkgManger === "pnpm",
dryRun: args["--dry-run"] ? args["--dry-run"] : false,
skipGit: args["--no-git"] ? args["--no-git"] : false,
skipInstall: !shouldInstallDeps,
version: latestBlitzVersion,
form: projectFormLib,
onPostInstall: async () => {
if (projectTemplate.skipDatabase) {
return
}
try {
// loadEnvConfig is required in order for DATABASE_URL to be available
// don't print info logs from loadEnvConfig for clear output
loadEnvConfig(
process.cwd(),
undefined,
{error: console.error, info: () => {}},
{ignoreCache: true},
)
const result = await runPrisma(["migrate", "dev", "--name", "Initial migration"], true)
if (!result.success) throw new Error()
} catch (error) {
postInstallSteps.push(
"blitz prisma migrate dev (when asked, you can name the migration anything)",
)
}
},
}
const generator = new AppGenerator(generatorOpts)
console.log(`Hang tight while we set up your new Blitz app!`)
await generator.run()
if (requireManualInstall) {
let cmd
switch (projectPkgManger) {
case "yarn":
cmd = "yarn"
case "npm":
cmd = "npm install"
case "pnpm":
cmd = "pnpm install"
}
postInstallSteps.push(cmd)
postInstallSteps.push(
"blitz prisma migrate dev (when asked, you can name the migration anything)",
)
}
postInstallSteps.push("blitz dev")
console.log("Your new Blitz app is ready! Next steps:")
postInstallSteps.forEach((step, index) => {
console.log(chalk.yellow(` ${index + 1}. ${step}`))
})
console.log("") // new line
} catch (error) {
console.error(error)
}
}
export {newApp}

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env node
import {NON_STANDARD_NODE_ENV} from "./utils/constants"
import arg from "arg"
import packageJson from "../../package.json"
const defaultCommand = "dev"
export type cliCommand = (argv?: string[]) => void
const commands: {[command: string]: () => Promise<cliCommand>} = {
dev: () => import("./commands/dev").then((i) => i.dev),
new: () => import("./commands/new").then((i) => i.newApp),
codegen: () => import("./commands/codegen").then((i) => i.codegen),
}
const args = arg(
{
// Types
"--version": Boolean,
"--help": Boolean,
"--inspect": Boolean,
"--env": String,
// Aliases
"-v": "--version",
"-h": "--help",
"-e": "--env",
},
{
permissive: true,
},
)
// Version is inlined into the file using taskr build pipeline
if (args["--version"]) {
console.log(`Blitz.js v${packageJson.version}`)
process.exit(0)
}
const foundCommand = Boolean(commands[args._[0] as string])
if (!foundCommand && args["--help"]) {
console.log(`
Usage
$ blitz <command>
Available commands
${Object.keys(commands).join(", ")}
Options
--env, -e App environment name
--version, -v Version number
--help, -h Displays this message
For more information run a command with the --help flag
$ blitz build --help
`)
process.exit(0)
}
const command = foundCommand ? (args._[0] as string) : defaultCommand
const forwardedArgs = foundCommand ? args._.slice(1) : args._
// Don't check for react or react-dom when running blitz new
if (command !== "new") {
;["react", "react-dom"].forEach((dependency) => {
try {
// When 'npm link' is used it checks the clone location. Not the project.
require.resolve(dependency)
} catch (err) {
console.warn(
`The module '${dependency}' was not found. Blitz.js requires that you include it in 'dependencies' of your 'package.json'. To add it, run 'npm install ${dependency}'`,
)
}
})
}
if (args["--help"]) {
forwardedArgs.push("--help")
}
if (args["--env"]) {
process.env.APP_ENV = args["--env"]
}
const defaultEnv = command === "dev" ? "development" : "production"
const standardEnv = ["production", "development", "test"]
if (process.env.NODE_ENV && !standardEnv.includes(process.env.NODE_ENV)) {
console.warn(NON_STANDARD_NODE_ENV)
}
;(process.env as any).NODE_ENV = process.env.NODE_ENV || defaultEnv
// Make sure commands gracefully respect termination signals (e.g. from Docker)
process.on("SIGTERM", () => process.exit(0))
process.on("SIGINT", () => process.exit(0))
commands[command]?.()
.then((exec: any) => exec(forwardedArgs))
.then(() => {
if (command === "build") {
// ensure process exits after build completes so open handles/connections
// don't cause process to hang
process.exit(0)
}
})
.catch((err) => {
console.log(err)
})
if (command === "dev") {
const {watchFile} = require("fs")
watchFile(`${process.cwd()}/blitz.config.js`, (cur: any, prev: any) => {
if (cur.size > 0 || prev.size > 0) {
console.log(
`\n> Found a change in blitz.config.js. Restart the server to see the changes in effect.`,
)
}
})
watchFile(`${process.cwd()}/blitz.config.ts`, (cur: any, prev: any) => {
if (cur.size > 0 || prev.size > 0) {
console.log(
`\n> Found a change in blitz.config.ts. Restart the server to see the changes in effect.`,
)
}
})
}

View File

@@ -0,0 +1 @@
export const NON_STANDARD_NODE_ENV = `You are using a non-standard "NODE_ENV" value in your environment. This creates inconsistencies in the project and is strongly advised against. Read more: https://nextjs.org/docs/messages/non-standard-node-env`

View File

@@ -0,0 +1,107 @@
import * as fs from "fs"
import * as path from "path"
import * as dotenv from "dotenv"
import dotenvExpand from "dotenv-expand"
export type Env = {[key: string]: string}
export type LoadedEnvFiles = Array<{
path: string
contents: string
}>
let combinedEnv: Env | undefined = undefined
let cachedLoadedEnvFiles: LoadedEnvFiles = []
type Log = {
info: (...args: any[]) => void
error: (...args: any[]) => void
}
export function processEnv(loadedEnvFiles: LoadedEnvFiles, dir?: string, log: Log = console) {
// don't reload env if we already have since this breaks escaped
// environment values e.g. \$ENV_FILE_KEY
if (process.env.__NEXT_PROCESSED_ENV || loadedEnvFiles.length === 0) {
return process.env as Env
}
// flag that we processed the environment values in case a serverless
// function is re-used or we are running in `next start` mode
process.env.__NEXT_PROCESSED_ENV = "true"
const origEnv = Object.assign({}, process.env)
const parsed: dotenv.DotenvParseOutput = {}
for (const envFile of loadedEnvFiles) {
try {
let result: dotenv.DotenvConfigOutput = {}
result.parsed = dotenv.parse(envFile.contents)
result = dotenvExpand.expand(result)
if (result.parsed) {
log.info(`Loaded env from ${path.join(dir || "", envFile.path)}`)
}
for (const key of Object.keys(result.parsed || {})) {
if (typeof parsed[key] === "undefined" && typeof origEnv[key] === "undefined") {
parsed[key] = result.parsed?.[key]!
}
}
} catch (err) {
log.error(`Failed to load env from ${path.join(dir || "", envFile.path)}`, err)
}
}
return Object.assign(process.env, parsed)
}
export function loadEnvConfig(
dir: string = process.cwd(),
_dev?: boolean,
log: Log = console,
{ignoreCache} = {ignoreCache: false},
): {
combinedEnv: Env
loadedEnvFiles: LoadedEnvFiles
} {
// don't reload env if we already have since this breaks escaped
// environment values e.g. \$ENV_FILE_KEY
if (combinedEnv && !ignoreCache) return {combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles}
const appEnv = process.env.APP_ENV ?? process.env.NODE_ENV ?? "development"
let dotenvFiles = [
`.env.${appEnv}.local`,
`.env.${appEnv}`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
appEnv !== "test" && `.env.local`,
".env",
].filter(Boolean) as string[]
for (const envFile of dotenvFiles) {
// only load .env if the user provided has an env config file
const dotEnvPath = path.join(dir, envFile)
try {
const stats = fs.statSync(dotEnvPath)
// make sure to only attempt to read files
if (!stats.isFile()) {
continue
}
const contents = fs.readFileSync(dotEnvPath, "utf8")
cachedLoadedEnvFiles.push({
path: envFile,
contents,
})
} catch (err: any) {
if (err.code !== "ENOENT") {
log.error(`Failed to load env from ${envFile}`, err)
}
}
}
combinedEnv = processEnv(cachedLoadedEnvFiles, dir, log)
return {combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles}
}

View File

@@ -1,5 +1,6 @@
import {spawn} from "cross-spawn"
import which from "npm-which"
import {Readable} from "stream"
export interface Constructor<T = unknown> {
new (...args: never[]): T
@@ -52,3 +53,26 @@ export const enhancePrisma = <TPrismaClientCtor extends Constructor>(
},
})
}
export const runPrisma = async (args: string[], silent = false) => {
const prismaBin = which(process.cwd()).sync("prisma")
const cp = spawn(prismaBin, args, {
stdio: silent ? "pipe" : "inherit",
env: process.env,
})
const cp_stderr: string[] = []
if (silent) {
cp?.stderr?.on("data", (chunk: Readable) => {
cp_stderr.push(chunk.toString())
})
}
const code = await require("p-event")(cp, "exit", {rejectionEvents: []})
return {
success: code === 0,
stderr: silent ? cp_stderr.join("") : undefined,
}
}

View File

@@ -2,7 +2,8 @@
"extends": "@blitzjs/config/tsconfig.library.json",
"compilerOptions": {
"lib": ["DOM", "ES2015"],
"esModuleInterop": true
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]

View File

@@ -9,10 +9,10 @@
"test-watch": "vitest",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"main": "./dist/index-server.cjs",
"module": "./dist/index-server.mjs",
"browser": "./dist/index-browser.mjs",
"types": "./dist/index-server.d.ts",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"browser": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"sideEffects": false,
"license": "MIT",
"files": [
@@ -54,6 +54,7 @@
"@types/vinyl": "2.0.6",
"@typescript-eslint/eslint-plugin": "5.9.1",
"@typescript-eslint/parser": "5.9.1",
"debug": "4.3.3",
"react": "18.0.0",
"unbuild": "0.6.9",
"watch": "1.0.2"

208
pnpm-lock.yaml generated
View File

@@ -65,23 +65,35 @@ importers:
packages/blitz:
specifiers:
"@blitzjs/config": workspace:*
"@blitzjs/generator": workspace:*
"@blitzjs/next": workspace:*
"@types/cookie": 0.4.1
"@types/cross-spawn": 6.0.2
"@types/express": 4.17.13
"@types/hasbin": 1.2.0
"@types/node-fetch": 2.6.1
"@types/npm-which": 3.0.1
"@types/prompts": 2.0.14
"@types/react": 17.0.43
"@types/react-dom": 17.0.14
"@types/test-listen": 1.1.0
arg: 5.0.1
chalk: ^4.1.0
console-table-printer: 2.10.0
cross-spawn: 7.0.3
dotenv: 16.0.0
dotenv-expand: 8.0.3
express: 4.17.3
hasbin: 1.2.3
http: 0.0.1-security
node-fetch: 3.2.3
npm-which: 3.0.1
ora: 5.3.0
p-event: 5.0.1
pkg-dir: 6.0.1
prompts: 2.4.2
react: 18.0.0
resolve-cwd: 3.0.0
superjson: 1.8.0
test-listen: 1.1.0
tslog: 3.3.1
@@ -90,11 +102,18 @@ importers:
watch: 1.0.2
zod: 3.10.1
dependencies:
"@blitzjs/generator": link:../generator
"@blitzjs/next": link:../blitz-next
arg: 5.0.1
chalk: 4.1.2
console-table-printer: 2.10.0
cross-spawn: 7.0.3
dotenv: 16.0.0
dotenv-expand: 8.0.3
hasbin: 1.2.3
npm-which: 3.0.1
ora: 5.3.0
prompts: 2.4.2
superjson: 1.8.0
tslog: 3.3.1
devDependencies:
@@ -102,15 +121,20 @@ importers:
"@types/cookie": 0.4.1
"@types/cross-spawn": 6.0.2
"@types/express": 4.17.13
"@types/hasbin": 1.2.0
"@types/node-fetch": 2.6.1
"@types/npm-which": 3.0.1
"@types/prompts": 2.0.14
"@types/react": 17.0.43
"@types/react-dom": 17.0.14
"@types/test-listen": 1.1.0
express: 4.17.3
http: 0.0.1-security
node-fetch: 3.2.3
p-event: 5.0.1
pkg-dir: 6.0.1
react: 18.0.0
resolve-cwd: 3.0.0
test-listen: 1.1.0
typescript: 4.5.5
unbuild: 0.6.9
@@ -184,6 +208,7 @@ importers:
"@types/react-dom": 17.0.14
blitz: workspace:*
debug: 4.3.3
fs-extra: ^9.1.0
next: 12.1.4
react: 18.0.0
react-dom: 18.0.0
@@ -193,6 +218,7 @@ importers:
watch: 1.0.2
dependencies:
debug: 4.3.3
fs-extra: 9.1.0
react-query: 3.34.12_react-dom@18.0.0+react@18.0.0
devDependencies:
"@blitzjs/config": link:../config
@@ -255,6 +281,7 @@ importers:
"@typescript-eslint/parser": 5.9.1
chalk: ^4.1.0
cross-spawn: 7.0.3
debug: 4.3.3
diff: 5.0.0
enquirer: 2.3.6
fs-extra: ^9.1.0
@@ -306,6 +333,7 @@ importers:
"@types/vinyl": 2.0.6
"@typescript-eslint/eslint-plugin": 5.9.1_8c696421d8bcc701d2ea1a734127ded5
"@typescript-eslint/parser": 5.9.1_eslint@7.32.0
debug: 4.3.3
react: 18.0.0
unbuild: 0.6.9
watch: 1.0.2
@@ -1858,6 +1886,13 @@ packages:
"@types/minimatch": 3.0.5
"@types/node": 17.0.16
/@types/hasbin/1.2.0:
resolution:
{
integrity: sha512-QhPPTycu+tr/RnGA4mvv+4P1Vebmq9TGEbDvBS9WjPT1pW7dheWeXXWcxb9zJ+YC38LbO8mwVW/DP+FwBroFKw==,
}
dev: true
/@types/http-cache-semantics/4.0.1:
resolution:
{
@@ -1999,6 +2034,15 @@ packages:
}
dev: true
/@types/prompts/2.0.14:
resolution:
{
integrity: sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA==,
}
dependencies:
"@types/node": 17.0.16
dev: true
/@types/prop-types/15.7.4:
resolution:
{
@@ -2574,6 +2618,13 @@ packages:
engines: {node: ">=12"}
dev: false
/arg/5.0.1:
resolution:
{
integrity: sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==,
}
dev: false
/argparse/1.0.10:
resolution:
{
@@ -2737,6 +2788,10 @@ packages:
resolution: {integrity: sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=}
dev: false
/async/1.5.2:
resolution: {integrity: sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=}
dev: false
/asynckit/0.4.0:
resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=}
@@ -3842,6 +3897,22 @@ packages:
webidl-conversions: 7.0.0
dev: false
/dotenv-expand/8.0.3:
resolution:
{
integrity: sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==,
}
engines: {node: ">=12"}
dev: false
/dotenv/16.0.0:
resolution:
{
integrity: sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==,
}
engines: {node: ">=12"}
dev: false
/duplexer/0.1.2:
resolution:
{
@@ -5392,6 +5463,17 @@ packages:
path-exists: 4.0.0
dev: false
/find-up/6.3.0:
resolution:
{
integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==,
}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
locate-path: 7.1.0
path-exists: 5.0.0
dev: true
/find-yarn-workspace-root/2.0.0:
resolution:
{
@@ -5881,6 +5963,13 @@ packages:
dependencies:
function-bind: 1.1.1
/hasbin/1.2.3:
resolution: {integrity: sha1-eMWSaJPIAhXCtWiuH9P8q3omlrA=}
engines: {node: ">=0.10"}
dependencies:
async: 1.5.2
dev: false
/hookable/5.1.1:
resolution:
{
@@ -6789,6 +6878,14 @@ packages:
graceful-fs: 4.2.9
dev: false
/kleur/3.0.3:
resolution:
{
integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==,
}
engines: {node: ">=6"}
dev: false
/language-subtag-registry/0.3.21:
resolution:
{
@@ -6912,6 +7009,16 @@ packages:
p-locate: 4.1.0
dev: false
/locate-path/7.1.0:
resolution:
{
integrity: sha512-HNx5uOnYeK4SxEoid5qnhRfprlJeGMzFRKPLCf/15N3/B4AiofNwC/yq7VBKdVk9dx7m+PiYCJOGg55JYTAqoQ==,
}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
p-locate: 6.0.0
dev: true
/lodash.clonedeep/4.5.0:
resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=}
dev: false
@@ -7845,6 +7952,16 @@ packages:
engines: {node: ">=4"}
dev: false
/p-event/5.0.1:
resolution:
{
integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==,
}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
p-timeout: 5.0.2
dev: true
/p-finally/1.0.0:
resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=}
engines: {node: ">=4"}
@@ -7878,6 +7995,16 @@ packages:
p-try: 2.2.0
dev: false
/p-limit/4.0.0:
resolution:
{
integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==,
}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
yocto-queue: 1.0.0
dev: true
/p-locate/2.0.0:
resolution: {integrity: sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=}
engines: {node: ">=4"}
@@ -7905,6 +8032,16 @@ packages:
p-limit: 2.3.0
dev: false
/p-locate/6.0.0:
resolution:
{
integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==,
}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
p-limit: 4.0.0
dev: true
/p-map/4.0.0:
resolution:
{
@@ -7915,6 +8052,14 @@ packages:
aggregate-error: 3.1.0
dev: false
/p-timeout/5.0.2:
resolution:
{
integrity: sha512-sEmji9Yaq+Tw+STwsGAE56hf7gMy9p0tQfJojIAamB7WHJYJKf1qlsg9jqBWG8q9VCxKPhZaP/AcXwEoBcYQhQ==,
}
engines: {node: ">=12"}
dev: true
/p-try/1.0.0:
resolution: {integrity: sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=}
engines: {node: ">=4"}
@@ -8019,6 +8164,14 @@ packages:
engines: {node: ">=8"}
dev: false
/path-exists/5.0.0:
resolution:
{
integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==,
}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: true
/path-is-absolute/1.0.1:
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
engines: {node: ">=0.10.0"}
@@ -8132,6 +8285,16 @@ packages:
find-up: 3.0.0
dev: false
/pkg-dir/6.0.1:
resolution:
{
integrity: sha512-C9R+PTCKGA32HG0n5I4JMYkdLL58ZpayVuncQHQrGeKa8o26A4o2x0u6BKekHG+Au0jv5ZW7Xfq1Cj6lm9Ag4w==,
}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
find-up: 6.3.0
dev: true
/pkg-types/0.3.2:
resolution:
{
@@ -8288,6 +8451,17 @@ packages:
}
engines: {node: ">=0.4.0"}
/prompts/2.4.2:
resolution:
{
integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==,
}
engines: {node: ">= 6"}
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
dev: false
/prop-types/15.8.1:
resolution:
{
@@ -8631,6 +8805,16 @@ packages:
}
dev: false
/resolve-cwd/3.0.0:
resolution:
{
integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==,
}
engines: {node: ">=8"}
dependencies:
resolve-from: 5.0.0
dev: true
/resolve-from/4.0.0:
resolution:
{
@@ -8638,6 +8822,14 @@ packages:
}
engines: {node: ">=4"}
/resolve-from/5.0.0:
resolution:
{
integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==,
}
engines: {node: ">=8"}
dev: true
/resolve-url/0.2.1:
resolution: {integrity: sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=}
deprecated: https://github.com/lydell/resolve-url#deprecated
@@ -9053,6 +9245,13 @@ packages:
totalist: 1.1.0
dev: true
/sisteransi/1.0.5:
resolution:
{
integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==,
}
dev: false
/slash/2.0.0:
resolution:
{
@@ -9209,6 +9408,7 @@ packages:
integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==,
}
engines: {node: ">=0.10.0"}
requiresBuild: true
/sourcemap-codec/1.4.8:
resolution:
@@ -10502,6 +10702,14 @@ packages:
engines: {node: ">= 6"}
dev: false
/yocto-queue/1.0.0:
resolution:
{
integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==,
}
engines: {node: ">=12.20"}
dev: true
/zod/3.10.1:
resolution:
{