rollipop 1.0.0-alpha.30 → 1.0.0-alpha.32

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/dist/constants.js CHANGED
@@ -20,7 +20,7 @@ var constants_exports = /* @__PURE__ */ __exportAll({
20
20
  ROLLIPOP_VIRTUAL_ENTRY_ID: () => ROLLIPOP_VIRTUAL_ENTRY_ID,
21
21
  ROLLIPOP_VIRTUAL_PREFIX: () => ROLLIPOP_VIRTUAL_PREFIX
22
22
  });
23
- const ROLLIPOP_VERSION = "1.0.0-alpha.30";
23
+ const ROLLIPOP_VERSION = "1.0.0-alpha.32";
24
24
  const ROLLIPOP_VIRTUAL_PREFIX = "\0rollipop/";
25
25
  const ROLLIPOP_VIRTUAL_BOOTSTRAP_ID = `${ROLLIPOP_VIRTUAL_PREFIX}bootstrap`;
26
26
  const ROLLIPOP_VIRTUAL_ENTRY_ID = `${ROLLIPOP_VIRTUAL_PREFIX}entry`;
@@ -3,6 +3,7 @@ import { EventBus } from "../events/event-bus.js";
3
3
  import { FileStorage } from "../storage/file-storage.js";
4
4
  import { resolveBuildOptions } from "../utils/build-options.js";
5
5
  import { createId } from "../utils/id.js";
6
+ import { replaceSourceMappingUrl } from "../utils/source-map.js";
6
7
  import { resolveRolldownOptions } from "./rolldown.js";
7
8
  import fs from "node:fs";
8
9
  import path from "node:path";
@@ -81,6 +82,18 @@ var Bundler = class Bundler {
81
82
  const sourcemapFile = path.join(outputDir, chunk.sourcemapFileName);
82
83
  if (!fs.existsSync(sourcemapDir)) fs.mkdirSync(sourcemapDir, { recursive: true });
83
84
  fs.renameSync(sourcemapFile, resolvedBuildOptions.sourcemapOutfile);
85
+ const sourcemapFileName = path.relative(outputDir, resolvedBuildOptions.sourcemapOutfile).split(path.sep).join("/");
86
+ let code = chunk.code;
87
+ if (output.sourcemap !== "hidden") {
88
+ const sourceMapUrl = sourcemapFileName.split("/").map(encodeURIComponent).join("/");
89
+ code = replaceSourceMappingUrl(code, output.sourcemapBaseUrl ? new URL(sourceMapUrl, output.sourcemapBaseUrl).href : sourceMapUrl);
90
+ fs.writeFileSync(resolvedBuildOptions.outfile, code);
91
+ }
92
+ return {
93
+ ...chunk,
94
+ code,
95
+ sourcemapFileName
96
+ };
84
97
  }
85
98
  return chunk;
86
99
  }
package/dist/core/env.js CHANGED
@@ -10,6 +10,7 @@ function loadEnv(options) {
10
10
  invariant(envPrefix.length > 0, "`envPrefix` is required");
11
11
  invariant(envFile.length > 0, "`envFile` is required");
12
12
  const env = {};
13
+ const parsed = {};
13
14
  const envFilesToLoad = [
14
15
  envFile,
15
16
  `${envFile}.local`,
@@ -20,15 +21,18 @@ function loadEnv(options) {
20
21
  const envPath = path.resolve(envDir, file);
21
22
  if (!fs.existsSync(envPath)) continue;
22
23
  logger.trace(`Loading environment variables from ${envPath}`);
23
- const parsed = dotenv.parse(fs.readFileSync(envPath, "utf-8"));
24
- const expanded = dotenvExpand.expand({
25
- parsed,
26
- processEnv: {}
27
- });
28
- if (expanded.parsed) Object.entries(expanded.parsed).forEach(([key, value]) => {
29
- if (key.startsWith(envPrefix)) env[key] = key in process.env ? process.env[key] : value;
30
- });
24
+ Object.assign(parsed, dotenv.parse(fs.readFileSync(envPath, "utf-8")));
31
25
  }
26
+ const processEnv = {};
27
+ for (const [key, value] of Object.entries(process.env)) if (value !== void 0) {
28
+ processEnv[key] = value;
29
+ if (key in parsed) parsed[key] = value;
30
+ }
31
+ const expanded = dotenvExpand.expand({
32
+ parsed,
33
+ processEnv
34
+ });
35
+ for (const [key, value] of Object.entries(expanded.parsed ?? {})) if (key.startsWith(envPrefix)) env[key] = process.env[key] ?? value;
32
36
  logger.trace("Loaded environment variables:", env);
33
37
  return env;
34
38
  }
@@ -25,12 +25,12 @@ const command = {
25
25
  },
26
26
  {
27
27
  name: "--dev [boolean]",
28
- description: "If false, warnings are disabled and the bundle is minified",
28
+ description: "Build in development mode. Defaults to whether config.mode is development",
29
29
  parse: parseBoolean
30
30
  },
31
31
  {
32
32
  name: "--minify [boolean]",
33
- description: "Allows overriding whether bundle is minified. This defaults to false if dev is true, and true if dev is false. Disabling minification can be useful for speeding up production builds for testing purposes.",
33
+ description: "Override output.minify from the configuration. Defaults to false when not configured, regardless of --dev.",
34
34
  parse: parseBoolean
35
35
  },
36
36
  {
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.0.0-alpha.30";
2
+ var version = "1.0.0-alpha.32";
3
3
  //#endregion
4
4
  export { version };
@@ -1,7 +1,7 @@
1
1
  import { resolveBuildOptions } from "../utils/build-options.js";
2
+ import { replaceSourceMappingUrl } from "../utils/source-map.js";
2
3
  import { bindReporter } from "../utils/config.js";
3
4
  import { getBaseUrl } from "../utils/server.js";
4
- import { replaceSourceMappingUrl } from "../utils/source-map.js";
5
5
  import { Bundler } from "../core/bundler.js";
6
6
  import { logger } from "./logger.js";
7
7
  import { getBaseBundleName } from "../utils/bundle.js";
@@ -121,7 +121,7 @@ async function createDevServer(config, options) {
121
121
  middlewares: { use: fastify.use.bind(fastify) }
122
122
  };
123
123
  const { invokePostConfigureServer } = await invokeConfigureServer(devServer, config.plugins ?? []);
124
- const devframeMiddleware = await setupDevframe(context, devServer);
124
+ const devframeMiddleware = await setupDevframe(context, devServer, websocketEndpoints["/inspector/device"]);
125
125
  fastify.use(requestLogger).use(devframeMiddleware).use(communityMiddleware).use(devMiddleware).register(plugin, { context }).register(plugin$3, { hotUpdateStore: bundlerPool.hotUpdateStore }).register(plugin$4, { context }).register(plugin$2, { context }).register(plugin$1, { context });
126
126
  fastify.setErrorHandler(errorHandler);
127
127
  const upgradeHandler = getWebSocketUpgradeHandler({
@@ -24,7 +24,7 @@ var RollipopDevframeController = class {
24
24
  this.definition = defineDevframe({
25
25
  id: "rollipop",
26
26
  name: "Rollipop",
27
- version: "1.0.0-alpha.30",
27
+ version: "1.0.0-alpha.32",
28
28
  packageName: "rollipop",
29
29
  homepage: "https://github.com/rollipop-dev/rollipop",
30
30
  description: "React Native development dashboard powered by Rollipop.",
@@ -11,12 +11,12 @@ const DEVFRAME_NAME = "Rollipop";
11
11
  const ROLLIPOP_DEVFRAME_BASE = "/__rollipop/";
12
12
  const ROLLIPOP_DEVFRAME_SSE_ROUTE = "__sse";
13
13
  const ROLLIPOP_DEVFRAME_MCP_PATH = "__mcp";
14
- async function setupDevframe(context, server) {
14
+ async function setupDevframe(context, server, inspectorDeviceServer) {
15
15
  const mcpEnabled = isAgenticInstalled();
16
16
  const controller = new RollipopDevframeController(context, createAgentToolContext(context));
17
17
  const devtools = initHub({
18
18
  name: DEVFRAME_NAME,
19
- version: "1.0.0-alpha.30",
19
+ version: "1.0.0-alpha.32",
20
20
  base: ROLLIPOP_DEVFRAME_BASE,
21
21
  cwd: context.config.root,
22
22
  ui: createUi({
@@ -44,6 +44,13 @@ async function setupDevframe(context, server) {
44
44
  }
45
45
  });
46
46
  await devtools.ready;
47
+ const refreshDevices = () => {
48
+ controller.refresh();
49
+ };
50
+ const onDeviceConnection = (socket) => {
51
+ socket.once("close", refreshDevices);
52
+ };
53
+ inspectorDeviceServer?.on("connection", onDeviceConnection);
47
54
  server.instance.addHook("onListen", () => {
48
55
  if (mcpEnabled) {
49
56
  const mcpUrl = getServerDisplayUrl(server.instance.listeningOrigin, "/__rollipop/__mcp");
@@ -52,6 +59,8 @@ async function setupDevframe(context, server) {
52
59
  controller.refresh();
53
60
  });
54
61
  server.instance.addHook("preClose", async () => {
62
+ inspectorDeviceServer?.off("connection", onDeviceConnection);
63
+ for (const socket of inspectorDeviceServer?.clients ?? []) socket.off("close", refreshDevices);
55
64
  controller.dispose();
56
65
  await devtools.close();
57
66
  });
@@ -16,20 +16,34 @@ const queryParamSchema = asConst({
16
16
  });
17
17
  const plugin = fp((fastify, options) => {
18
18
  const { context } = options;
19
- function resolveAsset(asset) {
20
- return path.resolve(context.config.root, asset);
21
- }
19
+ const projectRoot = path.resolve(context.config.root);
20
+ const workspaceRoot = findWorkspaceRoot(projectRoot);
21
+ const assetExtensions = new Set(context.config.resolve.assetExtensions);
22
22
  fastify.get(`/${DEV_SERVER_ASSET_PATH}/*`, {
23
23
  schema: { querystring: queryParamSchema },
24
24
  async handler(request, reply) {
25
25
  const { params, query } = request;
26
- const assetPath = resolveAsset(params["*"]);
26
+ const assetPath = path.resolve(projectRoot, params["*"]);
27
+ if (!assetExtensions.has(path.extname(assetPath).slice(1))) {
28
+ await reply.status(403).send();
29
+ return;
30
+ }
27
31
  try {
32
+ const realWorkspaceRoot = await fs.promises.realpath(workspaceRoot);
33
+ if (!isWithin(realWorkspaceRoot, await fs.promises.realpath(path.dirname(assetPath)))) {
34
+ await reply.status(403).send();
35
+ return;
36
+ }
28
37
  const resolvedAssetPath = resolveAssetPath(assetPath, {
29
38
  platform: query.platform,
30
39
  preferNativePlatform: context.config.resolve.preferNativePlatform
31
40
  });
32
- const [assetData, { size }] = await Promise.all([fs.promises.readFile(resolvedAssetPath), fs.promises.stat(resolvedAssetPath)]);
41
+ const realAssetPath = await fs.promises.realpath(resolvedAssetPath);
42
+ if (!isWithin(realWorkspaceRoot, realAssetPath)) {
43
+ await reply.status(403).send();
44
+ return;
45
+ }
46
+ const [assetData, { size }] = await Promise.all([fs.promises.readFile(realAssetPath), fs.promises.stat(realAssetPath)]);
33
47
  await reply.header("Content-Type", mime.getType(resolvedAssetPath) ?? "").header("Content-Length", size).send(assetData);
34
48
  } catch (error) {
35
49
  fastify.log.error(error, "Failed to serve asset");
@@ -38,5 +52,19 @@ const plugin = fp((fastify, options) => {
38
52
  }
39
53
  });
40
54
  }, { name: "serve-assets" });
55
+ function isWithin(root, file) {
56
+ const relative = path.relative(root, file);
57
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
58
+ }
59
+ function findWorkspaceRoot(projectRoot) {
60
+ let directory = projectRoot;
61
+ while (true) {
62
+ const packageJson = path.join(directory, "package.json");
63
+ if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml")) || fs.existsSync(packageJson) && JSON.parse(fs.readFileSync(packageJson, "utf-8")).workspaces) return directory;
64
+ const parent = path.dirname(directory);
65
+ if (parent === directory) return projectRoot;
66
+ directory = parent;
67
+ }
68
+ }
41
69
  //#endregion
42
70
  export { plugin as serveAssets };
@@ -28,7 +28,7 @@ var FileStorage = class FileStorage {
28
28
  this.dataFilePath = path.join(FileStorage.getPath(basePath, { prepare: true }), DATA_FILENAME);
29
29
  if (fs.existsSync(this.dataFilePath)) {
30
30
  const loadedData = JSON.parse(fs.readFileSync(this.dataFilePath, "utf-8"));
31
- if ("version" in loadedData && loadedData.version === "1.0.0-alpha.30") this.data = loadedData;
31
+ if ("version" in loadedData && loadedData.version === "1.0.0-alpha.32") this.data = loadedData;
32
32
  else {
33
33
  this.data = createDefaultData();
34
34
  this.flush();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rollipop",
3
- "version": "1.0.0-alpha.30",
3
+ "version": "1.0.0-alpha.32",
4
4
  "homepage": "https://github.com/rollipop-dev/rollipop#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/rollipop-dev/rollipop/issues"
@@ -67,7 +67,7 @@
67
67
  "@react-native-community/cli-server-api": "20.2.0",
68
68
  "@react-native-community/cli-types": "20.2.0",
69
69
  "@react-native/dev-middleware": "0.87.1",
70
- "@rollipop/dashboard": "1.0.0-alpha.30",
70
+ "@rollipop/dashboard": "1.0.0-alpha.32",
71
71
  "@rollipop/rolldown": "1.0.32",
72
72
  "@swc/core": "1.15.43",
73
73
  "@swc/helpers": "0.5.23",
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
+ import { stripVTControlCharacters } from 'node:util';
2
3
  import vm from 'node:vm';
3
4
 
4
5
  import { transformSync } from '@swc/core';
5
- import prettyFormat from 'pretty-format';
6
+ import * as prettyFormat from 'pretty-format';
6
7
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test';
7
8
 
8
9
  import type { HMRContext } from '../../types/hmr';
@@ -85,6 +86,7 @@ class FakeDevRuntime {
85
86
  }
86
87
 
87
88
  class FakeWebSocket {
89
+ static readonly CONNECTING = 0;
88
90
  static readonly OPEN = 1;
89
91
  static instances: FakeWebSocket[] = [];
90
92
  readonly sent: string[] = [];
@@ -141,6 +143,148 @@ describe('HMR runtime', () => {
141
143
  expect((runtime.createModuleHotContext('entry.js') as any).runtime).toBe(runtime);
142
144
  });
143
145
 
146
+ describe('client logs', () => {
147
+ const formatter = { format: prettyFormat.format, plugins: prettyFormat.plugins };
148
+
149
+ it.each([
150
+ ['named-only', { __esModule: true, ...formatter }],
151
+ ['default-only', { __esModule: true, default: formatter }],
152
+ ['CommonJS', formatter],
153
+ [
154
+ 'named and callable default',
155
+ { __esModule: true, ...formatter, default: prettyFormat.format },
156
+ ],
157
+ ])('sends formatted values with %s pretty-format exports', async (_, module) => {
158
+ await createRuntime();
159
+ const client = createClient(module);
160
+ client.setup('ios', 'index.bundle', 'localhost', 8081, true);
161
+ const socket = FakeWebSocket.instances[0];
162
+ const circular: { self?: unknown } = {};
163
+ circular.self = circular;
164
+ const element = {
165
+ $$typeof: Symbol.for('react.element'),
166
+ type: 'View',
167
+ key: null,
168
+ props: { testID: 'counter', children: 'hello' },
169
+ };
170
+
171
+ client.log('warn', [
172
+ 'object: %s',
173
+ { answer: 42 },
174
+ [1, 2],
175
+ true,
176
+ null,
177
+ undefined,
178
+ new Error('boom'),
179
+ circular,
180
+ element,
181
+ ]);
182
+
183
+ expect(socket.sent).toHaveLength(1);
184
+ const message = JSON.parse(socket.sent[0]);
185
+ expect(message.type).toBe('hmr:log');
186
+ expect(message.level).toBe('warn');
187
+ expect(message.data.map(stripVTControlCharacters)).toEqual([
188
+ 'object: %s',
189
+ '{"answer": 42}',
190
+ '[1, 2]',
191
+ 'true',
192
+ 'null',
193
+ 'undefined',
194
+ '[Error: boom]',
195
+ '{"self": [Circular]}',
196
+ '<View testID="counter">hello</View>',
197
+ ]);
198
+ });
199
+
200
+ it('flushes logs from before setup and while connecting in order without replaying them', async () => {
201
+ const runtime = await createRuntime();
202
+ const client = createClient();
203
+ const circular: { self?: unknown } = {};
204
+ circular.self = circular;
205
+ client.log('info', ['before setup', circular]);
206
+
207
+ client.setup('ios', 'index.bundle', 'localhost', 8081, true);
208
+ const socket = FakeWebSocket.instances[0];
209
+ socket.readyState = FakeWebSocket.CONNECTING;
210
+ client.log('warn', ['while connecting', { count: 1 }, undefined]);
211
+ expect(socket.sent).toEqual([]);
212
+
213
+ socket.readyState = FakeWebSocket.OPEN;
214
+ socket.emit('open', {});
215
+ client.log('error', ['after opening', [1, 2]]);
216
+
217
+ const expectedLogs = [
218
+ { type: 'hmr:log', level: 'info', data: ['before setup', '{"self": [Circular]}'] },
219
+ { type: 'hmr:log', level: 'warn', data: ['while connecting', '{"count": 1}', 'undefined'] },
220
+ { type: 'hmr:log', level: 'error', data: ['after opening', '[1, 2]'] },
221
+ ];
222
+ expect(socket.sent.map((message) => JSON.parse(message))).toEqual([
223
+ {
224
+ type: 'hmr:connected',
225
+ clientId: runtime.clientId,
226
+ bundleEntry: 'index.bundle',
227
+ platform: 'ios',
228
+ },
229
+ ...expectedLogs,
230
+ ]);
231
+
232
+ socket.emit('open', {});
233
+ expect(
234
+ socket.sent.map((message) => JSON.parse(message)).filter(({ type }) => type === 'hmr:log'),
235
+ ).toEqual(expectedLogs);
236
+ });
237
+
238
+ it('keeps only the latest 100 logs while the socket is connecting', async () => {
239
+ const { client, socket } = await setupClient();
240
+ socket.readyState = FakeWebSocket.CONNECTING;
241
+
242
+ for (let index = 0; index < 105; index++) {
243
+ client.log('debug', [index]);
244
+ }
245
+ expect(socket.sent).toEqual([]);
246
+
247
+ socket.readyState = FakeWebSocket.OPEN;
248
+ socket.emit('open', {});
249
+
250
+ expect(socket.sent.slice(1).map((message) => JSON.parse(message))).toEqual(
251
+ Array.from({ length: 100 }, (_, index) => ({
252
+ type: 'hmr:log',
253
+ level: 'debug',
254
+ data: [String(index + 5)],
255
+ })),
256
+ );
257
+ });
258
+
259
+ it('waits for the host socket even when a remote socket opens first', async () => {
260
+ const { runtime, client, socket } = await setupClient();
261
+ socket.readyState = FakeWebSocket.CONNECTING;
262
+ client.log('info', ['host log', { count: 1 }]);
263
+ globalThis.__rollipop_runtime__!.registerGraph({
264
+ id: 'remote',
265
+ origin: 'http://localhost:8082',
266
+ bundleEntry: 'remote.bundle',
267
+ platform: 'ios',
268
+ runtime: { ...runtime, setup: vi.fn(), setEnabled: vi.fn() } as unknown as HMRGraphRuntime,
269
+ });
270
+ const remoteSocket = FakeWebSocket.instances[1];
271
+ remoteSocket.emit('open', {});
272
+ client.log('warn', ['host still connecting']);
273
+
274
+ expect(socket.sent).toEqual([]);
275
+ expect(remoteSocket.sent.map((message) => JSON.parse(message).type)).toEqual([
276
+ 'hmr:connected',
277
+ ]);
278
+
279
+ socket.readyState = FakeWebSocket.OPEN;
280
+ socket.emit('open', {});
281
+ expect(socket.sent.slice(1).map((message) => JSON.parse(message))).toEqual([
282
+ { type: 'hmr:log', level: 'info', data: ['host log', '{"count": 1}'] },
283
+ { type: 'hmr:log', level: 'warn', data: ['host still connecting'] },
284
+ ]);
285
+ });
286
+ });
287
+
144
288
  it('evaluates a patch, acknowledges delivery, and applies accepted updates', async () => {
145
289
  // oxlint-disable-next-line no-eval
146
290
  const evaluate = vi.fn((source: string) => (0, eval)(source));
@@ -506,9 +650,15 @@ async function setupRuntime() {
506
650
 
507
651
  async function setupClient(enabled = true) {
508
652
  const runtime = await createRuntime();
653
+ const client = createClient();
654
+ client.setup('ios', 'index.bundle', 'localhost', 8081, enabled);
655
+ return { runtime, client, socket: FakeWebSocket.instances[0] };
656
+ }
657
+
658
+ function createClient(prettyFormatExports: unknown = prettyFormat) {
509
659
  // HMRClient is injected into React Native, so provide its native imports in a sandbox.
510
660
  const nativeModules: Record<string, unknown> = {
511
- 'pretty-format': prettyFormat,
661
+ 'pretty-format': prettyFormatExports,
512
662
  '../LogBox/LogBox': { clearAllLogs: vi.fn() },
513
663
  '../NativeModules/specs/NativeRedBox': { dismiss: vi.fn() },
514
664
  './DevLoadingView': { hide: vi.fn(), showMessage: vi.fn() },
@@ -528,9 +678,7 @@ async function setupClient(enabled = true) {
528
678
  },
529
679
  };
530
680
  vm.runInNewContext(clientCode, context);
531
- const client = context.exports.default;
532
- client.setup('ios', 'index.bundle', 'localhost', 8081, enabled);
533
- return { runtime, client, socket: FakeWebSocket.instances[0] };
681
+ return context.exports.default;
534
682
  }
535
683
 
536
684
  function emitPatch(socket: FakeWebSocket, seq: number) {
@@ -1,4 +1,4 @@
1
- import prettyFormat from 'pretty-format';
1
+ import * as prettyFormatModule from 'pretty-format';
2
2
 
3
3
  import LogBox from '../LogBox/LogBox';
4
4
  import NativeRedBox from '../NativeModules/specs/NativeRedBox';
@@ -35,6 +35,16 @@ interface HMRConnection {
35
35
  pendingUpdatesCount: number;
36
36
  }
37
37
 
38
+ function getPrettyFormat() {
39
+ // This replaces RN's HMRClient and uses RN's pretty-format version,
40
+ // which may expose named exports or only a default export independently of Rollipop's version.
41
+ return typeof prettyFormatModule.format === 'function'
42
+ ? prettyFormatModule
43
+ : (prettyFormatModule.default as unknown as typeof prettyFormatModule);
44
+ }
45
+
46
+ const prettyFormat = getPrettyFormat();
47
+
38
48
  class HMRClient implements HMRClientNativeInterface {
39
49
  static readonly STARTUP_ERROR = 'Expected HMRClient.setup() call at startup';
40
50
  static readonly MAX_PENDING_LOGS = 100;
@@ -89,7 +99,7 @@ class HMRClient implements HMRClientNativeInterface {
89
99
 
90
100
  log(level: HMRClientLogLevel, data: any[]) {
91
101
  const host = this.getHostConnection();
92
- if (host == null) {
102
+ if (host == null || host.socket.readyState !== WebSocket.OPEN) {
93
103
  this.pendingLogs.push([level, data]);
94
104
  if (this.pendingLogs.length > HMRClient.MAX_PENDING_LOGS) {
95
105
  this.pendingLogs.shift();
@@ -226,10 +236,9 @@ class HMRClient implements HMRClientNativeInterface {
226
236
  ) {
227
237
  return;
228
238
  }
229
- for (const [level, data] of this.pendingLogs) {
230
- this.send(connection, { type: 'hmr:log', level, data });
239
+ for (const [level, data] of this.pendingLogs.splice(0)) {
240
+ this.log(level, data);
231
241
  }
232
- this.pendingLogs.length = 0;
233
242
  }
234
243
 
235
244
  private dismissRedbox() {
@@ -24,14 +24,3 @@ declare module '*/Platform' {
24
24
 
25
25
  export default Platform;
26
26
  }
27
-
28
- declare module 'pretty-format' {
29
- const prettyFormat: {
30
- format: (value: any, options?: any) => string;
31
- plugins: {
32
- ReactElement: any;
33
- };
34
- };
35
-
36
- export default prettyFormat;
37
- }