@alexgorbatchev/pi-skill-library 0.1.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.
@@ -0,0 +1,549 @@
1
+ const noRegularCreateElementRule = {
2
+ meta: {
3
+ type: 'problem',
4
+ docs: {
5
+ description: 'Ban React createElement in regular application code; use JSX instead',
6
+ },
7
+ schema: [],
8
+ },
9
+ create(context) {
10
+ return {
11
+ ImportDeclaration(node) {
12
+ if (node.source?.value !== 'react') {
13
+ return;
14
+ }
15
+
16
+ node.specifiers.forEach((specifier) => {
17
+ if (specifier.type !== 'ImportSpecifier') {
18
+ return;
19
+ }
20
+
21
+ if (specifier.imported.type !== 'Identifier' || specifier.imported.name !== 'createElement') {
22
+ return;
23
+ }
24
+
25
+ context.report({
26
+ node: specifier,
27
+ message: 'Use JSX instead of importing createElement from react in regular application code.',
28
+ });
29
+ });
30
+ },
31
+ CallExpression(node) {
32
+ if (
33
+ node.callee.type !== 'MemberExpression'
34
+ || node.callee.computed
35
+ || node.callee.object.type !== 'Identifier'
36
+ || node.callee.object.name !== 'React'
37
+ || node.callee.property.type !== 'Identifier'
38
+ || node.callee.property.name !== 'createElement'
39
+ ) {
40
+ return;
41
+ }
42
+
43
+ context.report({
44
+ node,
45
+ message: 'Use JSX instead of React.createElement in regular application code.',
46
+ });
47
+ },
48
+ };
49
+ },
50
+ };
51
+
52
+ const requireComponentRootTestIdRule = {
53
+ meta: {
54
+ type: 'problem',
55
+ docs: {
56
+ description: 'Require exported React component roots to use ComponentName and child test ids to use ComponentName--thing',
57
+ },
58
+ schema: [],
59
+ },
60
+ create(context) {
61
+ return {
62
+ Program(program) {
63
+ const componentDefinitions = readComponentDefinitions(program);
64
+
65
+ componentDefinitions.forEach((componentDefinition) => {
66
+ const rootBranches = readRootBranchesForComponent(componentDefinition);
67
+ const rootNodes = new Set();
68
+
69
+ rootBranches.forEach((rootBranch) => {
70
+ rootBranch.testIdEntries.forEach((testIdEntry) => {
71
+ rootNodes.add(testIdEntry.node);
72
+ });
73
+
74
+ if (componentDefinition.isExported) {
75
+ reportInvalidExportedRoot(context, componentDefinition.name, rootBranch);
76
+ } else {
77
+ reportInvalidLocalRoot(context, componentDefinition.name, rootBranch);
78
+ }
79
+ });
80
+
81
+ const componentTestIdEntries = readComponentTestIdEntries(componentDefinition);
82
+ componentTestIdEntries.forEach((testIdEntry) => {
83
+ if (rootNodes.has(testIdEntry.node)) {
84
+ return;
85
+ }
86
+
87
+ testIdEntry.candidates.forEach((candidate) => {
88
+ if (isValidChildTestId(candidate, componentDefinition.name)) {
89
+ return;
90
+ }
91
+
92
+ context.report({
93
+ node: testIdEntry.node,
94
+ message:
95
+ `Component "${componentDefinition.name}" must use child test ids in the format "${componentDefinition.name}--thing". Received "${candidate}".`,
96
+ });
97
+ });
98
+ });
99
+ });
100
+ },
101
+ };
102
+ },
103
+ };
104
+
105
+ const plugin = {
106
+ meta: {
107
+ name: 'repo-react',
108
+ },
109
+ rules: {
110
+ 'no-regular-create-element': noRegularCreateElementRule,
111
+ 'require-component-root-testid': requireComponentRootTestIdRule,
112
+ },
113
+ };
114
+
115
+ export default plugin;
116
+
117
+ function readComponentDefinitions(program) {
118
+ return program.body.flatMap((statement) => readStatementComponentDefinitions(statement, false));
119
+ }
120
+
121
+ function readStatementComponentDefinitions(statement, isExported) {
122
+ if (statement.type === 'ExportNamedDeclaration') {
123
+ return statement.declaration ? readDeclarationComponentDefinitions(statement.declaration, true) : [];
124
+ }
125
+
126
+ if (statement.type === 'ExportDefaultDeclaration') {
127
+ return readDefaultComponentDefinitions(statement.declaration, true);
128
+ }
129
+
130
+ return readDeclarationComponentDefinitions(statement, isExported);
131
+ }
132
+
133
+ function readDeclarationComponentDefinitions(declaration, isExported) {
134
+ if (declaration.type === 'FunctionDeclaration') {
135
+ if (!declaration.id || !isPascalCase(declaration.id.name)) {
136
+ return [];
137
+ }
138
+
139
+ return [{ name: declaration.id.name, kind: 'function', node: declaration, isExported }];
140
+ }
141
+
142
+ if (declaration.type === 'ClassDeclaration') {
143
+ if (!declaration.id || !isPascalCase(declaration.id.name)) {
144
+ return [];
145
+ }
146
+
147
+ return [{ name: declaration.id.name, kind: 'class', node: declaration, isExported }];
148
+ }
149
+
150
+ if (declaration.type !== 'VariableDeclaration') {
151
+ return [];
152
+ }
153
+
154
+ return declaration.declarations.flatMap((declarator) => {
155
+ if (declarator.id.type !== 'Identifier' || !isPascalCase(declarator.id.name)) {
156
+ return [];
157
+ }
158
+
159
+ const componentNode = readWrappedFunctionLike(declarator.init);
160
+ if (!componentNode) {
161
+ return [];
162
+ }
163
+
164
+ return [{ name: declarator.id.name, kind: 'function', node: componentNode, isExported }];
165
+ });
166
+ }
167
+
168
+ function readDefaultComponentDefinitions(declaration, isExported) {
169
+ if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') {
170
+ if (!declaration.id || !isPascalCase(declaration.id.name)) {
171
+ return [];
172
+ }
173
+
174
+ return declaration.type === 'FunctionDeclaration'
175
+ ? [{ name: declaration.id.name, kind: 'function', node: declaration, isExported }]
176
+ : [{ name: declaration.id.name, kind: 'class', node: declaration, isExported }];
177
+ }
178
+
179
+ const componentNode = readWrappedFunctionLike(declaration);
180
+ if (!componentNode || !componentNode.id || !isPascalCase(componentNode.id.name)) {
181
+ return [];
182
+ }
183
+
184
+ return [{ name: componentNode.id.name, kind: 'function', node: componentNode, isExported }];
185
+ }
186
+
187
+ function readWrappedFunctionLike(initializer) {
188
+ if (!initializer) {
189
+ return null;
190
+ }
191
+
192
+ if (initializer.type === 'ArrowFunctionExpression' || initializer.type === 'FunctionExpression') {
193
+ return initializer;
194
+ }
195
+
196
+ if (initializer.type !== 'CallExpression') {
197
+ return null;
198
+ }
199
+
200
+ const wrappedInitializer = initializer.arguments[0];
201
+ if (!wrappedInitializer || wrappedInitializer.type === 'SpreadElement') {
202
+ return null;
203
+ }
204
+
205
+ return readWrappedFunctionLike(wrappedInitializer);
206
+ }
207
+
208
+ function readRootBranchesForComponent(componentDefinition) {
209
+ const returnExpressions = componentDefinition.kind === 'class'
210
+ ? readClassRenderReturnExpressions(componentDefinition.node)
211
+ : readFunctionReturnExpressions(componentDefinition.node);
212
+
213
+ return returnExpressions.flatMap((returnExpression) => readRootBranches(returnExpression));
214
+ }
215
+
216
+ function readClassRenderReturnExpressions(classDeclaration) {
217
+ const renderMethod = classDeclaration.body.body.find((member) => {
218
+ return member.type === 'MethodDefinition'
219
+ && !member.computed
220
+ && member.key.type === 'Identifier'
221
+ && member.key.name === 'render';
222
+ });
223
+
224
+ if (!renderMethod || !renderMethod.value.body) {
225
+ return [];
226
+ }
227
+
228
+ return readReturnExpressionsFromBlock(renderMethod.value.body, renderMethod.value);
229
+ }
230
+
231
+ function readFunctionReturnExpressions(functionNode) {
232
+ if (!functionNode.body) {
233
+ return [];
234
+ }
235
+
236
+ if (functionNode.body.type !== 'BlockStatement') {
237
+ return [functionNode.body];
238
+ }
239
+
240
+ return readReturnExpressionsFromBlock(functionNode.body, functionNode);
241
+ }
242
+
243
+ function readReturnExpressionsFromBlock(blockNode, rootFunctionNode) {
244
+ const returnExpressions = [];
245
+
246
+ visitNode(blockNode);
247
+ return returnExpressions;
248
+
249
+ function visitNode(node) {
250
+ if (!isAstNode(node)) {
251
+ return;
252
+ }
253
+
254
+ if (node !== rootFunctionNode && isNestedFunctionNode(node)) {
255
+ return;
256
+ }
257
+
258
+ if (node !== rootFunctionNode && isNestedClassNode(node)) {
259
+ return;
260
+ }
261
+
262
+ if (node.type === 'ReturnStatement') {
263
+ if (node.argument) {
264
+ returnExpressions.push(node.argument);
265
+ }
266
+ return;
267
+ }
268
+
269
+ readChildNodes(node).forEach(visitNode);
270
+ }
271
+ }
272
+
273
+ function readRootBranches(expression) {
274
+ const unwrappedExpression = unwrapExpression(expression);
275
+
276
+ if (unwrappedExpression.type === 'ConditionalExpression') {
277
+ return [
278
+ ...readRootBranches(unwrappedExpression.consequent),
279
+ ...readRootBranches(unwrappedExpression.alternate),
280
+ ];
281
+ }
282
+
283
+ if (unwrappedExpression.type === 'LogicalExpression' && unwrappedExpression.operator === '&&') {
284
+ return readRootBranches(unwrappedExpression.right);
285
+ }
286
+
287
+ if (isNullLiteral(unwrappedExpression)) {
288
+ return [{ kind: 'null', node: unwrappedExpression, testIdEntries: [] }];
289
+ }
290
+
291
+ if (unwrappedExpression.type === 'JSXFragment') {
292
+ return [{ kind: 'fragment', node: unwrappedExpression, testIdEntries: [] }];
293
+ }
294
+
295
+ if (unwrappedExpression.type === 'JSXElement') {
296
+ return [{
297
+ kind: 'jsx',
298
+ node: unwrappedExpression,
299
+ testIdEntries: readJsxAttributeTestIdEntries(unwrappedExpression.openingElement.attributes),
300
+ }];
301
+ }
302
+
303
+ if (unwrappedExpression.type === 'JSXSelfClosingElement') {
304
+ return [{
305
+ kind: 'jsx',
306
+ node: unwrappedExpression,
307
+ testIdEntries: readJsxAttributeTestIdEntries(unwrappedExpression.attributes),
308
+ }];
309
+ }
310
+
311
+ return [{ kind: 'other', node: unwrappedExpression, testIdEntries: [], summary: summarizeNode(unwrappedExpression) }];
312
+ }
313
+
314
+ function readComponentTestIdEntries(componentDefinition) {
315
+ const componentBody = componentDefinition.kind === 'class'
316
+ ? readClassRenderBody(componentDefinition.node)
317
+ : componentDefinition.node.body;
318
+
319
+ if (!componentBody) {
320
+ return [];
321
+ }
322
+
323
+ return readTestIdEntriesFromNode(componentBody, componentBody);
324
+ }
325
+
326
+ function readClassRenderBody(classDeclaration) {
327
+ const renderMethod = classDeclaration.body.body.find((member) => {
328
+ return member.type === 'MethodDefinition'
329
+ && !member.computed
330
+ && member.key.type === 'Identifier'
331
+ && member.key.name === 'render';
332
+ });
333
+
334
+ return renderMethod?.value.body ?? null;
335
+ }
336
+
337
+ function readTestIdEntriesFromNode(rootNode, boundaryNode) {
338
+ const testIdEntries = [];
339
+
340
+ visitNode(rootNode);
341
+ return testIdEntries;
342
+
343
+ function visitNode(node) {
344
+ if (!isAstNode(node)) {
345
+ return;
346
+ }
347
+
348
+ if (node !== boundaryNode && isNestedFunctionNode(node)) {
349
+ return;
350
+ }
351
+
352
+ if (node !== boundaryNode && isNestedClassNode(node)) {
353
+ return;
354
+ }
355
+
356
+ if (node.type === 'JSXElement') {
357
+ testIdEntries.push(...readJsxAttributeTestIdEntries(node.openingElement.attributes));
358
+ node.children.forEach(visitNode);
359
+ return;
360
+ }
361
+
362
+ if (node.type === 'JSXSelfClosingElement') {
363
+ testIdEntries.push(...readJsxAttributeTestIdEntries(node.attributes));
364
+ return;
365
+ }
366
+
367
+ readChildNodes(node).forEach(visitNode);
368
+ }
369
+ }
370
+
371
+ function readJsxAttributeTestIdEntries(attributesNode) {
372
+ return attributesNode.filter((attribute) => {
373
+ return attribute.type === 'JSXAttribute' && isTestIdAttributeName(readJsxAttributeName(attribute.name));
374
+ }).map((attribute) => ({
375
+ node: attribute,
376
+ candidates: readTestIdCandidatesFromJsxAttribute(attribute),
377
+ }));
378
+ }
379
+
380
+ function readTestIdCandidatesFromJsxAttribute(attribute) {
381
+ const initializer = attribute.value;
382
+ if (!initializer) {
383
+ return [];
384
+ }
385
+
386
+ if (initializer.type === 'Literal' && typeof initializer.value === 'string') {
387
+ return [initializer.value];
388
+ }
389
+
390
+ if (
391
+ initializer.type !== 'JSXExpressionContainer' || !initializer.expression
392
+ || initializer.expression.type === 'JSXEmptyExpression'
393
+ ) {
394
+ return [];
395
+ }
396
+
397
+ return readExpressionStringCandidates(initializer.expression);
398
+ }
399
+
400
+ function readExpressionStringCandidates(expression) {
401
+ const unwrappedExpression = unwrapExpression(expression);
402
+
403
+ if (unwrappedExpression.type === 'Literal') {
404
+ return typeof unwrappedExpression.value === 'string' ? [unwrappedExpression.value] : [];
405
+ }
406
+
407
+ if (unwrappedExpression.type === 'TemplateLiteral') {
408
+ if (unwrappedExpression.expressions.length !== 0 || unwrappedExpression.quasis.length !== 1) {
409
+ return [];
410
+ }
411
+
412
+ const cookedValue = unwrappedExpression.quasis[0]?.value.cooked;
413
+ return typeof cookedValue === 'string' ? [cookedValue] : [];
414
+ }
415
+
416
+ if (unwrappedExpression.type === 'ConditionalExpression') {
417
+ return [
418
+ ...readExpressionStringCandidates(unwrappedExpression.consequent),
419
+ ...readExpressionStringCandidates(unwrappedExpression.alternate),
420
+ ];
421
+ }
422
+
423
+ return [];
424
+ }
425
+
426
+ function reportInvalidExportedRoot(context, componentName, rootBranch) {
427
+ if (rootBranch.kind === 'null') {
428
+ return;
429
+ }
430
+
431
+ if (rootBranch.kind === 'fragment') {
432
+ context.report({
433
+ node: rootBranch.node,
434
+ message: `Exported component "${componentName}" returns a fragment root; wrap it in a DOM element with data-testid="${componentName}".`,
435
+ });
436
+ return;
437
+ }
438
+
439
+ if (rootBranch.kind === 'other') {
440
+ context.report({
441
+ node: rootBranch.node,
442
+ message: `Exported component "${componentName}" returns ${rootBranch.summary}; render a root data-testid="${componentName}" instead.`,
443
+ });
444
+ return;
445
+ }
446
+
447
+ const hasExactRootTestId = rootBranch.testIdEntries.some((testIdEntry) => {
448
+ return testIdEntry.candidates.some((candidate) => candidate === componentName);
449
+ });
450
+
451
+ if (hasExactRootTestId) {
452
+ return;
453
+ }
454
+
455
+ context.report({
456
+ node: rootBranch.testIdEntries[0]?.node ?? rootBranch.node,
457
+ message: `Exported component "${componentName}" must render a root data-testid or testId exactly equal to "${componentName}".`,
458
+ });
459
+ }
460
+
461
+ function reportInvalidLocalRoot(context, componentName, rootBranch) {
462
+ if (rootBranch.kind === 'null' || rootBranch.testIdEntries.length === 0) {
463
+ return;
464
+ }
465
+
466
+ rootBranch.testIdEntries.forEach((testIdEntry) => {
467
+ testIdEntry.candidates.forEach((candidate) => {
468
+ if (candidate === componentName) {
469
+ return;
470
+ }
471
+
472
+ context.report({
473
+ node: testIdEntry.node,
474
+ message: `Component "${componentName}" must use the plain root test id "${componentName}" on its root element. Received "${candidate}".`,
475
+ });
476
+ });
477
+ });
478
+ }
479
+
480
+ function isValidChildTestId(value, componentName) {
481
+ return new RegExp(`^${componentName}--[a-z0-9]+(?:-[a-z0-9]+)*$`, 'u').test(value);
482
+ }
483
+
484
+ function unwrapExpression(expression) {
485
+ let currentExpression = expression;
486
+
487
+ while (currentExpression.type === 'ParenthesizedExpression') {
488
+ currentExpression = currentExpression.expression;
489
+ }
490
+
491
+ return currentExpression;
492
+ }
493
+
494
+ function isTestIdAttributeName(attributeName) {
495
+ return attributeName === 'data-testid' || attributeName === 'testId';
496
+ }
497
+
498
+ function readJsxAttributeName(attributeName) {
499
+ if (attributeName.type === 'JSXIdentifier') {
500
+ return attributeName.name;
501
+ }
502
+
503
+ if (attributeName.type === 'JSXNamespacedName') {
504
+ return `${attributeName.namespace.name}:${attributeName.name.name}`;
505
+ }
506
+
507
+ return '';
508
+ }
509
+
510
+ function summarizeNode(node) {
511
+ const rawName = node.type.replace(/Expression$/u, ' expression');
512
+ return rawName.toLowerCase();
513
+ }
514
+
515
+ function readChildNodes(node) {
516
+ return Object.entries(node).flatMap(([key, value]) => {
517
+ if (key === 'parent' || key === 'loc' || key === 'range') {
518
+ return [];
519
+ }
520
+
521
+ if (Array.isArray(value)) {
522
+ return value.filter(isAstNode);
523
+ }
524
+
525
+ return isAstNode(value) ? [value] : [];
526
+ });
527
+ }
528
+
529
+ function isNestedFunctionNode(node) {
530
+ return node.type === 'FunctionDeclaration'
531
+ || node.type === 'FunctionExpression'
532
+ || node.type === 'ArrowFunctionExpression';
533
+ }
534
+
535
+ function isNestedClassNode(node) {
536
+ return node.type === 'ClassDeclaration' || node.type === 'ClassExpression';
537
+ }
538
+
539
+ function isAstNode(value) {
540
+ return value !== null && typeof value === 'object' && typeof value.type === 'string';
541
+ }
542
+
543
+ function isNullLiteral(node) {
544
+ return node.type === 'Literal' && node.value === null;
545
+ }
546
+
547
+ function isPascalCase(value) {
548
+ return /^[A-Z][A-Za-z0-9]*$/u.test(value);
549
+ }
@@ -0,0 +1,152 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ const PLUGIN_FILE_PATH = path.resolve(import.meta.dir, 'reactPolicyPlugin.js');
8
+
9
+ describe('reactPolicyPlugin', () => {
10
+ test('accepts valid root and child test ids', () => {
11
+ const result = runOxlint(`
12
+ export function Valid() {
13
+ return (
14
+ <section data-testid='Valid'>
15
+ <div data-testid='Valid--body'>Ready</div>
16
+ </section>
17
+ );
18
+ }
19
+ `);
20
+
21
+ expect(result.exitCode).toBe(0);
22
+ expect(result.messages).toEqual([]);
23
+ });
24
+
25
+ test('reports imported createElement usage', () => {
26
+ const result = runOxlint(`
27
+ import { createElement } from 'react';
28
+
29
+ export function Broken() {
30
+ return createElement('div', { 'data-testid': 'Broken' });
31
+ }
32
+ `, {
33
+ rules: {
34
+ 'repo-react/no-regular-create-element': 'error',
35
+ },
36
+ });
37
+
38
+ expect(result.exitCode).toBe(1);
39
+ expect(result.messages).toEqual([
40
+ {
41
+ code: 'repo-react(no-regular-create-element)',
42
+ message: 'Use JSX instead of importing createElement from react in regular application code.',
43
+ },
44
+ ]);
45
+ });
46
+
47
+ test('reports React.createElement usage', () => {
48
+ const result = runOxlint(`
49
+ export function Broken() {
50
+ return React.createElement('div', { 'data-testid': 'Broken' });
51
+ }
52
+ `, {
53
+ rules: {
54
+ 'repo-react/no-regular-create-element': 'error',
55
+ },
56
+ });
57
+
58
+ expect(result.exitCode).toBe(1);
59
+ expect(result.messages).toEqual([
60
+ {
61
+ code: 'repo-react(no-regular-create-element)',
62
+ message: 'Use JSX instead of React.createElement in regular application code.',
63
+ },
64
+ ]);
65
+ });
66
+
67
+ test('reports root and child test id violations', () => {
68
+ const result = runOxlint(`
69
+ export function Broken() {
70
+ return (
71
+ <div data-testid='Broken--root'>
72
+ <button data-testid='Wrong--action'>Action</button>
73
+ </div>
74
+ );
75
+ }
76
+ `);
77
+
78
+ expect(result.exitCode).toBe(1);
79
+ expect(result.messages).toEqual([
80
+ {
81
+ code: 'repo-react(require-component-root-testid)',
82
+ message: 'Exported component "Broken" must render a root data-testid or testId exactly equal to "Broken".',
83
+ },
84
+ {
85
+ code: 'repo-react(require-component-root-testid)',
86
+ message: 'Component "Broken" must use child test ids in the format "Broken--thing". Received "Wrong--action".',
87
+ },
88
+ ]);
89
+ });
90
+ });
91
+
92
+ interface IOxlintResult {
93
+ exitCode: number;
94
+ messages: Array<{
95
+ code: string;
96
+ message: string;
97
+ }>;
98
+ }
99
+
100
+ function runOxlint(
101
+ fileContents: string,
102
+ options: {
103
+ rules?: Record<string, 'error'>;
104
+ fileName?: string;
105
+ } = {},
106
+ ): IOxlintResult {
107
+ const tempDir = mkdtempSync(path.join(tmpdir(), 'react-policy-plugin-'));
108
+
109
+ try {
110
+ const fixtureFilePath = path.join(tempDir, options.fileName ?? 'fixture.tsx');
111
+ const configFilePath = path.join(tempDir, '.oxlintrc.json');
112
+
113
+ writeFileSync(fixtureFilePath, fileContents.trimStart());
114
+ writeFileSync(
115
+ configFilePath,
116
+ JSON.stringify({
117
+ jsPlugins: [PLUGIN_FILE_PATH],
118
+ rules: options.rules ?? {
119
+ 'repo-react/require-component-root-testid': 'error',
120
+ },
121
+ }),
122
+ );
123
+
124
+ const spawnResult = Bun.spawnSync({
125
+ cmd: [process.execPath, 'x', 'oxlint', '-c', configFilePath, '-f', 'json', fixtureFilePath],
126
+ stdout: 'pipe',
127
+ stderr: 'pipe',
128
+ });
129
+
130
+ const stdout = new TextDecoder().decode(spawnResult.stdout);
131
+ const stderr = new TextDecoder().decode(spawnResult.stderr);
132
+
133
+ assert.equal(stderr, '');
134
+
135
+ const payload = JSON.parse(stdout) as {
136
+ diagnostics?: Array<{
137
+ code?: string;
138
+ message?: string;
139
+ }>;
140
+ };
141
+
142
+ return {
143
+ exitCode: spawnResult.exitCode,
144
+ messages: (payload.diagnostics ?? []).map((diagnostic) => ({
145
+ code: diagnostic.code ?? '',
146
+ message: diagnostic.message ?? '',
147
+ })),
148
+ };
149
+ } finally {
150
+ rmSync(tempDir, { recursive: true, force: true });
151
+ }
152
+ }