diff --git a/packages/rspack-test-tools/tests/configCases/builtin-swc-loader/validate/errors.js b/packages/rspack-test-tools/tests/configCases/builtin-swc-loader/validate/errors.js index 1f9108a113ea..f413f44372a7 100644 --- a/packages/rspack-test-tools/tests/configCases/builtin-swc-loader/validate/errors.js +++ b/packages/rspack-test-tools/tests/configCases/builtin-swc-loader/validate/errors.js @@ -1,5 +1,5 @@ module.exports = [ [ - /Invalid options of 'builtin:swc-loader'/ + /Invalid options for 'builtin:swc-loader'/ ], ]; diff --git a/packages/rspack/src/config/utils.ts b/packages/rspack/src/config/utils.ts index a1980ae5a906..d58d1e534cb8 100644 --- a/packages/rspack/src/config/utils.ts +++ b/packages/rspack/src/config/utils.ts @@ -1,228 +1,4 @@ -import { - type DIRTY, - INVALID, - type IssueData, - type ParseContext, - type ParseInput, - type ParseReturnType, - type ProcessedCreateParams, - type RawCreateParams, - type SyncParseReturnType, - ZodError, - type ZodErrorMap, - ZodFirstPartyTypeKind, - type ZodIssue, - ZodIssueCode, - ZodType, - type ZodTypeDef, - ZodUnion, - type ZodUnionOptions, - addIssueToContext, - getParsedType, - z -} from "zod"; -import type { RspackOptions } from "./types"; - -/** - * The following code is modified based on - * https://github.com/colinhacks/zod/blob/f487d74ecd3ae703ef8932462d14d643e31658b3/src/types.ts - * - * MIT Licensed - * Author Colin McDonnell @colinhacks - * MIT License - * https://github.com/colinhacks/zod/blob/main/LICENSE - */ - -function processCreateParams(params: RawCreateParams): ProcessedCreateParams { - if (!params) return {}; - const { errorMap, invalid_type_error, required_error, description } = params; - if (errorMap && (invalid_type_error || required_error)) { - throw new Error( - `Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.` - ); - } - if (errorMap) return { errorMap: errorMap, description }; - const customMap: ZodErrorMap = (iss, ctx) => { - const { message } = params; - - if (iss.code === "invalid_enum_value") { - return { message: message ?? ctx.defaultError }; - } - if (typeof ctx.data === "undefined") { - return { message: message ?? required_error ?? ctx.defaultError }; - } - if (iss.code !== "invalid_type") return { message: ctx.defaultError }; - return { message: message ?? invalid_type_error ?? ctx.defaultError }; - }; - return { errorMap: customMap, description }; -} - -/** - * Modified `z.union` for overriding its `_parse` to support `parent` field of context. - * - * We need to use `parent` field to get the root config object. - */ -class RspackZodUnion extends z.ZodUnion { - _parse(input: ParseInput): ParseReturnType { - const { ctx } = this._processInputParams(input); - const options = this._def.options; - - function handleResults( - results: { ctx: ParseContext; result: SyncParseReturnType }[] - ) { - // return first issue-free validation if it exists - for (const result of results) { - if (result.result.status === "valid") { - return result.result; - } - } - - for (const result of results) { - if (result.result.status === "dirty") { - // add issues from dirty option - - ctx.common.issues.push(...result.ctx.common.issues); - return result.result; - } - } - - // return invalid - const unionErrors = results.map( - result => new ZodError(result.ctx.common.issues) - ); - - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_union, - unionErrors - }); - return INVALID; - } - - if (ctx.common.async) { - return Promise.all( - options.map(async option => { - const childCtx: ParseContext = { - ...ctx, - common: { - ...ctx.common, - issues: [] - }, - parent: ctx - }; - return { - result: await option._parseAsync({ - data: ctx.data, - path: ctx.path, - parent: childCtx - }), - ctx: childCtx - }; - }) - ).then(handleResults); - } - let dirty: undefined | { result: DIRTY; ctx: ParseContext } = - undefined; - const issues: ZodIssue[][] = []; - for (const option of options) { - const childCtx: ParseContext = { - ...ctx, - common: { - ...ctx.common, - issues: [] - }, - parent: ctx - }; - const result = option._parseSync({ - data: ctx.data, - path: ctx.path, - parent: childCtx - }); - - if (result.status === "valid") { - return result; - } - - if (result.status === "dirty" && !dirty) { - dirty = { result, ctx: childCtx }; - } - - if (childCtx.common.issues.length) { - issues.push(childCtx.common.issues); - } - } - - if (dirty) { - ctx.common.issues.push(...dirty.ctx.common.issues); - return dirty.result; - } - - const unionErrors = issues.map(issues => new ZodError(issues)); - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_union, - unionErrors - }); - - return INVALID; - } - - static create = ( - types: T, - params?: RawCreateParams - ): ZodUnion => { - return new RspackZodUnion({ - options: types, - typeName: ZodFirstPartyTypeKind.ZodUnion, - ...processCreateParams(params) - }); - }; -} - -ZodUnion.create = RspackZodUnion.create; - -export type ZodCrossFieldsOptions = ZodTypeDef & { - patterns: Array<{ - test: (root: RspackOptions, input: z.ParseInput) => boolean; - type: ZodType; - issue?: ( - res: ParseReturnType, - root: RspackOptions, - input: z.ParseInput - ) => Array; - }>; - default: ZodType; -}; - -export class ZodRspackCrossChecker extends ZodType { - constructor(private params: ZodCrossFieldsOptions) { - super(params); - } - _parse(input: z.ParseInput): z.ParseReturnType { - const ctx = this._getOrReturnCtx(input); - const root = this._getRootData(ctx); - - for (const pattern of this.params.patterns) { - if (pattern.test(root, input)) { - const res = pattern.type._parse(input); - const issues = - typeof pattern.issue === "function" - ? pattern.issue(res, root, input) - : []; - for (const issue of issues) { - addIssueToContext(ctx, issue); - } - return res; - } - } - return this.params.default._parse(input); - } - _getRootData(ctx: z.ParseContext) { - let root = ctx; - while (root.parent) { - root = root.parent; - } - return root.data; - } -} +import { getParsedType, z } from "zod"; export const anyFunction = z.custom<(...args: unknown[]) => any>( data => typeof data === "function", diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index 6ff045edc305..c94caccdb50f 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1,9 +1,9 @@ import nodePath from "node:path"; import { ZodIssueCode, z } from "zod"; +import { fromZodError } from "zod-validation-error"; import { getZodSwcLoaderOptionsSchema } from "../builtin-loader/swc/types"; -import { validate } from "../util/validate"; import type * as t from "./types"; -import { ZodRspackCrossChecker, anyFunction } from "./utils"; +import { anyFunction } from "./utils"; const filenameTemplate = z.string() satisfies z.ZodType; @@ -448,61 +448,39 @@ const ruleSetLoaderOptions = z z.record(z.string(), z.any()) ) satisfies z.ZodType; -const ruleSetLoaderWithOptions = - new ZodRspackCrossChecker({ - patterns: [ - { - test: (_, input) => - input?.data?.loader === "builtin:swc-loader" && - typeof input?.data?.options === "object", - type: z.strictObject({ - ident: z.string().optional(), - loader: z.literal("builtin:swc-loader"), - options: getZodSwcLoaderOptionsSchema(), - parallel: z.boolean().optional() - }), - issue: (res, _, input) => { - try { - const message = validate( - input.data.options, - getZodSwcLoaderOptionsSchema(), - { - output: false, - strategy: "strict" - } - ); - if (message) { - return [ - { - fatal: true, - code: ZodIssueCode.custom, - message: `Invalid options of 'builtin:swc-loader': ${message}` - } - ]; - } - return []; - } catch (e) { - return [ - { - fatal: true, - code: ZodIssueCode.custom, - message: `Invalid options of 'builtin:swc-loader': ${(e as Error).message}` - } - ]; - } - } - } - ], - default: z.strictObject({ - ident: z.string().optional(), - loader: ruleSetLoader, - options: ruleSetLoaderOptions.optional(), - parallel: z.boolean().optional() - }) - }) satisfies z.ZodType; +const ruleSetLoaderWithOptions = z.strictObject({ + ident: z.string().optional(), + loader: ruleSetLoader, + options: ruleSetLoaderOptions.optional(), + parallel: z.boolean().optional() +}) satisfies z.ZodType; + +const builtinSWCLoaderChecker = ( + data: t.RuleSetLoaderWithOptions | t.RuleSetRule | undefined, + ctx: z.RefinementCtx +) => { + if ( + data?.loader !== "builtin:swc-loader" || + typeof data?.options !== "object" + ) { + return; + } + + const res = getZodSwcLoaderOptionsSchema().safeParse(data.options); + + if (!res.success) { + const validationErr = fromZodError(res.error, { + prefix: "Invalid options for 'builtin:swc-loader'" + }); + ctx.addIssue({ + code: "custom", + message: validationErr.message + }); + } +}; const ruleSetUseItem = ruleSetLoader.or( - ruleSetLoaderWithOptions + ruleSetLoaderWithOptions.superRefine(builtinSWCLoaderChecker) ) satisfies z.ZodType; const ruleSetUse = ruleSetUseItem @@ -539,59 +517,11 @@ const extendedBaseRuleSetRule: z.ZodType = baseRuleSetRule.extend({ oneOf: z.lazy(() => ruleSetRule.or(falsy).array()).optional(), rules: z.lazy(() => ruleSetRule.or(falsy).array()).optional() - }); - -const extendedSwcRuleSetRule: z.ZodType = baseRuleSetRule - .extend({ - loader: z.literal("builtin:swc-loader"), - options: getZodSwcLoaderOptionsSchema() - }) - .extend({ - oneOf: z.lazy(() => ruleSetRule.or(falsy).array()).optional(), - rules: z.lazy(() => ruleSetRule.or(falsy).array()).optional() - }); + }) satisfies z.ZodType; -const ruleSetRule = new ZodRspackCrossChecker({ - patterns: [ - { - test: (_, input) => - input?.data?.loader === "builtin:swc-loader" && - typeof input?.data?.options === "object", - type: extendedSwcRuleSetRule, - issue: (res, _, input) => { - try { - const message = validate( - input.data.options, - getZodSwcLoaderOptionsSchema(), - { - output: false, - strategy: "strict" - } - ); - if (message) { - return [ - { - fatal: true, - code: ZodIssueCode.custom, - message: `Invalid options of 'builtin:swc-loader': ${message}` - } - ]; - } - return []; - } catch (e) { - return [ - { - fatal: true, - code: ZodIssueCode.custom, - message: `Invalid options of 'builtin:swc-loader': ${(e as Error).message}` - } - ]; - } - } - } - ], - default: extendedBaseRuleSetRule -}); +const ruleSetRule = extendedBaseRuleSetRule.superRefine( + builtinSWCLoaderChecker +); const ruleSetRules = z.array( z.literal("...").or(ruleSetRule).or(falsy)