aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/webpack-nmt/src/index.ts
blob: 40c45082f024ff9b4a76f19535ac70b0a3081b46 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { spawn } from "child_process";
import { join } from "path";

import { Compilation, WebpackPluginInstance, Compiler } from "webpack";

export interface NodeModuleTracePluginOptions {
  cwd?: string;
  // relative to cwd
  contextDirectory?: string;
  // additional PATH environment variable to use for spawning the `node-file-trace` process
  path?: string;
  // control the maximum number of files that are passed to the `node-file-trace` command
  // default is 128
  maxFiles?: number;
  // log options
  log?: {
    all?: boolean;
    detail?: boolean;
    // Default is `error`
    level?:
      | "bug"
      | "fatal"
      | "error"
      | "warning"
      | "hint"
      | "note"
      | "suggestions"
      | "info";
  };
}

export class NodeModuleTracePlugin implements WebpackPluginInstance {
  static PluginName = "NodeModuleTracePlugin";

  private readonly chunksToTrace = new Set<string>();

  constructor(private readonly options?: NodeModuleTracePluginOptions) {}

  apply(compiler: Compiler) {
    compiler.hooks.compilation.tap(
      NodeModuleTracePlugin.PluginName,
      (compilation) => {
        compilation.hooks.processAssets.tap(
          {
            name: NodeModuleTracePlugin.PluginName,
            stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
          },
          () => this.createTraceAssets(compilation)
        );
      }
    );
    compiler.hooks.afterEmit.tapPromise(NodeModuleTracePlugin.PluginName, () =>
      this.runTrace()
    );
  }

  private createTraceAssets(compilation: Compilation) {
    const outputPath = compilation.outputOptions.path!;

    const isTraceable = (file: string) =>
      !file.endsWith(".wasm") && !file.endsWith(".map");

    for (const entrypoint of compilation.entrypoints.values()) {
      const file = entrypoint.getFiles().pop();
      if (file && isTraceable(file)) {
        this.chunksToTrace.add(join(outputPath, file));
      }
    }
  }

  private async runTrace() {
    process.stdout.write("\n");
    const cwd = this.options?.cwd ?? process.cwd();
    const args = [
      "annotate",
      "--context-directory",
      // `npm_config_local_prefix` set by `npm` to the root of the project, include workspaces
      // `PROJECT_CWD` set by `yarn` to the root of the project, include workspaces
      this.options?.contextDirectory ??
        process.env.npm_config_local_prefix ??
        process.env.PROJECT_CWD ??
        cwd,
      "--exact",
    ];
    if (this.options?.log?.detail) {
      args.push("--log-detail");
    }
    if (this.options?.log?.all) {
      args.push("--show-all");
    }
    const logLevel = this.options?.log?.level;
    if (logLevel) {
      args.push(`--log-level`);
      args.push(logLevel);
    }
    let turboTracingPackagePath = "";
    let turboTracingBinPath = "";
    try {
      turboTracingPackagePath = require.resolve(
        "@vercel/experimental-nft/package.json"
      );
    } catch (e) {
      console.warn(
        `Could not resolve the @vercel/experimental-nft directory, turbo tracing may fail.`
      );
    }
    if (turboTracingPackagePath) {
      try {
        const turboTracingBinPackageJsonPath = require.resolve(
          `@vercel/experimental-nft-${process.platform}-${process.arch}/package.json`,
          {
            paths: [join(turboTracingPackagePath, "..")],
          }
        );
        turboTracingBinPath = join(turboTracingBinPackageJsonPath, "..");
      } catch (e) {
        console.warn(
          `Could not resolve the @vercel/experimental-nft-${process.platform}-${process.arch} directory, turbo tracing may fail.`
        );
      }
    }
    const pathSep = process.platform === "win32" ? ";" : ":";
    let paths = `${this.options?.path ?? ""}${pathSep}${process.env.PATH}`;
    if (turboTracingBinPath) {
      paths = `${turboTracingBinPath}${pathSep}${paths}`;
    }
    const maxFiles = this.options?.maxFiles ?? 128;
    let chunks = [...this.chunksToTrace];
    let restChunks = chunks.length > maxFiles ? chunks.splice(maxFiles) : [];
    while (chunks.length) {
      await traceChunks(args, paths, chunks, cwd);
      chunks = restChunks;
      if (restChunks.length) {
        restChunks = chunks.length > maxFiles ? chunks.splice(maxFiles) : [];
      }
    }
  }
}

function traceChunks(
  args: string[],
  paths: string,
  chunks: string[],
  cwd?: string
) {
  const turboTracingProcess = spawn("node-file-trace", [...args, ...chunks], {
    stdio: "pipe",
    env: {
      ...process.env,
      PATH: paths,
      RUST_BACKTRACE: "1",
    },
    cwd,
  });
  return new Promise<void>((resolve, reject) => {
    turboTracingProcess.on("error", (err) => {
      console.error(err);
    });
    turboTracingProcess.stdout.on("data", (chunk) => {
      process.stdout.write(chunk);
    });
    turboTracingProcess.stderr.on("data", (chunk) => {
      process.stderr.write(chunk);
    });
    turboTracingProcess.once("exit", (code) => {
      if (!code) {
        resolve();
      } else {
        reject(code);
      }
    });
  });
}