react-native-bundle-discovery 1.0.0-rc.9 → 1.0.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.
- package/.discoveryrc.js +11 -8
- package/README.md +47 -5
- package/lib/bin.js +29 -12
- package/lib/customSerializer.js +8 -5
- package/package.json +11 -10
- package/pages/module.js +22 -4
- package/prepare.js +102 -0
- package/queryHelpers.js +347 -0
- package/setup.js +12 -0
- package/views/foamtree.js +1 -1
- package/views/global.css +6 -0
- package/views/prettify.js +17 -0
package/.discoveryrc.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
name: "react-native-bundle-discovery",
|
|
3
5
|
data: () => require("./tmp/before_metro-stats.json"),
|
|
4
|
-
setup: "
|
|
6
|
+
setup: path.resolve(__dirname, "setup.js"),
|
|
5
7
|
view: {
|
|
6
8
|
assets: [
|
|
7
9
|
// Global styles
|
|
8
|
-
"views/global.css",
|
|
10
|
+
path.resolve(__dirname, "views/global.css"),
|
|
9
11
|
// Pages
|
|
10
|
-
"pages/default.js",
|
|
11
|
-
"pages/module.js",
|
|
12
|
-
"pages/package.js",
|
|
12
|
+
path.resolve(__dirname, "pages/default.js"),
|
|
13
|
+
path.resolve(__dirname, "pages/module.js"),
|
|
14
|
+
path.resolve(__dirname, "pages/package.js"),
|
|
13
15
|
// Custom views
|
|
14
|
-
"views/highcharts.css",
|
|
15
|
-
"views/
|
|
16
|
-
"views/
|
|
16
|
+
path.resolve(__dirname, "views/highcharts.css"),
|
|
17
|
+
path.resolve(__dirname, "views/prettify.js"),
|
|
18
|
+
path.resolve(__dirname, "views/highcharts.js"),
|
|
19
|
+
path.resolve(__dirname, "views/foamtree.js"),
|
|
17
20
|
],
|
|
18
21
|
},
|
|
19
22
|
};
|
package/README.md
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
> [!WARNING]
|
|
4
4
|
> Currently, everything is in a very early stage. The project is not yet ready for use.
|
|
5
5
|
|
|
6
|
+
A simple package that helps developers visualize and analyze the bundle size of React Native apps.
|
|
7
|
+
With this tool, you can easily explore your app's codebase, identify large or heavy packages, and inspect the structure of modules and code within your project.
|
|
6
8
|
|
|
7
|
-
<img width="800" src="
|
|
9
|
+
<img width="800" alt="" src="./assets/img.png" />
|
|
8
10
|
|
|
9
11
|
### Setup:
|
|
10
12
|
|
|
11
13
|
#### 1. Install
|
|
14
|
+
|
|
12
15
|
```bash
|
|
13
16
|
yarn add -D react-native-bundle-discovery
|
|
14
17
|
```
|
|
@@ -20,12 +23,12 @@ yarn add -D react-native-bundle-discovery
|
|
|
20
23
|
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
|
|
21
24
|
+const {createSerializer} = require('react-native-bundle-discovery');
|
|
22
25
|
|
|
23
|
-
+const mySerializer = createSerializer({
|
|
26
|
+
+const mySerializer = createSerializer({
|
|
24
27
|
+ includeCode: true, // Useful if you want to compare source/bundle code (but a report file will be larger)
|
|
25
|
-
+ projectRoot: __dirname,
|
|
28
|
+
+ projectRoot: __dirname,
|
|
26
29
|
+ //^^^ ⚠️ WARNING: In a monorepo setup, this should point to the monorepo root,
|
|
27
30
|
+ // not the individual package directory.
|
|
28
|
-
+})
|
|
31
|
+
+});
|
|
29
32
|
|
|
30
33
|
-const config = {};
|
|
31
34
|
+const config = {
|
|
@@ -37,7 +40,46 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
|
|
|
37
40
|
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
|
38
41
|
```
|
|
39
42
|
|
|
43
|
+
#### 3. Build the app
|
|
44
|
+
|
|
45
|
+
As example, for iOS you can run the following command, and it will generate the `metro-stats.json` file in the root of your project:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx react-native bundle \
|
|
49
|
+
--entry-file index.js \
|
|
50
|
+
--platform ios \
|
|
51
|
+
--dev false \
|
|
52
|
+
--bundle-output ios/main.jsbundle \
|
|
53
|
+
--assets-dest ios/assets
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### 4. View the report
|
|
57
|
+
|
|
58
|
+
Run webserver to view the report:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx react-native-bundle-discovery metro-stats.json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### `createSerializer(optiosn: Options)`
|
|
67
|
+
|
|
68
|
+
| Prop | Default value | Description |
|
|
69
|
+
| ---------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| serializer: Function | Default serializer | A custom serializer function. If not provided, a default serializer is used. |
|
|
71
|
+
| projectRoot: string | Required | The root directory of the project. ⚠️ In a monorepo setup, this should point to the monorepo root, not the individual package directory. |
|
|
72
|
+
| outputJsonPath: string | `<root>/metro-stats.json` | The path where the JSON report will be saved. Defaults to `metro-stats.json` in project root. |
|
|
73
|
+
| includeCode: boolean | `true` | Whether to include the source and output code in the JSON report. |
|
|
74
|
+
|
|
75
|
+
### Financial Contributors
|
|
76
|
+
|
|
77
|
+
Become a financial contributor at [OpenCollective](https://opencollective.com/react-native-bundle-discovery) or [GitHub Sponsors](https://github.com/sponsors/retyui)
|
|
78
|
+
|
|
79
|
+
### Other
|
|
80
|
+
|
|
40
81
|
**Similar projects:**
|
|
82
|
+
|
|
41
83
|
- https://github.com/expo/atlas
|
|
42
84
|
- https://github.com/v3ron/expo-atlas-without-expo
|
|
43
85
|
- https://github.com/callstack/react-native-bundle-visualizer
|
|
@@ -45,7 +87,7 @@ module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
|
|
45
87
|
- https://github.com/statoscope/statoscope
|
|
46
88
|
- https://github.com/relative-ci/bundle-stats/tree/master/packages/cli
|
|
47
89
|
|
|
90
|
+
**Built using [Discovery.js](https://github.com/discoveryjs/discovery):**
|
|
48
91
|
|
|
49
|
-
**Links:**
|
|
50
92
|
- Build blocks for pages: https://discoveryjs.github.io/discovery/#views-showcase
|
|
51
93
|
- Jora syntax: https://discoveryjs.github.io/jora/#article:jora-syntax-operators
|
package/lib/bin.js
CHANGED
|
@@ -1,35 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const fs = require("fs");
|
|
2
3
|
const path = require("path");
|
|
3
|
-
const
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const { createServer } = require("@discoveryjs/cli");
|
|
4
6
|
const config = require("../.discoveryrc.js");
|
|
5
7
|
|
|
6
8
|
const filePath = process.argv[2];
|
|
7
9
|
|
|
8
10
|
if (!filePath) {
|
|
9
11
|
console.error(
|
|
10
|
-
|
|
12
|
+
`Usage: '${chalk.green("npx react-native-bundle-discovery <path-to-file>")}', Please provide a path to a JSON file.`,
|
|
11
13
|
);
|
|
12
14
|
process.exit(1);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const jsonFilePath = path.resolve(process.cwd(), filePath);
|
|
16
18
|
|
|
17
|
-
let
|
|
19
|
+
let fullJsonPath;
|
|
18
20
|
|
|
19
21
|
try {
|
|
20
|
-
|
|
22
|
+
fullJsonPath = require.resolve(jsonFilePath);
|
|
21
23
|
} catch (err) {
|
|
22
|
-
console.error(`Error loading file: ${jsonFilePath}`);
|
|
24
|
+
console.error(`Error loading file: ${chalk.red(jsonFilePath)}`);
|
|
23
25
|
console.error(err.message);
|
|
24
26
|
process.exit(1);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const PORT = process.env.PORT || 8079;
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const configFile = path.resolve(__dirname, "./.tmp.js");
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
configFile,
|
|
35
|
+
`module.exports = ${JSON.stringify(
|
|
36
|
+
{ ...config, data: "<tmp>" },
|
|
37
|
+
null,
|
|
38
|
+
1,
|
|
39
|
+
).replace(`"<tmp>"`, `() => require("${fullJsonPath}")`)};`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
createServer({
|
|
43
|
+
cache: false,
|
|
44
|
+
minify: true,
|
|
45
|
+
dev: false,
|
|
46
|
+
config: configFile,
|
|
47
|
+
configFile,
|
|
48
|
+
}).then((server) =>
|
|
49
|
+
server.listen(PORT, () =>
|
|
50
|
+
console.log(`Server listen on ${chalk.green(`http://localhost:${PORT}`)}`),
|
|
51
|
+
),
|
|
52
|
+
);
|
package/lib/customSerializer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const { writeFileSync, existsSync } = require("
|
|
2
|
-
const { resolve } = require("
|
|
3
|
-
const { Buffer } = require("
|
|
1
|
+
const { writeFileSync, existsSync } = require("fs");
|
|
2
|
+
const { resolve } = require("path");
|
|
3
|
+
const { Buffer } = require("buffer");
|
|
4
|
+
const chalk = require("chalk");
|
|
4
5
|
|
|
5
6
|
const NAME = require("../package.json").name;
|
|
6
7
|
|
|
@@ -105,7 +106,7 @@ function createJsonReport({
|
|
|
105
106
|
return acc;
|
|
106
107
|
}, {}),
|
|
107
108
|
rootFolder,
|
|
108
|
-
packages: toPackages(dependencies),
|
|
109
|
+
packages: toPackages(preModules).concat(toPackages(dependencies)),
|
|
109
110
|
modules: preModules
|
|
110
111
|
.map((m) => toModuleStruct(m, includeCode))
|
|
111
112
|
.concat(dependencies.map((m) => toModuleStruct(m, includeCode))),
|
|
@@ -113,7 +114,9 @@ function createJsonReport({
|
|
|
113
114
|
|
|
114
115
|
writeFileSync(outputJsonPath, JSON.stringify(stats));
|
|
115
116
|
|
|
116
|
-
console.log(
|
|
117
|
+
console.log(
|
|
118
|
+
`${chalk.yellow(`[${NAME}]`)}: Saved stats to ${chalk.green(outputJsonPath)}`,
|
|
119
|
+
);
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-bundle-discovery",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"bin": "lib/bin.js",
|
|
6
6
|
"repository": "git@github.com:retyui/react-native-bundle-discovery.git",
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
"build": "npx discovery-build --config .discoveryrc.js --output build --serve-only-assets --single-file"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@
|
|
15
|
+
"@babel/parser": "^7.0.0",
|
|
16
|
+
"@discoveryjs/cli": "2.14.2",
|
|
17
|
+
"@discoveryjs/discovery": "1.0.0-beta.93",
|
|
18
|
+
"chalk": "^4.1.2",
|
|
19
|
+
"highcharts": "12.2.0",
|
|
20
|
+
"prettier": "^3.5.3"
|
|
16
21
|
},
|
|
17
22
|
"peerDependencies": {
|
|
18
23
|
"metro": "*"
|
|
@@ -28,13 +33,9 @@
|
|
|
28
33
|
"views",
|
|
29
34
|
"pages",
|
|
30
35
|
"index.js",
|
|
36
|
+
"setup.js",
|
|
37
|
+
"prepare.js",
|
|
38
|
+
"queryHelpers.js",
|
|
31
39
|
".discoveryrc.js"
|
|
32
|
-
]
|
|
33
|
-
"devDependencies": {
|
|
34
|
-
"@discoveryjs/discovery": "1.0.0-beta.93",
|
|
35
|
-
"highcharts": "12.2.0",
|
|
36
|
-
"lodash": "4.17.21",
|
|
37
|
-
"prettier": "3.5.3"
|
|
38
|
-
},
|
|
39
|
-
"packageManager": "yarn@4.6.0"
|
|
40
|
+
]
|
|
40
41
|
}
|
package/pages/module.js
CHANGED
|
@@ -119,11 +119,29 @@ discovery.page.define("module", {
|
|
|
119
119
|
view: "block",
|
|
120
120
|
className: "width-50p",
|
|
121
121
|
content: [
|
|
122
|
-
"h5: 'Output'",
|
|
123
122
|
{
|
|
124
|
-
view: "
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
view: "h5",
|
|
124
|
+
content: 'text:"Output"',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
view: "context",
|
|
128
|
+
modifiers: [
|
|
129
|
+
{
|
|
130
|
+
view: "checkbox",
|
|
131
|
+
name: "prettify",
|
|
132
|
+
checked: true,
|
|
133
|
+
className: "prettify-checkbox",
|
|
134
|
+
content: 'text:"Prettify"',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
view: "source-prettify",
|
|
140
|
+
syntax: "ts",
|
|
141
|
+
source:
|
|
142
|
+
"=(#.prettify ? $.currentModule.output.code.prettifyJS() : $.currentModule.output.code)",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
127
145
|
},
|
|
128
146
|
],
|
|
129
147
|
},
|
package/prepare.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
function getDuplicateId(path) {
|
|
2
|
+
const uniqueNodeModulePath = path.split("node_modules/").pop();
|
|
3
|
+
const ids = [uniqueNodeModulePath];
|
|
4
|
+
|
|
5
|
+
// Special case for lodash
|
|
6
|
+
if (
|
|
7
|
+
uniqueNodeModulePath.startsWith("lodash.") ||
|
|
8
|
+
uniqueNodeModulePath.startsWith("lodash-es/")
|
|
9
|
+
) {
|
|
10
|
+
// node_modules/lodash.debounce/index.js => node_modules/lodash/debounce.js
|
|
11
|
+
// node_modules/lodash-es/get.js => node_modules/lodash/get.js
|
|
12
|
+
const uniqueLodashPath = uniqueNodeModulePath
|
|
13
|
+
.replace("lodash-es/", "lodash/")
|
|
14
|
+
.replace("lodash.", "lodash/")
|
|
15
|
+
.replace("/index.js", ".js");
|
|
16
|
+
|
|
17
|
+
ids.push(uniqueLodashPath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return ids;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function prepare(data) {
|
|
24
|
+
// data.modules = data.modules.slice(875, 880); TODO for debugging a formtree chart
|
|
25
|
+
let moduleMap = new Map();
|
|
26
|
+
let duplicatesMap = new Map();
|
|
27
|
+
let allLodashModules = new Set();
|
|
28
|
+
|
|
29
|
+
data.packages.forEach((pkg) => {
|
|
30
|
+
pkg.path = pkg.absolutePath.replace(data.rootFolder + "/", "");
|
|
31
|
+
});
|
|
32
|
+
data.modules.forEach((m) => {
|
|
33
|
+
// 0. Data transformation
|
|
34
|
+
m.absolutePath = m.path;
|
|
35
|
+
if (m.absolutePath === data.entryPoint) {
|
|
36
|
+
m.isEntry = true;
|
|
37
|
+
}
|
|
38
|
+
m.path = m.path.replace(data.rootFolder + "/", "");
|
|
39
|
+
m.dependencies.forEach((d) => {
|
|
40
|
+
d.path = d.absolutePath.replace(data.rootFolder + "/", "");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 1. Duplicates
|
|
44
|
+
m._tmp_ids = getDuplicateId(m.path);
|
|
45
|
+
m.duplicates = [];
|
|
46
|
+
m._tmp_ids.forEach((id) => {
|
|
47
|
+
if (!duplicatesMap.has(id)) {
|
|
48
|
+
duplicatesMap.set(id, []);
|
|
49
|
+
}
|
|
50
|
+
duplicatesMap.get(id).push(m);
|
|
51
|
+
});
|
|
52
|
+
// lodash/*
|
|
53
|
+
// lodash-es/*
|
|
54
|
+
// lodash.*
|
|
55
|
+
if (m.path.includes("node_modules/lodash")) {
|
|
56
|
+
allLodashModules.add(m);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Dependencies
|
|
60
|
+
m.dependents = [];
|
|
61
|
+
moduleMap.set(m.absolutePath, m);
|
|
62
|
+
|
|
63
|
+
// 3. Other
|
|
64
|
+
// 3.1 Format prelude
|
|
65
|
+
if (m.path === "__prelude__") {
|
|
66
|
+
m.source.code = m.source.code
|
|
67
|
+
.replaceAll(";", ";\n\n")
|
|
68
|
+
.replaceAll(",", ",\n ");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
data.modules.forEach((m) => {
|
|
73
|
+
// 1. Duplicates
|
|
74
|
+
m._tmp_ids.forEach((id) => {
|
|
75
|
+
const duplicates = duplicatesMap.get(id);
|
|
76
|
+
if (duplicates.length > 1) {
|
|
77
|
+
m.duplicates.push(...duplicates.filter((d) => d !== m));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// lodash/index.js is a special case (as index.js includes all lodash functions)
|
|
82
|
+
if (m.path.endsWith("node_modules/lodash/index.js")) {
|
|
83
|
+
m.duplicates.push(...allLodashModules);
|
|
84
|
+
}
|
|
85
|
+
delete m._tmp_ids;
|
|
86
|
+
|
|
87
|
+
// 2. Dependencies
|
|
88
|
+
m.dependencies.forEach((dependency) => {
|
|
89
|
+
const dependentModule = moduleMap.get(dependency.absolutePath);
|
|
90
|
+
if (dependentModule) {
|
|
91
|
+
dependentModule.dependents.push(m); //add to the dependent module directly
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
allLodashModules = null;
|
|
97
|
+
moduleMap = null;
|
|
98
|
+
duplicatesMap = null;
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = prepare;
|
package/queryHelpers.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
const helpers = {
|
|
2
|
+
prettifyMap: new Map(),
|
|
3
|
+
prettifyJS(sourceStr) {
|
|
4
|
+
if (helpers.prettifyMap.has(sourceStr)) {
|
|
5
|
+
return helpers.prettifyMap.get(sourceStr);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const prettier = require("prettier/standalone");
|
|
9
|
+
const prettierPluginBabel = require("prettier/plugins/babel");
|
|
10
|
+
const prettierPluginEstree = require("prettier/plugins/estree");
|
|
11
|
+
|
|
12
|
+
return prettier
|
|
13
|
+
.format(sourceStr, {
|
|
14
|
+
parser: "babel",
|
|
15
|
+
plugins: [prettierPluginBabel, prettierPluginEstree],
|
|
16
|
+
})
|
|
17
|
+
.then((formatted) => {
|
|
18
|
+
helpers.prettifyMap.set(sourceStr, formatted);
|
|
19
|
+
return formatted;
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
plural(count, [singular, plural]) {
|
|
23
|
+
return count === 1 ? singular : plural;
|
|
24
|
+
},
|
|
25
|
+
pluralWithCount(count, [singular, plural]) {
|
|
26
|
+
return `${count} ${helpers.plural(count, [singular, plural])}`;
|
|
27
|
+
},
|
|
28
|
+
pluralBadge(count, [singular, plural], prefix = "") {
|
|
29
|
+
return {
|
|
30
|
+
text: prefix + count,
|
|
31
|
+
postfix: helpers.plural(count, [singular, plural]),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
getHighchartsColors() {
|
|
35
|
+
const Highcharts = require("highcharts");
|
|
36
|
+
return Highcharts.getOptions().colors;
|
|
37
|
+
},
|
|
38
|
+
getModulesName(path) {
|
|
39
|
+
const modules = path.split("node_modules/");
|
|
40
|
+
const lastModule = modules[modules.length - 1];
|
|
41
|
+
const [folder, subFolder] = lastModule.split("/");
|
|
42
|
+
if (folder.startsWith("@")) {
|
|
43
|
+
return `${folder}/${subFolder}`;
|
|
44
|
+
}
|
|
45
|
+
return folder;
|
|
46
|
+
},
|
|
47
|
+
getBestNetworkGraphSize(module, params) {
|
|
48
|
+
let maxParentDepth = 0;
|
|
49
|
+
let itemsCount = 0;
|
|
50
|
+
do {
|
|
51
|
+
maxParentDepth++;
|
|
52
|
+
itemsCount = helpers.getNetworkGraph(module, {
|
|
53
|
+
...params,
|
|
54
|
+
maxParentDepth,
|
|
55
|
+
}).data.length;
|
|
56
|
+
if (itemsCount < 10) {
|
|
57
|
+
const limit = itemsCount - 1;
|
|
58
|
+
return limit > 2 ? limit : 2;
|
|
59
|
+
}
|
|
60
|
+
} while (maxParentDepth < 6);
|
|
61
|
+
return maxParentDepth;
|
|
62
|
+
},
|
|
63
|
+
getNetworkGraph(
|
|
64
|
+
module,
|
|
65
|
+
{ maxParentDepth = 2, omitVisitedModules = true } = {},
|
|
66
|
+
) {
|
|
67
|
+
const queue = [{ module, parentId: "", level: 0 }];
|
|
68
|
+
const visited = new Set();
|
|
69
|
+
const result = [];
|
|
70
|
+
let entryPointPath = null;
|
|
71
|
+
const data = [];
|
|
72
|
+
|
|
73
|
+
while (queue.length > 0) {
|
|
74
|
+
const { module: currentModule, parentId, level } = queue.shift();
|
|
75
|
+
|
|
76
|
+
if (level >= Number(maxParentDepth)) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const id = currentModule.path;
|
|
81
|
+
|
|
82
|
+
if (omitVisitedModules) {
|
|
83
|
+
if (visited.has(id)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
visited.add(id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (parentId) {
|
|
90
|
+
const isEntryPoint = currentModule.dependents.length === 0;
|
|
91
|
+
result.push({ isEntryPoint, id, parentId });
|
|
92
|
+
data.push([parentId, id]);
|
|
93
|
+
if (isEntryPoint) {
|
|
94
|
+
entryPointPath = id;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (Array.isArray(currentModule.dependents)) {
|
|
99
|
+
for (const dependentModule of currentModule.dependents) {
|
|
100
|
+
queue.push({
|
|
101
|
+
module: dependentModule,
|
|
102
|
+
parentId: id,
|
|
103
|
+
level: level + 1,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!entryPointPath) {
|
|
110
|
+
for (const item of result) {
|
|
111
|
+
if (item.isEntryPoint) {
|
|
112
|
+
entryPointPath = item.id;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { entryPointPath, data };
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
getExtColor(extName) {
|
|
122
|
+
const colors = {
|
|
123
|
+
js: "#f1e05a50",
|
|
124
|
+
ts: "#2b748950",
|
|
125
|
+
tsx: "#2b748950",
|
|
126
|
+
json: "#e34c2650",
|
|
127
|
+
svg: "#e69f0d50",
|
|
128
|
+
css: "#563d7c50",
|
|
129
|
+
png: "#e44b2350",
|
|
130
|
+
};
|
|
131
|
+
return colors[extName] ?? colors["js"];
|
|
132
|
+
},
|
|
133
|
+
getFileExtension(filename) {
|
|
134
|
+
const idx = filename.lastIndexOf(".");
|
|
135
|
+
return idx === -1 ? "js" : filename.slice(idx + 1);
|
|
136
|
+
},
|
|
137
|
+
toFixed(value, fractionDigits = 2) {
|
|
138
|
+
return Number(value).toFixed(fractionDigits);
|
|
139
|
+
},
|
|
140
|
+
percent(value, fractionDigits = 2) {
|
|
141
|
+
return (100 * value).toFixed(fractionDigits) + "%";
|
|
142
|
+
},
|
|
143
|
+
formatBytes(bytes, decimals) {
|
|
144
|
+
if (bytes == 0) return "0 Bytes";
|
|
145
|
+
const k = 1024,
|
|
146
|
+
dm = decimals || 2,
|
|
147
|
+
sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
|
|
148
|
+
i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
149
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
|
150
|
+
},
|
|
151
|
+
isPackageImport(moduleName) {
|
|
152
|
+
return moduleName?.[0] !== ".";
|
|
153
|
+
},
|
|
154
|
+
// isRuntimeCode(moduleName) {
|
|
155
|
+
// return (
|
|
156
|
+
// moduleName === "__prelude__" ||
|
|
157
|
+
// moduleName.includes("@babel/runtime") ||
|
|
158
|
+
// moduleName.includes("metro-runtime") ||
|
|
159
|
+
// moduleName.includes("@react-native/js-polyfills")
|
|
160
|
+
// );
|
|
161
|
+
// },
|
|
162
|
+
transformFilesList(files, rootFolder, type) {
|
|
163
|
+
const nodeModulesMap = { children: {}, size: 0 };
|
|
164
|
+
const sourceCodeMap = { children: {}, size: 0 };
|
|
165
|
+
|
|
166
|
+
files.forEach(({ path, size }) => {
|
|
167
|
+
if (path === "__prelude__") {
|
|
168
|
+
path = "node_modules/__prelude__";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const shortPath = path.replace(rootFolder + "/", "");
|
|
172
|
+
const isNodeModule = shortPath.includes("node_modules/");
|
|
173
|
+
const parts = shortenPath(shortPath, nodeModulesMap).split("/");
|
|
174
|
+
let current = (isNodeModule ? nodeModulesMap : sourceCodeMap).children;
|
|
175
|
+
|
|
176
|
+
parts.forEach((part, index) => {
|
|
177
|
+
// console.log((" --- xdebug " + index + " ".repeat(50)).substr(0, 40), {
|
|
178
|
+
// part,
|
|
179
|
+
// parts,
|
|
180
|
+
// });
|
|
181
|
+
if (!current[part]) {
|
|
182
|
+
current[part] = { size: 0, children: {} };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (index === parts.length - 1) {
|
|
186
|
+
current[part].size = size;
|
|
187
|
+
current[part].path = shortPath;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
current = current[part].children;
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (type === "foamtree") {
|
|
195
|
+
sumSizes(nodeModulesMap);
|
|
196
|
+
sumSizes(sourceCodeMap);
|
|
197
|
+
|
|
198
|
+
const sourceCodeGroup = toGroups(sourceCodeMap, "Source Code");
|
|
199
|
+
const nodeModulesGroup = nodeModulesMap?.children?.node_modules
|
|
200
|
+
? toGroups(nodeModulesMap.children.node_modules, "node_modules")
|
|
201
|
+
: {};
|
|
202
|
+
|
|
203
|
+
if (!sourceCodeGroup.groups) {
|
|
204
|
+
return nodeModulesGroup;
|
|
205
|
+
}
|
|
206
|
+
if (!nodeModulesGroup.groups) {
|
|
207
|
+
return nodeModulesGroup;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const topLevelNode = { groups: [sourceCodeGroup, nodeModulesGroup] };
|
|
211
|
+
topLevelNode.weight = topLevelNode.groups.reduce(
|
|
212
|
+
(acc, group) => acc + group.weight,
|
|
213
|
+
0,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return topLevelNode;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (type === "highcharts-treemap") {
|
|
220
|
+
const ROOT_ID_1 = "~";
|
|
221
|
+
const ROOT_ID_2 = ".";
|
|
222
|
+
return [
|
|
223
|
+
{ id: ROOT_ID_1, name: "node_modules" },
|
|
224
|
+
{ id: ROOT_ID_2, name: "Source Code" },
|
|
225
|
+
]
|
|
226
|
+
.concat(flattenTree(nodeModulesMap.children.node_modules, ROOT_ID_1))
|
|
227
|
+
.concat(flattenTree(sourceCodeMap, ROOT_ID_2));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
throw new Error("Unsupported type: " + type);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
function flattenTree(
|
|
235
|
+
node,
|
|
236
|
+
parentId,
|
|
237
|
+
prevWasSkipped = false,
|
|
238
|
+
lvl = 0,
|
|
239
|
+
result = [],
|
|
240
|
+
overrideParentId,
|
|
241
|
+
) {
|
|
242
|
+
const nodeChildrenCount = Object.keys(node.children);
|
|
243
|
+
|
|
244
|
+
for (const key of nodeChildrenCount) {
|
|
245
|
+
const childNode = node.children[key];
|
|
246
|
+
const childId = parentId + "/" + key;
|
|
247
|
+
const childrenCount = Object.keys(childNode.children).length;
|
|
248
|
+
const hasChildren = childrenCount > 0;
|
|
249
|
+
const item = {
|
|
250
|
+
id: childId,
|
|
251
|
+
name: prevWasSkipped ? parentId : key,
|
|
252
|
+
parent: overrideParentId ?? parentId,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (!hasChildren) {
|
|
256
|
+
item.value = childNode.size;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (lvl === 0) {
|
|
260
|
+
item.color = Highcharts.getOptions().colors[randomInt(0, 9)];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const skipThisNode = nodeChildrenCount.length === 1 && childrenCount === 1;
|
|
264
|
+
|
|
265
|
+
if (!skipThisNode) {
|
|
266
|
+
result.push(item);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
flattenTree(
|
|
270
|
+
childNode,
|
|
271
|
+
childId,
|
|
272
|
+
skipThisNode,
|
|
273
|
+
lvl + 1,
|
|
274
|
+
result,
|
|
275
|
+
skipThisNode ? (overrideParentId ?? parentId) : undefined,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function sumSizes(node) {
|
|
283
|
+
const isFile = Object.keys(node.children).length === 0;
|
|
284
|
+
let totalFiles = isFile ? 1 : 0;
|
|
285
|
+
let totalSize = node.size || 0;
|
|
286
|
+
for (const key in node.children) {
|
|
287
|
+
const result = sumSizes(node.children[key]);
|
|
288
|
+
totalSize += result.totalSize;
|
|
289
|
+
totalFiles += result.totalFiles;
|
|
290
|
+
}
|
|
291
|
+
node.type = isFile ? "file" : "folder";
|
|
292
|
+
node.size = totalSize;
|
|
293
|
+
node.files = totalFiles;
|
|
294
|
+
return { totalSize, totalFiles };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function toGroups(node, label) {
|
|
298
|
+
const keys = Object.keys(node.children);
|
|
299
|
+
|
|
300
|
+
const common = {
|
|
301
|
+
label,
|
|
302
|
+
weight: node.size,
|
|
303
|
+
files: node.files,
|
|
304
|
+
type: node.type,
|
|
305
|
+
size: helpers.formatBytes(node.size),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (keys.length === 0) {
|
|
309
|
+
// File
|
|
310
|
+
return common;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Folder
|
|
314
|
+
return Object.assign(common, {
|
|
315
|
+
groups: keys.map((key) => toGroups(node.children[key], key)),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function randomInt(min, max) {
|
|
320
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const nm = "node_modules/";
|
|
324
|
+
function shortenPath(path, nodeModulesMap) {
|
|
325
|
+
let index = path.lastIndexOf(nm);
|
|
326
|
+
|
|
327
|
+
if (index > 0) {
|
|
328
|
+
const pathWithoutNestedNM = path.slice(index);
|
|
329
|
+
// Find the package name after "node_modules/"
|
|
330
|
+
const firstSlash = pathWithoutNestedNM.indexOf("/");
|
|
331
|
+
const secondSlash = pathWithoutNestedNM.indexOf("/", firstSlash + 1);
|
|
332
|
+
const pkgName =
|
|
333
|
+
secondSlash === -1
|
|
334
|
+
? pathWithoutNestedNM.slice(firstSlash + 1)
|
|
335
|
+
: pathWithoutNestedNM.slice(firstSlash + 1, secondSlash);
|
|
336
|
+
|
|
337
|
+
if (nodeModulesMap?.children?.node_modules?.children?.[pkgName]) {
|
|
338
|
+
const parentPackage = helpers.getModulesName(path.slice(0, index));
|
|
339
|
+
return nm + parentPackage + " ~ " + pathWithoutNestedNM.slice(nm.length);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return pathWithoutNestedNM;
|
|
343
|
+
}
|
|
344
|
+
return path;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = helpers;
|
package/setup.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const prepare = require("./prepare");
|
|
2
|
+
const queryHelpers = require("./queryHelpers");
|
|
3
|
+
|
|
4
|
+
module.exports = function setup({
|
|
5
|
+
defineObjectMarker,
|
|
6
|
+
addQueryHelpers,
|
|
7
|
+
setPrepare,
|
|
8
|
+
}) {
|
|
9
|
+
// extend queries with custom methods
|
|
10
|
+
addQueryHelpers(queryHelpers);
|
|
11
|
+
setPrepare(prepare);
|
|
12
|
+
};
|
package/views/foamtree.js
CHANGED
package/views/global.css
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
discovery.view.define(
|
|
2
|
+
"source-prettify",
|
|
3
|
+
async function renderPrettify(el, config, data, context) {
|
|
4
|
+
await this.render(
|
|
5
|
+
el,
|
|
6
|
+
this.composeConfig([
|
|
7
|
+
{
|
|
8
|
+
...config,
|
|
9
|
+
source: await config.source,
|
|
10
|
+
view: "source",
|
|
11
|
+
},
|
|
12
|
+
]),
|
|
13
|
+
data,
|
|
14
|
+
context,
|
|
15
|
+
);
|
|
16
|
+
},
|
|
17
|
+
);
|