-
-
Notifications
You must be signed in to change notification settings - Fork 500
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
fix: Preserve Social Link Order #381
Conversation
Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>
Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>
🦋 Changeset detectedLatest commit: d941155 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for astro-starlight ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
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.
Would you mind explaining what this does a bit more? By “order”, do you mean the order in which the icons are displayed on the site?
If so, why does this schema change do that? Seems like we might be leaning on a Zod implementation detail, which makes me a bit nervous 😅
Yeah, if I remember correctly, zod parses by looping through the schema keys so the order the user uses in their config won't affect the order of the icons in the header. Maybe accepting an array too could be another approach 🤔 |
Yeah, anywhere order is important, an array will be more reliable. We’d end up with an API something like this though, so I do wonder if order is important enough to complicate this. social: [
{ icon: 'twitter', link: 'https://twitter.com/astrodotbuild' },
{ icon: 'github', link: 'https://github.com/withastro/starlight' },
] Or in theory something like this style which a lot of VitePress config prefers, but I’m not a big fan of the unlabelled arguments personally. social: [
['twitter', 'https://twitter.com/astrodotbuild'],
['github', 'https://github.com/withastro/starlight'],
] |
Yup, this was the issue from what I could find. A little more detail is here: colinhacks/zod#1852. Only reason I didn't go with an array was because of breaking configs, but could go with whatever y'all would like |
Other (prolly way too late) random ideas:
|
Thanks for the extra context @lorenzolewis! I wonder if the response marked as an answer in that discussion could help us? Had to re-read the code several times because it’s not super clear, but rewritten in a way my brain understands: function preserveKeyOrder<Schema extends z.ZodObject<z.ZodRawShape>> (
schema: Schema
) {
return z.custom((value) => schema.safeParse(value).success) as Schema;
}
const objSchema = z.object({
one: z.number(),
two: z.number(),
});
const objSchemaRespectOrder = preserveKeyOrder(objSchema)
console.log(objSchema.parse({ two: 2, one: 1 }));
// { one: 1, two: 2 }
console.log(objSchemaRespectOrder.parse({ two: 2, one: 1 }));
// { two: 2, one: 1 } So basically uses a custom validator that just passes through the original values if they pass, thereby preserving order. Would let us keep the current shape, just with the extra helper to preserve key order. |
@delucis I did try that a couple of different ways but couldn't get it to work (admittedly I think that was getting a bit too far into zod and TS for me to wrap my head around 😅) But if someone is able to get it to work then it'd accomplish the same thing without affecting the config API (just maybe a little more complexity in that bit of the codebase that I wish anyone in the future best of luck with 💜 ) |
@delucis I wonder if this could just be a way to compose the whole navbar ordering via some Maybe could just add some classes/ids to make them a bit easier to target and then add to the docs on it? |
I agree, and I guess if this works, having this zod "helper" could maybe be useful for some other thing one day.
I can try to take a look at it tomorrow and post my findings here. |
So I played with it a little bit and the proposed solution in the discussion works but it has some drawbacks. First, as we return a
But if we use the
The usual solution is to use a string as second parameter of Another approach is to use a function as second parameter of
Taking all this into account, the best I could quickly come up with is this: function zOrderedObject<TShape extends z.ZodRawShape>(
shape: TShape,
prefix?: string
) {
const schema = z.object(shape);
return z.custom(
(input) => schema.safeParse(input).success,
(input) => {
const parsedInput = schema.safeParse(input);
const firstError = !parsedInput.success
? parsedInput.error.errors.at(0)
: undefined;
const path = `${prefix ? `${prefix}.` : ""}${firstError?.path.join(".")}`;
const msg = firstError?.message ?? "Validation error";
return { message: `**${path}**: ${msg}` };
}
) as z.ZodObject<TShape>;
} which can be used like this: {
social: zOrderedObject(
{
/** Link to the main Twitter profile for this site, e.g. `'https://twitter.com/astrodotbuild'`. */
twitter: z.string().url().optional(),
/** Link to the main Mastodon profile for this site, e.g. `'https://m.webtoo.ls/@astro'`. */
mastodon: z.string().url().optional(),
/** Link to the main GitHub org or repo for this site, e.g. `'https://github.com/withastro/starlight'`. */
github: z.string().url().optional(),
/** Link to the Discord server for this site, e.g. `'https://astro.build/chat'`. */
discord: z.string().url().optional(),
/** Link to the Codeberg profile or repository for this site, e.g. `'https://codeberg.org/knut/examples'`. */
codeberg: z.string().url().optional(),
/** Link to the Youtube channel for this site, e.g. `'https://www.youtube.com/@astrodotbuild'`. */
youtube: z.string().url().optional(),
/** Link to the Threads profile for this site, e.g. `'https://www.threads.net/@nmoodev'`. */
threads: z.string().url().optional(),
/** Link to the LinkedIn page for this site, e.g. `'https://www.linkedin.com/company/astroinc'`. */
linkedin: z.string().url().optional(),
/** Link to the Twitch profile or repository for this site, e.g. `'https://www.twitch.tv/bholmesdev'`. */
twitch: z.string().url().optional(),
},
// This is required to get the proper path in the error message.
"social"
).optional();
} Not sure how I personally feel about this as it seems a bit too much code to only returns the first error encountered vs all of them which is the case right now. Any thoughts on this? |
Thanks so much for the deep dive @HiDeoo! Appreciate it. We might be able to show all errors using our existing error map like we do when parsing user config: starlight/packages/starlight/index.ts Lines 18 to 25 in af48504
That said, I still think we might be leaning a bit too much on a detail in Zod, plus worse than the enum solution in this PR, it introduces that code complexity. I was about to suggest the CSS approach but then I remembered that that messes with tab order, so it’s not the most accessible result. I think I’d be OK maybe merging this enum approach with the caution that it may break if Zod changes and we won’t necessarily rush to fix it if that does happen. |
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
There aren't any snapshot tests in the repo yet, right? We could change it so that if this does break in zod we can catch it with a snapshot test. |
Unit tests can (and in fact do) include snapshots! A simple test would be to import the user config schema, parse an example options object, and snapshot the resulting Example test: starlight/packages/starlight/__tests__/basics/navigation.test.ts Lines 126 to 141 in af48504
|
Signed-off-by: Lorenzo Lewis <lorenzo@crabnebula.dev>
This reverts commit 4563185.
I added in a snapshot but I think the order gets smugged somewhere through the process: https://github.com/withastro/starlight/actions/runs/5658648982/job/15330419940#step:6:53 Apologies for that fun unverified commit sneaking in, multiple git identities are hard 😮💨 |
Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>
Got git good. Added that test back in so you can see the fail. It looks like Vitest orders it alphabetically? https://github.com/withastro/starlight/actions/runs/5659175483/job/15332146634?pr=381 |
Maybe in this case it would be better to not rely too much on Vitest helpers, maybe test("preserve social config order", () => {
const parsedConfig = StarlightConfigSchema.parse({
title: "Test Title",
social: {
github: "https://github.com/withastro/starlight",
discord: "https://astro.build/chat",
},
});
expect(Object.keys(parsedConfig.social ?? {})).toEqual(["github", "discord"]);
}); Any thoughts? |
Ooofff the more I look at this whole approach the more flakey it seems. I wonder if it'd be better to just switch over to an array sooner rather than later like @delucis suggested 🤔 |
Fixed the test based on @HiDeoo’s suggestion. Vitest use’s Jest’s As I said above, merging this on the basis that it’s an easy win — hopefully solves ordering in most cases, while simplifying the |
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.
Thanks again @lorenzolewis!
What kind of changes does this PR include?
Description
The only thing I wasn't a huge fan of in this was losing all the extra "helper text" in the config.