sverweij / dependency-cruiser

Validate and visualize dependencies. Your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.
https://npmjs.com/dependency-cruiser
MIT License
5.33k stars 251 forks source link

Error: No "exports" main defined in <project_name>/node_modules/dependency-cruiser/package.json #903

Closed CoenWarmer closed 9 months ago

CoenWarmer commented 9 months ago

Summary

Thank you for the great library!

I'm trying to get Dependency Cruiser to work on a large Typescript repository.

It has a tsconfig.json and .eslintrc.js file.

I'm calling it in a Node script like so:

export async function getRelatedFiles({ files, log }: { files: string[]; log: ToolingLog }) {
  try {
    const cruiseResult: IReporterOutput = await cruise([`${REPO_ROOT}/src/dev/preflight_check`], {
      tsConfig: { fileName: `${REPO_ROOT}/tsconfig.json` },
      exclude: 'node_modules',
    });
    console.log('cruiseResult', cruiseResult);
  } catch (error) {
    console.log(error);
  }
}

When I run the script this is my output:

Error: No "exports" main defined in <project_name>/node_modules/dependency-cruiser/package.json
    at exportsNotFound (node:internal/modules/esm/resolve:294:10)
    at packageExportsResolve (node:internal/modules/esm/resolve:584:13)
    at resolveExports (node:internal/modules/cjs/loader:591:36)
    at Function.Module._findPath (node:internal/modules/cjs/loader:668:31)
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:1130:27)
    at Function.Module._load (node:internal/modules/cjs/loader:985:27)
    at Module.require (node:internal/modules/cjs/loader:1235:19)

The package.json of this project has no main or export defined. I have tried running dependency-cruiser --init with different settings, and I've tried running it without.

Any idea?

Context

I am trying to get a graph of all imports inside the repo, so I can request which file(s) are importing a specific file I am trying to test.

Environment

sverweij commented 9 months ago

Hi @CoenWarmer my guess is the root cause is not that dependency-cruiser doesn't have exports or main fields in its package.json (it does). The error you see is one that occurs when attempting to require an ESM module from commonjs - and dependency-cruiser is ESM only. The snippet you show looks to be typescript, and a short peek at kibana's tsconfig base indeed shows the typescript compiler will emit commonjs (a.o. tsconfig.base.json#L1780 which explicitly sets that and tsconfig.base.json#L1794)

There's a few ways to work around that

"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ESNext" // or "ES2022"

TypeScript example The MS TypeScript compiler replaces the `await import` back to a require. However, in more modern alternatives (e.g. `npx tsx run-me.ts`) this works correctly. ```typescript // runme-ts.ts import type { ICruiseResult, IReporterOutput } from "dependency-cruiser"; type ToolingLog = any; const REPO_ROOT = "./"; export async function getRelatedFiles({ files, log, }: { files: string[]; log?: ToolingLog; }) { try { const { cruise } = await import("dependency-cruiser"); const startFiles = files.map((file) => `${REPO_ROOT}/${file}`); const cruiseResult: IReporterOutput = await cruise(startFiles, { tsConfig: { fileName: `${REPO_ROOT}/tsconfig.json` }, exclude: "node_modules", }); const output = cruiseResult.output as ICruiseResult; console.log("cruiseResult", output.modules); } catch (error) { console.log(error); } } // as getRelatedFiles returns a promise we either need to await it (can't use - // commonjs) or use .then but the example code emits console.logs anyway, so I've lazily getRelatedFiles({ files: ["src/", "dist/"] }); ```
JavaScript commonjs example Works out of the box with nodejs (`node runme.cjs`), tested on node 18 & 20 ```javascript // runme.cjs // @ts-check const REPO_ROOT = "./"; /** * @typedef {any} ToolingLog * @typedef {{files: string[]; log?: ToolingLog}} DingesType * @typedef {import("dependency-cruiser").IReporterOutput} IReporterOutput * @typedef {import("dependency-cruiser").ICruiseResult} ICruiseResult */ /** * @param {DingesType} dinges */ async function getRelatedFiles({ files, log }) { const { cruise } = await import("dependency-cruiser"); try { const startFiles = files.map((file) => `${REPO_ROOT}/${file}`); /** @type IReporterOutput */ const cruiseResult = await cruise(startFiles, { tsConfig: { fileName: `${REPO_ROOT}/tsconfig.json` }, exclude: "node_modules", }); const output = /** @type {ICruiseResult} */ (cruiseResult?.output); console.log("cruiseResult", output?.modules); } catch (error) { console.log(error); } } // as getRelatedFiles returns a promise we either need to await it (can't use - // commonjs) or use .then but the example code emits console.logs anyway, // so I've lazily left it at that. getRelatedFiles({ files: ["src/", "dist/"] }); ```
CoenWarmer commented 9 months ago

Amazing, thanks for your detailed help @sverweij! Going to try this, will report back.

CoenWarmer commented 9 months ago

Hey @sverweij, that was it. I'm still adapting the script to be used in the existing infra we have set up, but I at least have a working prototype. Thanks for your help!