Skip to content

Instantly share code, notes, and snippets.

@marcogrcr
Last active May 2, 2024 00:14
Show Gist options
  • Save marcogrcr/4c25e56ba7554dee614b5b65615ac7f5 to your computer and use it in GitHub Desktop.
Save marcogrcr/4c25e56ba7554dee614b5b65615ac7f5 to your computer and use it in GitHub Desktop.
Transforms files generated by contentful-typescript-codegen

This script transforms files generated by contentful-typescript-codegen to ensure the sys property is in sync with the contentful package.

For example, the following auto-generated file:

// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT.

import { Asset, Entry } from 'contentful';
import { Document } from '@contentful/rich-text-types';

export interface IMyContentTypeFields {
  myField: Document;
}

export interface IMyContentType extends Entry<IMyContentTypeFields> {
  sys: {
    id: string;
    type: string;
    createdAt: string;
    updatedAt: string;
    locale: string;
    contentType: {
      sys: {
        id: 'myContentType';
        linkType: 'ContentType';
        type: 'Link';
      };
    };
  };
}

export type CONTENT_TYPE = 'myContentType';
export type IEntry = IMyContentType;
export type LOCALE_CODE = 'en-US';
export type CONTENTFUL_DEFAULT_LOCALE_CODE = 'en-US';

Is transformed as follows:

// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT.
import { Asset, Entry, Sys } from "contentful";
import { Document } from "@contentful/rich-text-types";
export interface IMyContentTypeFields {
  myField: Document;
}
export interface IMyContentType extends Entry<IMyContentTypeFields> {
  sys: Sys & {
    contentType: {
      sys: {
        id: "myContentType";
        linkType: "ContentType";
        type: "Link";
      };
    };
  };
}

export type CONTENT_TYPE = "myContentType";
export type IEntry = IMyContentType;
export type LOCALE_CODE = "en-US";
export type CONTENTFUL_DEFAULT_LOCALE_CODE = "en-US";

Note that this script is not necessary once #162 is merged.

import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { extname } from 'node:path';
import ts from 'typescript';
/**
* If node {@link ts.Node.kind} is {@link ts.SyntaxKind.ImportDeclaration} for `contentful`, adds `Sys` and returns it.
* Otherwise returns `null`.
*
* Thus:
* ```
* import { Asset, Entry } from "contentful";
* ```
*
* Becomes:
* ```
* import { Asset, Entry, Sys } from "contentful";
* ```
*/
function addSysImport(node: ts.Node, factory: ts.NodeFactory): ts.Node | null {
if (
ts.isImportDeclaration(node) &&
ts.isStringTextContainingNode(node.moduleSpecifier) &&
node.moduleSpecifier.text === 'contentful' &&
node.importClause &&
node.importClause.namedBindings &&
ts.isNamedImports(node.importClause.namedBindings)
) {
return factory.updateImportDeclaration(
node,
node.modifiers,
factory.updateImportClause(
node.importClause,
node.importClause.isTypeOnly,
node.importClause.name,
factory.updateNamedImports(node.importClause.namedBindings, [
...node.importClause.namedBindings.elements,
factory.createImportSpecifier(
false,
undefined,
factory.createIdentifier('Sys')
)
])
),
node.moduleSpecifier,
node.assertClause
);
}
// if we got here, this is not the node we're looking for
return null;
}
/**
* If node {@link ts.Node.kind} is {@link ts.SyntaxKind.InterfaceDeclaration} that extends `Entry`, updates `sys` field
* and returns it. Otherwise returns `null`.
*
* ```
* interface IMyContentType extends Entry<IMyContentTypeFields> {
* sys: {
* id: string;
* type: string;
* createdAt: string;
* updatedAt: string;
* locale: string;
* contentType: {
* sys: {
* id: "myContentType";
* linkType: "ContentType";
* type: "Link";
* };
* };
* };
* }
* ```
*
* becomes:
*
* ```
* interface IMyContentType extends Entry<IMyContentTypeFields> {
* sys: Sys & {
* contentType: {
* sys: {
* id: "myContentType";
* linkType: "ContentType";
* type: "Link";
* };
* };
* };
* }
* ```
*/
function updateSysProperty(
node: ts.Node,
factory: ts.NodeFactory
): ts.Node | null {
if (
ts.isInterfaceDeclaration(node) &&
node.heritageClauses?.some((h) =>
h.types.some(
(t) =>
ts.isIdentifier(t.expression) &&
t.expression.text === 'Entry'
)
)
) {
const sysProperty = node.members.find(
(x) => x.name && ts.isIdentifier(x.name) && x.name.text === 'sys'
);
if (
sysProperty &&
ts.isPropertySignature(sysProperty) &&
sysProperty.type &&
ts.isTypeLiteralNode(sysProperty.type)
) {
return factory.updateInterfaceDeclaration(
node,
node.modifiers,
node.name,
node.typeParameters,
node.heritageClauses,
[
factory.updatePropertySignature(
sysProperty,
sysProperty.modifiers,
sysProperty.name,
sysProperty.questionToken,
factory.createIntersectionTypeNode([
factory.createTypeReferenceNode('Sys'),
factory.updateTypeLiteralNode(
sysProperty.type,
factory.createNodeArray(
sysProperty.type.members.filter(
(x) =>
x.name &&
ts.isIdentifier(x.name) &&
x.name.text === 'contentType'
)
)
)
])
),
...node.members.filter((x) => x !== sysProperty)
]
);
}
}
// if we got here, this is not the node we're looking for
return null;
}
const updateContentfulTypesFile: ts.TransformerFactory<ts.SourceFile> =
(context) => (rootNode) => {
const { factory } = context;
const visitor: ts.Visitor = (node) =>
addSysImport(node, factory) ??
updateSysProperty(node, factory) ??
ts.visitEachChild(node, visitor, context);
return ts.visitNode(rootNode, visitor);
};
function main() {
const files = process.argv.slice(2);
// at least one file is specified
if (!files.length) {
console.error(`Usage: ${process.argv[1]} {file.ts} [file.ts] [...]`);
process.exit(1);
}
// only typescript files are allowed
const nonTsFiles = files.filter((x) => extname(x) !== '.ts');
if (nonTsFiles.length) {
console.error('Non TypeScript files specified:', nonTsFiles.join());
process.exit(1);
}
// specified files must exist
const nonExistentFiles = files.filter((x) => !existsSync(x));
if (nonExistentFiles.length) {
console.error('Non-existent files specified:', nonExistentFiles.join());
process.exit(1);
}
// transform files
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
for (const file of files) {
console.log('Transforming:', file);
const sourceFile = ts.createSourceFile(
file,
readFileSync(file).toString(),
ts.ScriptTarget.ESNext
);
ts.transform(sourceFile, [
updateContentfulTypesFile
]).transformed.forEach((f) => {
writeFileSync(f.fileName, printer.printFile(f));
});
}
console.log('Done!');
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment