Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/social-objects-extend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@fujocoded/zod-transform-socials": patch
---

Exports `SocialLinkObjectSchema` so projects can extend the object form of a
social link without rebuilding the whole schema. `SocialLinkInputSchema` is also
available as the clearer name for the default one-item input schema, with
matching `SocialLinkObject` and `SocialLinkInput` types.

Adds standalone examples for preserving custom fields like `label` through
the transform.
34 changes: 34 additions & 0 deletions zod-transform-socials/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,40 @@ interface Props {
}
```

### Extending the input object

If your content needs extra fields before the social links are transformed,
extend `SocialLinkObjectSchema` and build your own schema:

```ts
import {
SocialLinkObjectSchema,
transformSocial,
urlSchema,
} from "@fujocoded/zod-transform-socials"; // or from "@fujocoded/zod-transform-socials/zod4"
import { z } from "zod";

const SocialLinkWithLabel = z.union([
urlSchema.transform(transformSocial),
SocialLinkObjectSchema.extend({
label: z.string().optional(),
}).transform((socialLinkData) => ({
...transformSocial(socialLinkData),
label: socialLinkData.label,
})),
]);

const Member = z.object({
name: z.string(),
contacts: z.array(SocialLinkWithLabel).default([]),
});
```

For complete runnable versions, see the standalone extension examples:
[`03-zod-3-standalone/parse-extension.ts`](./__examples__/03-zod-3-standalone/parse-extension.ts)
and
[`04-zod-4-standalone/parse-extension.ts`](./__examples__/04-zod-4-standalone/parse-extension.ts).

## Setting known domains

For platforms without a fixed domain, like `mastodon`, the built-in matchers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ it: you build the schema, you call `.parse()`, you read the result.
npm start
```

3. Run the type check. This checks the same `parse.ts` file and proves
3. Run the extension script. It shows how to extend the object form with a
`label` field and keep that label in the transformed output:

```bash
npm run extension
```

4. Run the type check. This checks both parse scripts and proves
the package's exported types resolve correctly under Zod 3:

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"extension": "node parse-extension.ts",
"start": "node parse.ts",
"typecheck": "tsc --noEmit"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { strict as assert } from "node:assert";
import {
SocialLinkObjectSchema,
transformSocial,
urlSchema,
} from "@fujocoded/zod-transform-socials";
import { z } from "zod";

const SocialLinkWithLabel = z.union([
// Simple links get transformed just like the library does
urlSchema.transform(transformSocial),
SocialLinkObjectSchema.extend({
// We extend object links with an optional label field
label: z.string().optional(),
}).transform((socialLinkData) => ({
// We transform the socialLinkData just like the library does
...transformSocial(socialLinkData),
// And we also add the label field
label: socialLinkData.label,
})),
]);

const SocialLinksWithLabels = z.array(SocialLinkWithLabel).default([]);

const Member = z.object({
name: z.string(),
contacts: SocialLinksWithLabels,
});

const parsed = Member.parse({
name: "essential-randomness",
contacts: [
{
url: "https://essentialrandomness.com",
label: "Website",
},
{
url: "https://indiepocalypse.social/@essentialrandom",
platform: "mastodon",
label: "Mastodon",
},
"https://github.com/essential-randomness",
],
});

assert.deepStrictEqual(parsed, {
name: "essential-randomness",
contacts: [
{
url: "https://essentialrandomness.com",
platform: "custom",
username: null,
icon: null,
label: "Website",
},
{
icon: "simple-icons:mastodon",
url: "https://indiepocalypse.social/@essentialrandom",
platform: "mastodon",
username: null,
label: "Mastodon",
},
{
url: "https://github.com/essential-randomness",
platform: "github",
username: "essential-randomness",
icon: "simple-icons:github",
},
],
});

console.log(JSON.stringify(parsed, null, 2));
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { strict as assert } from "node:assert";
import { z } from "zod";
import { SocialLinks } from "@fujocoded/zod-transform-socials";
import type {
SocialLinkInput,
SocialLinksData,
SocialsSchema,
} from "@fujocoded/zod-transform-socials";

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

// `SocialsSchema` types each item the schema will accept: a bare URL string,
// `SocialLinkInput` types each item the schema will accept: a bare URL string,
// or an object that overrides the detected platform / username / icon.
const contacts: SocialsSchema[] = [
const contacts: SocialLinkInput[] = [
"https://essentialrandomness.com",
"https://essential-randomness.tumblr.com",
"https://twitter.com/essentialrandom",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"skipLibCheck": false,
"noEmit": true
},
"include": ["parse.ts"]
"include": ["parse*.ts"]
}
7 changes: 7 additions & 0 deletions zod-transform-socials/__examples__/04-zod-4-standalone/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copy the local `file:../..` package into node_modules instead of symlinking it.
# Without this, TypeScript can realpath the package's `/zod4` declarations back
# to this workspace and resolve `zod/v4` against the workspace's Zod 3 dev
# dependency instead of this consumer's zod@4, mixing Zod type families. A real
# registry install has no workspace symlink, so this flag makes the local file
# install match published-package resolution.
install-links=true
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ standalone example, except the import comes from
npm start
```

3. Run the type check. This checks the same `parse.ts` file and proves
3. Run the extension script. It shows how to extend the object form with a
`label` field and keep that label in the transformed output:

```bash
npm run extension
```

4. Run the type check. This checks both parse scripts and proves
the package's `/zod4` exported types resolve correctly under Zod 4:

```bash
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"extension": "node parse-extension.ts",
"start": "node parse.ts",
"typecheck": "tsc --noEmit"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { strict as assert } from "node:assert";
import {
SocialLinkObjectSchema,
transformSocial,
urlSchema,
} from "@fujocoded/zod-transform-socials/zod4";
import { z } from "zod";

const SocialLinkWithLabel = z.union([
// Simple links get transformed just like the library does
urlSchema.transform(transformSocial),
SocialLinkObjectSchema.extend({
// We extend object links with an optional label field
label: z.string().optional(),
}).transform((socialLinkData) => ({
// We transform the socialLinkData just like the library does
...transformSocial(socialLinkData),
// And we also add the label field
label: socialLinkData.label,
})),
]);

const Member = z.object({
name: z.string(),
contacts: z.array(SocialLinkWithLabel).default([]),
});

const parsed = Member.parse({
name: "essential-randomness",
contacts: [
{
url: "https://essentialrandomness.com",
label: "Website",
},
{
url: "https://indiepocalypse.social/@essentialrandom",
platform: "mastodon",
label: "Mastodon",
},
"https://github.com/essential-randomness",
],
});

assert.deepStrictEqual(parsed, {
name: "essential-randomness",
contacts: [
{
url: "https://essentialrandomness.com",
platform: "custom",
username: null,
icon: null,
label: "Website",
},
{
icon: "simple-icons:mastodon",
url: "https://indiepocalypse.social/@essentialrandom",
platform: "mastodon",
username: null,
label: "Mastodon",
},
{
url: "https://github.com/essential-randomness",
platform: "github",
username: "essential-randomness",
icon: "simple-icons:github",
},
],
});

console.log(JSON.stringify(parsed, null, 2));
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
transformSocial,
} from "@fujocoded/zod-transform-socials/zod4";
import type {
SocialLinkInput,
SocialLinksData,
SocialsSchema,
} from "@fujocoded/zod-transform-socials/zod4";

// Use `SocialLinks` as a schema field to parse and enrich contact URLs.
Expand All @@ -16,15 +16,15 @@ const Member = z.object({
contacts: SocialLinks,
});

// `SocialsSchema` types each contact: a bare URL string, or an object that
// `SocialLinkInput` types each contact: a bare URL string, or an object that
// overrides the detected platform / username / icon (here we force Mastodon
// because the indiepocalypse.social host isn't a built-in match).
const mastodonContact = {
url: "https://indiepocalypse.social/@essentialrandom",
platform: "mastodon",
} satisfies SocialsSchema;
} satisfies SocialLinkInput;

const contacts: SocialsSchema[] = [
const contacts: SocialLinkInput[] = [
"https://essentialrandomness.com",
"https://essential-randomness.tumblr.com",
"https://twitter.com/essentialrandom",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"skipLibCheck": false,
"noEmit": true
},
"include": ["parse.ts"]
"include": ["parse*.ts"]
}
7 changes: 5 additions & 2 deletions zod-transform-socials/__examples__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ Pick the one that matches your needs:

The two Astro examples read the same YAML team profile through `glob()` and
render the parsed `contacts` array on the index page. The two standalone
examples use one `parse.ts` entry each to parse the same input inline, assert
the transformed output, print it, and type-check the package imports.
examples use `parse.ts` to parse the same input inline, assert the transformed
output, print it, and type-check the package imports.

The standalone examples also include `parse-extension.ts` to show how to extend
`SocialLinkObjectSchema` when you need extra fields to survive the transform.

These also double as the package's compatibility check.
2 changes: 2 additions & 0 deletions zod-transform-socials/scripts/check-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,15 @@ const cases: TestCase[] = [
commands: [
["npm", ["run", "typecheck"]],
["npm", ["start"]],
["npm", ["run", "extension"]],
],
},
{
dir: "04-zod-4-standalone",
commands: [
["npm", ["run", "typecheck"]],
["npm", ["start"]],
["npm", ["run", "extension"]],
],
},
];
Expand Down
Loading