rollipop 1.0.0-alpha.30 → 1.0.0-alpha.31

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.31";
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`;
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.31";
3
3
  //#endregion
4
4
  export { version };
@@ -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.31",
28
28
  packageName: "rollipop",
29
29
  homepage: "https://github.com/rollipop-dev/rollipop",
30
30
  description: "React Native development dashboard powered by Rollipop.",
@@ -16,7 +16,7 @@ async function setupDevframe(context, server) {
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.31",
20
20
  base: ROLLIPOP_DEVFRAME_BASE,
21
21
  cwd: context.config.root,
22
22
  ui: createUi({
@@ -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.31") 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.31",
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.31",
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
- }