Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support inline types #40

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 5 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }], "@babel/preset-typescript"]
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }],
"@babel/preset-react",
"@babel/preset-typescript"
]
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
"test": "jest",
"samples:check": "ts-node ./src/cli/typegen.ts check -r ./samples",
"samples:write": "ts-node ./src/cli/typegen.ts write -r ./samples --write-paths",
"samples:write:inline": "ts-node ./src/cli/typegen.ts write -r ./samples --write-paths --inline",
"samples:convert": "ts-node ./src/cli/typegen.ts write -r ./samples --convert-to-builders",
"samples:watch": "ts-node ./src/cli/typegen.ts watch -r ./samples",
"samples:write:posthog": "ts-node ./src/cli/typegen.ts write -c ../../PostHog/posthog/tsconfig.json",
"samples:write:posthog": "ts-node ./src/cli/typegen.ts write --inline --write-paths -c ../../PostHog/posthog/tsconfig.json",
"form-plugin:build": "cd form-plugin && yarn && yarn build && cd ..",
"form-plugin:rebuild": "yarn form-plugin:build && rm -rf node_modules && yarn"
},
Expand All @@ -28,11 +29,12 @@
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.0",
"@wessberg/ts-clone-node": "0.3.19",
"prettier": "^2.5.1",
"prettier": "^2.8.3",
"recast": "^0.20.5",
"yargs": "^16.2.0"
},
"devDependencies": {
"@babel/preset-react": "^7.18.6",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.31",
"@types/yargs": "^16.0.0",
Expand All @@ -41,7 +43,7 @@
"form-plugin": "file:./form-plugin",
"husky": ">=4",
"jest": "^27.0.5",
"kea": "^3.0.0-alpha.6",
"kea": "^3.1.3",
"kea-router": "^3.0.0-alpha.0",
"lint-staged": ">=12.1.2",
"react": "^16.13.1",
Expand Down
1 change: 1 addition & 0 deletions src/cli/typegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ yargs
type: 'string',
})
.option('quiet', { alias: 'q', describe: 'Write nothing to stdout', type: 'boolean' })
.option('inline', { alias: 'i', describe: 'Inline types', type: 'boolean' })
.option('no-import', { describe: 'Do not automatically import generated types in logic files', type: 'boolean' })
.option('write-paths', { describe: 'Write paths into logic files that have none', type: 'boolean' })
.option('add-ts-nocheck', { describe: 'Add @ts-nocheck to top of logicType.ts files', type: 'boolean' })
Expand Down
307 changes: 307 additions & 0 deletions src/inline/inline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
import { AppOptions, ParsedLogic } from '../types'
import * as fs from 'fs'
import { getShouldIgnore, nodeToString, runThroughPrettier } from '../print/print'
import { print, visit } from 'recast'
import type { NodePath } from 'ast-types/lib/node-path'
import type { namedTypes } from 'ast-types/gen/namedTypes'
import { t, b, visitAllKeaCalls, getAst, assureImport } from '../write/utils'
import { factory, SyntaxKind, Program } from 'typescript'
import { cleanDuplicateAnyNodes } from '../utils'
import { printInternalExtraInput } from '../print/printInternalExtraInput'
import { printInternalSelectorTypes } from '../print/printInternalSelectorTypes'
import { printInternalReducerActions } from '../print/printInternalReducerActions'
import * as ts from 'typescript'
import * as path from 'path'

export function inlineFiles(
program: Program,
appOptions: AppOptions,
parsedLogics: ParsedLogic[],
): { filesToWrite: number; writtenFiles: number; filesToModify: number } {
const groupedByFile: Record<string, ParsedLogic[]> = {}
for (const parsedLogic of parsedLogics) {
if (!groupedByFile[parsedLogic.fileName]) {
groupedByFile[parsedLogic.fileName] = []
}
groupedByFile[parsedLogic.fileName].push(parsedLogic)
}

for (const [filename, parsedLogics] of Object.entries(groupedByFile)) {
try {
const sourceFile = program.getSourceFile(filename)
const rawCode = sourceFile.getText()

const ast = getAst(filename, rawCode)

const parsedLogicTypeNames = new Set<string>(parsedLogics.map((l) => l.logicTypeName))
const foundLogicTypes = new Map<string, NodePath>()

let hasImportFromKea = false
let foundKeaLogicTypeImport = false
const importedVariables = new Set<string>()

visit(ast, {
visitTSTypeAliasDeclaration(path): any {
if (parsedLogicTypeNames.has(path.value.id.name)) {
foundLogicTypes.set(path.value.id.name, path)
}
return false
},
visitImportDeclaration(path) {
const isKeaImport =
path.value.source &&
t.StringLiteral.check(path.value.source) &&
path.value.source.value === 'kea'

if (isKeaImport) {
hasImportFromKea = true
for (const specifier of path.value.specifiers) {
if (specifier.imported.name === 'KeaLogicType') {
foundKeaLogicTypeImport = true
}
}
}

// remove non-inline imports from external loginType.ts files
for (const specifier of path.value.specifiers) {
if (specifier.imported?.name && parsedLogicTypeNames.has(specifier.imported.name)) {
path.value.specifiers = path.value.specifiers.filter((s) => s !== specifier)
if (path.value.specifiers.length === 0) {
path.prune()
}
} else {
importedVariables.add(specifier.local?.name ?? specifier.imported?.name)
}
}

return false
},
})

if (!foundKeaLogicTypeImport) {
assureImport(ast, 'kea', 'KeaLogicType', 'KeaLogicType', hasImportFromKea)
}

visitAllKeaCalls(ast, parsedLogics, filename, ({ path, parsedLogic }) => {
path.node.typeParameters = b.tsTypeParameterInstantiation([
b.tsTypeReference(b.identifier(parsedLogic.logicTypeName)),
])
if (foundLogicTypes.has(parsedLogic.logicTypeName)) {
const typeAlias: NodePath = foundLogicTypes.get(parsedLogic.logicTypeName)
typeAlias.parentPath.value.comments = createLogicTypeComments(parsedLogic)
if (t.TSTypeAliasDeclaration.check(typeAlias.value)) {
typeAlias.value.typeAnnotation = createLogicTypeReference(
program,
appOptions,
parsedLogic,
importedVariables,
ast.program.body,
)
}
} else {
let ptr: NodePath = path
while (ptr) {
if (ptr.parentPath?.value === ast.program.body) {
const index = ast.program.body.findIndex((n) => n === ptr.value)
const logicTypeNode = b.exportNamedDeclaration(
b.tsTypeAliasDeclaration(
b.identifier(parsedLogic.logicTypeName),
createLogicTypeReference(
program,
appOptions,
parsedLogic,
importedVariables,
ast.program.body,
),
),
)
logicTypeNode.comments = createLogicTypeComments(parsedLogic)
ast.program.body = [
...ast.program.body.slice(0, index + 1),
logicTypeNode,
...ast.program.body.slice(index + 1),
]
}
ptr = ptr.parentPath
}
}
})

const newText = runThroughPrettier(print(ast).code, filename)
fs.writeFileSync(filename, newText)
} catch (e) {
console.error(`Error updating logic types in ${filename}`)
console.error(e)
}
}

return { filesToWrite: 0, writtenFiles: 0, filesToModify: 0 }
}

export function createLogicTypeReference(
program: Program,
appOptions: AppOptions,
parsedLogic: ParsedLogic,
importedVariables: Set<string>,
body: namedTypes.Program['body'],
): ReturnType<typeof b.tsTypeReference> {
let typeReferenceNode: ts.TypeNode = factory.createTypeReferenceNode(factory.createIdentifier('KeaLogicType'), [
factory.createTypeLiteralNode(
[
// actions
factory.createPropertySignature(
undefined,
factory.createIdentifier('actions'),
undefined,
factory.createTypeLiteralNode(
[...parsedLogic.actions]
.sort((a, b) => a.name.localeCompare(b.name))
.map(({ name, parameters, returnTypeNode }) =>
factory.createPropertySignature(
undefined,
factory.createIdentifier(name),
undefined,
factory.createFunctionTypeNode(undefined, parameters, returnTypeNode),
),
),
),
),
// values
factory.createPropertySignature(
undefined,
factory.createIdentifier('values'),
undefined,
factory.createTypeLiteralNode(
cleanDuplicateAnyNodes(parsedLogic.reducers.concat(parsedLogic.selectors))
.sort((a, b) => a.name.localeCompare(b.name))
.map((reducer) =>
factory.createPropertySignature(
undefined,
factory.createIdentifier(reducer.name),
undefined,
reducer.typeNode,
),
),
),
),
// props
parsedLogic.propsType
? factory.createPropertySignature(
undefined,
factory.createIdentifier('props'),
undefined,
parsedLogic.propsType ||
factory.createTypeReferenceNode(factory.createIdentifier('Record'), [
factory.createKeywordTypeNode(SyntaxKind.StringKeyword),
factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword),
]),
)
: undefined,

parsedLogic.selectors.filter((s) => s.functionTypes.length > 0).length > 0
? factory.createPropertySignature(
undefined,
factory.createIdentifier('__keaTypeGenInternalSelectorTypes'),
undefined,
printInternalSelectorTypes(parsedLogic),
)
: null,

Object.keys(parsedLogic.extraActions).length > 0
? factory.createPropertySignature(
undefined,
factory.createIdentifier('__keaTypeGenInternalReducerActions'),
undefined,
printInternalReducerActions(parsedLogic),
)
: null,
Object.keys(parsedLogic.extraInput).length > 0
? factory.createPropertySignature(
undefined,
factory.createIdentifier('__keaTypeGenInternalExtraInput'),
undefined,
printInternalExtraInput(parsedLogic),
)
: null,
].filter((a) => !!a),
),
])

if (Object.keys(parsedLogic.extraLogicFields).length > 0) {
typeReferenceNode = factory.createIntersectionTypeNode([
typeReferenceNode,
factory.createTypeLiteralNode(
Object.entries(parsedLogic.extraLogicFields).map(([key, field]) =>
factory.createPropertySignature(undefined, factory.createIdentifier(key), undefined, field),
),
),
])
}

if (Object.keys(parsedLogic.typeReferencesToImportFromFiles).length > 0) {
const shouldIgnore = getShouldIgnore(program, appOptions)
const requiredImports = Object.entries(parsedLogic.typeReferencesToImportFromFiles)
.map(([file, list]): [string, string[]] => [file, [...list].filter((key) => !importedVariables.has(key))])
.filter(([file, list]) => list.length > 0 && !shouldIgnore(file) && file !== parsedLogic.fileName)

for (const [file, list] of requiredImports) {
let relativePath = path.relative(path.dirname(parsedLogic.fileName), file)
relativePath = relativePath.replace(/\.tsx?$/, '')
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`
}

let importDeclaration = body.find(
(node) => t.ImportDeclaration.check(node) && node.source.value === relativePath,
) as namedTypes.ImportDeclaration | undefined
if (importDeclaration) {
importDeclaration.specifiers.push(b.importSpecifier(b.identifier(list[0])))
} else {
importDeclaration = b.importDeclaration(
list.map((key) => b.importSpecifier(b.identifier(key))),
b.stringLiteral(relativePath),
)
let lastIndex = -1
for (let i = 0; i < body.length; i++) {
if (t.ImportDeclaration.check(body[i])) {
lastIndex = i
}
}
body.splice(lastIndex + 1, 0, importDeclaration)
}
}
}

// Transform Typescript API's AST to a string
let source: string = ''
try {
source = nodeToString(
factory.createTypeAliasDeclaration(
[],
[],
factory.createIdentifier(parsedLogic.logicTypeName),
undefined,
typeReferenceNode,
),
)
} catch (e) {
console.error(`Error emitting logic type ${parsedLogic.logicTypeName} to string`)
console.error(e)
debugger
}

// Convert that string to recast's AST
const node = getAst(parsedLogic.fileName, source).program.body[0].typeAnnotation

return node
}

function createLogicTypeComments(parsedLogic: ParsedLogic): namedTypes.CommentLine[] {
return [
{
type: 'CommentLine',
value: ` This is an auto-generated type for the logic "${parsedLogic.logicName}".`,
leading: true,
},
]
}
Loading