dependency-cruiser 18.6.0 → 18.8.0-beta-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ module.exports = {
2
2
  name: "no-duplicate-dep-types",
3
3
  comment:
4
4
  "Likely this module depends on an external ('npm') package that occurs more " +
5
- "than once in your package.json i.e. bot as a devDependencies and in dependencies. " +
5
+ "than once in your package.json i.e. both as a devDependencies and in dependencies. " +
6
6
  "This will cause maintenance problems later on. If it's intentional, you can " +
7
7
  "disable this rule by adding this override as a rule in the 'forbidden' section " +
8
8
  "of your dependency-cruiser configuration: " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dependency-cruiser",
3
- "version": "18.6.0",
3
+ "version": "18.8.0-beta-1",
4
4
  "description": "Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.",
5
5
  "keywords": [
6
6
  "static analysis",
@@ -1,4 +1,3 @@
1
- import { EOL } from "node:os";
2
1
  import addRuleSetUsed from "./add-rule-set-used.mjs";
3
2
  import summarizeModules from "./summarize-modules.mjs";
4
3
  import summarizeFolders from "./summarize-folders.mjs";
@@ -11,6 +10,7 @@ import {
11
10
  } from "./get-stats.mjs";
12
11
  import { compareViolations } from "#graph-utl/compare.mjs";
13
12
  import { getEnvironmentInfo } from "#environment.mjs";
13
+ import { determineEnvironmentIssues } from "./verify-environment.mjs";
14
14
 
15
15
  /**
16
16
  * @import { IOptions, IEnvironmentIssue, ISummary, IModule, IFolder } from "../../../types/cruise-result.mjs"
@@ -18,107 +18,6 @@ import { getEnvironmentInfo } from "#environment.mjs";
18
18
  * @import { IStrictCruiseOptions } from "../../../types/strict-options.mjs"
19
19
  */
20
20
 
21
- /**
22
- * @param { IOptions } pOptions
23
- * @param { IAvailableTranspiler[] } pAvailableTranspilers
24
- * @returns { boolean }
25
- */
26
- function swcIsConsistent(pOptions, pAvailableTranspilers) {
27
- const lSWCNeeded = (pOptions?.parser ?? "acorn") === "swc";
28
- return lSWCNeeded
29
- ? pAvailableTranspilers.some(
30
- (pTranspiler) => pTranspiler.name === "swc" && pTranspiler.available,
31
- )
32
- : true;
33
- }
34
-
35
- /**
36
- * @param { IOptions } pOptions
37
- * @param { IAvailableTranspiler[] } pAvailableTranspilers
38
- * @returns { boolean }
39
- */
40
- function typescriptIsConsistent(pOptions, pAvailableTranspilers) {
41
- const lTypeScriptNeeded =
42
- Boolean(pOptions?.tsConfig) ||
43
- Boolean(pOptions?.tsPreCompilationDeps) ||
44
- Boolean(pOptions?.detectJSDocImports) ||
45
- (pOptions?.parser ?? "acorn") === "tsc";
46
- return lTypeScriptNeeded
47
- ? pAvailableTranspilers.some(
48
- (pTranspiler) =>
49
- pTranspiler.name === "typescript" && pTranspiler.available,
50
- )
51
- : true;
52
- }
53
- /**
54
- * @param { IOptions } pOptions
55
- * @param { IAvailableTranspiler[] } pAvailableTranspilers
56
- * @returns { boolean }
57
- */
58
- function babelIsConsistent(pOptions, pAvailableTranspilers) {
59
- return Boolean(pOptions?.babelConfig)
60
- ? pAvailableTranspilers.some(
61
- (pTranspiler) => pTranspiler.name === "babel" && pTranspiler.available,
62
- )
63
- : true;
64
- }
65
-
66
- /**
67
- * @param {IOptions} pOptions
68
- * @param {IAvailableTranspiler[]} pAvailableTranspilers
69
- * @returns {{issues?: IEnvironmentIssue[]}}
70
- */
71
- function determineEnvironmentIssues(pOptions, pAvailableTranspilers) {
72
- const lIssues = [];
73
- if (!typescriptIsConsistent(pOptions, pAvailableTranspilers)) {
74
- const tsTranspiler = pAvailableTranspilers.find(
75
- (pTranspiler) => pTranspiler.name === "typescript",
76
- );
77
-
78
- lIssues.push({
79
- severity: "warn",
80
- name: "missing-typescript-transpiler",
81
- description:
82
- `dependency-cruiser detected a TypeScript environment,${EOL}` +
83
- ` but not a compatible TypeScript compiler (${tsTranspiler.name}: ${tsTranspiler.version}). This means${EOL}` +
84
- ` it's likely to have missed TypeScript sources and dependencies.${EOL}${EOL}` +
85
- ` Install typescript to get better results (e.g. npm i -D typescript@^6).${EOL}${EOL}` +
86
- ` => Support for typescript@>=7 will follow when its API is published and stable.${EOL}`,
87
- });
88
- }
89
- if (!swcIsConsistent(pOptions, pAvailableTranspilers)) {
90
- const swcTranspiler = pAvailableTranspilers.find(
91
- (pTranspiler) => pTranspiler.name === "swc",
92
- );
93
-
94
- lIssues.push({
95
- severity: "warn",
96
- name: "missing-swc-transpiler",
97
- description:
98
- `dependency-cruiser found swc configured as parser, but ${EOL}` +
99
- ` it did not find a compatible version of swc (${swcTranspiler.name}: ${swcTranspiler.version}).${EOL}` +
100
- ` dependency-cruiser might have missed some sources or dependencies.${EOL}${EOL}` +
101
- ` Install swc (e.g. npm i -D @swc/core) to get better results.${EOL}`,
102
- });
103
- }
104
- if (!babelIsConsistent(pOptions, pAvailableTranspilers)) {
105
- const babelTranspiler = pAvailableTranspilers.find(
106
- (pTranspiler) => pTranspiler.name === "babel",
107
- );
108
-
109
- lIssues.push({
110
- severity: "warn",
111
- name: "missing-babel-transpiler",
112
- description:
113
- `dependency-cruiser detected a babel configuration, but not a${EOL}` +
114
- ` compatible babel transpiler (${babelTranspiler.name}: ${babelTranspiler.version}). If you indeed use babel,${EOL}` +
115
- ` dependency-cruiser might have missed some sources or dependencies.${EOL}${EOL}` +
116
- ` Install babel (e.g. npm i -D @babel/core@^7) to get better results.${EOL}`,
117
- });
118
- }
119
- return lIssues.length > 0 ? { issues: lIssues } : {};
120
- }
121
-
122
21
  function getAdvisedExitCode(
123
22
  pErrorCount,
124
23
  pBaselineStaleCount,
@@ -0,0 +1,146 @@
1
+ import { EOL } from "node:os";
2
+ /**
3
+ * @import { IOptions, IEnvironmentIssue } from "../../../types/cruise-result.mjs"
4
+ * @import { IAvailableTranspiler } from "../../../types/environment.mjs"
5
+ */
6
+
7
+ /**
8
+ * @param { IOptions } pOptions
9
+ * @param { IAvailableTranspiler[] } pAvailableTranspilers
10
+ * @returns { boolean }
11
+ */
12
+ function swcIsConsistent(pOptions, pAvailableTranspilers) {
13
+ const lSWCNeeded = (pOptions?.parser ?? "acorn") === "swc";
14
+ return lSWCNeeded
15
+ ? pAvailableTranspilers.some(
16
+ (pTranspiler) => pTranspiler.name === "swc" && pTranspiler.available,
17
+ )
18
+ : true;
19
+ }
20
+
21
+ /**
22
+ * @param { IOptions } pOptions
23
+ * @param { IAvailableTranspiler[] } pAvailableTranspilers
24
+ * @returns { boolean }
25
+ */
26
+ function tscIsConsistent(pOptions, pAvailableTranspilers) {
27
+ const lTSCNeeded =
28
+ Boolean(pOptions?.tsPreCompilationDeps) ||
29
+ Boolean(pOptions?.detectJSDocImports) ||
30
+ (pOptions?.parser ?? "acorn") === "tsc";
31
+ return lTSCNeeded
32
+ ? pAvailableTranspilers.some(
33
+ (pTranspiler) =>
34
+ pTranspiler.name === "typescript" && pTranspiler.available,
35
+ )
36
+ : true;
37
+ }
38
+
39
+ function typeScriptIsConsistent(pOptions, pAvailableTranspilers) {
40
+ const lTypeScriptNeeded =
41
+ Boolean(pOptions?.tsConfig) &&
42
+ !(
43
+ // these two options require tsc (covered in tscIsConsistent), so
44
+ // to prevent typeScriptIsConsistent from also firing (and printing
45
+ // two messages) we exclude them here
46
+ Boolean(pOptions?.tsPreCompilationDeps) ||
47
+ Boolean(pOptions?.detectJSDocImports)
48
+ );
49
+ return lTypeScriptNeeded
50
+ ? pAvailableTranspilers.some(
51
+ (pTranspiler) =>
52
+ (pTranspiler.name === "typescript" ||
53
+ (pTranspiler.name === "swc" && pOptions?.parser === "swc")) &&
54
+ pTranspiler.available,
55
+ )
56
+ : true;
57
+ }
58
+
59
+ /**
60
+ * @param { IOptions } pOptions
61
+ * @param { IAvailableTranspiler[] } pAvailableTranspilers
62
+ * @returns { boolean }
63
+ */
64
+ function babelIsConsistent(pOptions, pAvailableTranspilers) {
65
+ return Boolean(pOptions?.babelConfig)
66
+ ? pAvailableTranspilers.some(
67
+ (pTranspiler) => pTranspiler.name === "babel" && pTranspiler.available,
68
+ )
69
+ : true;
70
+ }
71
+
72
+ /**
73
+ * @param {IOptions} pOptions
74
+ * @param {IAvailableTranspiler[]} pAvailableTranspilers
75
+ * @returns {{issues?: IEnvironmentIssue[]}}
76
+ */
77
+ export function determineEnvironmentIssues(pOptions, pAvailableTranspilers) {
78
+ const lIssues = [];
79
+ if (!tscIsConsistent(pOptions, pAvailableTranspilers)) {
80
+ const tsTranspiler = pAvailableTranspilers.find(
81
+ (pTranspiler) => pTranspiler.name === "typescript",
82
+ );
83
+
84
+ lIssues.push({
85
+ severity: "warn",
86
+ name: "missing-tsc-transpiler",
87
+ description:
88
+ `dependency-cruiser detected a TypeScript environment,${EOL}` +
89
+ ` but not a compatible TypeScript compiler (${tsTranspiler.name}: ${tsTranspiler.version}). This means${EOL}` +
90
+ ` it's likely to have missed TypeScript sources and dependencies.${EOL}${EOL}` +
91
+ ` Install typescript to get better results (e.g. npm i -D typescript@^6 or typescript@^7.1).${EOL}${EOL}` +
92
+ ` => typescript@7.0 is not supported as its API lacks what dependency-cruiser needs.${EOL}`,
93
+ });
94
+ }
95
+ if (!typeScriptIsConsistent(pOptions, pAvailableTranspilers)) {
96
+ const tsTranspilersString = pAvailableTranspilers
97
+ .filter(
98
+ (pTranspiler) =>
99
+ pTranspiler.name === "typescript" || pTranspiler.name === "swc",
100
+ )
101
+ .map((pTranspiler) => `${pTranspiler.name}: ${pTranspiler.version}`)
102
+ .join(", ");
103
+ lIssues.push({
104
+ severity: "warn",
105
+ name: "missing-typescript-transpiler",
106
+ description:
107
+ `dependency-cruiser detected an environment that needs a TypeScript compiler of some sort,${EOL}` +
108
+ ` but did not find one of the supported ones (${tsTranspilersString}).${EOL}` +
109
+ ` This means it's likely to have missed TypeScript sources and dependencies.${EOL}${EOL}` +
110
+ ` Install one of the above for better results.${EOL}${EOL}` +
111
+ ` => if you choose to use swc don't forget to set the "parser" option to "swc"${EOL}` +
112
+ ` => typescript@7.0 is not supported as its API lacks what dependency-cruiser needs.${EOL}`,
113
+ });
114
+ }
115
+ if (!swcIsConsistent(pOptions, pAvailableTranspilers)) {
116
+ const swcTranspiler = pAvailableTranspilers.find(
117
+ (pTranspiler) => pTranspiler.name === "swc",
118
+ );
119
+
120
+ lIssues.push({
121
+ severity: "warn",
122
+ name: "missing-swc-transpiler",
123
+ description:
124
+ `dependency-cruiser found swc configured as compiler, but ${EOL}` +
125
+ ` it did not find a compatible version of swc (${swcTranspiler.name}: ${swcTranspiler.version}).${EOL}` +
126
+ ` dependency-cruiser might have missed some sources or dependencies.${EOL}${EOL}` +
127
+ ` Install swc (e.g. npm i -D @swc/core) to get better results.${EOL}`,
128
+ });
129
+ }
130
+ if (!babelIsConsistent(pOptions, pAvailableTranspilers)) {
131
+ const babelTranspiler = pAvailableTranspilers.find(
132
+ (pTranspiler) => pTranspiler.name === "babel",
133
+ );
134
+
135
+ lIssues.push({
136
+ severity: "warn",
137
+ name: "missing-babel-transpiler",
138
+ description:
139
+ `dependency-cruiser detected a babel configuration, but not a${EOL}` +
140
+ ` compatible babel transpiler (${babelTranspiler.name}: ${babelTranspiler.version}). If you indeed use babel,${EOL}` +
141
+ ` dependency-cruiser might have missed some sources or dependencies.${EOL}${EOL}` +
142
+ ` Install babel (e.g. npm i -D @babel/core@^7) to get better results.${EOL}`,
143
+ });
144
+ }
145
+ return lIssues.length > 0 ? { issues: lIssues } : {};
146
+ }
@@ -112,7 +112,7 @@ module.exports = {
112
112
  name: 'no-duplicate-dep-types',
113
113
  comment:
114
114
  "Likely this module depends on an external ('npm') package that occurs more than once " +
115
- "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
115
+ "in your package.json i.e. both as a devDependencies and in dependencies. This will cause " +
116
116
  "maintenance problems later on.",
117
117
  severity: 'warn',
118
118
  from: {},
@@ -1,31 +1,4 @@
1
- import { dirname, resolve } from "node:path";
2
- import tryImport from "#utl/try-import.mjs";
3
- import meta from "#meta.cjs";
4
-
5
- const typescript = await tryImport(
6
- "typescript",
7
- meta.supportedTranspilers.typescript,
8
- );
9
-
10
- const FORMAT_DIAGNOSTICS_HOST = {
11
- getCanonicalFileName(pFileName) {
12
- let lReturnValue = pFileName.toLowerCase();
13
-
14
- // depends on the platform which branch is taken, hence the c8 ignore
15
- /* c8 ignore start */
16
- if (typescript?.sys?.useCaseSensitiveFileNames ?? false) {
17
- lReturnValue = pFileName;
18
- }
19
- /* c8 ignore stop */
20
- return lReturnValue;
21
- },
22
- getCurrentDirectory() {
23
- return process.cwd();
24
- },
25
- getNewLine() {
26
- return "\n";
27
- },
28
- };
1
+ import typescript from "#utl/typescript-facade.mjs";
29
2
 
30
3
  /**
31
4
  * Reads the file with name `pTSConfigFileName` and returns its parsed
@@ -42,31 +15,18 @@ export default function extractTSConfig(pTSConfigFileName) {
42
15
  let lReturnValue = {};
43
16
 
44
17
  if (typescript) {
45
- const lConfig = typescript.readConfigFile(
46
- pTSConfigFileName,
47
- typescript.sys.readFile,
48
- );
18
+ const lConfig = typescript.readConfigFile(pTSConfigFileName);
49
19
 
50
20
  if (typeof lConfig.error !== "undefined") {
51
- throw new TypeError(
52
- typescript.formatDiagnostics([lConfig.error], FORMAT_DIAGNOSTICS_HOST),
53
- );
21
+ throw new TypeError(typescript.formatDiagnostics([lConfig.error]));
54
22
  }
55
23
  lReturnValue = typescript.parseJsonConfigFileContent(
56
24
  lConfig.config,
57
- typescript.sys,
58
- dirname(resolve(pTSConfigFileName)),
59
- {},
60
25
  pTSConfigFileName,
61
26
  );
62
27
 
63
28
  if (lReturnValue.errors.length > 0) {
64
- throw new Error(
65
- typescript.formatDiagnostics(
66
- lReturnValue.errors,
67
- FORMAT_DIAGNOSTICS_HOST,
68
- ),
69
- );
29
+ throw new Error(typescript.formatDiagnostics(lReturnValue.errors));
70
30
  }
71
31
  }
72
32
 
@@ -22,7 +22,9 @@ function dependencyKeyHasModuleName(
22
22
  pPrefix,
23
23
  ) {
24
24
  return (pKey) =>
25
- pKey.includes("ependencies") &&
25
+ // dependencies, devDependencies, peerDependencies, optionalDependencies, bundleDependencies, bundledDependencies.
26
+ // But not peerDependenciesMeta. Or dependenciesIDoNotWantToTalkabout.
27
+ pKey.endsWith("ependencies") &&
26
28
  Object.hasOwn(pPackageDependencies[pKey], join(pPrefix, pModuleName));
27
29
  }
28
30
 
@@ -123,7 +123,7 @@ function nodeIsTypeOnly(pNode) {
123
123
  return true;
124
124
  }
125
125
  return (
126
- pNode?.specifiers.length > 0 &&
126
+ pNode?.specifiers?.length > 0 &&
127
127
  pNode.specifiers.every((pSpecifier) => pSpecifier.isTypeOnly)
128
128
  );
129
129
  }
@@ -144,7 +144,9 @@ export default Visitor
144
144
  module: pNode.source.value,
145
145
  moduleSystem: "es6",
146
146
  exoticallyRequired: false,
147
- dependencyTypes: ["export"],
147
+ dependencyTypes: nodeIsTypeOnly(pNode)
148
+ ? ["export", "type-only"]
149
+ : ["export"],
148
150
  });
149
151
  }
150
152
  }
@@ -55,10 +55,9 @@ const TRANSPILER2AVAILABLE = {
55
55
  meta.supportedTranspilers.svelte,
56
56
  ),
57
57
  swc: tryImportAvailable("@swc/core", meta.supportedTranspilers.swc),
58
- typescript: tryImportAvailable(
59
- "typescript",
60
- meta.supportedTranspilers.typescript,
61
- ),
58
+ // not tryImportAvailable as for typescript >=7 being installed isn't
59
+ // enough - the native compiler needs to be able to run as well
60
+ typescript: typescriptWrap.isAvailable(),
62
61
  "vue-template-compiler": tryImportAvailable(
63
62
  "vue-template-compiler",
64
63
  meta.supportedTranspilers["vue-template-compiler"],
@@ -109,7 +108,19 @@ export const EXTENSION2AVAILABLE = new Map([
109
108
  ]);
110
109
 
111
110
  const EXTENSIONS_PER_PARSER = {
112
- swc: new Set([".js", ".cjs", ".mjs", ".jsx", ".ts", ".tsx", ".d.ts"]),
111
+ swc: new Set([
112
+ ".js",
113
+ ".cjs",
114
+ ".mjs",
115
+ ".jsx",
116
+ ".ts",
117
+ ".tsx",
118
+ ".d.ts",
119
+ ".cts",
120
+ ".d.cts",
121
+ ".mts",
122
+ ".d.mts",
123
+ ]),
113
124
  // tsc: [".js", ".cjs", ".mjs", ".jsx", ".ts", ".tsx", ".d.ts"],
114
125
  // acorn: [".js", ".cjs", ".mjs", ".jsx"],
115
126
  };
@@ -1,32 +1,26 @@
1
- import tryImport from "#utl/try-import.mjs";
2
- import meta from "#meta.cjs";
3
-
4
- const typescript = await tryImport(
5
- "typescript",
6
- meta.supportedTranspilers.typescript,
7
- );
1
+ import typescript from "#utl/typescript-facade.mjs";
8
2
 
9
3
  function getCompilerOptions(pFlavor, pTSConfig) {
10
4
  const lCompilerOptions = {};
11
5
 
12
6
  if (pFlavor === "tsx") {
13
- lCompilerOptions.jsx = "react";
7
+ lCompilerOptions.jsx = typescript.JsxEmit.React;
14
8
  }
15
9
 
16
10
  if (pFlavor === "esm") {
17
11
  // see https://www.typescriptlang.org/docs/handbook/esm-node.html
18
- lCompilerOptions.module = "nodenext";
12
+ lCompilerOptions.module = typescript.ModuleKind.NodeNext;
19
13
  // when we're doing something with esm (so cts, mts extensions) we can
20
14
  // assume TypeScript >=4.7 - which supports target es2022 - which should
21
15
  // be easier (and hence we assume faster) to transpile
22
- lCompilerOptions.target = "es2022";
16
+ lCompilerOptions.target = typescript.ScriptTarget.ES2022;
23
17
  }
24
18
 
25
19
  return {
26
20
  // target: "es2022" should also be OK - but we don't know which version of
27
21
  // the TypeScript compiler we're using, so for we keep this for backwards
28
22
  // compatibility.
29
- target: "es2015",
23
+ target: typescript.ScriptTarget.ES2015,
30
24
  ...lCompilerOptions,
31
25
  ...(pTSConfig?.options ?? {}),
32
26
  };
@@ -2,19 +2,12 @@
2
2
  /* eslint-disable unicorn/prevent-abbreviations */
3
3
  /* eslint-disable max-lines */
4
4
  /* eslint-disable no-inline-comments */
5
- import tryImport from "#utl/try-import.mjs";
6
- import meta from "#meta.cjs";
5
+ import typescript from "#utl/typescript-facade.mjs";
7
6
 
8
7
  /**
9
- * @import typescript, {Node} from "typescript"
8
+ * @import {Node} from "typescript"
10
9
  */
11
10
 
12
- /** @type {typescript} */
13
- const typescript = await tryImport(
14
- "typescript",
15
- meta.supportedTranspilers.typescript,
16
- );
17
-
18
11
  const INTERESTING_NODE_KINDS = typescript
19
12
  ? new Set([
20
13
  typescript.SyntaxKind.CallExpression,
@@ -86,11 +79,17 @@ function getModuleBlock(pStatement) {
86
79
  }
87
80
 
88
81
  function getStatements(pAST, pDetectImportsInAmbientModules) {
82
+ // in javascript the native compiler adds statements it derived from jsdoc
83
+ // (e.g. from @import tags). Those are handled with the rest of the jsdoc.
84
+ const lStatements = (pAST.statements ?? []).filter(
85
+ (pStatement) => !typescript.isReparsed(pStatement),
86
+ );
87
+
89
88
  if (!pDetectImportsInAmbientModules) {
90
- return pAST.statements ?? [];
89
+ return lStatements;
91
90
  }
92
91
 
93
- return (pAST.statements ?? []).flatMap((pStatement) => {
92
+ return lStatements.flatMap((pStatement) => {
94
93
  const lModuleBlock = getModuleBlock(pStatement);
95
94
 
96
95
  return lModuleBlock
@@ -208,7 +207,9 @@ function extractTripleSlashDirectives(pAST) {
208
207
  })),
209
208
  )
210
209
  .concat(
211
- pAST.amdDependencies.map((pReference) => ({
210
+ // the native compiler doesn't support amd anymore, so it doesn't
211
+ // deliver amdDependencies either
212
+ (pAST.amdDependencies ?? []).map((pReference) => ({
212
213
  module: pReference.path,
213
214
  moduleSystem: "tsd",
214
215
  exoticallyRequired: false,
@@ -237,19 +238,11 @@ function isRequireCallExpression(pASTNode) {
237
238
  pASTNode.kind === typescript.SyntaxKind.CallExpression &&
238
239
  pASTNode.expression
239
240
  ) {
240
- /*
241
- * from typescript 5.0.0 the `originalKeywordKind` attribute is deprecated
242
- * and from 5.2.0 it will be gone. However, in typescript < 5.0.0 (still used
243
- * heavily IRL) it's the only way to get it - hence this test for the
244
- * existence of the * identifierToKeywordKind function
245
- */
246
- const lSyntaxKind = typescript.identifierToKeywordKind
247
- ? typescript.SyntaxKind[
248
- typescript.identifierToKeywordKind(pASTNode.expression)
249
- ]
250
- : /* c8 ignore next 1 */
251
- typescript.SyntaxKind[pASTNode.expression.originalKeywordKind];
252
- return lSyntaxKind === "RequireKeyword" && firstArgumentIsAString(pASTNode);
241
+ return (
242
+ pASTNode.expression.kind === typescript.SyntaxKind.Identifier &&
243
+ pASTNode.expression.text === "require" &&
244
+ firstArgumentIsAString(pASTNode)
245
+ );
253
246
  }
254
247
  return false;
255
248
  }
@@ -272,10 +265,10 @@ function isCompositeExoticRequire(pASTNode, pObjectName, pPropertyName) {
272
265
  typescript.SyntaxKind.PropertyAccessExpression &&
273
266
  pASTNode.expression.expression &&
274
267
  pASTNode.expression.expression.kind === typescript.SyntaxKind.Identifier &&
275
- pASTNode.expression.expression.escapedText === pObjectName &&
268
+ pASTNode.expression.expression.text === pObjectName &&
276
269
  pASTNode.expression.name &&
277
270
  pASTNode.expression.name.kind === typescript.SyntaxKind.Identifier &&
278
- pASTNode.expression.name.escapedText === pPropertyName &&
271
+ pASTNode.expression.name.text === pPropertyName &&
279
272
  firstArgumentIsAString(pASTNode)
280
273
  );
281
274
  }
@@ -298,15 +291,15 @@ function isTripleCursedCompositeExoticRequire(
298
291
  pASTNode.expression.expression.expression &&
299
292
  pASTNode.expression.expression.expression.kind ===
300
293
  typescript.SyntaxKind.Identifier &&
301
- pASTNode.expression.expression.expression.escapedText === pObjectName1 &&
294
+ pASTNode.expression.expression.expression.text === pObjectName1 &&
302
295
  // process
303
296
  pASTNode.expression.expression.name.kind ===
304
297
  typescript.SyntaxKind.Identifier &&
305
- pASTNode.expression.expression.name.escapedText === pObjectName2 &&
298
+ pASTNode.expression.expression.name.text === pObjectName2 &&
306
299
  // getBuiltinModule
307
300
  pASTNode.expression.name &&
308
301
  pASTNode.expression.name.kind === typescript.SyntaxKind.Identifier &&
309
- pASTNode.expression.name.escapedText === pPropertyName &&
302
+ pASTNode.expression.name.text === pPropertyName &&
310
303
  firstArgumentIsAString(pASTNode)
311
304
  );
312
305
  }
@@ -344,7 +337,7 @@ function extractJSDocImportTags(pJSDocTags) {
344
337
  return pJSDocTags
345
338
  .filter(
346
339
  (pTag) =>
347
- pTag.tagName.escapedText === "import" &&
340
+ pTag.tagName.text === "import" &&
348
341
  pTag.moduleSpecifier &&
349
342
  pTag.moduleSpecifier.kind === typescript.SyntaxKind.StringLiteral &&
350
343
  pTag.moduleSpecifier.text,
@@ -367,33 +360,13 @@ function isJSDocImport(pTypeNode) {
367
360
  );
368
361
  }
369
362
 
370
- const IGNORABLE_JSDOC_KEYS = new Set([
371
- "parent",
372
- "pos",
373
- "end",
374
- "flags",
375
- "emitNode",
376
- "modifierFlagsCache",
377
- "transformFlags",
378
- "id",
379
- "flowNode",
380
- "symbol",
381
- "original",
382
- ]);
383
-
384
- export function walkJSDoc(pObject, pCollection = new Set()) {
385
- if (isJSDocImport(pObject)) {
386
- pCollection.add(pObject.argument.literal.text);
387
- } else if (Array.isArray(pObject)) {
388
- for (const lValue of pObject) {
389
- walkJSDoc(lValue, pCollection);
390
- }
391
- } else if (typeof pObject === "object") {
392
- for (const lKey in pObject) {
393
- if (!IGNORABLE_JSDOC_KEYS.has(lKey) && pObject[lKey]) {
394
- walkJSDoc(pObject[lKey], pCollection);
395
- }
396
- }
363
+ export function walkJSDoc(pNode, pCollection = new Set()) {
364
+ if (isJSDocImport(pNode)) {
365
+ pCollection.add(pNode.argument.literal.text);
366
+ } else {
367
+ typescript.forEachChild(pNode, (pChildNode) => {
368
+ walkJSDoc(pChildNode, pCollection);
369
+ });
397
370
  }
398
371
  }
399
372
 
@@ -408,7 +381,7 @@ function extractJSDocBracketImports(pJSDocTags) {
408
381
  return pJSDocTags
409
382
  .filter(
410
383
  (pTag) =>
411
- pTag.tagName.escapedText !== "import" &&
384
+ pTag.tagName.text !== "import" &&
412
385
  pTag.typeExpression?.kind === typescript.SyntaxKind.FirstJSDocNode,
413
386
  )
414
387
  .flatMap((pTag) => getJSDocImports(pTag))
@@ -534,6 +507,14 @@ function walk(
534
507
  pDetectProcessBuiltinModuleCalls,
535
508
  ) {
536
509
  return (pASTNode) => {
510
+ // in javascript the native compiler adds nodes it derived from jsdoc
511
+ // (e.g. for types in @type tags). Those are handled with the rest of the
512
+ // jsdoc. As only the native compiler has these, the c8 ignore.
513
+ /* c8 ignore start */
514
+ if (typescript.isReparsed(pASTNode)) {
515
+ return;
516
+ }
517
+ /* c8 ignore stop */
537
518
  // /** @import thing from './module' */ etc
538
519
  // /** @type {import('module').thing}*/ etc
539
520
  if (pDetectJSDocImports && pASTNode.jsDoc) {
@@ -1,14 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import transpile from "../transpile/index.mjs";
3
- import tryImport from "#utl/try-import.mjs";
4
- import meta from "#meta.cjs";
5
3
  import getExtension from "#utl/get-extension.mjs";
4
+ import typescript from "#utl/typescript-facade.mjs";
6
5
 
7
- /** @type {import('typescript')} */
8
- const typescript = await tryImport(
9
- "typescript",
10
- meta.supportedTranspilers.typescript,
11
- );
12
6
  /** @type {Map<string, any>} */
13
7
  const CACHE = new Map();
14
8
 
@@ -29,8 +23,6 @@ export function getASTFromSource(pFileRecord, pTranspileOptions) {
29
23
  return typescript.createSourceFile(
30
24
  pFileRecord.filename || "$internal-file-name",
31
25
  lSource,
32
- typescript.ScriptTarget.Latest,
33
- false,
34
26
  );
35
27
  }
36
28
 
package/src/meta.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /* generated - don't edit */
2
2
  module.exports = {
3
- version: "18.6.0",
3
+ version: "18.8.0-beta-1",
4
4
  engines: {
5
5
  node: "^22||^24||>=26",
6
6
  },
@@ -11,7 +11,7 @@ module.exports = {
11
11
  livescript: ">=1.0.0 <2.0.0",
12
12
  svelte: ">=3.0.0 <6.0.0",
13
13
  swc: ">=1.0.0 <2.0.0",
14
- typescript: ">=2.0.0 <7.0.0",
14
+ typescript: ">=2.0.0 <7.0.0||>=7.1.0 <8.0.0",
15
15
  "vue-template-compiler": ">=2.0.0 <3.0.0",
16
16
  "@vue/compiler-sfc": ">=3.0.0 <4.0.0",
17
17
  },
@@ -0,0 +1,238 @@
1
+ /* eslint-disable security/detect-object-injection */
2
+ import { dirname, resolve } from "node:path";
3
+ import tryImport from "#utl/try-import.mjs";
4
+ import meta from "#meta.cjs";
5
+
6
+ /**
7
+ * One interface over the two flavors of the typescript compiler
8
+ * - typescript <7: the compiler written in typescript, with its API
9
+ * available in-process on the module itself.
10
+ * - typescript >=7.1: the native compiler. Its API (typescript/sync and
11
+ * typescript/ast) talks to a separate process running the compiler.
12
+ *
13
+ * @typedef {object} ITypeScriptFacade
14
+ * @property {boolean} isNative - true when this is the native (>=7.1) compiler
15
+ * @property {string} version - version of the typescript compiler
16
+ * @property {Record<string, any>} SyntaxKind - the compiler's SyntaxKind enum
17
+ * @property {Record<string, any>} ScriptTarget - the compiler's ScriptTarget enum
18
+ * @property {Record<string, any>} ModuleKind - the compiler's ModuleKind enum
19
+ * @property {Record<string, any>} JsxEmit - the compiler's JsxEmit enum
20
+ * @property {(pNode: any, pCallback: (pNode: any) => void) => void} forEachChild -
21
+ * calls pCallback for each child node of pNode
22
+ * @property {(pNode: any) => boolean} isReparsed - true for nodes the
23
+ * compiler synthesized from something else (e.g. jsdoc in javascript)
24
+ * @property {(pFileName: string, pSource: string) => any} createSourceFile -
25
+ * parses pSource into an AST
26
+ * @property {(pSource: string, pOptions?: {compilerOptions?: Record<string, any>, fileName?: string}) => {outputText: string}} transpileModule -
27
+ * transpiles pSource to javascript
28
+ * @property {(pFileName: string) => {config?: any, error?: any}} readConfigFile -
29
+ * reads the tsconfig pFileName
30
+ * @property {(pConfig: any, pFileName: string) => import("typescript").ParsedCommandLine} parseJsonConfigFileContent -
31
+ * parses (and flattens) the tsconfig pConfig that was read from pFileName
32
+ * @property {(pDiagnostics: readonly any[]) => string} formatDiagnostics -
33
+ * formats pDiagnostics into a human readable string
34
+ */
35
+
36
+ const FIRST_NATIVE_MAJOR_VERSION = 7;
37
+
38
+ /**
39
+ * @param {typeof import("typescript")} pTypeScript
40
+ * @returns {ITypeScriptFacade}
41
+ */
42
+ function getClassicFacade(pTypeScript) {
43
+ const lFormatDiagnosticsHost = {
44
+ getCanonicalFileName(pFileName) {
45
+ let lReturnValue = pFileName.toLowerCase();
46
+
47
+ // depends on the platform which branch is taken, hence the c8 ignore
48
+ /* c8 ignore start */
49
+ if (pTypeScript?.sys?.useCaseSensitiveFileNames ?? false) {
50
+ lReturnValue = pFileName;
51
+ }
52
+ /* c8 ignore stop */
53
+ return lReturnValue;
54
+ },
55
+ getCurrentDirectory() {
56
+ return process.cwd();
57
+ },
58
+ getNewLine() {
59
+ return "\n";
60
+ },
61
+ };
62
+
63
+ return {
64
+ isNative: false,
65
+ version: pTypeScript.version,
66
+ SyntaxKind: pTypeScript.SyntaxKind,
67
+ ScriptTarget: pTypeScript.ScriptTarget,
68
+ ModuleKind: pTypeScript.ModuleKind,
69
+ JsxEmit: pTypeScript.JsxEmit,
70
+ forEachChild: (pNode, pCallback) => {
71
+ pTypeScript.forEachChild(pNode, pCallback);
72
+ },
73
+ isReparsed: () => false,
74
+ createSourceFile: (pFileName, pSource) =>
75
+ pTypeScript.createSourceFile(
76
+ pFileName,
77
+ pSource,
78
+ pTypeScript.ScriptTarget.Latest,
79
+ false,
80
+ ),
81
+ transpileModule: (pSource, pOptions) =>
82
+ pTypeScript.transpileModule(pSource, pOptions),
83
+ readConfigFile: (pFileName) =>
84
+ pTypeScript.readConfigFile(pFileName, pTypeScript.sys.readFile),
85
+ parseJsonConfigFileContent: (pConfig, pFileName) =>
86
+ pTypeScript.parseJsonConfigFileContent(
87
+ pConfig,
88
+ pTypeScript.sys,
89
+ dirname(resolve(pFileName)),
90
+ {},
91
+ pFileName,
92
+ ),
93
+ formatDiagnostics: (pDiagnostics) =>
94
+ pTypeScript.formatDiagnostics(pDiagnostics, lFormatDiagnosticsHost),
95
+ };
96
+ }
97
+
98
+ // The code below only runs with the native compiler (or without any
99
+ // typescript compiler). The regular test (& coverage) run uses the classic
100
+ // one, so c8 can't see this is covered by the run against the native one.
101
+ /* c8 ignore start */
102
+ /**
103
+ * The classic compiler's transpileModule accepts both names ("commonjs") and
104
+ * values (1) for options that are enums. The native one only takes values,
105
+ * so this lets its own tsconfig parser translate any names into values.
106
+ * Options it doesn't recognize (anymore) are left out.
107
+ *
108
+ * @param {any} pAPI - instance of the native compiler's API
109
+ * @returns {(pCompilerOptions?: Record<string, any>) => Record<string, any>}
110
+ */
111
+ function getCompilerOptionsNormalizer(pAPI) {
112
+ /** @type {Map<string, Record<string, any>>} */
113
+ const lCache = new Map();
114
+
115
+ return (pCompilerOptions = {}) => {
116
+ const lStringOptions = Object.fromEntries(
117
+ Object.entries(pCompilerOptions).filter(
118
+ ([, pValue]) => typeof pValue === "string",
119
+ ),
120
+ );
121
+ const lStringOptionKeys = Object.keys(lStringOptions);
122
+
123
+ if (lStringOptionKeys.length === 0) {
124
+ return pCompilerOptions;
125
+ }
126
+ const lCacheKey = JSON.stringify(lStringOptions);
127
+
128
+ if (!lCache.has(lCacheKey)) {
129
+ lCache.set(
130
+ lCacheKey,
131
+ pAPI.parseJsonConfigFileContent(
132
+ { compilerOptions: lStringOptions, files: [] },
133
+ { configDirectory: process.cwd() },
134
+ ).options,
135
+ );
136
+ }
137
+ const lParsedOptions = lCache.get(lCacheKey);
138
+
139
+ return Object.fromEntries(
140
+ Object.entries(pCompilerOptions)
141
+ .map(([pKey, pValue]) =>
142
+ lStringOptionKeys.includes(pKey)
143
+ ? [pKey, lParsedOptions[pKey]]
144
+ : [pKey, pValue],
145
+ )
146
+ .filter(([, pValue]) => pValue !== undefined),
147
+ );
148
+ };
149
+ }
150
+
151
+ /**
152
+ * @param {{version: string}} pTypeScript
153
+ * @returns {Promise<ITypeScriptFacade>}
154
+ * @throws {Error} when the native compiler's binary isn't available (e.g.
155
+ * on unsupported platforms, or when optional dependencies weren't
156
+ * installed)
157
+ */
158
+ async function getNativeFacade(pTypeScript) {
159
+ const [lSync, lAST] = await Promise.all([
160
+ import("typescript/sync"),
161
+ import("typescript/ast"),
162
+ ]);
163
+ // this starts the compiler process. That process doesn't keep node from
164
+ // exiting and stops when node does, so there's no need to close it.
165
+ const lAPI = new lSync.API({ cwd: process.cwd() });
166
+ const normalizeCompilerOptions = getCompilerOptionsNormalizer(lAPI);
167
+
168
+ return {
169
+ isNative: true,
170
+ version: pTypeScript.version,
171
+ SyntaxKind: lAST.SyntaxKind,
172
+ ScriptTarget: lAST.ScriptTarget,
173
+ ModuleKind: lSync.ModuleKind,
174
+ JsxEmit: lSync.JsxEmit,
175
+ // nodes created by anything else than the native compiler (e.g. ASTs
176
+ // from json) don't have the forEachChild method. They're treated as
177
+ // having no children.
178
+ forEachChild: (pNode, pCallback) => {
179
+ pNode.forEachChild?.(pCallback);
180
+ },
181
+ isReparsed: (pNode) =>
182
+ // flags is a bit field
183
+ // oxlint-disable-next-line no-bitwise
184
+ ((pNode.flags ?? 0) & lAST.NodeFlags.Reparsed) !== 0,
185
+ createSourceFile: (pFileName, pSource) => {
186
+ const lRetainedSourceFile = lAPI.createSourceFile(pFileName, pSource);
187
+ // the source file is already copied over from the compiler process,
188
+ // so it stays usable after releasing it there
189
+ lRetainedSourceFile.dispose();
190
+ return lRetainedSourceFile.sourceFile;
191
+ },
192
+ transpileModule: (pSource, pOptions) =>
193
+ lAPI.transpileModule(pSource, {
194
+ compilerOptions: normalizeCompilerOptions(pOptions?.compilerOptions),
195
+ fileName: pOptions?.fileName,
196
+ }),
197
+ // The compiler process resolves relative paths against the directory it
198
+ // was started in - which can differ from the current one => resolve
199
+ // here
200
+ readConfigFile: (pFileName) => lAPI.readConfigFile(resolve(pFileName)),
201
+ parseJsonConfigFileContent: (pConfig, pFileName) =>
202
+ lAPI.parseJsonConfigFileContent(pConfig, {
203
+ configFileName: resolve(pFileName),
204
+ }),
205
+ formatDiagnostics: (pDiagnostics) =>
206
+ lSync.formatDiagnostics(pDiagnostics, lAPI),
207
+ };
208
+ }
209
+
210
+ /**
211
+ * @param {any} pTypeScript - the typescript module, or false if it isn't available
212
+ * @returns {Promise<ITypeScriptFacade|false>}
213
+ */
214
+ async function getFacade(pTypeScript) {
215
+ if (!pTypeScript) {
216
+ return false;
217
+ }
218
+ if (Number.parseInt(pTypeScript.version, 10) >= FIRST_NATIVE_MAJOR_VERSION) {
219
+ try {
220
+ return await getNativeFacade(pTypeScript);
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ /* c8 ignore stop */
226
+ return getClassicFacade(pTypeScript);
227
+ }
228
+
229
+ /**
230
+ * The typescript compiler behind a facade that hides the differences
231
+ * between the classic (<7) and the native (>=7.1) compiler, or false when
232
+ * no supported version of the typescript compiler is available.
233
+ *
234
+ * @type {ITypeScriptFacade|false}
235
+ */
236
+ export default await getFacade(
237
+ await tryImport("typescript", meta.supportedTranspilers.typescript),
238
+ );
@@ -1,4 +1,28 @@
1
- import type { ParsedCommandLine } from "typescript";
1
+ /**
2
+ * A tsconfig as parsed by the typescript compiler. Declared here as from
3
+ * typescript 7 the package's main entry point doesn't export its
4
+ * ParsedCommandLine type anymore.
5
+ */
6
+ export interface IParsedTSConfig {
7
+ /**
8
+ * The compiler options. Options with an enum type (target, module,
9
+ * moduleResolution, jsx, ...) have the enum's (numerical) value.
10
+ */
11
+ options: Record<string, any>;
12
+ /**
13
+ * The (absolute) names of the files the tsconfig includes
14
+ */
15
+ fileNames: string[];
16
+ /**
17
+ * Errors the typescript compiler encountered parsing the tsconfig
18
+ */
19
+ errors: any[];
20
+ /**
21
+ * The tsconfig as read from disk
22
+ */
23
+ raw?: any;
24
+ [key: string]: any;
25
+ }
2
26
 
3
27
  /**
4
28
  * Reads the file with name `pTSConfigFileName` and returns its parsed
@@ -13,4 +37,4 @@ import type { ParsedCommandLine } from "typescript";
13
37
  */
14
38
  export default function extractTSConfig(
15
39
  pTSConfigFileName: string,
16
- ): ParsedCommandLine;
40
+ ): IParsedTSConfig;