257 lines
8.8 KiB
TypeScript
257 lines
8.8 KiB
TypeScript
import { z, ZodTypeAny } from 'zod'
|
|
import * as path from 'path'
|
|
import { accessSync, constants, lstatSync, existsSync } from 'fs'
|
|
|
|
import { isString } from '@polymech/'
|
|
|
|
import { logger } from '../logger'
|
|
|
|
import { sync as exists } from '@polymech/fs/exists'
|
|
import { sync as read } from '@polymech/fs/read'
|
|
|
|
import { DEFAULT_VARS, resolve, resolveVariables } from '../variables'
|
|
|
|
import { getDescription } from '../'
|
|
import { isFile } from '../lib/fs'
|
|
|
|
|
|
type TResult = { resolved: string, source: string, value: unknown }
|
|
type TRefine = (src: string, ctx: any, variables: Record<string, string>) => string | z.ZodNever
|
|
type TTransform = (src: string, variables?: Record<string, string>) => string | TResult
|
|
type TExtend = { refine: Array<TRefine>, transform: Array<TTransform> }
|
|
|
|
const DefaultPathSchemaBase = z.string().describe('Path to a file or directory')
|
|
|
|
const PathErrorMessages = {
|
|
INVALID_INPUT: 'INVALID_INPUT: ${inputPath}',
|
|
PATH_DOES_NOT_EXIST: 'Path does not exist ${inputPath} = ${resolvedPath}',
|
|
DIRECTORY_NOT_WRITABLE: 'Directory is not writable ${inputPath} = ${resolvedPath}',
|
|
NOT_A_DIRECTORY: 'Path is not a directory or does not exist ${inputPath} = ${resolvedPath}',
|
|
NOT_A_JSON_FILE: 'File is not a JSON file or does not exist ${inputPath} = ${resolvedPath}',
|
|
PATH_NOT_ABSOLUTE: 'Path is not absolute ${inputPath} = ${resolvedPath}',
|
|
PATH_NOT_RELATIVE: 'Path is not relative ${inputPath} = ${resolvedPath}',
|
|
} as const
|
|
|
|
export enum E_PATH {
|
|
ENSURE_PATH_EXISTS = 1,
|
|
INVALID_INPUT,
|
|
ENSURE_DIRECTORY_WRITABLE,
|
|
ENSURE_FILE_IS_JSON,
|
|
ENSURE_PATH_IS_ABSOLUTE,
|
|
ENSURE_PATH_IS_RELATIVE,
|
|
GET_PATH_INFO
|
|
}
|
|
export const Transformers = {
|
|
resolve: (val: string, variables: Record<string, string> = {}) => {
|
|
if (!val) {
|
|
return null
|
|
}
|
|
return {
|
|
resolved: path.resolve(resolve(val, false, variables)),
|
|
source: val
|
|
}
|
|
},
|
|
json: (val: string | { resolved: string, source: string }, variables: Record<string, string> = {}) => {
|
|
if (!val) {
|
|
return null
|
|
}
|
|
const resolved = path.resolve(resolve(isString(val) ? val : val.source, false, variables))
|
|
return {
|
|
resolved,
|
|
source: val,
|
|
value: read(resolved, 'json')
|
|
}
|
|
},
|
|
string: (val: string | { resolved: string, source: string }, variables: Record<string, string> = {}) => {
|
|
if (!val) {
|
|
return null
|
|
}
|
|
let src = isString(val) ? val : val.source
|
|
src = resolve(src, false, variables)
|
|
const resolved = path.resolve(src)
|
|
if (!exists(resolved) || !isFile(resolved)) {
|
|
return {
|
|
resolved,
|
|
source: val,
|
|
value: null
|
|
}
|
|
}
|
|
else {
|
|
let value = null
|
|
try {
|
|
value = read(resolved, 'string')
|
|
} catch (e) {
|
|
logger.error('Failed to read file', { resolved, source: val, error: e.message })
|
|
}
|
|
return {
|
|
resolved,
|
|
source: val,
|
|
value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const TransformersDescription = [
|
|
{
|
|
description: 'RESOLVE_PATH',
|
|
fn: Transformers.resolve
|
|
},
|
|
{
|
|
description: 'READ_JSON',
|
|
fn: Transformers.json
|
|
},
|
|
{
|
|
description: 'READ_STRING',
|
|
fn: Transformers.string
|
|
}
|
|
]
|
|
const extendType = (type: ZodTypeAny, extend: TExtend, variables: Record<string, string> = {}) => {
|
|
if (Array.isArray(extend.refine)) {
|
|
for (const refine of extend.refine) {
|
|
type = type.refine(refine as any)
|
|
}
|
|
} else {
|
|
type = type.refine(extend.refine)
|
|
}
|
|
if (Array.isArray(extend.transform)) {
|
|
for (const transform of extend.transform) {
|
|
type = type.transform((val) => transform(val, variables))
|
|
}
|
|
} else {
|
|
type = type.transform(extend.transform)
|
|
}
|
|
return type
|
|
}
|
|
|
|
const extendTypeDescription = (type: ZodTypeAny, extension: TExtend, variables: Record<string, string> = {}) => {
|
|
const description = getDescription(type) || ''
|
|
let transformerDescriptions = 'Transformers:\n'
|
|
if (Array.isArray(extension.transform)) {
|
|
for (const transform of extension.transform) {
|
|
transformerDescriptions += transformerDescription(transform) + '\n'
|
|
}
|
|
} else {
|
|
transformerDescriptions += transformerDescription(extension.transform) + '\n'
|
|
}
|
|
type = type.describe(description + '\n' + transformerDescriptions)
|
|
return type
|
|
}
|
|
|
|
const transformerDescription = (fn: TTransform) => {
|
|
const description = TransformersDescription.find((t) => t.fn === fn)
|
|
return description ? description.description : 'Unknown'
|
|
}
|
|
|
|
export const extendSchema = (baseSchema: z.ZodObject<any>, extend: Record<string, any>) => {
|
|
const baseShape = baseSchema.shape
|
|
const extendedShape: Record<string, ZodTypeAny> = { ...baseShape }
|
|
for (const [key, refines] of Object.entries(extend)) {
|
|
if (!baseShape[key])
|
|
continue
|
|
|
|
let fieldSchema = baseShape[key]
|
|
if (Array.isArray(refines.refine)) {
|
|
for (const refine of refines.refine) {
|
|
fieldSchema = fieldSchema.superRefine(refine)
|
|
}
|
|
} else {
|
|
fieldSchema = fieldSchema.superRefine(refines)
|
|
}
|
|
if (Array.isArray(refines.transform)) {
|
|
for (const transform of refines.transform) {
|
|
fieldSchema = fieldSchema.transform((val) => transform(val))
|
|
}
|
|
} else {
|
|
fieldSchema = fieldSchema.transform(refines.transform)
|
|
}
|
|
extendedShape[key] = fieldSchema
|
|
|
|
}
|
|
return z.object(extendedShape)
|
|
}
|
|
|
|
export const ENSURE_DIRECTORY_WRITABLE = (inputPath: string, ctx: any, variables: Record<string, string>) => {
|
|
const resolvedPath = path.resolve(resolve(inputPath, false, variables))
|
|
const parts = path.parse(resolvedPath)
|
|
if (resolvedPath && existsSync(parts.dir) && lstatSync(parts.dir).isDirectory()) {
|
|
try {
|
|
accessSync(resolvedPath, constants.W_OK)
|
|
return resolvedPath
|
|
} catch (e) {
|
|
ctx.addIssue({
|
|
code: E_PATH.ENSURE_DIRECTORY_WRITABLE,
|
|
message: resolveVariables(PathErrorMessages.DIRECTORY_NOT_WRITABLE, false, { inputPath, resolvedPath })
|
|
})
|
|
return z.NEVER
|
|
}
|
|
} else {
|
|
ctx.addIssue({
|
|
code: E_PATH.ENSURE_DIRECTORY_WRITABLE,
|
|
message: resolveVariables(PathErrorMessages.NOT_A_DIRECTORY, false, { inputPath, resolvedPath })
|
|
})
|
|
return z.NEVER
|
|
}
|
|
|
|
}
|
|
export const IS_VALID_STRING = (inputPath: string) => {
|
|
return isString(inputPath)
|
|
}
|
|
export const ENSURE_PATH_EXISTS = (inputPath: string, ctx: any, variables: Record<string, string>) => {
|
|
if (!inputPath || !ctx) {
|
|
return z.NEVER
|
|
}
|
|
if (!isString(inputPath)) {
|
|
ctx.addIssue({
|
|
code: E_PATH.INVALID_INPUT,
|
|
message: resolveVariables(PathErrorMessages.INVALID_INPUT, false, {})
|
|
})
|
|
return z.NEVER
|
|
}
|
|
const resolvedPath = path.resolve(resolve(inputPath, false, variables))
|
|
if (!exists(resolvedPath)) {
|
|
ctx.addIssue({
|
|
code: E_PATH.ENSURE_PATH_EXISTS,
|
|
message: resolveVariables(PathErrorMessages.PATH_DOES_NOT_EXIST, false, { inputPath, resolvedPath })
|
|
})
|
|
|
|
return z.NEVER
|
|
}
|
|
return resolvedPath
|
|
}
|
|
|
|
export const test = () => {
|
|
const BaseCompilerOptions = () => z.object({
|
|
root: DefaultPathSchemaBase.default(`${process.cwd()}`)
|
|
})
|
|
const ret = extendSchema(BaseCompilerOptions(), {
|
|
root: {
|
|
refine: [
|
|
(val, ctx) => ENSURE_DIRECTORY_WRITABLE(val, ctx, DEFAULT_VARS({ exampleVar: 'exampleValue' })),
|
|
(val, ctx) => ENSURE_PATH_EXISTS(val, ctx, DEFAULT_VARS({ exampleVar: 'exampleValue' }))
|
|
],
|
|
transform: [
|
|
(val) => path.resolve(resolve(val, false, DEFAULT_VARS({ exampleVar: 'exampleValue' })))
|
|
]
|
|
}
|
|
})
|
|
return ret
|
|
}
|
|
|
|
export const Templates =
|
|
{
|
|
json: {
|
|
refine: [IS_VALID_STRING, ENSURE_PATH_EXISTS],
|
|
transform: [Transformers.resolve, Transformers.json]
|
|
},
|
|
string: {
|
|
refine: [ENSURE_PATH_EXISTS],
|
|
transform: [Transformers.resolve, Transformers.string]
|
|
}
|
|
}
|
|
|
|
export const extend = (baseSchema: ZodTypeAny, template: any, variables: Record<string, string> = {}) => {
|
|
const type = extendType(baseSchema, template, variables)
|
|
return extendTypeDescription(type, template, variables)
|
|
}
|