@openrewrite/rewrite 8.62.1 → 8.62.3

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@openrewrite/rewrite",
3
- "version": "8.62.1",
3
+ "version": "8.62.3",
4
4
  "license": "Moderne Source Available License",
5
5
  "description": "OpenRewrite JavaScript.",
6
6
  "homepage": "https://github.com/openrewrite/rewrite",
@@ -170,16 +170,63 @@ export class JavaScriptTypeMapping {
170
170
  if (ts.isPropertyAccessExpression(node.expression)) {
171
171
  methodName = node.expression.name.getText();
172
172
  const exprType = this.checker.getTypeAtLocation(node.expression.expression);
173
- declaringType = this.getType(exprType) as Type.FullyQualified;
173
+ const mappedType = this.getType(exprType);
174
+
175
+ // For string methods like 'hello'.split(), ensure we have a proper declaring type
176
+ if (!mappedType || mappedType.kind !== Type.Kind.Class) {
177
+ // If the expression type is a primitive string, use lib.String as declaring type
178
+ const typeString = this.checker.typeToString(exprType);
179
+ if (typeString === 'string' || exprType.flags & ts.TypeFlags.String || exprType.flags & ts.TypeFlags.StringLiteral) {
180
+ declaringType = {
181
+ kind: Type.Kind.Class,
182
+ fullyQualifiedName: 'lib.String'
183
+ } as Type.FullyQualified;
184
+ } else if (typeString === 'number' || exprType.flags & ts.TypeFlags.Number || exprType.flags & ts.TypeFlags.NumberLiteral) {
185
+ declaringType = {
186
+ kind: Type.Kind.Class,
187
+ fullyQualifiedName: 'lib.Number'
188
+ } as Type.FullyQualified;
189
+ } else {
190
+ // Fallback for other primitive types or unknown
191
+ declaringType = Type.unknownType as Type.FullyQualified;
192
+ }
193
+ } else {
194
+ declaringType = mappedType as Type.FullyQualified;
195
+ }
174
196
  } else if (ts.isIdentifier(node.expression)) {
175
197
  methodName = node.expression.getText();
176
- // For standalone functions, use the symbol's parent or module
177
- const parent = (symbol as any).parent;
178
- if (parent) {
179
- const parentType = this.checker.getDeclaredTypeOfSymbol(parent);
180
- declaringType = this.getType(parentType) as Type.FullyQualified;
198
+ // For standalone functions, we need to determine the appropriate declaring type
199
+ const exprType = this.checker.getTypeAtLocation(node.expression);
200
+ const funcType = this.getType(exprType);
201
+
202
+ if (funcType && funcType.kind === Type.Kind.Class) {
203
+ const fqn = (funcType as Type.Class).fullyQualifiedName;
204
+ const lastDot = fqn.lastIndexOf('.');
205
+
206
+ if (lastDot > 0) {
207
+ // For functions from modules, use the module part as declaring type
208
+ // Examples:
209
+ // - "node.assert" -> declaring type: "node"
210
+ // - "@types/lodash.map" -> declaring type: "@types/lodash"
211
+ // - "@types/express.express" -> declaring type: "@types/express"
212
+ declaringType = {
213
+ kind: Type.Kind.Class,
214
+ fullyQualifiedName: fqn.substring(0, lastDot)
215
+ } as Type.FullyQualified;
216
+ } else {
217
+ // No dots in the name - the type IS the module itself
218
+ // This handles single-name modules like "axios", "lodash" etc.
219
+ declaringType = funcType as Type.FullyQualified;
220
+ }
181
221
  } else {
182
- declaringType = Type.unknownType as Type.FullyQualified;
222
+ // Try to use the symbol's parent or module
223
+ const parent = (symbol as any).parent;
224
+ if (parent) {
225
+ const parentType = this.checker.getDeclaredTypeOfSymbol(parent);
226
+ declaringType = this.getType(parentType) as Type.FullyQualified;
227
+ } else {
228
+ declaringType = Type.unknownType as Type.FullyQualified;
229
+ }
183
230
  }
184
231
  } else {
185
232
  methodName = symbol.getName();
@@ -330,6 +377,30 @@ export class JavaScriptTypeMapping {
330
377
  if (sourceFile.isDeclarationFile || fileName.includes("node_modules")) {
331
378
  const packageName = this.extractPackageName(fileName);
332
379
  if (packageName) {
380
+ // Special handling for @types/node - these are Node.js built-in modules
381
+ // and should be mapped to "node.*" instead of "@types/node.*"
382
+ if (packageName === "@types/node") {
383
+ // Extract the module name from the file path
384
+ // e.g., /node_modules/@types/node/assert.d.ts -> assert
385
+ // e.g., /node_modules/@types/node/fs/promises.d.ts -> fs/promises
386
+ const nodeMatch = fileName.match(/node_modules\/@types\/node\/([^.]+)\.d\.ts/);
387
+ if (nodeMatch) {
388
+ const modulePath = nodeMatch[1];
389
+ // For default exports from Node modules, we want the module to be the "class"
390
+ // But we still need to include the type name for proper identification
391
+ if (typeName === "default" || typeName === modulePath) {
392
+ // This is likely the default export, just use the module name
393
+ return `node.${modulePath}`;
394
+ }
395
+ // For named exports, include both module and type name
396
+ if (modulePath.includes('/')) {
397
+ return `node.${modulePath.replace(/\//g, '.')}.${typeName}`;
398
+ }
399
+ return `node.${modulePath}.${typeName}`;
400
+ }
401
+ // Fallback for @types/node types that don't match the pattern
402
+ return `node.${typeName}`;
403
+ }
333
404
  return `${packageName}.${typeName}`;
334
405
  }
335
406
  }
@@ -392,8 +463,22 @@ export class JavaScriptTypeMapping {
392
463
  const typesMatch = fileName.match(/node_modules\/@types\/([^/]+)/);
393
464
  if (typesMatch) {
394
465
  const packageName = typesMatch[1];
395
- // Replace the module specifier part with @types/package
396
- fullyQualifiedName = fullyQualifiedName.replace(/^[^.]+\./, `@types/${packageName}.`);
466
+ // Special handling for @types/node - use "node" prefix instead
467
+ if (packageName === "node") {
468
+ // Extract the module name from the file path if possible
469
+ const nodeMatch = fileName.match(/node_modules\/@types\/node\/([^.]+)\.d\.ts/);
470
+ if (nodeMatch) {
471
+ const modulePath = nodeMatch[1];
472
+ // Replace the module specifier with node.module
473
+ fullyQualifiedName = fullyQualifiedName.replace(/^[^.]+\./, `node.${modulePath}.`);
474
+ } else {
475
+ // Fallback: just use "node" prefix
476
+ fullyQualifiedName = fullyQualifiedName.replace(/^[^.]+\./, `node.`);
477
+ }
478
+ } else {
479
+ // Replace the module specifier part with @types/package
480
+ fullyQualifiedName = fullyQualifiedName.replace(/^[^.]+\./, `@types/${packageName}.`);
481
+ }
397
482
  }
398
483
  }
399
484
  }
package/src/print.ts CHANGED
@@ -143,11 +143,13 @@ export class TreePrinters {
143
143
  * @param target Helps to determine what kind of language we are dealing with when
144
144
  * printing a subtree whose LST type is shared between multiple languages in a language family.
145
145
  */
146
- static printer(target: Cursor | SourceFile): TreePrinter {
147
- const sourceFileKind = (isSourceFile(target) ?
146
+ static printer(target: Cursor | SourceFile | string): TreePrinter {
147
+ const sourceFileKind = typeof target === 'string' ?
148
+ target :
149
+ (isSourceFile(target) ?
148
150
  target as SourceFile :
149
151
  target.firstEnclosing(isSourceFile)
150
- )!.kind
152
+ )!.kind
151
153
 
152
154
  if (!this._registry.has(sourceFileKind)) {
153
155
  throw new Error(`No printer registered for ${sourceFileKind}`)
@@ -160,6 +162,6 @@ export class TreePrinters {
160
162
  }
161
163
  }
162
164
 
163
- export function printer(target: Cursor | SourceFile): TreePrinter {
165
+ export function printer(target: Cursor | SourceFile | string): TreePrinter {
164
166
  return TreePrinters.printer(target);
165
167
  }
@@ -0,0 +1,373 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import * as v8 from 'v8';
19
+ import * as inspector from 'inspector';
20
+
21
+ export class ChromeProfiler {
22
+ private traceEvents: any[] = [];
23
+ private memoryInterval?: NodeJS.Timeout;
24
+ private saveInterval?: NodeJS.Timeout;
25
+ private session?: inspector.Session;
26
+ private readonly pid = process.pid;
27
+ private readonly tid = 1; // Main thread
28
+ private readonly tracePath: string;
29
+ private exitHandlersRegistered = false;
30
+ private startTime: number = 0;
31
+ private ttsCounter: number = 0;
32
+ private lastProfileTime: number = 0;
33
+ private profileNodes = new Map();
34
+
35
+ constructor(outputPath?: string) {
36
+ this.tracePath = outputPath || path.join(process.cwd(), 'chrome-trace.json');
37
+ }
38
+
39
+ async start() {
40
+ this.startTime = Date.now() * 1000; // Convert to microseconds
41
+ this.lastProfileTime = this.startTime;
42
+
43
+ // Add initial metadata events
44
+ this.addMetadataEvents();
45
+
46
+ // Start V8 Inspector session for CPU profiling
47
+ this.session = new inspector.Session();
48
+ this.session.connect();
49
+
50
+ // Enable and start CPU profiling
51
+ await this.enableCpuProfiling();
52
+
53
+ // Start collecting memory data
54
+ this.startMemoryTracking();
55
+
56
+ // Save trace periodically and collect CPU profile samples
57
+ this.saveInterval = setInterval(async () => {
58
+ await this.collectCpuProfile();
59
+ this.saveTrace();
60
+ }, 10000); // Save every 10 seconds
61
+
62
+ // Register exit handlers
63
+ if (!this.exitHandlersRegistered) {
64
+ this.registerExitHandlers();
65
+ this.exitHandlersRegistered = true;
66
+ }
67
+ }
68
+
69
+ private async enableCpuProfiling() {
70
+ if (!this.session) return;
71
+
72
+ // Enable profiler
73
+ await new Promise<void>((resolve, reject) => {
74
+ this.session!.post('Profiler.enable', (err) => {
75
+ if (err) reject(err);
76
+ else resolve();
77
+ });
78
+ });
79
+
80
+ // Start sampling
81
+ await new Promise<void>((resolve, reject) => {
82
+ this.session!.post('Profiler.start', {
83
+ // Sample at high frequency for detailed profiling
84
+ samplingInterval: 100
85
+ }, (err) => {
86
+ if (err) reject(err);
87
+ else resolve();
88
+ });
89
+ });
90
+
91
+ // Subscribe to console profile events
92
+ this.session.on('Profiler.consoleProfileStarted', (params: any) => {
93
+ this.traceEvents.push({
94
+ cat: 'disabled-by-default-devtools.timeline',
95
+ name: 'Profile',
96
+ ph: 'P',
97
+ id: params.id || '1',
98
+ pid: this.pid,
99
+ tid: this.tid,
100
+ ts: Date.now() * 1000
101
+ });
102
+ });
103
+ }
104
+
105
+ private addMetadataEvents() {
106
+ // Thread name metadata
107
+ this.traceEvents.push({
108
+ args: { name: 'CrRendererMain' },
109
+ cat: '__metadata',
110
+ name: 'thread_name',
111
+ ph: 'M',
112
+ pid: this.pid,
113
+ tid: this.tid,
114
+ ts: 0
115
+ });
116
+
117
+ // Process name metadata
118
+ this.traceEvents.push({
119
+ args: { name: 'Node.js' },
120
+ cat: '__metadata',
121
+ name: 'process_name',
122
+ ph: 'M',
123
+ pid: this.pid,
124
+ tid: 0,
125
+ ts: 0
126
+ });
127
+
128
+ // TracingStartedInBrowser event - required for Chrome DevTools
129
+ this.traceEvents.push({
130
+ args: {
131
+ data: {
132
+ frameTreeNodeId: 1,
133
+ frames: [{
134
+ frame: '0x1',
135
+ name: '',
136
+ processId: this.pid,
137
+ url: 'node://process'
138
+ }],
139
+ persistentIds: true
140
+ }
141
+ },
142
+ cat: 'disabled-by-default-devtools.timeline',
143
+ name: 'TracingStartedInBrowser',
144
+ ph: 'I',
145
+ pid: this.pid,
146
+ s: 't',
147
+ tid: this.tid,
148
+ ts: this.startTime,
149
+ tts: this.ttsCounter++
150
+ });
151
+ }
152
+
153
+ private startMemoryTracking() {
154
+ // Collect memory data every 50ms (matching Chrome's frequency)
155
+ this.memoryInterval = setInterval(() => {
156
+ const memStats = v8.getHeapStatistics();
157
+ const timestamp = Date.now() * 1000;
158
+
159
+ // UpdateCounters event with correct format
160
+ this.traceEvents.push({
161
+ args: {
162
+ data: {
163
+ jsHeapSizeUsed: memStats.used_heap_size,
164
+ jsEventListeners: 0,
165
+ documents: 1,
166
+ nodes: 0
167
+ }
168
+ },
169
+ cat: 'disabled-by-default-devtools.timeline',
170
+ name: 'UpdateCounters',
171
+ ph: 'I', // Instant event, not Counter
172
+ pid: this.pid,
173
+ s: 't', // Required for instant events
174
+ tid: this.tid,
175
+ ts: timestamp,
176
+ tts: this.ttsCounter++
177
+ });
178
+ }, 50); // Every 50ms
179
+ }
180
+
181
+ private saveTrace() {
182
+ const trace = {
183
+ metadata: {
184
+ 'command-line': process.argv.join(' '),
185
+ 'cpu-brand': 'Node.js V8',
186
+ 'dataOrigin': 'TraceEvents',
187
+ 'highres-ticks': true,
188
+ 'hostname': 'localhost',
189
+ 'num-cpus': require('os').cpus().length,
190
+ 'physical-memory': require('os').totalmem(),
191
+ 'platform': process.platform,
192
+ 'process-uptime': process.uptime(),
193
+ 'product-version': `Node.js ${process.version}`,
194
+ 'protocol-version': '1.0',
195
+ 'source': 'NodeProfiler',
196
+ 'startTime': new Date(this.startTime / 1000).toISOString(),
197
+ 'trace-config': '',
198
+ 'user-agent': `Node.js/${process.version}`,
199
+ 'v8-version': process.versions.v8
200
+ },
201
+ traceEvents: this.traceEvents
202
+ };
203
+
204
+ try {
205
+ fs.writeFileSync(this.tracePath, JSON.stringify(trace, null, 2));
206
+ } catch (e) {
207
+ // Ignore write errors
208
+ }
209
+ }
210
+
211
+ private async collectCpuProfile() {
212
+ if (!this.session) return;
213
+
214
+ try {
215
+ // Stop current profiling to get samples
216
+ const profile = await new Promise<any>((resolve, reject) => {
217
+ this.session!.post('Profiler.stop', (err, params) => {
218
+ if (err) reject(err);
219
+ else resolve(params.profile);
220
+ });
221
+ });
222
+
223
+ // Convert CPU profile samples to trace events
224
+ if (profile && profile.samples) {
225
+ this.addCpuProfileSamples(profile);
226
+ }
227
+
228
+ // Restart profiling for the next interval
229
+ await new Promise<void>((resolve, reject) => {
230
+ this.session!.post('Profiler.start', {
231
+ samplingInterval: 100
232
+ }, (err) => {
233
+ if (err) reject(err);
234
+ else resolve();
235
+ });
236
+ });
237
+ } catch (e) {
238
+ // Ignore errors and try to restart
239
+ try {
240
+ await new Promise<void>((resolve, reject) => {
241
+ this.session!.post('Profiler.start', {
242
+ samplingInterval: 100
243
+ }, (err) => {
244
+ if (err) reject(err);
245
+ else resolve();
246
+ });
247
+ });
248
+ } catch (e2) {
249
+ // Ignore restart errors
250
+ }
251
+ }
252
+ }
253
+
254
+ async stop() {
255
+ // Clear intervals
256
+ if (this.memoryInterval) {
257
+ clearInterval(this.memoryInterval);
258
+ this.memoryInterval = undefined;
259
+ }
260
+ if (this.saveInterval) {
261
+ clearInterval(this.saveInterval);
262
+ this.saveInterval = undefined;
263
+ }
264
+
265
+ // Collect final CPU profile
266
+ await this.collectCpuProfile();
267
+
268
+ // Disconnect session
269
+ if (this.session) {
270
+ this.session.disconnect();
271
+ this.session = undefined;
272
+ }
273
+
274
+ // Save final trace
275
+ this.saveTrace();
276
+ }
277
+
278
+ private addCpuProfileSamples(profile: any) {
279
+ if (!profile.samples || !profile.timeDeltas || !profile.nodes) return;
280
+
281
+ // Use the last profile time as starting point to maintain continuity
282
+ let currentTime = this.lastProfileTime;
283
+
284
+ // Update nodes map with new nodes
285
+ profile.nodes.forEach((node: any) => {
286
+ this.profileNodes.set(node.id, node);
287
+ });
288
+
289
+ // Convert samples to trace events with actual function names
290
+ profile.samples.forEach((nodeId: number, index: number) => {
291
+ const node = this.profileNodes.get(nodeId);
292
+ if (!node) return;
293
+
294
+ currentTime += (profile.timeDeltas[index] || 0);
295
+
296
+ const callFrame = node.callFrame;
297
+ if (callFrame) {
298
+ // Clean up function name for display
299
+ let functionName = callFrame.functionName || '(anonymous)';
300
+ if (functionName === '' || functionName === '(root)') {
301
+ functionName = '(program)';
302
+ }
303
+
304
+ // Extract filename from URL or use a meaningful default
305
+ let url = callFrame.url || '';
306
+ let fileName: string;
307
+
308
+ if (url) {
309
+ // Clean up the URL for display
310
+ if (url.startsWith('file://')) {
311
+ url = url.substring(7);
312
+ }
313
+ const parts = url.split('/');
314
+ fileName = parts[parts.length - 1] || url;
315
+
316
+ // Special handling for node internals
317
+ if (url.startsWith('node:')) {
318
+ fileName = url;
319
+ }
320
+ } else {
321
+ // No URL - try to provide context from function name
322
+ if (functionName === '(garbage collector)') {
323
+ fileName = 'v8::gc';
324
+ } else if (functionName === '(idle)') {
325
+ fileName = 'v8::idle';
326
+ } else if (functionName === '(program)') {
327
+ fileName = 'main';
328
+ } else if (functionName.includes('::')) {
329
+ // C++ internal function
330
+ fileName = 'native';
331
+ } else {
332
+ // JavaScript code without source mapping
333
+ fileName = 'javascript';
334
+ }
335
+ }
336
+
337
+ this.traceEvents.push({
338
+ args: {
339
+ data: {
340
+ columnNumber: callFrame.columnNumber || 0,
341
+ frame: `0x${nodeId.toString(16)}`,
342
+ functionName: functionName,
343
+ lineNumber: callFrame.lineNumber || 0,
344
+ scriptId: String(callFrame.scriptId || 0),
345
+ url: url || fileName // Use fileName as URL if no real URL
346
+ }
347
+ },
348
+ cat: 'devtools.timeline',
349
+ dur: Math.max(profile.timeDeltas[index] || 1, 1),
350
+ name: 'FunctionCall',
351
+ ph: 'X',
352
+ pid: this.pid,
353
+ tid: this.tid,
354
+ ts: Math.floor(currentTime),
355
+ tts: this.ttsCounter++
356
+ });
357
+ }
358
+ });
359
+
360
+ // Update lastProfileTime to maintain continuity for the next batch
361
+ this.lastProfileTime = currentTime;
362
+ }
363
+
364
+ private registerExitHandlers() {
365
+ const cleanup = async () => {
366
+ await this.stop();
367
+ };
368
+
369
+ process.once('beforeExit', cleanup);
370
+ process.once('SIGINT', cleanup);
371
+ process.once('SIGTERM', cleanup);
372
+ }
373
+ }
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import * as rpc from "vscode-jsonrpc/node";
17
- import {Cursor, isSourceFile, Tree} from "../../tree";
17
+ import {isSourceFile, Tree} from "../../tree";
18
18
  import {MarkerPrinter as PrintMarkerPrinter, printer, PrintOutputCapture} from "../../print";
19
19
  import {UUID} from "../../uuid";
20
20
 
@@ -25,20 +25,18 @@ export const enum MarkerPrinter {
25
25
  }
26
26
 
27
27
  export class Print {
28
- constructor(private readonly treeId: UUID, private readonly cursor?: string[], readonly markerPrinter: MarkerPrinter = MarkerPrinter.DEFAULT) {
28
+ constructor(private readonly treeId: UUID, private readonly sourceFileType: string, readonly markerPrinter: MarkerPrinter = MarkerPrinter.DEFAULT) {
29
29
  }
30
30
 
31
31
  static handle(connection: rpc.MessageConnection,
32
- getObject: (id: string) => any,
33
- getCursor: (cursorIds: string[] | undefined) => Promise<Cursor>): void {
32
+ getObject: (id: string) => any): void {
34
33
  connection.onRequest(new rpc.RequestType<Print, string, Error>("Print"), async request => {
35
34
  const tree: Tree = await getObject(request.treeId.toString());
36
35
  const out = new PrintOutputCapture(PrintMarkerPrinter[request.markerPrinter]);
37
36
  if (isSourceFile(tree)) {
38
37
  return await printer(tree).print(tree, out);
39
38
  } else {
40
- const cursor = await getCursor(request.cursor);
41
- return await printer(cursor).print(tree, out);
39
+ return await printer(request.sourceFileType).print(tree, out);
42
40
  }
43
41
  });
44
42
  }
@@ -82,7 +82,7 @@ export class RewriteRpc {
82
82
  GetLanguages.handle(this.connection);
83
83
  PrepareRecipe.handle(this.connection, registry, preparedRecipes);
84
84
  Parse.handle(this.connection, this.localObjects);
85
- Print.handle(this.connection, getObject, getCursor);
85
+ Print.handle(this.connection, getObject);
86
86
  InstallRecipes.handle(this.connection, options.recipeInstallDir ?? ".rewrite", registry, options.logger);
87
87
 
88
88
  this.connection.listen();
@@ -160,7 +160,8 @@ export class RewriteRpc {
160
160
  this.localObjects.set(tree.id.toString(), tree);
161
161
  return await this.connection.sendRequest(
162
162
  new rpc.RequestType<Print, string, Error>("Print"),
163
- new Print(tree.id, this.getCursorIds(cursor))
163
+ new Print(tree.id, isSourceFile(tree) ? tree.kind :
164
+ cursor!.firstEnclosing(t => isSourceFile(t))!.kind)
164
165
  );
165
166
  }
166
167
 
package/src/rpc/server.ts CHANGED
@@ -20,6 +20,7 @@ import {RewriteRpc} from "./rewrite-rpc";
20
20
  import * as fs from "fs";
21
21
  import {Command} from 'commander';
22
22
  import {dir} from 'tmp-promise';
23
+ import {ChromeProfiler} from './chrome-profiler';
23
24
 
24
25
  // Include all languages you want this server to support.
25
26
  import "../text";
@@ -37,6 +38,7 @@ interface ProgramOptions {
37
38
  traceGetObjectOutput?: boolean;
38
39
  traceGetObjectInput?: boolean;
39
40
  recipeInstallDir?: string;
41
+ profile?: boolean;
40
42
  }
41
43
 
42
44
  async function main() {
@@ -49,10 +51,18 @@ async function main() {
49
51
  .option('--trace-get-object-output', 'enable `GetObject` output tracing')
50
52
  .option('--trace-get-object-input', 'enable `GetObject` input tracing')
51
53
  .option('--recipe-install-dir <install_dir>', 'Recipe installation directory (default is a temporary directory)')
54
+ .option('--profile', 'enable profiling')
52
55
  .parse();
53
56
 
54
57
  const options = program.opts() as ProgramOptions;
55
58
 
59
+ // Chrome profiling
60
+ let profiler: ChromeProfiler | undefined;
61
+ if (options.profile) {
62
+ profiler = new ChromeProfiler();
63
+ profiler.start().catch(console.error);
64
+ }
65
+
56
66
  let recipeInstallDir: string;
57
67
  if (!options.recipeInstallDir) {
58
68
  let recipeCleanup: () => Promise<void>;
@@ -65,6 +75,9 @@ async function main() {
65
75
 
66
76
  // Register cleanup on exit
67
77
  process.on('SIGINT', async () => {
78
+ if (profiler) {
79
+ await profiler.stop();
80
+ }
68
81
  if (recipeCleanup) {
69
82
  await recipeCleanup();
70
83
  }
@@ -72,6 +85,9 @@ async function main() {
72
85
  });
73
86
 
74
87
  process.on('SIGTERM', async () => {
88
+ if (profiler) {
89
+ await profiler.stop();
90
+ }
75
91
  if (recipeCleanup) {
76
92
  await recipeCleanup();
77
93
  }
@@ -81,6 +97,19 @@ async function main() {
81
97
  recipeInstallDir = await setupRecipeDir();
82
98
  } else {
83
99
  recipeInstallDir = options.recipeInstallDir;
100
+
101
+ // Register cleanup for profiler when no recipe cleanup is needed
102
+ if (profiler) {
103
+ process.on('SIGINT', async () => {
104
+ await profiler.stop();
105
+ process.exit(0);
106
+ });
107
+
108
+ process.on('SIGTERM', async () => {
109
+ await profiler.stop();
110
+ process.exit(0);
111
+ });
112
+ }
84
113
  }
85
114
 
86
115
  const log = options.logFile ? fs.createWriteStream(options.logFile, {flags: 'a'}) : undefined;