unframer 2.25.4 → 2.26.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.
Files changed (119) hide show
  1. package/dist/babel-jsx.d.ts +15 -0
  2. package/dist/babel-jsx.d.ts.map +1 -0
  3. package/dist/babel-jsx.js +223 -0
  4. package/dist/babel-jsx.js.map +1 -0
  5. package/dist/babel-plugin-imports.d.ts +0 -6
  6. package/dist/babel-plugin-imports.d.ts.map +1 -1
  7. package/dist/babel-plugin-imports.js +2 -135
  8. package/dist/babel-plugin-imports.js.map +1 -1
  9. package/dist/cli.d.ts +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +31 -6
  12. package/dist/cli.js.map +1 -1
  13. package/dist/css.js +13 -13
  14. package/dist/esbuild.d.ts.map +1 -1
  15. package/dist/esbuild.js +82 -66
  16. package/dist/esbuild.js.map +1 -1
  17. package/dist/example-code.test.js +39 -39
  18. package/dist/example-code.test.js.map +1 -1
  19. package/dist/exporter.d.ts.map +1 -1
  20. package/dist/exporter.js +137 -87
  21. package/dist/exporter.js.map +1 -1
  22. package/dist/flat-cache-interceptor.d.ts +27 -0
  23. package/dist/flat-cache-interceptor.d.ts.map +1 -0
  24. package/dist/flat-cache-interceptor.js +99 -0
  25. package/dist/flat-cache-interceptor.js.map +1 -0
  26. package/dist/framer.d.ts.map +1 -1
  27. package/dist/framer.js +895 -741
  28. package/dist/framer.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/react.d.ts.map +1 -1
  32. package/dist/react.js +15 -3
  33. package/dist/react.js.map +1 -1
  34. package/dist/sentry.d.ts +1 -1
  35. package/dist/sentry.d.ts.map +1 -1
  36. package/dist/sentry.js +2 -17
  37. package/dist/sentry.js.map +1 -1
  38. package/dist/undici-dispatcher.d.ts +2 -0
  39. package/dist/undici-dispatcher.d.ts.map +1 -0
  40. package/dist/undici-dispatcher.js +13 -0
  41. package/dist/undici-dispatcher.js.map +1 -0
  42. package/dist/utils.d.ts +3 -3
  43. package/dist/utils.d.ts.map +1 -1
  44. package/dist/utils.js +4 -10
  45. package/dist/utils.js.map +1 -1
  46. package/dist/utils.test.d.ts +2 -0
  47. package/dist/utils.test.d.ts.map +1 -0
  48. package/dist/utils.test.js +143 -0
  49. package/dist/utils.test.js.map +1 -0
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/esm/babel-jsx.d.ts +15 -0
  53. package/esm/babel-jsx.d.ts.map +1 -0
  54. package/esm/babel-jsx.js +219 -0
  55. package/esm/babel-jsx.js.map +1 -0
  56. package/esm/babel-plugin-imports.d.ts +0 -6
  57. package/esm/babel-plugin-imports.d.ts.map +1 -1
  58. package/esm/babel-plugin-imports.js +2 -134
  59. package/esm/babel-plugin-imports.js.map +1 -1
  60. package/esm/cli.d.ts +1 -0
  61. package/esm/cli.d.ts.map +1 -1
  62. package/esm/cli.js +31 -6
  63. package/esm/cli.js.map +1 -1
  64. package/esm/css.js +13 -13
  65. package/esm/esbuild.d.ts.map +1 -1
  66. package/esm/esbuild.js +82 -66
  67. package/esm/esbuild.js.map +1 -1
  68. package/esm/example-code.test.js +40 -40
  69. package/esm/example-code.test.js.map +1 -1
  70. package/esm/exporter.d.ts.map +1 -1
  71. package/esm/exporter.js +100 -50
  72. package/esm/exporter.js.map +1 -1
  73. package/esm/flat-cache-interceptor.d.ts +27 -0
  74. package/esm/flat-cache-interceptor.d.ts.map +1 -0
  75. package/esm/flat-cache-interceptor.js +95 -0
  76. package/esm/flat-cache-interceptor.js.map +1 -0
  77. package/esm/framer.d.ts.map +1 -1
  78. package/esm/framer.js +871 -729
  79. package/esm/framer.js.map +1 -1
  80. package/esm/index.d.ts +1 -1
  81. package/esm/index.d.ts.map +1 -1
  82. package/esm/react.d.ts.map +1 -1
  83. package/esm/react.js +15 -3
  84. package/esm/react.js.map +1 -1
  85. package/esm/sentry.d.ts +1 -1
  86. package/esm/sentry.d.ts.map +1 -1
  87. package/esm/sentry.js +2 -17
  88. package/esm/sentry.js.map +1 -1
  89. package/esm/undici-dispatcher.d.ts +2 -0
  90. package/esm/undici-dispatcher.d.ts.map +1 -0
  91. package/esm/undici-dispatcher.js +10 -0
  92. package/esm/undici-dispatcher.js.map +1 -0
  93. package/esm/utils.d.ts +3 -3
  94. package/esm/utils.d.ts.map +1 -1
  95. package/esm/utils.js +3 -9
  96. package/esm/utils.js.map +1 -1
  97. package/esm/utils.test.d.ts +2 -0
  98. package/esm/utils.test.d.ts.map +1 -0
  99. package/esm/utils.test.js +141 -0
  100. package/esm/utils.test.js.map +1 -0
  101. package/esm/version.d.ts +1 -1
  102. package/esm/version.js +1 -1
  103. package/package.json +8 -10
  104. package/src/babel-jsx.ts +277 -0
  105. package/src/babel-plugin-imports.ts +6 -169
  106. package/src/cli.ts +45 -6
  107. package/src/css.ts +13 -13
  108. package/src/esbuild.ts +93 -74
  109. package/src/example-code.test.ts +40 -41
  110. package/src/exporter.ts +124 -54
  111. package/src/flat-cache-interceptor.ts +114 -0
  112. package/src/framer.js +921 -764
  113. package/src/index.ts +1 -1
  114. package/src/react.tsx +15 -1
  115. package/src/sentry.ts +3 -22
  116. package/src/undici-dispatcher.ts +13 -0
  117. package/src/utils.test.ts +148 -0
  118. package/src/utils.ts +4 -17
  119. package/src/version.ts +1 -1
@@ -0,0 +1,277 @@
1
+ import type * as BabelTypes from '@babel/types'
2
+ import type { PluginObj } from '@babel/core'
3
+
4
+ const noContainerTypes = new Set([
5
+ 'JSXElement',
6
+ // 'StringLiteral',
7
+ 'NumericLiteral',
8
+ ])
9
+
10
+ export function removeJsxExpressionContainer({
11
+ types: t,
12
+ }: {
13
+ types: typeof BabelTypes
14
+ }): PluginObj {
15
+ const plugin: PluginObj = {
16
+ name: 'remove-jsx-expression-container',
17
+ visitor: {
18
+ JSXExpressionContainer: {
19
+ exit(path) {
20
+ const expr = path.node.expression
21
+
22
+ if (t.isJSXElement(expr) || t.isJSXFragment(expr)) {
23
+ path.replaceWith(expr)
24
+ } else if (t.isArrayExpression(expr)) {
25
+ // Check if array contains only JSX elements/fragments
26
+ const allJsx = expr.elements.every(
27
+ (element) =>
28
+ element &&
29
+ (t.isJSXElement(element) ||
30
+ t.isJSXFragment(element)),
31
+ )
32
+ if (allJsx && 'elements' in expr && expr.elements) {
33
+ try {
34
+ const fragment: BabelTypes.JSXFragment = {
35
+ type: 'JSXFragment',
36
+ openingFragment: {
37
+ type: 'JSXOpeningFragment',
38
+ },
39
+ closingFragment: {
40
+ type: 'JSXClosingFragment',
41
+ },
42
+ children: expr.elements.filter(isTruthy) as any,
43
+ }
44
+ path.replaceWith(fragment)
45
+ } catch (e) {
46
+ console.error(
47
+ `cannot remove expression container for`,
48
+ expr,
49
+ e,
50
+ )
51
+ }
52
+ }
53
+ }
54
+ },
55
+ },
56
+ },
57
+ }
58
+ return plugin
59
+ }
60
+
61
+ function isTruthy<T>(value: T): value is NonNullable<T> {
62
+ return value != null
63
+ }
64
+
65
+ export function babelPluginJsxTransform({
66
+ types: t,
67
+ }: {
68
+ types: typeof BabelTypes
69
+ }) {
70
+ const jsxFunctions = new Set<string>()
71
+
72
+ return {
73
+ name: 'jsx-transform',
74
+ visitor: {
75
+ ImportDeclaration(path) {
76
+ const source = path.node.source.value
77
+
78
+ // Track React JSX runtime imports
79
+ if (
80
+ source === 'react/jsx-runtime' ||
81
+ source === 'react/jsx-dev-runtime'
82
+ ) {
83
+ path.node.specifiers.forEach((specifier) => {
84
+ if (
85
+ t.isImportSpecifier(specifier) &&
86
+ t.isIdentifier(specifier.imported)
87
+ ) {
88
+ const importName = specifier.imported.name
89
+ const localName = specifier.local.name
90
+
91
+ // Track common JSX runtime functions
92
+ if (
93
+ importName === 'jsx' ||
94
+ importName === 'jsxs' ||
95
+ importName === 'Fragment'
96
+ ) {
97
+ jsxFunctions.add(localName)
98
+ }
99
+ }
100
+ })
101
+ }
102
+
103
+ // Track React imports that could be JSX functions
104
+ if (source === 'react') {
105
+ path.node.specifiers.forEach((specifier) => {
106
+ if (
107
+ t.isImportSpecifier(specifier) &&
108
+ t.isIdentifier(specifier.imported)
109
+ ) {
110
+ const importName = specifier.imported.name
111
+ const localName = specifier.local.name
112
+
113
+ // Track createElement and other JSX-related functions
114
+ if (
115
+ importName === 'createElement' ||
116
+ importName === 'Fragment'
117
+ ) {
118
+ jsxFunctions.add(localName)
119
+ }
120
+ }
121
+ })
122
+ }
123
+ },
124
+ CallExpression(path) {
125
+ // Check if it's a JSX function call
126
+ if (
127
+ !path.node.callee ||
128
+ !path.node.callee.name ||
129
+ !jsxFunctions.has(path.node.callee.name)
130
+ ) {
131
+ return
132
+ }
133
+
134
+ // Remove /* @__PURE__ */ comments
135
+ if (path.node.leadingComments) {
136
+ path.node.leadingComments =
137
+ path.node.leadingComments.filter(
138
+ (comment) => !comment.value.includes('@__PURE__'),
139
+ )
140
+ }
141
+
142
+ const [elementArg, propsArg] = path.node.arguments
143
+
144
+ // Get the element type name
145
+ let elementName = ''
146
+ if (elementArg.type === 'MemberExpression') {
147
+ elementName = `${elementArg.object.name}.${elementArg.property.name}`
148
+ } else if (elementArg.type === 'StringLiteral') {
149
+ elementName = elementArg.value
150
+ } else if (elementArg.type === 'Identifier') {
151
+ if (!canRenderAsJsx(elementArg.name)) {
152
+ return
153
+ }
154
+ elementName = elementArg.name
155
+ } else {
156
+ // Skip if we can't determine element name
157
+ return
158
+ }
159
+
160
+ // Convert to JSX element
161
+ const jsxElement: BabelTypes.JSXElement = {
162
+ type: 'JSXElement',
163
+ openingElement: {
164
+ type: 'JSXOpeningElement',
165
+ name: {
166
+ type: 'JSXIdentifier',
167
+ name: elementName,
168
+ },
169
+ attributes: [],
170
+ selfClosing: !propsArg.properties.find(
171
+ (p) => p.key?.name === 'children',
172
+ ),
173
+ },
174
+ closingElement: propsArg.properties.find(
175
+ (p) => p.key?.name === 'children',
176
+ )
177
+ ? {
178
+ type: 'JSXClosingElement',
179
+ name: {
180
+ type: 'JSXIdentifier',
181
+ name: elementName,
182
+ },
183
+ }
184
+ : null,
185
+ children: [],
186
+ }
187
+
188
+ // Add attributes
189
+ if (propsArg && propsArg.properties) {
190
+ propsArg.properties.forEach((prop) => {
191
+ if (prop.type === 'SpreadElement') {
192
+ jsxElement.openingElement.attributes.push({
193
+ type: 'JSXSpreadAttribute',
194
+ argument: prop.argument,
195
+ })
196
+ } else if (prop.key?.name === 'children') {
197
+ if (prop.value.type === 'ArrayExpression') {
198
+ jsxElement.children = prop.value.elements.map(
199
+ (element) => {
200
+ if (
201
+ noContainerTypes.has(element.type)
202
+ ) {
203
+ return element
204
+ }
205
+ return {
206
+ type: 'JSXExpressionContainer',
207
+ expression: element,
208
+ }
209
+ },
210
+ )
211
+ } else {
212
+ if (noContainerTypes.has(prop.value.type)) {
213
+ jsxElement.children = [prop.value]
214
+ } else {
215
+ jsxElement.children = [
216
+ {
217
+ type: 'JSXExpressionContainer',
218
+ expression: prop.value,
219
+ },
220
+ ]
221
+ }
222
+ }
223
+ } else {
224
+ let attrName = prop.key?.name
225
+ if (
226
+ !attrName &&
227
+ prop.key?.type === 'StringLiteral'
228
+ ) {
229
+ attrName = prop.key.value
230
+ }
231
+ if (!attrName) {
232
+ console.log(
233
+ `no prop.key?.name for ${JSON.stringify(
234
+ prop,
235
+ )}`,
236
+ )
237
+ return
238
+ }
239
+
240
+ jsxElement.openingElement.attributes.push({
241
+ type: 'JSXAttribute',
242
+ name: {
243
+ type: 'JSXIdentifier',
244
+ name: attrName,
245
+ },
246
+ value: {
247
+ type: 'JSXExpressionContainer',
248
+ expression: prop.value,
249
+ },
250
+ })
251
+ }
252
+ })
253
+ }
254
+
255
+ path.replaceWith(jsxElement)
256
+ },
257
+ },
258
+ }
259
+ }
260
+
261
+ function jsonStringifyWithMaps(map) {
262
+ return JSON.stringify(
263
+ [...map],
264
+ (key, value) => (value instanceof Map ? [...value] : value),
265
+ 2,
266
+ )
267
+ }
268
+
269
+ function canRenderAsJsx(name: string): boolean {
270
+ // 1. Valid JS identifier?
271
+ const isIdentifier = /^[$A-Za-z_][$\w]*$/.test(name)
272
+ if (!isIdentifier) return false
273
+
274
+ // 2. First char not lowercase letter?
275
+ const first = name[0]
276
+ return first.toUpperCase() === first
277
+ }
@@ -1,9 +1,11 @@
1
1
  import * as BabelTypes from '@babel/types'
2
2
 
3
3
  import { PluginObj } from '@babel/core'
4
- import { ImportDeclaration, ImportSpecifier, Identifier } from '@babel/types'
4
+ import type {
5
+ ImportDeclaration,
6
+ ImportSpecifier
7
+ } from '@babel/types'
5
8
  import BatchRenamer from './renamer'
6
- import { logger } from './utils'
7
9
 
8
10
  export function babelPluginDeduplicateImports({
9
11
  types: t,
@@ -116,7 +118,7 @@ export function babelPluginDeduplicateImports({
116
118
  },
117
119
  Program: {
118
120
  exit(path) {
119
- console.log(`renaming imports...`)
121
+ // console.log(`renaming imports...`)
120
122
  for (const [source, modMap] of importAliasMap) {
121
123
  // rename import names to consolidated names
122
124
  for (let [local, { consolidated, path: p }] of modMap) {
@@ -150,7 +152,7 @@ export function babelPluginDeduplicateImports({
150
152
 
151
153
  const definedImports = new Set<string>()
152
154
  const later = [] as Function[]
153
- console.log(`removing duplicates...`)
155
+ // console.log(`removing duplicates...`)
154
156
  for (let importDec of importDecs) {
155
157
  const source = importDec.source.value
156
158
 
@@ -274,168 +276,3 @@ export function babelPluginRenameExports({
274
276
  },
275
277
  }
276
278
  }
277
-
278
- // Set of types that don't need expression containers
279
- const noContainerTypes = new Set([
280
- 'JSXElement',
281
- // 'StringLiteral',
282
- 'NumericLiteral',
283
- ])
284
-
285
- export function babelPluginJsxTransform() {
286
- return {
287
- name: 'jsx-transform',
288
- visitor: {
289
- CallExpression(path) {
290
- // Check if it's a _jsx or _jsxs call
291
- if (
292
- !path.node.callee ||
293
- !path.node.callee.name?.startsWith('_jsx')
294
- ) {
295
- return
296
- }
297
-
298
- // Remove /* @__PURE__ */ comments
299
- if (path.node.leadingComments) {
300
- path.node.leadingComments =
301
- path.node.leadingComments.filter(
302
- (comment) => !comment.value.includes('@__PURE__'),
303
- )
304
- }
305
-
306
- const [elementArg, propsArg] = path.node.arguments
307
-
308
- // Get the element type name
309
- let elementName = ''
310
- if (elementArg.type === 'MemberExpression') {
311
- elementName = `${elementArg.object.name}.${elementArg.property.name}`
312
- } else if (elementArg.type === 'StringLiteral') {
313
- elementName = elementArg.value
314
- } else if (elementArg.type === 'Identifier') {
315
- elementName = elementArg.name
316
- } else {
317
- // Skip if we can't determine element name
318
- return
319
- }
320
-
321
- // Convert to JSX element
322
- const jsxElement: BabelTypes.JSXElement = {
323
- type: 'JSXElement',
324
- openingElement: {
325
- type: 'JSXOpeningElement',
326
- name: {
327
- type: 'JSXIdentifier',
328
- name: elementName,
329
- },
330
- attributes: [],
331
- selfClosing: !propsArg.properties.find(
332
- (p) => p.key?.name === 'children',
333
- ),
334
- },
335
- closingElement: propsArg.properties.find(
336
- (p) => p.key?.name === 'children',
337
- )
338
- ? {
339
- type: 'JSXClosingElement',
340
- name: {
341
- type: 'JSXIdentifier',
342
- name: elementName,
343
- },
344
- }
345
- : null,
346
- children: [],
347
- }
348
-
349
- // Add attributes
350
- if (propsArg && propsArg.properties) {
351
- propsArg.properties.forEach((prop) => {
352
- if (prop.type === 'SpreadElement') {
353
- jsxElement.openingElement.attributes.push({
354
- type: 'JSXSpreadAttribute',
355
- argument: prop.argument,
356
- })
357
- } else if (prop.key?.name === 'children') {
358
- if (prop.value.type === 'ArrayExpression') {
359
- jsxElement.children = prop.value.elements.map(
360
- (element) => {
361
- if (
362
- noContainerTypes.has(
363
- element.type,
364
- ) ||
365
- (element.type ===
366
- 'CallExpression' &&
367
- element.callee?.name?.startsWith(
368
- '_jsx',
369
- ))
370
- ) {
371
- return element
372
- }
373
- return {
374
- type: 'JSXExpressionContainer',
375
- expression: element,
376
- }
377
- },
378
- )
379
- } else {
380
- if (
381
- noContainerTypes.has(prop.value.type) ||
382
- (prop.value.type === 'CallExpression' &&
383
- prop.value.callee?.name?.startsWith(
384
- '_jsx',
385
- ))
386
- ) {
387
- jsxElement.children = [prop.value]
388
- } else {
389
- jsxElement.children = [
390
- {
391
- type: 'JSXExpressionContainer',
392
- expression: prop.value,
393
- },
394
- ]
395
- }
396
- }
397
- } else {
398
- let attrName = prop.key?.name
399
- if (
400
- !attrName &&
401
- prop.key?.type === 'StringLiteral'
402
- ) {
403
- attrName = prop.key.value
404
- }
405
- if (!attrName) {
406
- console.log(
407
- `no prop.key?.name for ${JSON.stringify(
408
- prop,
409
- )}`,
410
- )
411
- return
412
- }
413
-
414
- jsxElement.openingElement.attributes.push({
415
- type: 'JSXAttribute',
416
- name: {
417
- type: 'JSXIdentifier',
418
- name: attrName,
419
- },
420
- value: {
421
- type: 'JSXExpressionContainer',
422
- expression: prop.value,
423
- },
424
- })
425
- }
426
- })
427
- }
428
-
429
- path.replaceWith(jsxElement)
430
- },
431
- },
432
- }
433
- }
434
-
435
- function jsonStringifyWithMaps(map) {
436
- return JSON.stringify(
437
- [...map],
438
- (key, value) => (value instanceof Map ? [...value] : value),
439
- 2,
440
- )
441
- }
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { setMaxListeners } from 'events'
2
2
  import { fetch } from 'undici'
3
3
  import './sentry.js'
4
- import JSON from 'json5'
4
+
5
5
  import { bundle, StyleToken } from './exporter.js'
6
6
  import { createClient } from './generated/api-client.js'
7
7
 
@@ -12,13 +12,13 @@ import path, { basename } from 'path'
12
12
  import { BreakpointSizes, defaultBreakpointSizes } from './css.js'
13
13
  import {
14
14
  componentNameToPath,
15
- dispatcher,
16
15
  isTruthy,
17
16
  logger,
18
17
  sleep,
19
18
  spinner,
20
19
  } from './utils.js'
21
20
  import { notifyError } from './sentry.js'
21
+ import { dispatcher } from './undici-dispatcher.js'
22
22
  const configNames = ['unframer.config.json', 'unframer.json']
23
23
 
24
24
  export const cli = cac('unframer')
@@ -35,6 +35,9 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
35
35
  },
36
36
  )
37
37
  .option('--watch', 'Watch for changes and rebuild', { default: false })
38
+ .option('--jsx', 'Output jsx code instead of minified .js code', {
39
+ default: false,
40
+ })
38
41
  .option('--debug', 'Enable debug logging', { default: false })
39
42
  .action(async function main(projectId, options) {
40
43
  const external_ = options.external
@@ -59,8 +62,12 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
59
62
  outDir,
60
63
  projectId,
61
64
  })
65
+ let jsx = options.jsx
62
66
  const { rebuild, buildContext } = await bundle({
63
- config,
67
+ config: {
68
+ jsx,
69
+ ...config,
70
+ },
64
71
  watch,
65
72
  cwd,
66
73
  signal,
@@ -105,8 +112,8 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
105
112
  fixOldUnframerPath()
106
113
  const cwd = process.cwd()
107
114
  logger.log(`Looking for ${configNames.join(', ')} in ${cwd}`)
108
- const { findUp } = await import('find-up')
109
- const configPath = await findUp(configNames, { cwd })
115
+
116
+ const configPath = findUp(configNames, { cwd })
110
117
  if (!configPath) {
111
118
  logger.log(`No ${configNames.join(', ')} found`)
112
119
  return
@@ -117,7 +124,12 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
117
124
  logger.log(`No ${configBasename} contents found`)
118
125
  return
119
126
  }
120
- const config = JSON.parse(configContent)
127
+ const configContentWithoutComments = configContent.replace(
128
+ /^\s*\/\/.*$/gm,
129
+ '',
130
+ )
131
+
132
+ const config = JSON.parse(configContentWithoutComments)
121
133
  if (outDir !== defaultOutDir) {
122
134
  config.outDir = outDir
123
135
  }
@@ -177,6 +189,7 @@ cli.command('init', 'Init the unframer.config.json config').action(
177
189
  )
178
190
 
179
191
  export type Config = {
192
+ jsx?: boolean
180
193
  components: {
181
194
  [name: string]: string
182
195
  }
@@ -282,6 +295,7 @@ export async function configFromFetch({
282
295
  }) || []
283
296
  const config: Config = {
284
297
  ...data,
298
+
285
299
  breakpoints: defaultBreakpointSizes,
286
300
  outDir,
287
301
  externalPackages,
@@ -315,3 +329,28 @@ export async function configFromFetch({
315
329
  }
316
330
  return { websiteUrl, cwd, config }
317
331
  }
332
+
333
+ function findUp(
334
+ configNames: string[],
335
+ { cwd }: { cwd: string },
336
+ ): string | null {
337
+ let currentDir = cwd
338
+
339
+ while (true) {
340
+ for (const configName of configNames) {
341
+ const configPath = path.join(currentDir, configName)
342
+ if (fs.existsSync(configPath)) {
343
+ return configPath
344
+ }
345
+ }
346
+
347
+ const parentDir = path.dirname(currentDir)
348
+ if (parentDir === currentDir) {
349
+ // Reached the root directory
350
+ break
351
+ }
352
+ currentDir = parentDir
353
+ }
354
+
355
+ return null
356
+ }