From 9e81153b34fac9956d97b20bc4c9c8c49f1b4f97 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:58:07 +0800 Subject: [PATCH 1/7] refactor(zod/v4): use `.superRefine()` for `builtin:swc-loader` --- packages/rspack/src/config/zod.ts | 134 ++++++++---------------------- 1 file changed, 34 insertions(+), 100 deletions(-) diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index e9628942ec1f..d25bdd6008cb 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1,9 +1,8 @@ import nodePath from "node:path"; -import { ZodIssueCode, z } from "zod"; +import { z, ZodIssueCode } from "zod"; import { ZodSwcLoaderOptions } 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,57 +447,36 @@ 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: ZodSwcLoaderOptions, - parallel: z.boolean().optional() - }), - issue: (res, _, input) => { - try { - const message = validate(input.data.options, ZodSwcLoaderOptions, { - 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 = ZodSwcLoaderOptions.safeParse(data.options); + + if (!res.success) { + ctx.addIssue({ + code: "custom", + message: `Invalid options of 'builtin:swc-loader': ${res.error.message}` + }); + } +}; const ruleSetUseItem = ruleSetLoader.or( - ruleSetLoaderWithOptions + ruleSetLoaderWithOptions.superRefine(builtinSWCLoaderChecker) ) satisfies z.ZodType; const ruleSetUse = ruleSetUseItem @@ -535,55 +513,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: ZodSwcLoaderOptions - }) - .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, ZodSwcLoaderOptions, { - 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) From cc7d6998c489c0ea62c839cd58b77c5a9561f54b Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sun, 15 Jun 2025 19:33:40 +0800 Subject: [PATCH 2/7] refactor(zod/v4): remove `ZodRspackCrossChecker` --- packages/rspack/src/config/utils.ts | 226 +--------------------------- 1 file changed, 1 insertion(+), 225 deletions(-) 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", From 2f9f7e494e1a710d8ad73029844a811626878970 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sun, 15 Jun 2025 19:36:47 +0800 Subject: [PATCH 3/7] fix: lint --- packages/rspack/src/config/zod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index d25bdd6008cb..974912c095ce 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1,5 +1,5 @@ import nodePath from "node:path"; -import { z, ZodIssueCode } from "zod"; +import { ZodIssueCode, z } from "zod"; import { ZodSwcLoaderOptions } from "../builtin-loader/swc/types"; import type * as t from "./types"; import { anyFunction } from "./utils"; From bd5fb8d9d159be773ecdcfe58a6d0f607d089ba1 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sun, 15 Jun 2025 23:53:54 +0800 Subject: [PATCH 4/7] fix: better error message --- .../tests/configCases/builtin-swc-loader/validate/errors.js | 2 +- packages/rspack/src/config/zod.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) 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/zod.ts b/packages/rspack/src/config/zod.ts index 974912c095ce..c5169652c55d 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1,5 +1,6 @@ import nodePath from "node:path"; import { ZodIssueCode, z } from "zod"; +import { createMessageBuilder, fromError } from "zod-validation-error"; import { ZodSwcLoaderOptions } from "../builtin-loader/swc/types"; import type * as t from "./types"; import { anyFunction } from "./utils"; @@ -468,9 +469,12 @@ const builtinSWCLoaderChecker = ( const res = ZodSwcLoaderOptions.safeParse(data.options); if (!res.success) { + const validationErr = fromError(res.error, { + prefix: "Invalid options for 'builtin:swc-loader'", + }); ctx.addIssue({ code: "custom", - message: `Invalid options of 'builtin:swc-loader': ${res.error.message}` + message: validationErr.message }); } }; From e6b1bf4b880a424c69211fc0456ffd0658f65756 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sun, 15 Jun 2025 23:59:17 +0800 Subject: [PATCH 5/7] fix: format --- packages/rspack/src/config/zod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index c5169652c55d..f46c2a2ec214 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -470,7 +470,7 @@ const builtinSWCLoaderChecker = ( if (!res.success) { const validationErr = fromError(res.error, { - prefix: "Invalid options for 'builtin:swc-loader'", + prefix: "Invalid options for 'builtin:swc-loader'" }); ctx.addIssue({ code: "custom", From 388c48aca3b1fa3d56e21ca62ef4c051dfd9f52f Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Jun 2025 00:06:35 +0800 Subject: [PATCH 6/7] fix: lint --- packages/rspack/src/config/zod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index f46c2a2ec214..80de43cc76d8 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1,6 +1,6 @@ import nodePath from "node:path"; import { ZodIssueCode, z } from "zod"; -import { createMessageBuilder, fromError } from "zod-validation-error"; +import { fromError } from "zod-validation-error"; import { ZodSwcLoaderOptions } from "../builtin-loader/swc/types"; import type * as t from "./types"; import { anyFunction } from "./utils"; From b65b8fe03cc86c1fad4a812a7a9812b8bd699836 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Jun 2025 10:56:19 +0800 Subject: [PATCH 7/7] fix: use `fromZodError` --- packages/rspack/src/config/zod.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index 3534b1ef8dd0..c94caccdb50f 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1,6 +1,6 @@ import nodePath from "node:path"; import { ZodIssueCode, z } from "zod"; -import { fromError } from "zod-validation-error"; +import { fromZodError } from "zod-validation-error"; import { getZodSwcLoaderOptionsSchema } from "../builtin-loader/swc/types"; import type * as t from "./types"; import { anyFunction } from "./utils"; @@ -469,7 +469,7 @@ const builtinSWCLoaderChecker = ( const res = getZodSwcLoaderOptionsSchema().safeParse(data.options); if (!res.success) { - const validationErr = fromError(res.error, { + const validationErr = fromZodError(res.error, { prefix: "Invalid options for 'builtin:swc-loader'" }); ctx.addIssue({