ContactSign inSign up
Contact
Setup
Interaction tests
Publish
Composition

TurboSnap dependency tracing

TurboSnap examines your project’s Git history and Webpack’s dependency graph to determine which stories might be affected by changes. If you’re new to these concepts, then this guide is for you.

It’ll help you better understand dependency tracing, enabling you to use TurboSnap more effectively. You’ll also learn how to trace dependencies in projects using Vite.

Tracing dependencies with Webpack

To start, let’s visit Webpack’s dependency graph concept:

❝ Any time one file depends on another, webpack treats this as a dependency. This allows webpack to take non-code assets, such as images or web fonts, and also provide them as dependencies for your application.

When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points, webpack recursively builds a dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, only one - to be loaded by the browser. ❞

TurboSnap leverages Webpack’s Stats Data API to generate a JSON file with detailed statistics about a project’s modules. These statistics allow TurboSnap to analyze a project’s dependency graph by providing information on asset objects, related chunks (or grouped modules), and module dependencies linked to the assets.

If Chromatic builds your Storybook, it will automatically generate the stats file. But if you provide Chromatic with a prebuilt Storybook, you must add the --stats-json (or --webpack for Storybook versions 7 and lower) flag to the build-storybook command.

The stats file can be a bit challenging to read. Therefore, the Chromatic CLI offers a trim-stats-file option to make the file more human-readable. Use it like so:

npx chromatic trim-stats-file

Or, if you’re using a custom build directory:

npx chromatic trim-stats-file ./path/to/preview-stats.json

Reading the trimmed stats file

After running the trim-stats-file command, Chromatic will output a preview-stats.trimmed.json file. While this file is more readable, it can still be a bit daunting, so let’s break it down with an example.

preview-stats.trimmed
{
  "id": "./src/inputs/PinInput/PinInput.tsx",
  "name": "./src/inputs/PinInput/PinInput.tsx + 4 modules",
  "modules": [
    { "name": "./src/inputs/PinInput/PinInput.tsx" },
    { "name": "./src/inputs/PinInput/SeparatedPinInput.tsx" },
    { "name": "./src/inputs/PinInput/SinglePinInput.tsx" },
    { "name": "./src/inputs/utils.ts" },
    { "name": "./src/inputs/PinInput/PinCaret.tsx" }
  ],
  "reasons": [
    { "moduleName": "./src/data/DataGrid/cells/EditablePinInput.tsx" },
    { "moduleName": "./src/inputs/PinInput/index.ts" }
  ]
}

In this example, we have an asset with the chunkName of ./src/inputs/PinInput/PinInput.tsx + 4 modules. There’s a total of five modules within this chunk:

./src/inputs/PinInput/PinInput.tsx
./src/inputs/PinInput/SeparatedPinInput.tsx
./src/inputs/PinInput/SinglePinInput.tsx
./src/inputs/utils.ts
./src/inputs/PinInput/PinCaret.tsx

The "reasons" key provides information about the asset’s dependency graph. In other words, modules that depend on this asset. Any changes to the asset chunk’s modules may have an impact on its dependent modules:

./src/data/DataGrid/cells/EditablePinInput.tsx
./src/inputs/PinInput/index.ts

Asset object for a story file

The asset object for a story file usually looks a bit different:

preview-stats.trimmed
{
  "id": "./src/inputs/PinInput/stories/PinInput.stories.tsx",
  "name": "./src/inputs/PinInput/stories/PinInput.stories.tsx + 4 modules",
  "modules": [
    { "name": "./src/inputs/PinInput/stories/PinInput.stories.tsx" },
    {
      "name": "./src/inputs/PinInput/stories/PinInputWithNoCopyPaste.storyfile.tsx"
    },
    {
      "name": "./src/inputs/PinInput/stories/PinInputWithNoCopyPaste.storyfile.tsx?raw"
    },
    {
      "name": "./src/inputs/PinInput/stories/PinInputWithValidation.storyfile.tsx"
    },
    {
      "name": "./src/inputs/PinInput/stories/PinInputWithValidation.storyfile.tsx?raw"
    }
  ],
  "reasons": [
    {
      "moduleName": "./src/ lazy ^\\.\\/.*$ include: (?%21.*node_modules)(?:\\/src(?:\\/(?%21\\.)(?:(?:(?%21(?:^%7C\\/)\\.).)*?)\\/%7C\\/%7C$)(?%21\\.)(?=.)[^/]*?\\.stories\\.(js%7Cjsx%7Cts%7Ctsx))$ chunkName: [request] namespace object"
    }
  ]
}

Under “reasons,” you’ll notice a moduleName resembling a path pattern. In the full, untrimmed file, the issuerPath’s are "./storybook-config-entry.js" and "./storybook-stories.js". That’s because the dependency is related to this project’s Storybook configuration:

.storybook/main.js
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
const config = {
  stories: [
    {
      directory: "../",
      files: "**/*.@(mdx|stories.@(js|jsx|ts|tsx))",
    },
  ],
  // ...
};
export default config;

Use the trimmed stats file to trace dependencies

If you’re trying to understand why a story file is being detected as changed, you can search for its file path in the trimmed stats file and trace its dependencies to see which modules were affected.

Tracing dependencies with Vite

Vite support for TurboSnap is available out of the box, starting with Storybook 8.0 and later, and does not require any additional configuration. However, if you’re using an older version of Storybook, you must install the vite-plugin-turbosnap plugin to enable TurboSnap support with Vite.

Similar to Webpack, when you run the build-storybook command, the preview-stats.json file is automatically generated. It contains information about each module being built and a mapping between each file and all the imported files. This structure mirrors that used by Wepack but only includes the information TurboSnap needs to perform dependency checks.


Troubleshooting

How do circular dependencies impact tracing?

TurboSnap uses tree-shaking to efficiently identify which files in your project have changed and how those changes propagate through your dependency graph. Tree-shaking may not work as intended when circular dependencies exist because the loop can obscure the true “consequences” of changes. Circular dependencies occur when two or more files depend on each other directly or indirectly, creating a loop. For example:

  • Button/index.ts exports Button.tsx.

  • If Button.tsx imports something from Card/index.ts (directly or indirectly), and Card.tsx imports something from Button/index.ts, a Circular Dependency is created.

When TurboSnap flags unchanged files as changed, you can use the --untraced flag to correct its behavior by excluding certain files. However, restructuring your project to avoid circular dependencies may be a better, long-lasting solution.

Why transitive dependencies are not tracked?

Transitive dependencies occur when a module indirectly depends on another through other modules. Here, module C is a transitive dependency of module A because it is imported into module B:

// Module A
import B from './B';

// Module B
import C from './C';

Sometimes, Webpack struggles to handle transitive dependencies effectively due to standard configurations, specific project structures, or dependency patterns, making it hard to identify the root cause of dependency tracking issues.

Using the sideEffects Property

Set the sideEffects property in your package.json to prevent accidental removal of dependencies and track side effects in third files. There are two ways to set it:

  1. Set sideEffects to true to track all transitive dependencies:
"sideEffects": true
  1. Specify sideEffects as an array to explicitly list files or patterns:
"sideEffects": [
"**/*.css",
"**/*.scss",
"./src/global.js"
]

Testing the Configuration

To verify this solution, add a specific file with transitive imports to the sideEffects array and observe whether it behaves as expected.

Adjusting the sideEffects property can cause other unexpected consequences, so we recommend reviewing the Webpack Documentation on Tree-Shaking.