react-native-bundle-discovery 2.6.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/package.json CHANGED
@@ -1,17 +1,28 @@
1
1
  {
2
2
  "name": "react-native-bundle-discovery",
3
- "version": "2.6.0",
4
- "main": "index.js",
5
- "repository": "git@github.com:retyui/react-native-bundle-discovery.git",
3
+ "version": "2.7.0-rc.0",
4
+ "description": "Metro serializer and Webpack/Rspack plugin that generates a JSON report of your React Native bundle",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/retyui/react-native-bundle-discovery.git",
8
+ "directory": "packages/serializer"
9
+ },
6
10
  "author": "David <4661784+retyui@users.noreply.github.com>",
7
11
  "license": "MIT",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "files": [
15
+ "dist"
16
+ ],
8
17
  "scripts": {
9
- "prepublishOnly": "cp ../README.md README.md"
18
+ "build": "run -T tsdown",
19
+ "typecheck": "run -T tsc -p tsconfig.json",
20
+ "prepack": "cp ../../README.md README.md"
10
21
  },
11
22
  "dependencies": {
12
- "chalk": "^4.1.2",
13
- "acorn": "^8.0.4",
14
- "acorn-walk": "^8.0.0"
23
+ "acorn": "^8.18.0",
24
+ "acorn-walk": "^8.3.5",
25
+ "chalk": "^4.1.2"
15
26
  },
16
27
  "peerDependencies": {
17
28
  "metro": "*"
@@ -21,7 +32,7 @@
21
32
  "optional": true
22
33
  }
23
34
  },
24
- "files": [
25
- "*.js"
26
- ]
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
27
38
  }
package/index.js DELETED
@@ -1,301 +0,0 @@
1
- const { parse, resolve } = require("path");
2
- const { writeFileSync, existsSync } = require("fs");
3
- const { Buffer } = require("buffer");
4
- const chalk = require("chalk");
5
- const BundleDiscoveryPlugin = require("./webpack").BundleDiscoveryPlugin;
6
- const { withPackagesMetadata } = require("./packageMetadata");
7
- const NAME = require("./package.json").name;
8
-
9
- function getDefault(module) {
10
- return module.__esModule ? module.default : module;
11
- }
12
-
13
- function getDefaultSerializer() {
14
- const metroPath = parse(require.resolve("metro/package.json")).dir;
15
- const bundleToString = getDefault(
16
- require(`${metroPath}/src/lib/bundleToString.js`),
17
- );
18
- const baseJSBundle = getDefault(
19
- require(`${metroPath}/src/DeltaBundler/Serializers/baseJSBundle.js`),
20
- );
21
-
22
- return function defaultSerializer(entryPoint, preModules, graph, options) {
23
- let bundle = baseJSBundle(entryPoint, preModules, graph, options);
24
-
25
- // Sentry support
26
- // https://docs.sentry.io/platforms/react-native/manual-setup/metro/#wrap-your-custom-serializer
27
- if (typeof options?.sentryBundleCallback === "function") {
28
- bundle = options.sentryBundleCallback(bundle);
29
- }
30
-
31
- return bundleToString(bundle).code;
32
- };
33
- }
34
-
35
- function getStringSizeInBytes(str) {
36
- return Buffer.byteLength(str, "utf8");
37
- }
38
-
39
- /**
40
- * `/Users/i/app/node_modules/metro/node_modules/@babel/runtime/helpers/createClass.js` -> `@babel/runtime`
41
- * `/Users/i/app/node_modules/react/jsx-runtime.js` -> `react`
42
- */
43
- function getPackageNameFromPath(path) {
44
- const parts = path.split("node_modules/");
45
- const lastPart = parts[parts.length - 1];
46
- if (lastPart.startsWith("@")) {
47
- return lastPart.split("/").slice(0, 2).join("/");
48
- }
49
- return lastPart.split("/")[0];
50
- }
51
-
52
- /**
53
- * `/Users/i/app/node_modules/metro/node_modules/@babel/runtime/helpers/createClass.js` -> `/Users/i/app/node_modules/metro/node_modules/@babel/runtime`
54
- * `/Users/i/app/node_modules/react/jsx-runtime.js` -> `/Users/i/app/node_modules/react`
55
- */
56
- function getPackageAbsolutePath(path, pkgName) {
57
- const parts = path.split("node_modules/");
58
- parts[parts.length - 1] = pkgName;
59
- return parts.join("node_modules/");
60
- }
61
-
62
- function toPackages(modules) {
63
- const packages = new Map();
64
- modules.forEach((module) => {
65
- if (!module.path.includes("node_modules/")) {
66
- return;
67
- }
68
-
69
- const pkgName = getPackageNameFromPath(module.path);
70
- const absolutePkgPath = getPackageAbsolutePath(module.path, pkgName);
71
-
72
- if (!packages.has(absolutePkgPath)) {
73
- packages.set(absolutePkgPath, {
74
- name: pkgName,
75
- absolutePath: absolutePkgPath,
76
- version: require(resolve(absolutePkgPath, "package.json")).version,
77
- });
78
- }
79
- });
80
-
81
- return Array.from(packages.values()).sort((a, b) =>
82
- a.name.localeCompare(b.name),
83
- );
84
- }
85
-
86
- function toModuleStruct(m, includeCode) {
87
- const sourceCode = m.getSource().toString("utf8");
88
- const outputCode = m.output[0].data.code;
89
- return {
90
- path: m.path,
91
- source: {
92
- code: includeCode ? sourceCode : "",
93
- lineCount: sourceCode.split("\n").length,
94
- sizeInBytes: getStringSizeInBytes(sourceCode),
95
- },
96
- output: {
97
- code: includeCode ? outputCode : "",
98
- lineCount: m.output[0].data.lineCount,
99
- sizeInBytes: getStringSizeInBytes(outputCode),
100
- },
101
- dependencies: Array.from(m?.dependencies?.values?.() ?? [])
102
- .filter((e) => e.absolutePath)
103
- .map((e) => ({
104
- absolutePath: e.absolutePath,
105
- name: e.data.name,
106
- })),
107
- };
108
- }
109
-
110
- async function createJsonReport({
111
- graph,
112
- entryPoint,
113
- includeEnvs,
114
- preModules,
115
- includeCode,
116
- outputJsonPath,
117
- rootFolder,
118
- silent,
119
- options,
120
- fetchPackagesMetadata,
121
- }) {
122
- const { processModuleFilter = () => true } = options || {};
123
- const dependencies = Array.from(graph.dependencies.values()).filter(
124
- processModuleFilter,
125
- );
126
-
127
- let packages = toPackages(preModules).concat(toPackages(dependencies));
128
- const modules = preModules
129
- .map((m) => toModuleStruct(m, includeCode))
130
- .concat(dependencies.map((m) => toModuleStruct(m, includeCode)));
131
-
132
- if (fetchPackagesMetadata) {
133
- packages = await withPackagesMetadata(packages, {
134
- onError: (error, pkg) => {
135
- if (!silent) {
136
- console.warn(
137
- `${chalk.yellow(`[${NAME}]`)}: Failed to fetch metadata for ${pkg.name}@${pkg.version}: ${error.message}`,
138
- );
139
- }
140
- },
141
- });
142
- }
143
-
144
- const stats = {
145
- date: Date.now(),
146
- entryPoint,
147
- transformOptions: graph.transformOptions,
148
- envs: includeEnvs.reduce((acc, envName) => {
149
- acc[envName] = process.env[envName];
150
- return acc;
151
- }, {}),
152
- rootFolder,
153
- packages,
154
- modules,
155
- };
156
-
157
- writeFileSync(outputJsonPath, JSON.stringify(stats));
158
-
159
- if (!silent) {
160
- console.log(
161
- `${chalk.yellow(`[${NAME}]`)}: Saved stats to ${chalk.green(outputJsonPath)}`,
162
- );
163
- }
164
- }
165
-
166
- /**
167
- * Creates a custom serializer function for Metro bundler, which generates a JSON report
168
- * and optionally modifies the serialization process.
169
- *
170
- * @param {Object} options - Configuration options for the serializer.
171
- * @param {Function} [options.serializer] - A custom serializer function. If not provided, a default serializer is used.
172
- * @param {string} options.projectRoot - The root directory of the project. Must exist.
173
- * @param {string} [options.outputJsonPath] - The path where the JSON report will be saved. Defaults to "metro-stats.json" in the project root.
174
- * @param {boolean} [options.includeCode=true] - Whether to include the source and output code in the JSON report.
175
- * @param {string[]} [options.includeEnvs=[]] - A list of environment variable names to include in the JSON report.
176
- * @param {boolean} [options.fetchPackagesMetadata=true] - Whether to fetch packages metadata (publish date, deprecation, latest version) from the npm registry.
177
- * @returns {Function} - A custom serializer function to be used by Metro.
178
- * @throws {Error} - Throws an error if the project root does not exist.
179
- */
180
- function createSerializer({
181
- serializer,
182
- projectRoot,
183
- outputJsonPath,
184
- includeCode = true,
185
- silent = false,
186
- includeEnvs = [],
187
- fetchPackagesMetadata = true,
188
- } = {}) {
189
- const mySerializer = serializer || getDefaultSerializer();
190
-
191
- if (!existsSync(projectRoot)) {
192
- throw new Error(`[${NAME}]: Project root does not exist: ${projectRoot}`);
193
- }
194
-
195
- const myOutputJsonPath =
196
- outputJsonPath ?? resolve(projectRoot, "metro-stats.json");
197
-
198
- function customSerializer(entryPoint, preModules, graph, options) {
199
- const code = mySerializer(entryPoint, preModules, graph, options);
200
-
201
- // Keeps the Node.js process alive until the report is written
202
- // (otherwise it can exit while metadata requests are still pending)
203
- const keepAlive = setInterval(() => {}, 1000);
204
-
205
- // Graph/modules are read synchronously before the first `await`,
206
- // so the report is not affected by later Metro graph mutations
207
- createJsonReport({
208
- graph,
209
- entryPoint,
210
- includeEnvs,
211
- preModules,
212
- includeCode,
213
- outputJsonPath: myOutputJsonPath,
214
- rootFolder: projectRoot,
215
- silent,
216
- options,
217
- fetchPackagesMetadata,
218
- }).finally(() => clearInterval(keepAlive));
219
-
220
- return code;
221
- }
222
-
223
- return customSerializer;
224
- }
225
-
226
- /**
227
- * Usage:
228
- *
229
- * ```js
230
- * // metro.config.js
231
- * const { createResolveRequest } = require("react-native-bundle-discovery");
232
- * const resolveRequest = createResolveRequest({
233
- * removePromisePolyfill: true,
234
- * });
235
- * const config = {
236
- * resolver: {
237
- * resolveRequest,
238
- * },
239
- * };
240
- * ```
241
- */
242
- const empty = { type: "empty" };
243
- const createResolveRequest = ({
244
- removeNewRenderer = false, // Should be true when new ARCH is disabled
245
- removeUTFSequence = false, // Remove useless code
246
- } = {}) => {
247
- const resolveRequest = (context, ...rest) => {
248
- const result = context.resolveRequest(context, ...rest);
249
- if (context.dev) {
250
- return result;
251
- }
252
- if (
253
- removeUTFSequence &&
254
- result?.filePath?.endsWith("/react-native/Libraries/UTFSequence.js")
255
- ) {
256
- return empty;
257
- }
258
- if (
259
- removeNewRenderer &&
260
- result?.filePath?.endsWith(
261
- "/react-native/Libraries/Renderer/shims/ReactFabric.js",
262
- )
263
- ) {
264
- return empty;
265
- }
266
- return result;
267
- };
268
- return resolveRequest;
269
- };
270
-
271
- /**
272
- * @deprecated Use `createResolveRequest` instead.
273
- */
274
- const createProcessModuleFilter =
275
- ({
276
- removePromisePolyfill = false, // Remove useless polyfill (Hermes already has Promise)
277
- removeUTFSequence = false, // Remove useless code
278
- } = {}) =>
279
- (module) => {
280
- if (
281
- removePromisePolyfill &&
282
- module.path.endsWith("/react-native/Libraries/Promise.js")
283
- ) {
284
- return false;
285
- }
286
- if (
287
- removeUTFSequence &&
288
- module.path.endsWith("/react-native/Libraries/UTFSequence.js")
289
- ) {
290
- return false;
291
- }
292
-
293
- return true;
294
- };
295
-
296
- module.exports = {
297
- createSerializer,
298
- createResolveRequest,
299
- createProcessModuleFilter,
300
- BundleDiscoveryPlugin,
301
- };
@@ -1,155 +0,0 @@
1
- const DEFAULT_REGISTRY = "https://registry.npmjs.org";
2
- const DEFAULT_TIMEOUT_MS = 5_000;
3
- const DEFAULT_CONCURRENCY = 5;
4
-
5
- // Simple in-memory cache: `${registry}|${name}` -> Promise<packument summary | null>
6
- // Keeps promises (not values) so concurrent lookups of the same package share one request,
7
- // and different versions of the same package share one packument.
8
- const cache = new Map();
9
-
10
- /**
11
- * `@babel/runtime` -> `@babel%2fruntime`
12
- */
13
- function encodePackageName(name) {
14
- return name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : name;
15
- }
16
-
17
- async function fetchJson(url, timeoutMs) {
18
- const res = await fetch(url, {
19
- headers: { Accept: "application/json" },
20
- signal: AbortSignal.timeout(timeoutMs),
21
- });
22
- if (res.status === 404) {
23
- return null;
24
- }
25
- if (!res.ok) {
26
- throw new Error(`${res.status} ${res.statusText} (${url})`);
27
- }
28
- return res.json();
29
- }
30
-
31
- /**
32
- * The version manifest (`/<name>/<version>`) has no publish date,
33
- * so the packument is fetched once and only the fields we need are kept.
34
- */
35
- async function fetchPackageSummary(name, { registry, timeoutMs }) {
36
- const packument = await fetchJson(
37
- `${registry}/${encodePackageName(name)}`,
38
- timeoutMs,
39
- );
40
-
41
- if (!packument) {
42
- return null;
43
- }
44
-
45
- const versions = {};
46
- for (const [version, manifest] of Object.entries(packument.versions ?? {})) {
47
- versions[version] = {
48
- createdAt: packument.time?.[version] ?? null,
49
- deprecated: manifest.deprecated || false,
50
- };
51
- }
52
-
53
- return {
54
- latestVersion: packument["dist-tags"]?.latest ?? null,
55
- versions,
56
- };
57
- }
58
-
59
- function getPackageSummary(name, { registry, timeoutMs }) {
60
- const key = `${registry}|${name}`;
61
-
62
- if (!cache.has(key)) {
63
- const promise = fetchPackageSummary(name, { registry, timeoutMs }).catch(
64
- (error) => {
65
- // Don't cache failures (network issues, timeouts), retry on next report
66
- cache.delete(key);
67
- throw error;
68
- },
69
- );
70
- cache.set(key, promise);
71
- }
72
-
73
- return cache.get(key);
74
- }
75
-
76
- /**
77
- * Returns metadata of a specific (installed) package version, not the latest one.
78
- *
79
- * @returns {Promise<{createdAt: string|null, deprecated: string|false, isLatest: boolean, latestVersion: string|null} | null>}
80
- * `null` when the package/version is not found in the registry.
81
- */
82
- async function getPackageMetadata(
83
- name,
84
- version,
85
- { registry = DEFAULT_REGISTRY, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
86
- ) {
87
- const summary = await getPackageSummary(name, { registry, timeoutMs });
88
- const info = summary?.versions[version];
89
-
90
- if (!info) {
91
- return null;
92
- }
93
-
94
- return {
95
- createdAt: info.createdAt,
96
- deprecated: info.deprecated,
97
- isLatest: summary.latestVersion === version,
98
- latestVersion: summary.latestVersion,
99
- };
100
- }
101
-
102
- async function mapWithConcurrency(items, limit, fn) {
103
- const results = new Array(items.length);
104
- let index = 0;
105
-
106
- async function worker() {
107
- while (index < items.length) {
108
- const i = index++;
109
- results[i] = await fn(items[i], i);
110
- }
111
- }
112
-
113
- await Promise.all(
114
- Array.from({ length: Math.min(limit, items.length) }, worker),
115
- );
116
-
117
- return results;
118
- }
119
-
120
- /**
121
- * Adds a `metadata` field to every package of the report.
122
- * Failed lookups result in `metadata: null` and are passed to `onError`.
123
- *
124
- * @param {Array<{name: string, version: string}>} packages
125
- */
126
- async function withPackagesMetadata(
127
- packages,
128
- {
129
- registry = DEFAULT_REGISTRY,
130
- timeoutMs = DEFAULT_TIMEOUT_MS,
131
- concurrency = DEFAULT_CONCURRENCY,
132
- onError = () => {},
133
- } = {},
134
- ) {
135
- return mapWithConcurrency(packages, concurrency, async (pkg) => {
136
- if (!pkg.version) {
137
- return { ...pkg, metadata: null };
138
- }
139
- try {
140
- const metadata = await getPackageMetadata(pkg.name, pkg.version, {
141
- registry,
142
- timeoutMs,
143
- });
144
- return { ...pkg, metadata };
145
- } catch (error) {
146
- onError(error, pkg);
147
- return { ...pkg, metadata: null };
148
- }
149
- });
150
- }
151
-
152
- module.exports = {
153
- getPackageMetadata,
154
- withPackagesMetadata,
155
- };