Skip to content

Commit b4332bc

Browse files
Make it easier to extend zod-transform-socials
1 parent 4c266d6 commit b4332bc

18 files changed

Lines changed: 322 additions & 58 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@fujocoded/zod-transform-socials": patch
3+
---
4+
5+
Exports `SocialLinkObjectSchema` so projects can extend the object form of a
6+
social link without rebuilding the whole schema. `SocialLinkInputSchema` is also
7+
available as the clearer name for the default one-item input schema, with
8+
matching `SocialLinkObject` and `SocialLinkInput` types.
9+
10+
Adds standalone examples for preserving custom fields like `label` through
11+
the transform.

zod-transform-socials/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,40 @@ interface Props {
156156
}
157157
```
158158
159+
### Extending the input object
160+
161+
If your content needs extra fields before the social links are transformed,
162+
extend `SocialLinkObjectSchema` and build your own schema:
163+
164+
```ts
165+
import {
166+
SocialLinkObjectSchema,
167+
transformSocial,
168+
urlSchema,
169+
} from "@fujocoded/zod-transform-socials"; // or from "@fujocoded/zod-transform-socials/zod4"
170+
import { z } from "zod";
171+
172+
const SocialLinkWithLabel = z.union([
173+
urlSchema.transform(transformSocial),
174+
SocialLinkObjectSchema.extend({
175+
label: z.string().optional(),
176+
}).transform((socialLinkData) => ({
177+
...transformSocial(socialLinkData),
178+
label: socialLinkData.label,
179+
})),
180+
]);
181+
182+
const Member = z.object({
183+
name: z.string(),
184+
contacts: z.array(SocialLinkWithLabel).default([]),
185+
});
186+
```
187+
188+
For complete runnable versions, see the standalone extension examples:
189+
[`03-zod-3-standalone/parse-extension.ts`](./__examples__/03-zod-3-standalone/parse-extension.ts)
190+
and
191+
[`04-zod-4-standalone/parse-extension.ts`](./__examples__/04-zod-4-standalone/parse-extension.ts).
192+
159193
## Setting known domains
160194
161195
For platforms without a fixed domain, like `mastodon`, the built-in matchers

zod-transform-socials/__examples__/03-zod-3-standalone/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ it: you build the schema, you call `.parse()`, you read the result.
2626
npm start
2727
```
2828

29-
3. Run the type check. This checks the same `parse.ts` file and proves
29+
3. Run the extension script. It shows how to extend the object form with a
30+
`label` field and keep that label in the transformed output:
31+
32+
```bash
33+
npm run extension
34+
```
35+
36+
4. Run the type check. This checks both parse scripts and proves
3037
the package's exported types resolve correctly under Zod 3:
3138

3239
```bash

zod-transform-socials/__examples__/03-zod-3-standalone/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7+
"extension": "node parse-extension.ts",
78
"start": "node parse.ts",
89
"typecheck": "tsc --noEmit"
910
},
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { strict as assert } from "node:assert";
2+
import {
3+
SocialLinkObjectSchema,
4+
transformSocial,
5+
urlSchema,
6+
} from "@fujocoded/zod-transform-socials";
7+
import { z } from "zod";
8+
9+
const SocialLinkWithLabel = z.union([
10+
// Simple links get transformed just like the library does
11+
urlSchema.transform(transformSocial),
12+
SocialLinkObjectSchema.extend({
13+
// We extend object links with an optional label field
14+
label: z.string().optional(),
15+
}).transform((socialLinkData) => ({
16+
// We transform the socialLinkData just like the library does
17+
...transformSocial(socialLinkData),
18+
// And we also add the label field
19+
label: socialLinkData.label,
20+
})),
21+
]);
22+
23+
const SocialLinksWithLabels = z.array(SocialLinkWithLabel).default([]);
24+
25+
const Member = z.object({
26+
name: z.string(),
27+
contacts: SocialLinksWithLabels,
28+
});
29+
30+
const parsed = Member.parse({
31+
name: "essential-randomness",
32+
contacts: [
33+
{
34+
url: "https://essentialrandomness.com",
35+
label: "Website",
36+
},
37+
{
38+
url: "https://indiepocalypse.social/@essentialrandom",
39+
platform: "mastodon",
40+
label: "Mastodon",
41+
},
42+
"https://github.com/essential-randomness",
43+
],
44+
});
45+
46+
assert.deepStrictEqual(parsed, {
47+
name: "essential-randomness",
48+
contacts: [
49+
{
50+
url: "https://essentialrandomness.com",
51+
platform: "custom",
52+
username: null,
53+
icon: null,
54+
label: "Website",
55+
},
56+
{
57+
icon: "simple-icons:mastodon",
58+
url: "https://indiepocalypse.social/@essentialrandom",
59+
platform: "mastodon",
60+
username: null,
61+
label: "Mastodon",
62+
},
63+
{
64+
url: "https://github.com/essential-randomness",
65+
platform: "github",
66+
username: "essential-randomness",
67+
icon: "simple-icons:github",
68+
},
69+
],
70+
});
71+
72+
console.log(JSON.stringify(parsed, null, 2));

zod-transform-socials/__examples__/03-zod-3-standalone/parse.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { strict as assert } from "node:assert";
22
import { z } from "zod";
33
import { SocialLinks } from "@fujocoded/zod-transform-socials";
44
import type {
5+
SocialLinkInput,
56
SocialLinksData,
6-
SocialsSchema,
77
} from "@fujocoded/zod-transform-socials";
88

99
// Use `SocialLinks` as a schema field to parse and enrich contact URLs.
@@ -12,9 +12,9 @@ const Member = z.object({
1212
contacts: SocialLinks,
1313
});
1414

15-
// `SocialsSchema` types each item the schema will accept: a bare URL string,
15+
// `SocialLinkInput` types each item the schema will accept: a bare URL string,
1616
// or an object that overrides the detected platform / username / icon.
17-
const contacts: SocialsSchema[] = [
17+
const contacts: SocialLinkInput[] = [
1818
"https://essentialrandomness.com",
1919
"https://essential-randomness.tumblr.com",
2020
"https://twitter.com/essentialrandom",

zod-transform-socials/__examples__/03-zod-3-standalone/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"skipLibCheck": false,
88
"noEmit": true
99
},
10-
"include": ["parse.ts"]
10+
"include": ["parse*.ts"]
1111
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copy the local `file:../..` package into node_modules instead of symlinking it.
2+
# Without this, TypeScript can realpath the package's `/zod4` declarations back
3+
# to this workspace and resolve `zod/v4` against the workspace's Zod 3 dev
4+
# dependency instead of this consumer's zod@4, mixing Zod type families. A real
5+
# registry install has no workspace symlink, so this flag makes the local file
6+
# install match published-package resolution.
7+
install-links=true

zod-transform-socials/__examples__/04-zod-4-standalone/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ standalone example, except the import comes from
2626
npm start
2727
```
2828

29-
3. Run the type check. This checks the same `parse.ts` file and proves
29+
3. Run the extension script. It shows how to extend the object form with a
30+
`label` field and keep that label in the transformed output:
31+
32+
```bash
33+
npm run extension
34+
```
35+
36+
4. Run the type check. This checks both parse scripts and proves
3037
the package's `/zod4` exported types resolve correctly under Zod 4:
3138

3239
```bash

zod-transform-socials/__examples__/04-zod-4-standalone/package-lock.json

Lines changed: 9 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)