-
-
Notifications
You must be signed in to change notification settings - Fork 141
feat: support per-target output roots #2055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,8 +58,11 @@ const SourceEntrySchema = z.object({ | |
| }); | ||
| export type SourceEntry = z.infer<typeof SourceEntrySchema>; | ||
|
|
||
| const ConfigParamsSchema = z.object({ | ||
| outputRoots: z.array(z.string()), | ||
| export const ConfigParamsSchema = z.object({ | ||
| outputRoots: z.union([ | ||
| z.array(z.string()), | ||
| z.record(z.string(), z.union([z.string(), z.array(z.string())])), | ||
| ]), | ||
| targets: RulesyncConfigTargetsSchema, | ||
| features: RulesyncFeaturesSchema, | ||
| verbose: z.boolean(), | ||
|
|
@@ -99,7 +102,9 @@ const ConfigParamsSchema = z.object({ | |
| // `assertTargetsOrFeaturesProvided`. Programmatic callers constructing | ||
| // `Config` directly must respect these invariants. | ||
| type InferredConfigParams = z.infer<typeof ConfigParamsSchema>; | ||
| export type OutputRoots = string[] | Partial<Record<ToolTarget, string | string[]>>; | ||
| export type ConfigParams = Omit<InferredConfigParams, "targets" | "features"> & { | ||
| outputRoots: OutputRoots; | ||
| targets?: RulesyncConfigTargets; | ||
| features?: RulesyncFeatures; | ||
| configFileTargets?: ToolTarget[]; | ||
|
|
@@ -108,6 +113,7 @@ export type ConfigParams = Omit<InferredConfigParams, "targets" | "features"> & | |
| const PartialConfigParamsSchema = z.partial(ConfigParamsSchema); | ||
| type InferredPartialConfigParams = z.infer<typeof PartialConfigParamsSchema>; | ||
| export type PartialConfigParams = Omit<InferredPartialConfigParams, "targets" | "features"> & { | ||
| outputRoots?: OutputRoots; | ||
| targets?: RulesyncConfigTargets; | ||
| features?: RulesyncFeatures; | ||
| }; | ||
|
|
@@ -119,13 +125,15 @@ export const ConfigFileSchema = z.object({ | |
| }); | ||
| type InferredConfigFile = z.infer<typeof ConfigFileSchema>; | ||
| export type ConfigFile = Omit<InferredConfigFile, "targets" | "features"> & { | ||
| outputRoots?: OutputRoots; | ||
| targets?: RulesyncConfigTargets; | ||
| features?: RulesyncFeatures; | ||
| }; | ||
|
|
||
| const RequiredConfigParamsSchema = z.required(ConfigParamsSchema); | ||
| type InferredRequiredConfigParams = z.infer<typeof RequiredConfigParamsSchema>; | ||
| export type RequiredConfigParams = Omit<InferredRequiredConfigParams, "targets" | "features"> & { | ||
| outputRoots: OutputRoots; | ||
| targets?: RulesyncConfigTargets; | ||
| features?: RulesyncFeatures; | ||
| }; | ||
|
|
@@ -207,7 +215,7 @@ const assertTargetsOrFeaturesProvided = ({ | |
| }; | ||
|
|
||
| export class Config { | ||
| private readonly outputRoots: string[]; | ||
| private readonly outputRoots: OutputRoots; | ||
| private readonly targets: RulesyncConfigTargets; | ||
| private readonly features: RulesyncFeatures; | ||
| /** | ||
|
|
@@ -274,6 +282,7 @@ export class Config { | |
| // Reject unknown keys in the object form of `targets`. Array-form values | ||
| // are already validated at the Zod schema level. | ||
| this.validateObjectFormTargetKeys(resolvedTargets); | ||
| this.validateObjectFormOutputRootKeys(outputRoots); | ||
|
|
||
| // Validate conflicting targets (accepts array and object forms) | ||
| this.validateConflictingTargets(resolvedTargets); | ||
|
|
@@ -346,6 +355,18 @@ export class Config { | |
| } | ||
| } | ||
|
|
||
| private validateObjectFormOutputRootKeys(outputRoots: OutputRoots): void { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This duplicates |
||
| if (Array.isArray(outputRoots)) return; | ||
| const validTargets = new Set<string>(ALL_TOOL_TARGETS); | ||
| for (const key of Object.keys(outputRoots)) { | ||
| if (!validTargets.has(key)) { | ||
| throw new Error( | ||
| `Unknown outputRoots target '${key}'. Valid targets: ${ALL_TOOL_TARGETS.join(", ")}.`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateConflictingTargets(targets: RulesyncConfigTargets): void { | ||
| // Wildcard (*) doesn't include legacy targets, so conflicts can only | ||
| // occur when both sides of a conflicting pair are explicitly present. | ||
|
|
@@ -366,8 +387,25 @@ export class Config { | |
| } | ||
| } | ||
|
|
||
| public getOutputRoots(): string[] { | ||
| return this.outputRoots; | ||
| public getOutputRoots(): string[]; | ||
| public getOutputRoots(target: ToolTarget): string[]; | ||
| public getOutputRoots(target?: ToolTarget): string[] { | ||
| if (Array.isArray(this.outputRoots)) { | ||
| return this.outputRoots; | ||
| } | ||
|
|
||
| if (target) { | ||
| const targetOutputRoots = this.outputRoots[target]; | ||
| if (targetOutputRoots === undefined) return []; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a target that is present in |
||
| return Array.isArray(targetOutputRoots) ? targetOutputRoots : [targetOutputRoots]; | ||
| } | ||
|
|
||
| const allRoots: string[] = []; | ||
| for (const value of Object.values(this.outputRoots)) { | ||
| if (value === undefined) continue; | ||
| allRoots.push(...(Array.isArray(value) ? value : [value])); | ||
| } | ||
| return [...new Set(allRoots)]; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolvedOutputRootsis built as a plain{}and populated via bracket assignment. A config with a key literally named__proto__would hit theObject.prototype.__proto__setter instead of creating an own property, so the entry silently disappears fromObject.keys()and never reachesvalidateObjectFormOutputRootKeys's unknown-key check (it just gets dropped instead of raising the intended error). Not exploitable beyond this local object, but building it withObject.create(null)(or explicitly rejecting__proto__/constructor/prototypekeys) would make malformed config fail loudly instead of silently.