dependency-cruiser 15.4.0 → 15.5.0

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.
Files changed (63) hide show
  1. package/configs/plugins/3d-reporter-plugin.mjs +127 -0
  2. package/configs/plugins/stats-reporter-plugin.mjs +82 -0
  3. package/package.json +33 -23
  4. package/src/cache/metadata-strategy.mjs +1 -1
  5. package/src/cli/init-config/config-template.mjs +9 -1
  6. package/src/cli/tools/svg-in-html-snippets/{script.snippet.js → script.js} +47 -12
  7. package/src/cli/tools/svg-in-html-snippets/style.css +101 -0
  8. package/src/cli/tools/wrap-stream-in-html.mjs +57 -15
  9. package/src/config-utl/extract-depcruise-config/index.mjs +1 -1
  10. package/src/config-utl/extract-webpack-resolve-config.mjs +14 -11
  11. package/src/enrich/add-validations.mjs +3 -3
  12. package/src/enrich/soften-known-violations.mjs +4 -4
  13. package/src/enrich/summarize/index.mjs +3 -3
  14. package/src/extract/gather-initial-sources.mjs +2 -2
  15. package/src/extract/get-dependencies.mjs +2 -2
  16. package/src/extract/resolve/determine-dependency-types.mjs +3 -3
  17. package/src/extract/resolve/index.mjs +3 -3
  18. package/src/extract/transpile/meta.d.ts +1 -1
  19. package/src/graph-utl/filter-bank.mjs +2 -2
  20. package/src/main/index.d.ts +1 -1
  21. package/src/main/options/normalize.mjs +1 -1
  22. package/src/main/rule-set/normalize.mjs +1 -1
  23. package/src/meta.js +1 -1
  24. package/src/report/anon/index.mjs +7 -7
  25. package/src/report/azure-devops.mjs +11 -11
  26. package/src/report/csv.mjs +3 -2
  27. package/src/report/dot/default-theme.mjs +1 -1
  28. package/src/report/error-html/index.mjs +6 -6
  29. package/src/report/error.mjs +1 -1
  30. package/src/report/html/index.mjs +1 -1
  31. package/src/report/markdown.mjs +5 -5
  32. package/src/validate/index.d.ts +4 -4
  33. package/src/validate/match-folder-dependency-rule.mjs +2 -2
  34. package/src/validate/match-module-rule.mjs +12 -12
  35. package/src/validate/violates-required-rule.mjs +2 -2
  36. package/types/baseline-violations.d.mts +3 -0
  37. package/types/{extract-babel-config.d.ts → config-utl/extract-babel-config.d.mts} +1 -1
  38. package/types/{extract-depcruise-config.d.ts → config-utl/extract-depcruise-config.d.mts} +2 -2
  39. package/types/{extract-ts-config.d.ts → config-utl/extract-ts-config.d.mts} +1 -1
  40. package/types/{extract-webpack-resolve-config.d.ts → config-utl/extract-webpack-resolve-config.d.mts} +4 -3
  41. package/types/{configuration.d.ts → configuration.d.mts} +2 -2
  42. package/types/{cruise-result.d.ts → cruise-result.d.mts} +12 -8
  43. package/types/{dependency-cruiser.d.ts → dependency-cruiser.d.mts} +9 -9
  44. package/types/{filter-types.d.ts → filter-types.d.mts} +1 -1
  45. package/types/{options.d.ts → options.d.mts} +7 -7
  46. package/types/plugins/3d-reporter-plugin.d.mts +13 -0
  47. package/types/plugins/mermaid-reporter-plugin.d.mts +14 -0
  48. package/types/plugins/stats-reporter-plugin.d.mts +13 -0
  49. package/types/{reporter-options.d.ts → reporter-options.d.mts} +2 -2
  50. package/types/{resolve-options.d.ts → resolve-options.d.mts} +1 -1
  51. package/types/{restrictions.d.ts → restrictions.d.mts} +1 -1
  52. package/types/{rule-set.d.ts → rule-set.d.mts} +3 -3
  53. package/types/{rule-summary.d.ts → rule-summary.d.mts} +1 -1
  54. package/types/{strict-filter-types.d.ts → strict-filter-types.d.mts} +3 -3
  55. package/types/{strict-options.d.ts → strict-options.d.mts} +7 -7
  56. package/types/{strict-restrictions.d.ts → strict-restrictions.d.mts} +2 -2
  57. package/types/{strict-rule-set.d.ts → strict-rule-set.d.mts} +5 -5
  58. package/types/{violations.d.ts → violations.d.mts} +2 -2
  59. package/src/cli/tools/svg-in-html-snippets/footer.snippet.html +0 -2
  60. package/src/cli/tools/svg-in-html-snippets/header.snippet.html +0 -108
  61. package/types/baseline-violations.d.ts +0 -3
  62. /package/types/{cache-options.d.ts → cache-options.d.mts} +0 -0
  63. /package/types/{shared-types.d.ts → shared-types.d.mts} +0 -0
@@ -0,0 +1,127 @@
1
+ import * as path from "node:path";
2
+ import figures from "figures";
3
+
4
+ const TEMPLATE = `
5
+ <html>
6
+ <head>
7
+ <style> body { margin: 0; } </style>
8
+ <script type="text/javascript" src="https://unpkg.com/three"></script>
9
+ <script type="text/javascript" src="https://unpkg.com/three-spritetext"></script>
10
+ <script type="text/javascript" src="https://unpkg.com/3d-force-graph"></script>
11
+ </head>
12
+
13
+ <body>
14
+ <div id="3d-graph"></div>
15
+
16
+ <script>
17
+ const gData = {
18
+ nodes: @@NODES@@,
19
+ links: @@LINKS@@
20
+ };
21
+
22
+ const elem = document.getElementById('3d-graph')
23
+ const Graph = ForceGraph3D()
24
+ (elem)
25
+ .graphData(gData)
26
+ .nodeAutoColorBy('group')
27
+ .nodeLabel(node => node.label)
28
+ .onNodeHover(node => elem.style.cursor = node ? 'pointer' : null)
29
+ .onNodeClick(node => {
30
+ // Aim at node from outside it
31
+ const distance = 40;
32
+ const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
33
+
34
+ Graph.cameraPosition(
35
+ { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }, // new position
36
+ node, // lookAt ({ x, y, z })
37
+ 3000 // ms transition duration
38
+ )
39
+ })
40
+ /* nice idea, but the 3D graph tends to look cluttered. Also GPU/ CPU
41
+ intensive when run on a serious code base (e.g. react that has
42
+ ~4500 nodes and ~10000 links
43
+ */
44
+ .nodeThreeObject(node => {
45
+ const sprite = new SpriteText(node.displayname);
46
+ sprite.material.depthWrite = true; // make sprite background transparent
47
+ sprite.color = node.color;
48
+ sprite.textHeight = 6;
49
+ return sprite;
50
+ })
51
+ .linkOpacity(0.2)
52
+ .linkWidth(2)
53
+ .linkDirectionalArrowLength(4)
54
+ .linkDirectionalParticles(7) // cool but a bit GPU intensive
55
+ .linkLabel(link => link.label)
56
+ .onLinkHover(link => elem.style.cursor = link ? 'pointer' : null)
57
+
58
+ </script>
59
+ </body>
60
+ </html>`;
61
+
62
+ function deriveGroup(pFileName) {
63
+ let lReturnValue = "unknown";
64
+ const lGroupPositionInRe = 2;
65
+ const lMatch = path.dirname(pFileName).match(/^([^/]+)\/([^/]+)/);
66
+ if (lMatch) {
67
+ // eslint-disable-next-line security/detect-object-injection
68
+ lReturnValue = lMatch[lGroupPositionInRe];
69
+ }
70
+ return lReturnValue;
71
+ }
72
+
73
+ function formatFileName(pFileName) {
74
+ return `${path.dirname(pFileName)}/<b>${path.basename(pFileName)}</b>`;
75
+ }
76
+ function formatDependency(pFrom, pTo) {
77
+ return `${formatFileName(pFrom)} ${figures.arrowRight}</br>${formatFileName(
78
+ pTo
79
+ )}`;
80
+ }
81
+
82
+ /**
83
+ *
84
+ * @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult
85
+ */
86
+ function render3DThing(pCruiseResult) {
87
+ const lNodes = pCruiseResult.modules.map((pModule) => {
88
+ return {
89
+ id: pModule.source,
90
+ displayname: path.basename(pModule.source),
91
+ dirname: path.dirname(pModule.source),
92
+ label: formatFileName(pModule.source),
93
+ group: deriveGroup(pModule.source),
94
+ };
95
+ });
96
+ const lLinks = pCruiseResult.modules.reduce(
97
+ (pAll, pCurrentModule) =>
98
+ pAll.concat(
99
+ pCurrentModule.dependencies.map((pDependency) => ({
100
+ source: pCurrentModule.source,
101
+ target: pDependency.resolved,
102
+ label: formatDependency(pCurrentModule.source, pDependency.resolved),
103
+ }))
104
+ ),
105
+ []
106
+ );
107
+
108
+ return TEMPLATE.replace(/@@NODES@@/g, JSON.stringify(lNodes)).replace(
109
+ /@@LINKS@@/g,
110
+ JSON.stringify(lLinks)
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Sample plugin: 3d representation
116
+ *
117
+ * @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult -
118
+ * the output of a dependency-cruise adhering to dependency-cruiser's
119
+ * cruise result schema
120
+ * @return {import('../../types/dependency-cruiser').IReporterOutput} -
121
+ * output: a string
122
+ * exitCode: 0
123
+ */
124
+ export default (pCruiseResult) => ({
125
+ output: render3DThing(pCruiseResult),
126
+ exitCode: 0,
127
+ });
@@ -0,0 +1,82 @@
1
+ const MEDIAN = 0.5;
2
+ const P75 = 0.75;
3
+ const DEFAULT_JSON_INDENT = 2;
4
+
5
+ function doMagic(pCruiseResult) {
6
+ let lReturnValue = {};
7
+
8
+ if (pCruiseResult.modules.some((pModule) => pModule.dependents)) {
9
+ const lDependentCounts = pCruiseResult.modules
10
+ .map((pModule) => pModule.dependents.length)
11
+ .sort();
12
+
13
+ lReturnValue = {
14
+ minDependentsPerModule: lDependentCounts[0] || 0,
15
+ maxDependentsPerModule:
16
+ lDependentCounts[Math.max(lDependentCounts.length - 1, 0)] || 0,
17
+ meanDependentsPerModule:
18
+ lDependentCounts.reduce((pAll, pCurrent) => pAll + pCurrent, 0) /
19
+ pCruiseResult.summary.totalCruised,
20
+ medianDependentsPerModule:
21
+ lDependentCounts[
22
+ Math.max(0, Math.floor(lDependentCounts.length * MEDIAN))
23
+ ],
24
+ p75DependentsPerModule:
25
+ lDependentCounts[
26
+ Math.max(0, Math.floor(lDependentCounts.length * P75))
27
+ ],
28
+ };
29
+ }
30
+ return lReturnValue;
31
+ }
32
+ /**
33
+ * returns an object with some stats from the ICruiseResult pCruiseResult it
34
+ * got passed
35
+ *
36
+ * @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult - a result from a cruise.
37
+ * @return {string} an object with some stats
38
+ */
39
+ function samplePluginReporter(pCruiseResult) {
40
+ const lDependencyCounts = pCruiseResult.modules
41
+ .map((pModule) => pModule.dependencies.length)
42
+ .sort();
43
+
44
+ return {
45
+ moduleCount: pCruiseResult.summary.totalCruised,
46
+ dependencyCount: pCruiseResult.summary.totalDependenciesCruised,
47
+ minDependenciesPerModule: lDependencyCounts[0] || 0,
48
+ maxDependenciesPerModule:
49
+ lDependencyCounts[Math.max(lDependencyCounts.length - 1, 0)] || 0,
50
+ meanDependenciesPerModule:
51
+ pCruiseResult.summary.totalDependenciesCruised /
52
+ pCruiseResult.summary.totalCruised,
53
+ medianDependenciesPerModule:
54
+ lDependencyCounts[
55
+ Math.max(0, Math.floor(lDependencyCounts.length * MEDIAN))
56
+ ],
57
+ p75DependenciesPerModule:
58
+ lDependencyCounts[
59
+ Math.max(0, Math.floor(lDependencyCounts.length * P75))
60
+ ],
61
+ ...doMagic(pCruiseResult),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Sample plugin
67
+ *
68
+ * @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult -
69
+ * the output of a dependency-cruise adhering to dependency-cruiser's
70
+ * cruise result schema
71
+ * @return {import('../../types/dependency-cruiser').IReporterOutput} -
72
+ * output: some stats on modules and dependencies in json format
73
+ * exitCode: 0
74
+ */
75
+ export default (pCruiseResult) => ({
76
+ output: JSON.stringify(
77
+ samplePluginReporter(pCruiseResult),
78
+ null,
79
+ DEFAULT_JSON_INDENT
80
+ ),
81
+ exitCode: 0,
82
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dependency-cruiser",
3
- "version": "15.4.0",
3
+ "version": "15.5.0",
4
4
  "description": "Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.",
5
5
  "keywords": [
6
6
  "static analysis",
@@ -51,39 +51,49 @@
51
51
  },
52
52
  "exports": {
53
53
  ".": {
54
- "import": "./src/main/index.mjs",
55
- "types": "./types/dependency-cruiser.d.ts"
54
+ "types": "./types/dependency-cruiser.d.mts",
55
+ "import": "./src/main/index.mjs"
56
56
  },
57
57
  "./config-utl/extract-babel-config": {
58
- "import": "./src/config-utl/extract-babel-config.mjs",
59
- "types": "./types/extract-babel-config.d.ts"
58
+ "types": "./types/config-utl/extract-babel-config.d.mts",
59
+ "import": "./src/config-utl/extract-babel-config.mjs"
60
60
  },
61
61
  "./config-utl/extract-depcruise-config": {
62
- "import": "./src/config-utl/extract-depcruise-config/index.mjs",
63
- "types": "./types/extract-depcruise-config.d.ts"
62
+ "types": "./types/config-utl/extract-depcruise-config.d.mts",
63
+ "import": "./src/config-utl/extract-depcruise-config/index.mjs"
64
64
  },
65
65
  "./config-utl/extract-ts-config": {
66
- "import": "./src/config-utl/extract-ts-config.mjs",
67
- "types": "./types/extract-ts-config.d.ts"
66
+ "types": "./types/config-utl/extract-ts-config.d.mts",
67
+ "import": "./src/config-utl/extract-ts-config.mjs"
68
68
  },
69
69
  "./config-utl/extract-webpack-resolve-config": {
70
- "import": "./src/config-utl/extract-webpack-resolve-config.mjs",
71
- "types": "./types/extract-webpack-resolve-config.d.ts"
70
+ "types": "./types/config-utl/extract-webpack-resolve-config.d.mts",
71
+ "import": "./src/config-utl/extract-webpack-resolve-config.mjs"
72
72
  },
73
- "./sample-reporter-plugin": "./configs/plugins/stats-reporter-plugin.mjs",
74
- "./sample-3d-reporter-plugin": "./configs/plugins/3d-reporter-plugin.mjs",
75
- "./mermaid-reporter-plugin": "./src/report/mermaid.mjs"
73
+ "./sample-reporter-plugin": {
74
+ "types": "./types/plugins/stats-reporter-plugin.d.mts",
75
+ "import": "./configs/plugins/stats-reporter-plugin.mjs"
76
+ },
77
+ "./sample-3d-reporter-plugin": {
78
+ "types": "./types/plugins/3d-reporter-plugin.d.mts",
79
+ "import": "./configs/plugins/3d-reporter-plugin.mjs"
80
+ },
81
+ "./mermaid-reporter-plugin": {
82
+ "types": "./types/plugins/mermaid-reporter-plugin.d.mts",
83
+ "import": "./src/report/mermaid.mjs"
84
+ }
76
85
  },
77
- "types": "types/dependency-cruiser.d.ts",
86
+ "types": "types/dependency-cruiser.d.mts",
78
87
  "files": [
79
88
  "bin",
80
89
  "configs/**/*.js",
90
+ "configs/plugins/",
81
91
  "src",
82
92
  "!src/**/*.json",
83
93
  "!src/**/*.hbs",
84
94
  "!src/**/*.md",
85
95
  "!**/*.DS_Store",
86
- "types/*.d.ts",
96
+ "types/**/*.d.mts",
87
97
  "LICENSE",
88
98
  "package.json",
89
99
  "README.md"
@@ -103,7 +113,7 @@
103
113
  "depcruise:graph:doc:fmt-archi": "./bin/depcruise-fmt.mjs -T archi -f - node_modules/.cache/tmp_graph_deps.json | dot -T svg -Gordering=in -Grankdir=TD | tee doc/real-world-samples/dependency-cruiser-archi-graph.svg | node bin/wrap-stream-in-html.mjs > docs/dependency-cruiser-archi-graph.html",
104
114
  "depcruise:graph:doc:fmt-dir": "./bin/depcruise-fmt.mjs -T ddot -f - node_modules/.cache/tmp_graph_deps.json | dot -T svg -Grankdir=TD | tee doc/real-world-samples/dependency-cruiser-dir-graph.svg | node bin/wrap-stream-in-html.mjs > docs/dependency-cruiser-dir-graph.html",
105
115
  "depcruise:graph:doc:fmt-schema": "cd tools/schema && node ../../bin/dependency-cruise.mjs . --output-type dot | dot -T svg | tee ../overview.svg | node ../../bin/wrap-stream-in-html.mjs > ../../docs/schema-overview.html && cd -",
106
- "depcruise:graph:doc:fmt-types": "cd types && node ../bin/dependency-cruise.mjs . --output-type dot | dot -T svg > overview.svg && cd -",
116
+ "depcruise:graph:doc:fmt-types": "cd types && node ../bin/dependency-cruise.mjs . --output-type dot | dot -T svg | tee overview.svg | ../bin/wrap-stream-in-html.mjs > overview.html && cd -",
107
117
  "depcruise:graph:doc:samples": "sh tools/generate-samples.sh",
108
118
  "depcruise:graph:mermaid": "node ./bin/dependency-cruise.mjs bin src --include-only ^src/ --collapse 2 --output-type mermaid",
109
119
  "depcruise:graph:mermaid:diff": "node ./bin/dependency-cruise.mjs bin src test types tools --config configs/.dependency-cruiser-unlimited.mjs --output-type mermaid --reaches \"$(watskeburt $SHA)\"",
@@ -113,8 +123,8 @@
113
123
  "depcruise:report:view": "node ./bin/dependency-cruise.mjs src bin test configs types --output-type err-html --config configs/.dependency-cruiser-show-metrics-config.mjs --output-to - | browser",
114
124
  "depcruise:focus": "node ./bin/dependency-cruise.mjs src bin test configs types tools --progress --no-cache --output-type text --focus",
115
125
  "depcruise:reaches": "node ./bin/dependency-cruise.mjs src bin test configs types tools --progress --no-cache --config configs/.dependency-cruiser-unlimited.mjs --output-type text --reaches",
116
- "format": "prettier --log-level warn --write \"src/**/*.js\" \"configs/**/*.js\" \"tools/**/*.mjs\" \"bin/*\" \"types/*.d.ts\" \"test/**/*.spec.{cjs,js}\" \"test/**/*.{spec,utl}.mjs\"",
117
- "format:check": "prettier --log-level warn --check \"src/**/*.js\" \"configs/**/*.js\" \"tools/**/*.mjs\" \"bin/*\" \"types/*.d.ts\" \"test/**/*.spec.{cjs,js}\" \"test/**/*.{spec,utl}.mjs\"",
126
+ "format": "prettier --log-level warn --write \"src/**/*.js\" \"configs/**/*.js\" \"tools/**/*.mjs\" \"bin/*\" \"types/*.d.mts\" \"test/**/*.spec.{cjs,js}\" \"test/**/*.{spec,utl}.mjs\"",
127
+ "format:check": "prettier --log-level warn --check \"src/**/*.js\" \"configs/**/*.js\" \"tools/**/*.mjs\" \"bin/*\" \"types/*.d.mts\" \"test/**/*.spec.{cjs,js}\" \"test/**/*.{spec,utl}.mjs\"",
118
128
  "lint": "npm-run-all --parallel --aggregate-output lint:eslint format:check lint:types",
119
129
  "lint:eslint": "eslint bin/dependency-cruise.mjs bin src test configs tools/**/*.mjs --cache --cache-location node_modules/.cache/eslint/",
120
130
  "lint:eslint:fix": "eslint --fix bin src test configs tools/**/*.mjs --cache --cache-location node_modules/.cache/eslint/",
@@ -122,8 +132,8 @@
122
132
  "lint:fix": "npm-run-all lint:eslint:fix format lint:types:fix",
123
133
  "lint:types": "npm-run-all lint:types:tsc lint:types:lint",
124
134
  "lint:types:tsc": "tsc --project types/tsconfig.json",
125
- "lint:types:lint": "eslint --no-ignore --config types/.eslintrc.json types/*.d.ts",
126
- "lint:types:fix": "eslint --no-ignore --config types/.eslintrc.json --fix types/*.d.ts",
135
+ "lint:types:lint": "eslint --no-ignore --config types/.eslintrc.json types/*.d.mts",
136
+ "lint:types:fix": "eslint --no-ignore --config types/.eslintrc.json --fix types/*.d.mts",
127
137
  "prepare": "husky install",
128
138
  "scm:push": "run-p --aggregate-output scm:push:*",
129
139
  "scm:push:bitbucket-mirror": "run-p --aggregate-output scm:push:bitbucket-mirror:*",
@@ -186,11 +196,11 @@
186
196
  "@babel/preset-typescript": "7.23.3",
187
197
  "@swc/core": "1.3.99",
188
198
  "@types/lodash": "4.14.202",
189
- "@types/node": "20.9.3",
199
+ "@types/node": "20.10.0",
190
200
  "@types/prompts": "2.4.9",
191
201
  "@typescript-eslint/eslint-plugin": "6.12.0",
192
202
  "@typescript-eslint/parser": "6.12.0",
193
- "@vue/compiler-sfc": "3.3.8",
203
+ "@vue/compiler-sfc": "3.3.9",
194
204
  "c8": "8.0.1",
195
205
  "coffeescript": "2.7.0",
196
206
  "eslint": "8.54.0",
@@ -15,7 +15,7 @@ import { bus } from "#utl/bus.mjs";
15
15
  export default class MetaDataStrategy {
16
16
  /**
17
17
  * @param {string} _pDirectory
18
- * @param {import("../../types/cruise-result.js").ICruiseResult} _pCachedCruiseResult
18
+ * @param {import("../../types/cruise-result.mjs").ICruiseResult} _pCachedCruiseResult
19
19
  * @param {Object} pOptions
20
20
  * @param {Set<string>} pOptions.extensions
21
21
  * @param {Set<import("watskeburt").changeTypeType>=} pOptions.interestingChangeTypes
@@ -154,7 +154,15 @@ module.exports = {
154
154
  },
155
155
  to: {
156
156
  dependencyTypes: [
157
- 'npm-dev'
157
+ 'npm-dev',
158
+ ],
159
+ // type only dependencies are not a problem as they don't end up in the
160
+ // production code or are ignored by the runtime.
161
+ dependencyTypesNot: [
162
+ 'type-only'
163
+ ],
164
+ pathNot: [
165
+ 'node_modules/@types/'
158
166
  ]
159
167
  }
160
168
  },
@@ -10,7 +10,7 @@ var title2ElementMap = (function makeElementMap() {
10
10
 
11
11
  function getHoverHandler(pTitle2ElementMap) {
12
12
  /** @type {string} */
13
- var currentHighlightedTitle;
13
+ var currentHighlightedTitle = "";
14
14
 
15
15
  /** @param {MouseEvent} pMouseEvent */
16
16
  return function hoverHighlightHandler(pMouseEvent) {
@@ -31,7 +31,7 @@ function getHoverHandler(pTitle2ElementMap) {
31
31
 
32
32
  function getSelectHandler(pTitle2ElementMap) {
33
33
  /** @type {string} */
34
- var currentHighlightedTitle;
34
+ var currentHighlightedTitle = "";
35
35
 
36
36
  /** @param {MouseEvent} pMouseEvent */
37
37
  return function selectHighlightHandler(pMouseEvent) {
@@ -64,6 +64,9 @@ function Mode() {
64
64
  this._mode = SELECT;
65
65
  }
66
66
 
67
+ /**
68
+ * @returns {number}
69
+ */
67
70
  function get() {
68
71
  return this._mode || HOVER;
69
72
  }
@@ -210,23 +213,23 @@ function addHighlight(pGroup) {
210
213
  }
211
214
  }
212
215
 
213
- var hints = {
216
+ var gHints = {
214
217
  HIDDEN: 1,
215
218
  SHOWN: 2,
216
219
  state: this.HIDDEN,
217
220
  show: function () {
218
221
  document.getElementById("hints").removeAttribute("style");
219
- hints.state = hints.SHOWN;
222
+ gHints.state = gHints.SHOWN;
220
223
  },
221
224
  hide: function () {
222
225
  document.getElementById("hints").style = "display:none";
223
- hints.state = hints.HIDDEN;
226
+ gHints.state = gHints.HIDDEN;
224
227
  },
225
228
  toggle: function () {
226
- if ((hints.state || hints.HIDDEN) === hints.HIDDEN) {
227
- hints.show();
229
+ if ((gHints.state || gHints.HIDDEN) === gHints.HIDDEN) {
230
+ gHints.show();
228
231
  } else {
229
- hints.hide();
232
+ gHints.hide();
230
233
  }
231
234
  },
232
235
  };
@@ -236,16 +239,48 @@ function keyboardEventHandler(pKeyboardEvent) {
236
239
  if (pKeyboardEvent.key === "Escape") {
237
240
  resetNodesAndEdges();
238
241
  gMode.setToHover();
239
- hints.hide();
242
+ gHints.hide();
240
243
  }
241
244
  if (pKeyboardEvent.key === "F1") {
242
245
  pKeyboardEvent.preventDefault();
243
- hints.toggle();
246
+ gHints.toggle();
244
247
  }
245
248
  }
246
249
 
247
250
  document.addEventListener("contextmenu", getSelectHandler(title2ElementMap));
248
251
  document.addEventListener("mouseover", getHoverHandler(title2ElementMap));
249
252
  document.addEventListener("keydown", keyboardEventHandler);
250
- document.getElementById("close-hints").addEventListener("click", hints.hide);
251
- document.getElementById("button_help").addEventListener("click", hints.toggle);
253
+ document.getElementById("close-hints").addEventListener("click", gHints.hide);
254
+ document.getElementById("button_help").addEventListener("click", gHints.toggle);
255
+ document.querySelector("svg").insertAdjacentHTML(
256
+ "afterbegin",
257
+ `<linearGradient id="edgeGradient">
258
+ <stop offset="0%" stop-color="fuchsia"/>
259
+ <stop offset="100%" stop-color="purple"/>
260
+ </linearGradient>
261
+ `,
262
+ );
263
+
264
+ // Add a small increment to the last value of the path to make gradients on
265
+ // horizontal paths work. Without them all browsers I tested with (firefox,
266
+ // chrome) do not render the gradient, but instead make the line transparent
267
+ // (or the color of the background, I haven't looked into it that deeply,
268
+ // but for the hack it doesn't matter which).
269
+ function skewLineABit(lDrawingInstructions) {
270
+ var lLastValue = lDrawingInstructions.match(/(\d+\.?\d*)$/)[0];
271
+ // Smaller values than .001 _should_ work as well, but don't in all
272
+ // cases. Even this value is so small that it is not visible to the
273
+ // human eye (tested with the two I have at my disposal).
274
+ var lIncrement = 0.001;
275
+ var lNewLastValue = parseFloat(lLastValue) + lIncrement;
276
+
277
+ return lDrawingInstructions.replace(lLastValue, lNewLastValue);
278
+ }
279
+
280
+ nodeListToArray(document.querySelectorAll("path"))
281
+ .filter(function (pElement) {
282
+ return pElement.parentElement.classList.contains("edge");
283
+ })
284
+ .forEach(function (pElement) {
285
+ pElement.attributes.d.value = skewLineABit(pElement.attributes.d.value);
286
+ });
@@ -0,0 +1,101 @@
1
+ .node:active path,
2
+ .node:hover path,
3
+ .node.current path,
4
+ .node:active polygon,
5
+ .node:hover polygon,
6
+ .node.current polygon {
7
+ stroke: fuchsia;
8
+ stroke-width: 2;
9
+ }
10
+
11
+ .edge:active path,
12
+ .edge:hover path,
13
+ .edge.current path,
14
+ .edge:active ellipse,
15
+ .edge:hover ellipse,
16
+ .edge.current ellipse {
17
+ stroke: url(#edgeGradient);
18
+ stroke-width: 3;
19
+ stroke-opacity: 1;
20
+ }
21
+
22
+ .edge:active polygon,
23
+ .edge:hover polygon,
24
+ .edge.current polygon {
25
+ stroke: fuchsia;
26
+ stroke-width: 3;
27
+ fill: fuchsia;
28
+ stroke-opacity: 1;
29
+ fill-opacity: 1;
30
+ }
31
+
32
+ .edge:active text,
33
+ .edge:hover text {
34
+ fill: fuchsia;
35
+ }
36
+
37
+ .cluster path {
38
+ stroke-width: 3;
39
+ }
40
+
41
+ .cluster:active path,
42
+ .cluster:hover path {
43
+ fill: #ffff0011;
44
+ }
45
+
46
+ div.hint {
47
+ background-color: #000000aa;
48
+ color: white;
49
+ font-family: Arial, Helvetica, sans-serif;
50
+ border-radius: 1rem;
51
+ position: fixed;
52
+ top: calc(50% - 4em);
53
+ right: calc(50% - 10em);
54
+ border: none;
55
+ padding: 1em 3em 1em 1em;
56
+ }
57
+
58
+ .hint button {
59
+ position: absolute;
60
+ font-weight: bolder;
61
+ right: 0.6em;
62
+ top: 0.6em;
63
+ color: inherit;
64
+ background-color: inherit;
65
+ border: 1px solid currentColor;
66
+ border-radius: 1em;
67
+ margin-left: 0.6em;
68
+ }
69
+
70
+ .hint a {
71
+ color: inherit;
72
+ }
73
+
74
+ #button_help {
75
+ color: white;
76
+ background-color: #00000011;
77
+ border-radius: 1em;
78
+ position: fixed;
79
+ top: 1em;
80
+ right: 1em;
81
+ font-size: 24pt;
82
+ font-weight: bolder;
83
+ width: 2em;
84
+ height: 2em;
85
+ border: none;
86
+ }
87
+
88
+ #button_help:hover {
89
+ cursor: pointer;
90
+ background-color: #00000077;
91
+ }
92
+
93
+ @media print {
94
+ #button_help {
95
+ display: none;
96
+ }
97
+
98
+ div.hint {
99
+ display: none;
100
+ }
101
+ }
@@ -1,19 +1,56 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { fileURLToPath } from "node:url";
3
3
 
4
- const HEADER_FILE = fileURLToPath(
5
- new URL("svg-in-html-snippets/header.snippet.html", import.meta.url)
4
+ const STYLESHEET_FILE = fileURLToPath(
5
+ new URL("svg-in-html-snippets/style.css", import.meta.url),
6
6
  );
7
7
  const SCRIPT_FILE = fileURLToPath(
8
- new URL("svg-in-html-snippets/script.snippet.js", import.meta.url)
9
- );
10
- const FOOTER_FILE = fileURLToPath(
11
- new URL("svg-in-html-snippets/footer.snippet.html", import.meta.url)
8
+ new URL("svg-in-html-snippets/script.js", import.meta.url),
12
9
  );
13
10
 
11
+ /**
12
+ * @param {string} pStylesheet
13
+ * @returns {string}
14
+ */
15
+ function getHeader(pStylesheet) {
16
+ return `<!doctype html>
17
+ <html lang="en" dir="ltr">
18
+ <head>
19
+ <meta charset="utf-8" />
20
+ <title>dependency graph</title>
21
+ <style>
22
+ ${pStylesheet}
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <button id="button_help">?</button>
27
+ <div id="hints" class="hint" style="display: none">
28
+ <button id="close-hints">x</button>
29
+ <span id="hint-text"></span>
30
+ <ul>
31
+ <li><b>Hover</b> - highlight</li>
32
+ <li><b>Right-click</b> - pin highlight</li>
33
+ <li><b>ESC</b> - clear</li>
34
+ </ul>
35
+ </div>
36
+ `;
37
+ }
38
+
39
+ /**
40
+ * @param {string} pScript
41
+ * @returns {string}
42
+ */
43
+ function getFooter(pScript) {
44
+ return ` <script>
45
+ ${pScript}
46
+ </script>
47
+ </body>
48
+ </html>`;
49
+ }
50
+
14
51
  /**
15
52
  * Slaps the stuff in the passed stream in between the contents
16
- * of the header and the footer file and returns it as a string.
53
+ * of the header and the footer and returns it as a string.
17
54
  *
18
55
  * Almost exactly the same as:
19
56
  * ```sh
@@ -26,18 +63,15 @@ const FOOTER_FILE = fileURLToPath(
26
63
  * @param {writeStream} pOutStream stream to write to
27
64
  */
28
65
  export default async function wrap(pInStream, pOutStream) {
29
- const [lHeader, lScript, lEnd] = await Promise.all([
30
- readFile(HEADER_FILE, "utf8"),
66
+ const [lStylesheet, lScript] = await Promise.all([
67
+ readFile(STYLESHEET_FILE, "utf8"),
31
68
  readFile(SCRIPT_FILE, "utf8"),
32
- readFile(FOOTER_FILE, "utf8"),
33
69
  ]);
34
70
 
35
- const lFooter = `<script>${lScript}</script>${lEnd}`;
36
-
37
- pOutStream.write(lHeader);
71
+ pOutStream.write(getHeader(lStylesheet));
38
72
  pInStream
39
73
  .on("end", () => {
40
- pOutStream.write(lFooter);
74
+ pOutStream.write(getFooter(lScript));
41
75
  pOutStream.end();
42
76
  })
43
77
  .on(
@@ -45,9 +79,17 @@ export default async function wrap(pInStream, pOutStream) {
45
79
  /* c8 ignore start */
46
80
  (pError) => {
47
81
  process.stderr.write(`${pError}\n`);
48
- }
82
+ },
49
83
  /* c8 ignore stop */
50
84
  )
85
+ /*
86
+ * We could have put the whole html in a template literal, but we don't know
87
+ * how large the svg that's going to be injected is going to be - it could just
88
+ * as well be as large that if we're going to buffer it, it's going into out
89
+ * of memory territory.
90
+ *
91
+ * We circumvent that by streaming the svg in between the header and the footer.
92
+ */
51
93
  .on("data", (pChunk) => {
52
94
  pOutStream.write(pChunk);
53
95
  });