export function computeGraph()

in packages/cdk-graph/src/core/compute.ts [52:324]


export function computeGraph(root: IConstruct): Graph.Store {
  const store = new Graph.Store();

  // List of nodes that should not be processed (extraneous nodes detected during compute)
  // NB: example is stack template construct which cdk creates as sibling of nested stacks,
  // we care about the stack and not the marshalled construct.
  const nodesToIgnore: UUID[] = [];

  // List of all unresolved referenced detected during compute, which are resolved after all nodes have been stored
  const allUnresolvedReferences: SerializedGraph.SGUnresolvedReference[] = [];
  // List of all unresolved dependencies detected during compute, which are resolved after all nodes have been stored
  const allUnresolvedDependencies: UnresolvedDependency[] = [];

  const visit = (construct: IConstruct, parent: Graph.Node): void => {
    // Do not graph the CdkGraph itself
    if (construct.node.id === GRAPH_ID) {
      return;
    }

    // Do not graph the Tree construct (synthesizes tree.json)
    if (Graph.AppNode.isAppNode(parent) && construct.node.id === "Tree") {
      return;
    }

    // Do not graph stack CDKMetadata, BootstrapVersion, and similar constructs
    if (
      Graph.StackNode.isStackNode(parent) &&
      IGNORED_STACK_CHILDREN.includes(construct.node.id)
    ) {
      return;
    }

    const {
      uuid,
      attributes = {},
      metadata = [],
      tags = {},
      logicalId,
      cfnType,
      constructInfo,
      dependencies,
      unresolvedReferences,
      flags,
    } = inferNodeProps(construct);

    if (nodesToIgnore.includes(uuid)) {
      return;
    }

    // Infer the stack this construct belongs to
    let stack: Graph.StackNode | undefined;
    try {
      if (construct.node.scope) {
        const stackUUID = getConstructUUID(Stack.of(construct));
        stack = store.getStack(stackUUID);
      }
    } catch {
      // ignore - expected to throw if construct is not contained in a stack
    }

    const nodeProps: Graph.ITypedNodeProps = {
      store,
      stack,
      parent,
      uuid,
      id: construct.node.id,
      path: construct.node.path,
      attributes,
      metadata,
      tags,
      constructInfo,
      logicalId,
      cfnType,
      flags,
    };
    let node: Graph.Node;
    switch (constructInfo?.fqn as ConstructInfoFqnEnum) {
      case ConstructInfoFqnEnum.PDKAPP_MONO:
      case ConstructInfoFqnEnum.PDKAPP:
      case ConstructInfoFqnEnum.APP: {
        node = new Graph.AppNode({
          store,
          parent,
          attributes,
          metadata,
          tags,
          constructInfo,
          logicalId,
          flags,
        });
        break;
      }
      case ConstructInfoFqnEnum.STAGE: {
        node = new Graph.StageNode(nodeProps);
        break;
      }
      case ConstructInfoFqnEnum.STACK: {
        node = new Graph.StackNode(nodeProps);
        break;
      }
      case ConstructInfoFqnEnum.NESTED_STACK: {
        // NB: handle NestedStack<->CfnStack as single Node with NestedStack construct as source
        // https://github.com/aws/aws-cdk/blob/119c92f65bf26c3fdf4bb818a4a4812022a3744a/packages/%40aws-cdk/core/lib/nested-stack.ts#L119

        const parentStack = inferNodeProps(
          (construct as NestedStack).nestedStackParent!
        );
        const _nestedStack = construct.node.scope!.node.findChild(
          construct.node.id + ".NestedStack"
        );
        const cfnStackWrapper = inferNodeProps(_nestedStack);
        const cfnStack = inferNodeProps(
          _nestedStack.node.findChild(
            construct.node.id + ".NestedStackResource"
          ) as CfnStack
        );

        // ignore parent scope cfn stack (template) constructs
        nodesToIgnore.push(cfnStackWrapper.uuid, cfnStackWrapper.uuid);

        node = new Graph.NestedStackNode({
          ...nodeProps,
          logicalId: cfnStack.logicalId,
          attributes: merge(
            {},
            attributes,
            cfnStackWrapper.attributes,
            cfnStack.attributes
          ),
          metadata: [
            ...metadata,
            ...(cfnStackWrapper.metadata || []),
            ...(cfnStack.metadata || []),
          ],
          parentStack: store.getStack(parentStack.uuid),
        });

        // Only add uniq dependencies as wrapper and stack may duplicate
        dependencies.push(
          ...uniq([...cfnStackWrapper.dependencies, ...cfnStack.dependencies])
        );

        // Only add uniq references as wrapper and stack may duplicate
        unresolvedReferences.push(
          ...uniqBy(
            [
              ...cfnStackWrapper.unresolvedReferences.map((ref) => ({
                ...ref,
                source: uuid,
              })),
              ...cfnStack.unresolvedReferences.map((ref) => ({
                ...ref,
                source: uuid,
              })),
            ],
            (v) =>
              `${v.referenceType}::${v.source}::${v.target}::${JSON.stringify(
                v.value || ""
              )}`
          )
        );

        break;
      }
      case ConstructInfoFqnEnum.CFN_STACK: {
        // CfnStack always proceeds NestedStack, based on above case we merge CfnStack into
        // NestedStack (mirror developer expectations) and then ignore CfnStack.
        throw new Error(`CfnStack should be ignored by NestedStack: ${uuid}`);
      }
      case ConstructInfoFqnEnum.CFN_OUTPUT: {
        node = new Graph.OutputNode({
          ...nodeProps,
          value: Stack.of(construct).resolve((construct as CfnOutput).value),
          description: Stack.of(construct).resolve(
            (construct as CfnOutput).description
          ),
          exportName: Stack.of(construct).resolve(
            (construct as CfnOutput).exportName
          ),
        });

        // extract unresolved references from value
        unresolvedReferences.push(
          ...extractUnresolvedReferences(
            node.uuid,
            (node as Graph.OutputNode).value || {}
          )
        );

        break;
      }
      case ConstructInfoFqnEnum.CFN_PARAMETER: {
        const cfnParameter = construct as CfnParameter;
        node = new Graph.ParameterNode({
          ...nodeProps,
          value: Stack.of(construct).resolve(cfnParameter.value),
          description: Stack.of(construct).resolve(cfnParameter.description),
          parameterType: cfnParameter.type,
        });
        break;
      }
      default: {
        if (flags?.includes(FlagEnum.IMPORT)) {
          node = new Graph.CfnResourceNode({
            ...nodeProps,
            cfnType: inferImportCfnType(construct, constructInfo),
            importArnToken: resolveImportedConstructArnToken(construct),
          });
        } else if (Resource.isResource(construct)) {
          node = new Graph.ResourceNode({
            ...nodeProps,
            cdkOwned: Resource.isOwnedResource(construct),
          });
        } else if (cfnType) {
          node = new Graph.CfnResourceNode(nodeProps);
        } else {
          node = new Graph.Node({
            nodeType: NodeTypeEnum.DEFAULT,
            ...nodeProps,
          });

          // Cdk Stack.Exports is proxy to actual Cfn exports and extraneous in the graph
          if (
            construct.node.id === CdkConstructIds.EXPORTS &&
            Stack.isStack(construct.node.scope)
          ) {
            node.addFlag(FlagEnum.EXTRANEOUS);
          }
        }
      }
    }

    // Track unresolved dependencies, since nodes might not be created in the store yet
    allUnresolvedDependencies.push(
      ...dependencies.map((dep) => [uuid, dep] as [string, string])
    );

    // Track all logicalId references in the nodes attributes
    // this will get resolved after all nodes have been stored
    allUnresolvedReferences.push(...unresolvedReferences);

    // Visit all child to compute the tree
    for (const child of construct.node.children) {
      try {
        visit(child, node);
      } catch (e) {
        Annotations.of(root).addWarning(
          `Failed to render graph for node ${child.node.path}. Reason: ${e}`
        );
        throw e;
      }
    }
  };

  visit(root, store.root);

  // Resolve all references - now that the tree is stored
  for (const unresolved of allUnresolvedReferences) {
    try {
      resolveReference(store, unresolved);
    } catch (e) {
      IS_DEBUG && console.warn(e, unresolved);
      // TODO: consider saving unresolved references if become valuable.
    }
  }

  // Resolve all dependencies - now that the tree is stored
  for (const unresolved of allUnresolvedDependencies) {
    resolveDependency(store, unresolved);
  }

  return store;
}