diff --git a/packages/core/core/src/requests/BundleGraphRequest.js b/packages/core/core/src/requests/BundleGraphRequest.js index d7f063c53..c0cfa8ddb 100644 --- a/packages/core/core/src/requests/BundleGraphRequest.js +++ b/packages/core/core/src/requests/BundleGraphRequest.js @@ -15,6 +15,7 @@ import type { } from '../types'; import type {ConfigAndCachePath} from './AtlaspackConfigRequest'; +import fs from 'fs'; import invariant from 'assert'; import assert from 'assert'; import nullthrows from 'nullthrows'; @@ -57,6 +58,11 @@ import createAssetGraphRequestJS from './AssetGraphRequest'; import {createAssetGraphRequestRust} from './AssetGraphRequestRust'; import {tracer, PluginTracer} from '@atlaspack/profiler'; import {requestTypes} from '../RequestTracker'; +import { + assetGraphToDot, + getDebugAssetGraphDotPath, + getDebugAssetGraphDotOptions, +} from './asset-graph-dot'; type BundleGraphRequestInput = {| requestedAssetIds: Set, @@ -119,6 +125,15 @@ export default function createBundleGraphRequest( }, ); + let debugAssetGraphDotPath = getDebugAssetGraphDotPath(); + if (debugAssetGraphDotPath !== null) { + await fs.promises.writeFile( + debugAssetGraphDotPath, + assetGraphToDot(assetGraph, getDebugAssetGraphDotOptions()), + 'utf8', + ); + } + // if (input.rustAtlaspack && process.env.NATIVE_COMPARE === 'true') { // let {assetGraph: jsAssetGraph} = await api.runRequest( // createAssetGraphRequestJS({ diff --git a/packages/core/core/src/requests/asset-graph-dot.js b/packages/core/core/src/requests/asset-graph-dot.js new file mode 100644 index 000000000..5e3865636 --- /dev/null +++ b/packages/core/core/src/requests/asset-graph-dot.js @@ -0,0 +1,144 @@ +// @flow +import path from 'path'; + +import type {AssetGraphNode} from '../types'; +import AssetGraph from '../AssetGraph'; + +export type AssetGraphToDotOptions = { + style?: boolean, + sort?: boolean, + ... +}; + +/** @description Renders AssetGraph into GraphViz Dot format */ +export function assetGraphToDot( + assetGraph: AssetGraph, + {sort = false, style = true}: AssetGraphToDotOptions = {}, +): string { + const edges = []; + const nodeStyles: {[key: string]: string} = {}; + + assetGraph.traverse((nodeId) => { + let node: AssetGraphNode | null = assetGraph.getNode(nodeId) ?? null; + if (!node) return; + + const fromIds = assetGraph.getNodeIdsConnectedTo(nodeId); + + for (const fromId of fromIds) { + let fromNode: AssetGraphNode | null = assetGraph.getNode(fromId) ?? null; + if (!fromNode) throw new Error('No Node'); + const edgeStyle = getEdgeStyle(node); + const nodeStyle = getNodeStyle(node); + + let entry = `"${getNodeName(fromNode)}" -> "${getNodeName(node)}"`; + if (edgeStyle) { + entry += ` [${edgeStyle}]`; + } + + if (nodeStyle) { + nodeStyles[getNodeName(node)] = nodeStyle; + } + + edges.push(entry); + } + }); + + // $FlowFixMe + const nodeStylesList: Array<[string, string]> = Object.entries(nodeStyles); + if (sort) { + edges.sort(); + nodeStylesList.sort(); + } + + let digraph = `digraph {\n\tnode [shape=rectangle]\n`; + if (style) { + digraph += nodeStylesList + .map(([node, style]) => `\t"${node}" [${style}]\n`) + .join(''); + } + digraph += edges.map((v) => `\t${v}\n`).join(''); + digraph += `}\n`; + return digraph; +} + +export function getDebugAssetGraphDotPath(): string | null { + let debugAssetGraphDot = process.env.DEBUG_ASSET_GRAPH_DOT; + if (debugAssetGraphDot === undefined || debugAssetGraphDot === '') { + return null; + } + if (!path.isAbsolute(debugAssetGraphDot)) { + debugAssetGraphDot = path.join(process.cwd(), debugAssetGraphDot); + } + return debugAssetGraphDot; +} + +export function getDebugAssetGraphDotOptions(): AssetGraphToDotOptions { + const options: AssetGraphToDotOptions = {}; + + let style = process.env.DEBUG_ASSET_GRAPH_DOT_STYLE; + if (style !== undefined) { + options.style = style === 'true'; + } + + let sort = process.env.DEBUG_ASSET_GRAPH_DOT_SORT; + if (sort !== undefined) { + options.sort = sort === 'true'; + } + + return options; +} + +function fromCwd(input: string): string { + return path.relative(process.cwd(), input); +} + +function getNodeName(node: AssetGraphNode): string { + if (node.type === 'asset_group') { + // $FlowFixMe + return [`asset_group`, node.id, fromCwd(node.value.filePath)].join('\\n'); + } else if (node.type === 'asset') { + // $FlowFixMe + return [`asset`, node.id, fromCwd(node.value.filePath)].join('\\n'); + } else if (node.type === 'dependency') { + return [`dependency`, node.id, node.value.specifier].join('\\n'); + } else if (node.type === 'entry_specifier') { + // $FlowFixMe + return [`entry_specifier`, node.value].join('\\n'); + } else if (node.type === 'entry_file') { + // $FlowFixMe + return [`entry_file`, fromCwd(node.value.filePath)].join('\\n'); + } + return 'ROOT'; +} + +function getEdgeStyle(node: AssetGraphNode): string { + if (node.type === 'asset_group') { + return ``; + } else if (node.type === 'asset') { + return ``; + } else if (node.type === 'dependency') { + if (node.value.priority === 2) { + return `style="dashed"`; + } + } else if (node.type === 'entry_specifier') { + return ``; + } else if (node.type === 'entry_file') { + return ``; + } + return ''; +} + +function getNodeStyle(node: AssetGraphNode): string { + if (node.type === 'asset_group') { + return `fillcolor="#E8F5E9", style="filled"`; + } else if (node.type === 'asset') { + return `fillcolor="#DCEDC8", style="filled"`; + } else if (node.type === 'dependency') { + return `fillcolor="#BBDEFB", style="filled"`; + } else if (node.type === 'entry_specifier') { + return `fillcolor="#FFF9C4", style="filled"`; + } else if (node.type === 'entry_file') { + return `fillcolor="#FFE0B2", style="filled"`; + } + return ''; +}