|
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(); |