Compare commits
4 Commits
blitz@2.1.
...
routes-man
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
377fd5c0e8 | ||
|
|
5c7fc7d1f2 | ||
|
|
8b8a36ab5d | ||
|
|
c1dd5cd38f |
@@ -4,4 +4,7 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
esmExternals: "loose",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"debug": "4.3.3",
|
||||
"fs-extra": "^9.1.0",
|
||||
"react-query": "3.34.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -73,3 +73,5 @@ export const setupBlitz = ({plugins}: SetupBlitzOptions) => {
|
||||
|
||||
return {gSSP, gSP, api}
|
||||
}
|
||||
|
||||
export * from "./routes-manifest"
|
||||
|
||||
514
packages/blitz-next/src/routes-manifest.ts
Normal file
514
packages/blitz-next/src/routes-manifest.ts
Normal 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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
9
packages/blitz/src/cli/commands/codegen.ts
Normal file
9
packages/blitz/src/cli/commands/codegen.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {cliCommand} from "../index"
|
||||
/* @ts-ignore */
|
||||
import {generateManifest} from "@blitzjs/next"
|
||||
|
||||
const codegen: cliCommand = async () => {
|
||||
await generateManifest()
|
||||
}
|
||||
|
||||
export {codegen}
|
||||
7
packages/blitz/src/cli/commands/dev.ts
Normal file
7
packages/blitz/src/cli/commands/dev.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {cliCommand} from "../index"
|
||||
|
||||
const dev: cliCommand = (argv) => {
|
||||
console.log("dev hit")
|
||||
}
|
||||
|
||||
export {dev}
|
||||
302
packages/blitz/src/cli/commands/new.ts
Normal file
302
packages/blitz/src/cli/commands/new.ts
Normal 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}
|
||||
122
packages/blitz/src/cli/index.ts
Normal file
122
packages/blitz/src/cli/index.ts
Normal 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.`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
1
packages/blitz/src/cli/utils/constants.ts
Normal file
1
packages/blitz/src/cli/utils/constants.ts
Normal 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`
|
||||
107
packages/blitz/src/env-utils.ts
Normal file
107
packages/blitz/src/env-utils.ts
Normal 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}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
208
pnpm-lock.yaml
generated
@@ -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:
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user