react-native-bundle-discovery 2.5.0 → 2.7.0-rc.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.
- package/README.md +178 -103
- package/dist/index.d.ts +262 -0
- package/dist/index.js +731 -0
- package/package.json +21 -10
- package/index.js +0 -301
- package/packageMetadata.js +0 -155
- package/parseUtils.js +0 -615
- package/webpack.js +0 -373
package/webpack.js
DELETED
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
const fs = require("node:fs");
|
|
2
|
-
const path = require("node:path");
|
|
3
|
-
|
|
4
|
-
const chalk = require("chalk");
|
|
5
|
-
|
|
6
|
-
const { parseBundle } = require("./parseUtils.js");
|
|
7
|
-
const { withPackagesMetadata } = require("./packageMetadata.js");
|
|
8
|
-
const NAME = require("./package.json").name;
|
|
9
|
-
const packageRegex = /(?:^|\/)node_modules\/((?:@[^\/]+\/)?[^\/]+)/g;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Simple Webpack/Rspack plugin
|
|
13
|
-
* to adapt stats object https://webpack.js.org/api/stats/
|
|
14
|
-
* to metro-stats.json format used by `react-native-bundle-discovery`
|
|
15
|
-
*/
|
|
16
|
-
class BundleDiscoveryPlugin {
|
|
17
|
-
constructor({ filename, options, enabled, fetchPackagesMetadata } = {}) {
|
|
18
|
-
this.options = options;
|
|
19
|
-
this.filename = filename || "metro-stats.json";
|
|
20
|
-
this.enabled = enabled ?? true;
|
|
21
|
-
this.fetchPackagesMetadata = fetchPackagesMetadata ?? true;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
extractSizesFromJsBundle(statAsset, options) {
|
|
25
|
-
const assetFile = path.join(options.output.path, statAsset.name);
|
|
26
|
-
try {
|
|
27
|
-
return parseBundle(assetFile, {
|
|
28
|
-
sourceType: statAsset.info.javascriptModule ? "module" : "script",
|
|
29
|
-
});
|
|
30
|
-
} catch (e) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
apply(compiler) {
|
|
36
|
-
if (!this.enabled) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
compiler.hooks.done.tap("GenerateStatsJsonPlugin", (stats) => {
|
|
41
|
-
// Extract the relevant information from the stats object
|
|
42
|
-
const json = stats.toJson({
|
|
43
|
-
version: true,
|
|
44
|
-
builtAt: true,
|
|
45
|
-
modules: true,
|
|
46
|
-
reasons: true,
|
|
47
|
-
assets: true,
|
|
48
|
-
ids: true,
|
|
49
|
-
...this.options,
|
|
50
|
-
all: false,
|
|
51
|
-
source: true,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Format the stats into the desired structure
|
|
55
|
-
const statsJson = this.formatStat(
|
|
56
|
-
json,
|
|
57
|
-
// Parse JS bundle to extract modules sizes
|
|
58
|
-
// WANR: The stats asset list can contain multiple emitted files and is not guaranteed to put the JavaScript bundle first (and can be empty after a failed compilation)
|
|
59
|
-
this.extractSizesFromJsBundle(json.assets[0], compiler.options),
|
|
60
|
-
compiler.options,
|
|
61
|
-
);
|
|
62
|
-
// Write the formatted stats to the specified output file
|
|
63
|
-
const outputPath = path.resolve(compiler.options.context, this.filename);
|
|
64
|
-
|
|
65
|
-
// Keeps the Node.js process alive until the report is written
|
|
66
|
-
// (otherwise it can exit while metadata requests are still pending)
|
|
67
|
-
const keepAlive = setInterval(() => {}, 1000);
|
|
68
|
-
|
|
69
|
-
this.writeReport(statsJson, outputPath)
|
|
70
|
-
.catch((error) => {
|
|
71
|
-
console.error(
|
|
72
|
-
`${chalk.yellow(`[${NAME}]`)}: Failed to create JSON report: ${error.message}`,
|
|
73
|
-
);
|
|
74
|
-
})
|
|
75
|
-
.finally(() => clearInterval(keepAlive));
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async writeReport(statsJson, outputPath) {
|
|
80
|
-
if (this.fetchPackagesMetadata) {
|
|
81
|
-
statsJson.packages = await withPackagesMetadata(statsJson.packages, {
|
|
82
|
-
onError: (error, pkg) => {
|
|
83
|
-
console.warn(
|
|
84
|
-
`${chalk.yellow(`[${NAME}]`)}: Failed to fetch metadata for ${pkg.name}@${pkg.version}: ${error.message}`,
|
|
85
|
-
);
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
fs.writeFileSync(outputPath, JSON.stringify(statsJson, null, 2));
|
|
91
|
-
|
|
92
|
-
console.log(
|
|
93
|
-
`${chalk.yellow(`[${NAME}]`)}: Saved stats to ${chalk.green(outputPath)}`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
readSource(filePath) {
|
|
98
|
-
const fallback = { code: "", lineCount: 0, sizeInBytes: 0 };
|
|
99
|
-
try {
|
|
100
|
-
const code = fs.readFileSync(filePath, "utf-8");
|
|
101
|
-
const lineCount = code.split("\n").length;
|
|
102
|
-
const sizeInBytes = Buffer.byteLength(code, "utf-8");
|
|
103
|
-
return {
|
|
104
|
-
code: this.options?.source === false ? "" : code,
|
|
105
|
-
lineCount,
|
|
106
|
-
sizeInBytes,
|
|
107
|
-
};
|
|
108
|
-
} catch {
|
|
109
|
-
return fallback;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
getPkgVersionFromPath(path) {
|
|
114
|
-
try {
|
|
115
|
-
// Extract the segment right before node_modules in `.pnpm/.../node_modules/`
|
|
116
|
-
const matches = [...path.matchAll(/\.pnpm\/([^/]+)\/node_modules/g)];
|
|
117
|
-
if (!matches.length) return null;
|
|
118
|
-
|
|
119
|
-
const lastPnpmFolder = matches[matches.length - 1][1];
|
|
120
|
-
|
|
121
|
-
// Folder format is `<name>@<version>[_<peers>]`, where `<name>` may be scoped
|
|
122
|
-
// (`@scope+name`, `@scope_name`), so skip the leading `@` of the scope
|
|
123
|
-
const versionMatch = lastPnpmFolder.match(/^@?[^@]+@([^_]+)/);
|
|
124
|
-
|
|
125
|
-
return versionMatch ? versionMatch[1] : null;
|
|
126
|
-
} catch {
|
|
127
|
-
return "";
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
getPackages(statsJson) {
|
|
132
|
-
const packagesMap = new Map();
|
|
133
|
-
|
|
134
|
-
for (const mdl of statsJson.modules) {
|
|
135
|
-
const filePath = mdl.nameForCondition;
|
|
136
|
-
if (!filePath) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let match;
|
|
141
|
-
let lastMatch = null;
|
|
142
|
-
|
|
143
|
-
// Reset regex state for global matching
|
|
144
|
-
packageRegex.lastIndex = 0;
|
|
145
|
-
|
|
146
|
-
// Find the LAST valid node_modules/<pkg> pattern in the path
|
|
147
|
-
while ((match = packageRegex.exec(filePath)) !== null) {
|
|
148
|
-
lastMatch = match;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!lastMatch) continue;
|
|
152
|
-
|
|
153
|
-
const packageName = lastMatch[1];
|
|
154
|
-
const matchIndex = lastMatch.index;
|
|
155
|
-
|
|
156
|
-
// Account for potential leading slash in the regex match
|
|
157
|
-
const prefixOffset = filePath[matchIndex] === "/" ? 1 : 0;
|
|
158
|
-
const cutIndex =
|
|
159
|
-
matchIndex + prefixOffset + "node_modules/".length + packageName.length;
|
|
160
|
-
|
|
161
|
-
const absolutePath = filePath.slice(0, cutIndex);
|
|
162
|
-
|
|
163
|
-
let version = "";
|
|
164
|
-
try {
|
|
165
|
-
version = require(path.join(absolutePath, "package.json")).version;
|
|
166
|
-
} catch (e) {
|
|
167
|
-
const verFromPath = this.getPkgVersionFromPath(absolutePath);
|
|
168
|
-
if (verFromPath) {
|
|
169
|
-
version = verFromPath;
|
|
170
|
-
} else {
|
|
171
|
-
if (e?.code === "MODULE_NOT_FOUND") {
|
|
172
|
-
console.warn(`[${NAME}] getPackages: unexpected path resolution`, {
|
|
173
|
-
filePath,
|
|
174
|
-
absolutePath,
|
|
175
|
-
});
|
|
176
|
-
} else {
|
|
177
|
-
throw e;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (!packagesMap.has(absolutePath)) {
|
|
183
|
-
packagesMap.set(absolutePath, {
|
|
184
|
-
name: packageName,
|
|
185
|
-
absolutePath: absolutePath,
|
|
186
|
-
version: version,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return Array.from(packagesMap.values());
|
|
191
|
-
}
|
|
192
|
-
getEntryFile(statsJson, rootFolder) {
|
|
193
|
-
const fallbackValue = path.join(rootFolder, "index.js");
|
|
194
|
-
return (
|
|
195
|
-
statsJson.modules.find(
|
|
196
|
-
(mdl) =>
|
|
197
|
-
!mdl.nameForCondition?.includes("node_modules/") &&
|
|
198
|
-
mdl.reasons?.length === 1 &&
|
|
199
|
-
mdl.reasons?.[0].type === "entry",
|
|
200
|
-
)?.nameForCondition || fallbackValue
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
getModuleOutput(mdl, bundleModules) {
|
|
204
|
-
const jsCodeStrFromJSBundle = bundleModules?.modules?.[mdl.id];
|
|
205
|
-
if (jsCodeStrFromJSBundle) {
|
|
206
|
-
const formatted =
|
|
207
|
-
jsCodeStrFromJSBundle.startsWith("function(") &&
|
|
208
|
-
jsCodeStrFromJSBundle.endsWith("}")
|
|
209
|
-
? jsCodeStrFromJSBundle.replace("function(", "function module(")
|
|
210
|
-
: jsCodeStrFromJSBundle;
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
code: formatted,
|
|
214
|
-
lineCount: formatted.split("\n").length ?? 0,
|
|
215
|
-
sizeInBytes: Buffer.byteLength(jsCodeStrFromJSBundle, "utf-8"),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
code: mdl.source ?? "",
|
|
220
|
-
lineCount: mdl.source?.split("\n").length ?? 0,
|
|
221
|
-
sizeInBytes: mdl.size ?? 0,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
formatStat(statsJson, bundleModules, compilerOptions) {
|
|
226
|
-
const modulesDeps = this.buildImportsMapFromReasons(
|
|
227
|
-
statsJson,
|
|
228
|
-
compilerOptions,
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
kind: "webpack",
|
|
233
|
-
date: statsJson.builtAt || Date.now(),
|
|
234
|
-
entryPoint: this.getEntryFile(statsJson, compilerOptions.context),
|
|
235
|
-
rootFolder:
|
|
236
|
-
this.findCommonRoot(
|
|
237
|
-
statsJson.modules.map((m) => m.nameForCondition).filter(Boolean),
|
|
238
|
-
) || compilerOptions.context,
|
|
239
|
-
// envs: {},
|
|
240
|
-
transformOptions: {
|
|
241
|
-
// customTransformOptions: {},
|
|
242
|
-
dev: compilerOptions.mode !== "production",
|
|
243
|
-
minify: compilerOptions.optimization?.minimize ?? false,
|
|
244
|
-
platform: compilerOptions.name ?? "ios",
|
|
245
|
-
// type: "module",
|
|
246
|
-
// unstable_transformProfile: "default",
|
|
247
|
-
},
|
|
248
|
-
modules: statsJson.modules
|
|
249
|
-
.map((mdl) => {
|
|
250
|
-
if (mdl.type === "runtime modules") {
|
|
251
|
-
if (!bundleModules) {
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
const sourceAndOutput = {
|
|
255
|
-
code: bundleModules.runtimeSrc ?? "",
|
|
256
|
-
lineCount: bundleModules.runtimeSrc?.split("\n").length ?? 0,
|
|
257
|
-
sizeInBytes: Buffer.byteLength(
|
|
258
|
-
bundleModules.runtimeSrc ?? "",
|
|
259
|
-
"utf-8",
|
|
260
|
-
),
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
path: "__runtime__",
|
|
265
|
-
source: sourceAndOutput,
|
|
266
|
-
output: sourceAndOutput,
|
|
267
|
-
dependencies: [],
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
path: mdl.nameForCondition,
|
|
273
|
-
source: this.readSource(mdl.nameForCondition),
|
|
274
|
-
output: this.getModuleOutput(mdl, bundleModules),
|
|
275
|
-
dependencies: modulesDeps.get(mdl.nameForCondition) ?? [],
|
|
276
|
-
};
|
|
277
|
-
})
|
|
278
|
-
.filter((e) => e !== null && e.path),
|
|
279
|
-
packages: this.getPackages(statsJson),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
buildImportsMapFromReasons(statsJson, compilerOptions) {
|
|
284
|
-
const modules = Array.isArray(statsJson?.modules) ? statsJson.modules : [];
|
|
285
|
-
const importsMap = new Map();
|
|
286
|
-
|
|
287
|
-
const getAbs = (p) => {
|
|
288
|
-
if (!p || typeof p !== "string") return null;
|
|
289
|
-
if (path.isAbsolute(p)) return p;
|
|
290
|
-
return compilerOptions?.context
|
|
291
|
-
? path.resolve(compilerOptions?.context, p)
|
|
292
|
-
: p;
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const getModulePath = (m) =>
|
|
296
|
-
m?.path || m?.nameForCondition || m?.identifier || null;
|
|
297
|
-
|
|
298
|
-
for (const depModule of modules) {
|
|
299
|
-
const depAbsolutePath = getAbs(getModulePath(depModule));
|
|
300
|
-
if (!depAbsolutePath) continue;
|
|
301
|
-
|
|
302
|
-
const reasons = Array.isArray(depModule.reasons) ? depModule.reasons : [];
|
|
303
|
-
for (const reason of reasons) {
|
|
304
|
-
if (reason?.type === "entry") continue;
|
|
305
|
-
|
|
306
|
-
// importer (who imports current dep module)
|
|
307
|
-
const importerPath = getAbs(
|
|
308
|
-
reason?.modulePath ||
|
|
309
|
-
reason?.moduleNameForCondition ||
|
|
310
|
-
reason?.resolvedModule ||
|
|
311
|
-
reason?.moduleIdentifier,
|
|
312
|
-
);
|
|
313
|
-
if (!importerPath) continue;
|
|
314
|
-
|
|
315
|
-
if (!importsMap.has(importerPath)) {
|
|
316
|
-
importsMap.set(importerPath, []);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const deps = importsMap.get(importerPath);
|
|
320
|
-
|
|
321
|
-
// uniq by absolutePath
|
|
322
|
-
if (!deps.some((d) => d.absolutePath === depAbsolutePath)) {
|
|
323
|
-
deps.push({
|
|
324
|
-
absolutePath: depAbsolutePath,
|
|
325
|
-
name: reason?.userRequest || "",
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return importsMap;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
findCommonRoot(paths) {
|
|
335
|
-
if (!paths || paths.length === 0) return "";
|
|
336
|
-
if (paths.length === 1) {
|
|
337
|
-
const lastSlash = paths[0].lastIndexOf("/");
|
|
338
|
-
return lastSlash === -1 ? "" : paths[0].slice(0, lastSlash);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const first = paths[0];
|
|
342
|
-
let maxLen = first.length;
|
|
343
|
-
|
|
344
|
-
// Short-circuit scanning loop across all paths
|
|
345
|
-
for (let i = 1; i < paths.length; i++) {
|
|
346
|
-
const current = paths[i];
|
|
347
|
-
const limit = Math.min(maxLen, current.length);
|
|
348
|
-
let j = 0;
|
|
349
|
-
|
|
350
|
-
// Fast character match using engine string optimizations
|
|
351
|
-
while (j < limit && first.charCodeAt(j) === current.charCodeAt(j)) {
|
|
352
|
-
j++;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
maxLen = j;
|
|
356
|
-
if (maxLen === 0) return "";
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Exact match down to the root or full path segment
|
|
360
|
-
if (maxLen === first.length) {
|
|
361
|
-
const lastSlash = first.lastIndexOf("/");
|
|
362
|
-
return lastSlash === -1 ? "" : first.slice(0, lastSlash);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Truncate to the last valid '/' boundary within maxLen
|
|
366
|
-
const commonSlice = first.slice(0, maxLen);
|
|
367
|
-
const lastSlash = commonSlice.lastIndexOf("/");
|
|
368
|
-
|
|
369
|
-
return lastSlash === -1 ? "" : commonSlice.slice(0, lastSlash);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
module.exports = { BundleDiscoveryPlugin };
|