diff --git a/.changeset/kind-laws-bake.md b/.changeset/kind-laws-bake.md new file mode 100644 index 0000000..f5b727b --- /dev/null +++ b/.changeset/kind-laws-bake.md @@ -0,0 +1,5 @@ +--- +"@magical-types/macro": minor +--- + +Share common nodes within a single module diff --git a/.changeset/slimy-zoos-joke.md b/.changeset/slimy-zoos-joke.md new file mode 100644 index 0000000..6aca74b --- /dev/null +++ b/.changeset/slimy-zoos-joke.md @@ -0,0 +1,5 @@ +--- +"@magical-types/types": patch +--- + +Add `MagicalNodeWithIndexes` and `MagicalNodeIndex` diff --git a/packages/macro/runtime/src/index.tsx b/packages/macro/runtime/src/index.tsx index 8d1a47e..5ec07a0 100644 --- a/packages/macro/runtime/src/index.tsx +++ b/packages/macro/runtime/src/index.tsx @@ -1,19 +1,148 @@ import { Types, PropTypes as PrettyPropTypes } from "@magical-types/pretty"; import * as React from "react"; -import { MagicalNode } from "@magical-types/types"; -import flatted from "flatted"; - -function parseStringified(val: string): MagicalNode { - try { - return flatted.parse(val); - } catch (err) { - console.error("error parsing stringified node:", val); - throw err; - } +import { + MagicalNode, + MagicalNodeWithIndexes, + MagicalNodeIndex, + TypeParameterNode +} from "@magical-types/types"; + +let wrapInCache = (arg: (type: Arg) => Return) => { + let cache = new WeakMap(); + return (type: Arg): Return => { + let cachedNode = cache.get(type); + if (cachedNode !== undefined) { + return cachedNode; + } + let obj = {} as Return; + cache.set(type, obj); + let node = arg(type); + Object.assign(obj, node); + return obj; + }; +}; + +function parseStringified( + nodes: MagicalNodeWithIndexes[], + index: MagicalNodeIndex +): MagicalNode { + let getMagicalNodeWithIndexes = wrapInCache( + (node: MagicalNodeWithIndexes): MagicalNode => { + switch (node.type) { + case "StringLiteral": + case "NumberLiteral": + case "TypeParameter": + case "Symbol": + case "Intrinsic": { + return node; + } + case "Union": { + return { + type: "Union", + types: node.types.map(x => getNodeFromIndex(x)), + name: node.name + }; + } + case "Intersection": { + return { + type: "Intersection", + types: node.types.map(x => getNodeFromIndex(x)) + }; + } + case "Array": + case "Promise": + case "ReadonlyArray": { + return { + type: node.type, + value: getNodeFromIndex(node.value) + }; + } + case "Tuple": { + return { + type: "Tuple", + value: node.value.map(x => getNodeFromIndex(x)) + }; + } + case "IndexedAccess": { + return { + type: "IndexedAccess", + index: getNodeFromIndex(node.index), + object: getNodeFromIndex(node.object) + }; + } + case "Class": { + return { + type: "Class", + name: node.name, + properties: node.properties.map(x => { + return { ...x, value: getNodeFromIndex(x.value) }; + }), + thisNode: node.thisNode ? getNodeFromIndex(node.thisNode) : null, + typeParameters: node.typeParameters.map(x => getNodeFromIndex(x)) + }; + } + case "Object": { + return { + type: "Object", + name: node.name, + properties: node.properties.map(x => { + return { ...x, value: getNodeFromIndex(x.value) }; + }), + callSignatures: node.callSignatures.map(x => { + return { + return: getNodeFromIndex(x.return), + parameters: x.parameters.map(x => ({ + ...x, + type: getNodeFromIndex(x.type) + })), + typeParameters: x.typeParameters.map(x => + getNodeFromIndex(x) + ) as TypeParameterNode[] + }; + }), + constructSignatures: node.constructSignatures.map(x => { + return { + return: getNodeFromIndex(x.return), + parameters: x.parameters.map(x => ({ + ...x, + type: getNodeFromIndex(x.type) + })), + typeParameters: x.typeParameters.map(x => + getNodeFromIndex(x) + ) as TypeParameterNode[] + }; + }), + aliasTypeArguments: node.aliasTypeArguments.map(x => + getNodeFromIndex(x) + ) as TypeParameterNode[] + }; + } + case "Conditional": { + return { + type: "Conditional", + check: getNodeFromIndex(node.check), + extends: getNodeFromIndex(node.extends), + false: getNodeFromIndex(node.false), + true: getNodeFromIndex(node.true) + }; + } + + default: { + let _thisMakesTypeScriptEnsureThatAllNodesAreSpecifiedHere: never = node; + // @ts-ignore + throw new Error("this should never happen: " + node.type); + } + } + } + ); + + let getNodeFromIndex = (index: MagicalNodeIndex) => + getMagicalNodeWithIndexes(nodes[index]); + return getNodeFromIndex(index); } let getMagicalNode = (props: any): MagicalNode => { - return parseStringified((props as any).__types); + return parseStringified((props as any).__types, props.__typeIndex); }; export let FunctionTypes = (props: { @@ -35,5 +164,5 @@ export let PropTypes = (props: { }; export function getNode() { - return parseStringified(arguments[0]); + return parseStringified(arguments[0], arguments[1]); } diff --git a/packages/macro/src/get-types.ts b/packages/macro/src/get-types.ts index 024ae75..ce167d2 100644 --- a/packages/macro/src/get-types.ts +++ b/packages/macro/src/get-types.ts @@ -3,8 +3,9 @@ import { NodePath } from "@babel/core"; import * as BabelTypes from "@babel/types"; import { InternalError } from "@magical-types/errors"; import { Project } from "ts-morph"; -import * as flatted from "flatted"; import { convertType, getPropTypesType } from "@magical-types/convert-type"; +import { MagicalNode, MagicalNodeIndex } from "@magical-types/types/src"; +import { serializeNodes } from "./serialize"; export function getTypes( filename: string, @@ -19,7 +20,8 @@ export function getTypes( | { exportName: "getNode"; path: NodePath } > >, - numOfThings: number + numOfThings: number, + babelProgram: NodePath ) { let configFileName = typescript.findConfigFile( filename, @@ -38,6 +40,20 @@ export function getTypes( let sourceFile = project.getSourceFileOrThrow(filename).compilerNode; let typeChecker = project.getTypeChecker().compilerObject; + let rootNodes: MagicalNode[] = []; + let callbacks: (( + nodesArrayReference: string, + index: MagicalNodeIndex + ) => void)[] = []; + + let insertNode = ( + node: MagicalNode, + cb: (nodesArrayReference: string, index: MagicalNodeIndex) => void + ) => { + rootNodes.push(node); + callbacks.push(cb); + }; + let num = 0; let visit = (node: typescript.Node) => { typescript.forEachChild(node, node => { @@ -101,14 +117,22 @@ export function getTypes( ); } let converted = convertType(type, []); - val.path.node.attributes.push( - BabelTypes.jsxAttribute( - BabelTypes.jsxIdentifier("__types"), - BabelTypes.jsxExpressionContainer( - BabelTypes.stringLiteral(flatted.stringify(converted)) + insertNode(converted, (nodesArrayReference, index) => { + val.path.node.attributes.push( + BabelTypes.jsxAttribute( + BabelTypes.jsxIdentifier("__typeIndex"), + BabelTypes.jsxExpressionContainer( + BabelTypes.numericLiteral(index) + ) + ), + BabelTypes.jsxAttribute( + BabelTypes.jsxIdentifier("__types"), + BabelTypes.jsxExpressionContainer( + BabelTypes.identifier(nodesArrayReference) + ) ) - ) - ); + ); + }); } else if (val.exportName === "getNode") { if (!typescript.isCallExpression(node.parent)) { throw new InternalError("not call expression for getNode"); @@ -119,9 +143,12 @@ export function getTypes( ); let converted = convertType(type, []); - val.path.node.arguments.push( - BabelTypes.stringLiteral(flatted.stringify(converted)) - ); + insertNode(converted, (nodesArrayReference, index) => { + val.path.node.arguments.push( + BabelTypes.identifier(nodesArrayReference), + BabelTypes.numericLiteral(index) + ); + }); } else { throw new InternalError("unexpected node type"); } @@ -135,4 +162,23 @@ export function getTypes( if (num !== numOfThings) { throw new InternalError("num !== numOfThings"); } + let { nodes, nodesToIndex } = serializeNodes(rootNodes); + let id = babelProgram.scope.generateDeclaredUidIdentifier(); + babelProgram.node.body.unshift( + BabelTypes.variableDeclaration("var", [ + BabelTypes.variableDeclarator( + id, + BabelTypes.callExpression( + BabelTypes.memberExpression( + BabelTypes.identifier("JSON"), + BabelTypes.identifier("parse") + ), + [BabelTypes.stringLiteral(JSON.stringify(nodes))] + ) + ) + ]) + ); + callbacks.forEach((cb, i) => { + cb(id.name, nodesToIndex.get(rootNodes[i])!); + }); } diff --git a/packages/macro/src/index.ts b/packages/macro/src/index.ts index 1709dac..309817d 100644 --- a/packages/macro/src/index.ts +++ b/packages/macro/src/index.ts @@ -51,6 +51,8 @@ export default createMacro(({ references, state, babel }: MacroArgs) => { > = new Map(); let num = 0; + let programPath: NodePath; + for (let exportName of [ "PropTypes", "FunctionTypes", @@ -65,6 +67,10 @@ export default createMacro(({ references, state, babel }: MacroArgs) => { ).name; references[exportName].forEach(reference => { + if (!programPath) { + // @ts-ignore + programPath = reference.findParent(x => x.isProgram()); + } let { parentPath } = reference; if ( @@ -107,7 +113,13 @@ export default createMacro(({ references, state, babel }: MacroArgs) => { } } if (things.size) { - getTypes(state.filename, things, num); + getTypes( + state.filename, + things, + num, + // @ts-ignore + programPath + ); } } catch (err) { console.error(err); diff --git a/packages/macro/src/serialize.ts b/packages/macro/src/serialize.ts new file mode 100644 index 0000000..56dcd51 --- /dev/null +++ b/packages/macro/src/serialize.ts @@ -0,0 +1,157 @@ +import { + MagicalNode, + MagicalNodeWithIndexes, + MagicalNodeIndex +} from "@magical-types/types"; +import { getChildPositionedMagicalNodes } from "./utils"; + +let weakMemoize = function( + func: (arg: Arg) => Return +): (arg: Arg) => Return { + // @ts-ignore + let cache: WeakMap = new WeakMap(); + // @ts-ignore + return arg => { + if (cache.has(arg)) { + return cache.get(arg); + } + let ret = func(arg); + cache.set(arg, ret); + return ret; + }; +}; + +export function serializeNodes(rootNodes: MagicalNode[]) { + let i = 0; + + // because of circular references, we don't want to visit a node more than once + let visitedNodes = new Map(); + + let queue = [...rootNodes]; + + while (queue.length) { + let currentNode = queue.shift()!; + if (!visitedNodes.has(currentNode)) { + visitedNodes.set(currentNode, i++ as MagicalNodeIndex); + + let childPositionedNodes = getChildPositionedMagicalNodes({ + node: currentNode, + path: [] + }); + + queue.push(...childPositionedNodes.map(x => x.node)); + } + } + let newNodes: MagicalNodeWithIndexes[] = []; + for (let [node] of visitedNodes) { + newNodes.push(getMagicalNodeWithIndexes(node, visitedNodes)); + } + return { nodes: newNodes, nodesToIndex: visitedNodes }; +} + +function getMagicalNodeWithIndexes( + node: MagicalNode, + visitedNodes: Map +): MagicalNodeWithIndexes { + let getIndexForNode: (node: MagicalNode) => MagicalNodeIndex = node => + visitedNodes.get(node)!; + switch (node.type) { + case "StringLiteral": + case "NumberLiteral": + case "TypeParameter": + case "Symbol": + case "Intrinsic": { + return node; + } + case "Union": { + return { + type: "Union", + types: node.types.map(x => getIndexForNode(x)), + name: node.name + }; + } + case "Intersection": { + return { + type: "Intersection", + types: node.types.map(x => getIndexForNode(x)) + }; + } + case "Array": + case "Promise": + case "ReadonlyArray": { + return { + type: node.type, + value: getIndexForNode(node.value) + }; + } + case "Tuple": { + return { + type: "Tuple", + value: node.value.map(x => getIndexForNode(x)) + }; + } + case "IndexedAccess": { + return { + type: "IndexedAccess", + index: getIndexForNode(node.index), + object: getIndexForNode(node.object) + }; + } + case "Class": { + return { + type: "Class", + name: node.name, + properties: node.properties.map(x => { + return { ...x, value: getIndexForNode(x.value) }; + }), + thisNode: node.thisNode ? getIndexForNode(node.thisNode) : null, + typeParameters: node.typeParameters.map(x => getIndexForNode(x)) + }; + } + case "Object": { + return { + type: "Object", + name: node.name, + properties: node.properties.map(x => { + return { ...x, value: getIndexForNode(x.value) }; + }), + callSignatures: node.callSignatures.map(x => { + return { + return: getIndexForNode(x.return), + parameters: x.parameters.map(x => ({ + ...x, + type: getIndexForNode(x.type) + })), + typeParameters: x.typeParameters.map(x => getIndexForNode(x)) + }; + }), + constructSignatures: node.constructSignatures.map(x => { + return { + return: getIndexForNode(x.return), + parameters: x.parameters.map(x => ({ + ...x, + type: getIndexForNode(x.type) + })), + typeParameters: x.typeParameters.map(x => getIndexForNode(x)) + }; + }), + aliasTypeArguments: node.aliasTypeArguments.map(x => getIndexForNode(x)) + }; + } + case "Conditional": { + return { + type: "Conditional", + check: getIndexForNode(node.check), + extends: getIndexForNode(node.extends), + false: getIndexForNode(node.false), + true: getIndexForNode(node.true) + }; + } + + default: { + let _thisMakesTypeScriptEnsureThatAllNodesAreSpecifiedHere: never = node; + // @ts-ignore + throw new Error("this should never happen: " + node.type); + } + } +} diff --git a/packages/macro/src/utils.ts b/packages/macro/src/utils.ts new file mode 100644 index 0000000..e21acd5 --- /dev/null +++ b/packages/macro/src/utils.ts @@ -0,0 +1,129 @@ +import { PositionedMagicalNode } from "@magical-types/types"; + +export function getChildPositionedMagicalNodes({ + node, + path +}: PositionedMagicalNode): Array { + function getPositionedNodeFromKey( + obj: Obj, + key: Key + ): { + node: Obj[Key]; + path: Array; + } { + return { node: obj[key], path: path.concat(key as string | number) }; + } + switch (node.type) { + case "Symbol": + case "StringLiteral": + case "NumberLiteral": + case "TypeParameter": + case "Intrinsic": { + return []; + } + case "Union": + case "Intersection": { + return node.types.map((node, index) => { + return { node, path: path.concat("types", index) }; + }); + } + case "Array": + case "Promise": + case "ReadonlyArray": { + return [{ node: node.value, path: path.concat("value") }]; + } + case "Tuple": { + return node.value.map((node, index) => { + return { node, path: path.concat("value", index) }; + }); + } + case "IndexedAccess": { + return [ + { node: node.object, path: path.concat("object") }, + { node: node.index, path: path.concat("index") } + ]; + } + case "Class": { + return [ + ...node.properties.map((param, index) => { + return { + node: param.value, + path: path.concat("properties", index, "value") + }; + }) + ]; + } + case "Object": { + return [ + ...flatMap(node.callSignatures, (signature, index) => { + return [ + ...signature.typeParameters.map(x => ({ + node: x, + path: path.concat("callSignatures", "typeParameters", index) + })), + ...signature.parameters.map(x => ({ + node: x.type, + path: path.concat("callSignatures", "parameters", index, "type") + })), + { + node: signature.return, + path: path.concat("callSignatures", "return") + } + ]; + }), + ...flatMap(node.constructSignatures, (signature, index) => { + return [ + ...signature.typeParameters.map(x => ({ + node: x, + path: path.concat("callSignatures", "typeParameters", index) + })), + ...signature.parameters.map(x => ({ + node: x.type, + path: path.concat("callSignatures", "parameters", index, "type") + })), + { + node: signature.return, + path: path.concat("constructSignatures", "return") + } + ]; + }), + ...node.aliasTypeArguments.map((node, index) => ({ + node, + path: path.concat("aliasTypeArguments", index) + })), + ...node.properties.map((param, index) => { + return { + node: param.value, + path: path.concat("properties", index, "value") + }; + }) + ]; + } + case "Conditional": { + return [ + getPositionedNodeFromKey(node, "check"), + getPositionedNodeFromKey(node, "true"), + getPositionedNodeFromKey(node, "false"), + getPositionedNodeFromKey(node, "extends") + ]; + } + + default: { + let _thisMakesTypeScriptEnsureThatAllNodesAreSpecifiedHere: never = node; + // @ts-ignore + throw new Error("this should never happen: " + node.type); + } + } +} + +export function flatMap( + array: Array, + callback: (value: T, index: number) => U | ReadonlyArray +): U[] { + // @ts-ignore + return array.reduce((acc, x, index) => { + const r = callback(x, index); + if (Array.isArray(r)) return [...acc, ...r]; + return [...acc, r]; + }, []); +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f6755c9..8ad0f6a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,5 +1,4 @@ export type SignatureNode = { - type: "Signature"; return: MagicalNode; parameters: Array; typeParameters: Array; @@ -70,3 +69,19 @@ export type Parameter = { type: MagicalNode; required: boolean; }; + +export type MagicalNodeIndex = number & { __magicalNodeIndex: any }; + +type InnerReplace = Thing extends MagicalNode + ? MagicalNodeIndex + : Thing extends object + ? ReplaceMagicalNode + : Thing; + +type ReplaceMagicalNode = { + [Key in keyof Thing]: Thing[Key] extends Array + ? Array> + : InnerReplace; +}; + +export type MagicalNodeWithIndexes = ReplaceMagicalNode;