Created
April 19, 2024 11:29
-
-
Save bm-vs/98709e6263638af50243278440dac270 to your computer and use it in GitHub Desktop.
sanity.types post processing with fragment generation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {readFileSync, writeFileSync} from 'fs'; | |
import path from 'path'; | |
type TsAlias = string; | |
type OriginalFragment = string; | |
type ClosureMap = Map<TsAlias, OriginalFragment>; | |
const main = () => { | |
console.info('Minimizing Sanity types...'); | |
const inputFile = '../sanity/sanity.types.ts'; | |
const outputFile = '../sanity/sanity.types.ts'; | |
const {sanityTypes, sanityQueries} = readFile(inputFile); | |
const {minimizedQueries, closureMap} = parseQueries(sanityQueries); | |
const fragments = closureMapToTypes(closureMap); | |
const manualTypes = generateManualTypes(); | |
const minimizedTypes = sanityTypes + minimizedQueries + fragments + manualTypes; | |
const finalTypes = globalOverrides(minimizedTypes); | |
writeFileSync(path.resolve(__dirname, outputFile), finalTypes); | |
}; | |
// Reads files and returns two strings | |
// sanityTypes is the part of sanity.types that contains the types generated | |
// from the schema (should remain untouched) | |
// sanityQueries is the part of sanity.types that contains the types generated | |
// from the queries (should be minimized) | |
const readFile = (inputFile: string) => { | |
const sanityTypesFile = readFileSync(path.resolve(__dirname, inputFile), 'utf8'); | |
const firstQueryIndex = sanityTypesFile.indexOf('// Query'); | |
const sanityTypes = sanityTypesFile.slice(0, firstQueryIndex); | |
const sanityQueries = sanityTypesFile.slice(firstQueryIndex); | |
return {sanityTypes, sanityQueries}; | |
}; | |
// Iterates over the generated queries and dedups snippets of queries | |
// These snippets are identified by the _ts field | |
// If this field does not exist in a closure, the closure is returned as is | |
// If a closure has the _ptId field, that means it's a unresolved portable text field | |
// Since we never deal with unresolved portable text fields (because our special LiveQuery | |
// handles that), we can safely replace it with SanityRichPortableText that corresponds to | |
// the resolved portable text blocks | |
const parseQueries = (sanityQueries: string) => { | |
let cursor = 0; | |
const closureMap: ClosureMap = new Map(); | |
// Parses a closure and returns a minimized version of it | |
const parseClosure = (queries: string) => { | |
let tsAlias: string | null = null; | |
let parsedClosure = ''; | |
for (; cursor < queries.length; cursor++) { | |
// If the closure has a typegen alias, store it | |
if (queries.startsWith('_ts', cursor)) { | |
tsAlias = | |
queries | |
.slice(cursor) | |
.match(/_ts: ["'](.*)["']/) | |
?.at(1) ?? null; | |
parsedClosure += queries.at(cursor); | |
continue; | |
} | |
// If the closure has a _ptId field, replace it with SanityRichPortableText | |
if (queries.startsWith('_ptId', cursor)) { | |
cursor = queries.indexOf('};', cursor); | |
return 'SanityRichPortableText[] | null'; | |
} | |
// If the line is the query the type refers to, skip it (since it can get super big) | |
if (queries.startsWith('// Query', cursor)) { | |
cursor = queries.indexOf('\n', cursor); | |
continue; | |
} | |
// If starting a closure, parse it | |
if (queries[cursor] === '{') { | |
cursor++; | |
const closure = parseClosure(queries); | |
parsedClosure += closure; | |
continue; | |
} | |
// If the closure is ending, and it doesn't have a type, | |
// return whatever is inside of it, without any modifications | |
if (queries[cursor] === '}' && !tsAlias) { | |
return `{${parsedClosure}}`; | |
} | |
// If the closure is ending, use the _ts field to create a type | |
if (queries[cursor] === '}' && tsAlias) { | |
if (!closureMap.has(tsAlias)) { | |
closureMap.set(tsAlias, parsedClosure); | |
} | |
return tsAlias; | |
} | |
parsedClosure += queries.at(cursor); | |
} | |
return parsedClosure; | |
}; | |
const minimizedQueries = parseClosure(sanityQueries); | |
return {minimizedQueries, closureMap}; | |
}; | |
// Any closures that were found to have a _ts field should have types generated for them | |
const closureMapToTypes = (closureMap: ClosureMap) => { | |
return [...closureMap.entries()].reduce( | |
(acc, [name, query]) => `${acc}${overrideClosureType(name, query)}`, | |
'', | |
); | |
}; | |
// Since the generated types are not always correct, this allows us to override fields within closures | |
// This mostly happens when the query contains some sort of exotic filter like: | |
// *[_type == 'guideStep'][^._id in articles[]._ref][0] (the _id part is the problem) | |
// *[_type == 'customerServiceCategory' && _id == ^.category._ref][0] | |
// Also hierarchy tree things | |
// If Sanity typegen screws up at the query level, add a _ts field to the closures that are problematic | |
// (check @/groq/queries/get-site-map-query) | |
const overrideClosureType = (name: string, query: string) => { | |
const overrides = { | |
SanityInternalReferenceguideArticle: [ | |
{ | |
original: 'step: null;', | |
replacement: `step: { | |
slug: Slug | null; | |
guide: { | |
slug: Slug | null; | |
} | null; | |
} | null;`, | |
}, | |
], | |
SanityCustomerServiceArticleCards: [ | |
{ | |
original: 'articles: null;', | |
replacement: `articles: Array<{ | |
_id: string; | |
_type: "customerServiceArticle"; | |
slug: Slug | null; | |
title: string | null; | |
lead: string | null; | |
}> | null;`, | |
}, | |
], | |
SanityDocumentGroupList: [ | |
{ | |
original: 'documentGroups: Array<null>;', | |
replacement: 'documentGroups: SanityDocumentGroup[] | null;', | |
}, | |
], | |
SanityDocumentGroupHierarchy: [ | |
{ | |
original: 'tree: null;', | |
replacement: ` | |
tree: { | |
_key: string; | |
parent: string | null; | |
value: { | |
reference: { | |
_id: string; | |
slug: Slug | null; | |
} | null; | |
} | null; | |
}[] | null; | |
`, | |
}, | |
], | |
SanitySiteMap: [ | |
{ | |
original: 'tree: null;', | |
replacement: ` | |
tree: { | |
_key: string; | |
parent: string | null; | |
value: { | |
reference: SanityInternalReference | null; | |
} | null; | |
}[] | null; | |
`, | |
}, | |
], | |
} as const; | |
if (name in overrides) { | |
const replacements = overrides[name as keyof typeof overrides]; | |
for (const {original, replacement} of replacements) { | |
query = query.replace(original, replacement); | |
} | |
} | |
return `\n\nexport type ${name} = {${query}};\n`; | |
}; | |
// Global overrides that are applied to the final types | |
// Mostly used to override some types that aren't generated correctly yet | |
// For example: some functions aren't supported yet, like count() | |
const globalOverrides = (types: string) => { | |
const overrides = [ | |
{ | |
// All count functions in the queries are coallesced with 0, | |
// creating this very unique type that can be overriden | |
original: '0 | unknown', | |
replacement: 'number', | |
}, | |
]; | |
for (const {original, replacement} of overrides) { | |
types = types.replaceAll(original, replacement); | |
} | |
return types; | |
}; | |
// Use this to generate types that get used in the sanity.types file itself | |
// If it doesn't get used in the sanity.types file, you can just create the type elsewhere | |
// But in some circumstances, like with PortableText and InternalReference, | |
// these types save a lot of repetition in the sanity.types file | |
const generateManualTypes = () => { | |
return ` | |
export type SanityRichPortableText = NonNullable<NonNullable<DummyPortableTextQueryResult>["content"]>[number]; | |
export type SanityInternalReference = | |
| SanityInternalReferencearticle | |
| SanityInternalReferencecontactFormPage | |
| SanityInternalReferencecustomerServiceArticle | |
| SanityInternalReferencecustomerServiceCategory | |
| SanityInternalReferencecustomerServiceHome | |
| SanityInternalReferencedocumentGroup | |
| SanityInternalReferencedoorType | |
| SanityInternalReferencefaq | |
| SanityInternalReferenceglassFunction | |
| SanityInternalReferenceglassOption | |
| SanityInternalReferenceguide | |
| SanityInternalReferenceguideArticle | |
| SanityInternalReferenceinspirationalStory | |
| SanityInternalReferencematerial | |
| SanityInternalReferencemuntinBarStyle | |
| SanityInternalReferencepage | |
| SanityInternalReferenceprofile | |
| SanityInternalReferenceuValue | |
| SanityInternalReferencevacancy | |
| SanityInternalReferencewindowType; | |
`; | |
}; | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment