frappe-ui 1.0.0-beta.68 → 1.0.0-beta.69

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 (113) hide show
  1. package/experimental/Calendar/Calendar.vue +2 -2
  2. package/experimental/Calendar/stories/CustomHeader.vue +2 -2
  3. package/package.json +7 -1
  4. package/scripts/migrate-list-v1.js +436 -0
  5. package/scripts/migrate-navigation-v1.js +300 -0
  6. package/scripts/migrate-overlays-v1.js +519 -0
  7. package/src/components/Combobox/Combobox.cy.ts +2 -6
  8. package/src/components/Combobox/Combobox.md +4 -0
  9. package/src/components/Combobox/Combobox.vue +6 -1
  10. package/src/components/Combobox/types.ts +2 -0
  11. package/src/components/ContextMenu/ContextMenu.api.md +36 -0
  12. package/src/components/ContextMenu/ContextMenu.cy.ts +141 -29
  13. package/src/components/ContextMenu/ContextMenu.md +5 -0
  14. package/src/components/ContextMenu/ContextMenu.vue +81 -8
  15. package/src/components/ContextMenu/types.ts +19 -7
  16. package/src/components/DatePicker/DatePicker.api.md +0 -7
  17. package/src/components/DatePicker/DatePicker.cy.ts +11 -15
  18. package/src/components/DatePicker/DatePicker.md +4 -0
  19. package/src/components/DatePicker/DatePicker.vue +17 -6
  20. package/src/components/DatePicker/DateRangePicker.cy.ts +22 -8
  21. package/src/components/DatePicker/DateRangePicker.vue +4 -1
  22. package/src/components/DatePicker/DateTimePicker.cy.ts +4 -4
  23. package/src/components/DatePicker/DateTimePicker.vue +5 -3
  24. package/src/components/DatePicker/stories/Examples.vue +2 -2
  25. package/src/components/DatePicker/stories/Range.vue +3 -3
  26. package/src/components/DatePicker/types.ts +15 -5
  27. package/src/components/DesktopShell/stories/Default.vue +1 -1
  28. package/src/components/Dropdown/Dropdown.api.md +30 -0
  29. package/src/components/Dropdown/Dropdown.cy.ts +23 -3
  30. package/src/components/Dropdown/Dropdown.md +3 -0
  31. package/src/components/Dropdown/Dropdown.vue +11 -4
  32. package/src/components/Dropdown/types.ts +20 -9
  33. package/src/components/HoverCard/HoverCard.api.md +5 -5
  34. package/src/components/HoverCard/HoverCard.cy.ts +88 -1
  35. package/src/components/HoverCard/HoverCard.md +2 -2
  36. package/src/components/HoverCard/HoverCard.vue +65 -18
  37. package/src/components/HoverCard/index.ts +1 -0
  38. package/src/components/HoverCard/stories/Delays.vue +1 -1
  39. package/src/components/HoverCard/types.ts +11 -4
  40. package/src/components/Menu/Menu.api.md +1 -1
  41. package/src/components/Menu/Menu.vue +3 -3
  42. package/src/components/Menu/MenuItemContent.vue +2 -2
  43. package/src/components/Menu/types.test.ts +49 -0
  44. package/src/components/Menu/types.ts +9 -10
  45. package/src/components/MobileNav/MobileNavItem.vue +5 -20
  46. package/src/components/MultiSelect/MultiSelect.cy.ts +4 -1
  47. package/src/components/MultiSelect/MultiSelect.md +2 -2
  48. package/src/components/MultiSelect/MultiSelect.vue +6 -1
  49. package/src/components/MultiSelect/types.ts +2 -0
  50. package/src/components/Popover/Popover.api.md +5 -5
  51. package/src/components/Popover/Popover.cy.ts +9 -6
  52. package/src/components/Popover/Popover.md +3 -3
  53. package/src/components/Popover/Popover.vue +4 -9
  54. package/src/components/Popover/types.ts +2 -2
  55. package/src/components/Select/Select.cy.ts +2 -6
  56. package/src/components/Select/Select.md +2 -2
  57. package/src/components/Select/Select.vue +6 -0
  58. package/src/components/Select/types.ts +2 -0
  59. package/src/components/SettingsDialog/SettingsDialog.api.md +4 -4
  60. package/src/components/SettingsDialog/SettingsDialog.cy.ts +84 -1
  61. package/src/components/SettingsDialog/SettingsDialog.vue +15 -15
  62. package/src/components/SettingsDialog/index.ts +1 -0
  63. package/src/components/SettingsDialog/types.ts +3 -2
  64. package/src/components/Sidebar/Sidebar.api.md +5 -4
  65. package/src/components/Sidebar/Sidebar.cy.ts +22 -8
  66. package/src/components/Sidebar/Sidebar.md +2 -2
  67. package/src/components/Sidebar/Sidebar.vue +2 -2
  68. package/src/components/Sidebar/SidebarItem.vue +2 -2
  69. package/src/components/Sidebar/stories/Default.vue +1 -1
  70. package/src/components/Sidebar/stories/Section.vue +1 -1
  71. package/src/components/Sidebar/types.ts +4 -7
  72. package/src/components/SidebarRail/SidebarRail.api.md +3 -3
  73. package/src/components/SidebarRail/SidebarRail.cy.ts +88 -14
  74. package/src/components/SidebarRail/SidebarRail.md +4 -2
  75. package/src/components/SidebarRail/SidebarRailItem.vue +46 -41
  76. package/src/components/SidebarRail/types.ts +2 -2
  77. package/src/components/TabButtons/TabButtons.api.md +2 -2
  78. package/src/components/TabButtons/TabButtons.cy.ts +6 -6
  79. package/src/components/TabButtons/TabButtons.md +1 -1
  80. package/src/components/TabButtons/TabButtons.vue +4 -4
  81. package/src/components/TabButtons/types.ts +2 -2
  82. package/src/components/Tabs/TabTrigger.vue +2 -4
  83. package/src/components/Tabs/Tabs.cy.ts +3 -3
  84. package/src/components/Tabs/Tabs.md +1 -1
  85. package/src/components/Tabs/types.ts +1 -1
  86. package/src/components/TimePicker/TimePicker.api.md +1 -1
  87. package/src/components/TimePicker/TimePicker.cy.ts +48 -62
  88. package/src/components/TimePicker/TimePicker.md +4 -1
  89. package/src/components/TimePicker/TimePicker.vue +27 -15
  90. package/src/components/Tooltip/Tooltip.api.md +6 -6
  91. package/src/components/Tooltip/Tooltip.cy.ts +5 -5
  92. package/src/components/Tooltip/Tooltip.playground.vue +2 -2
  93. package/src/components/Tooltip/Tooltip.vue +6 -4
  94. package/src/components/Tooltip/TooltipProvider.vue +5 -5
  95. package/src/components/Tooltip/stories/Group.vue +1 -1
  96. package/src/components/Tooltip/types.ts +1 -1
  97. package/src/components/shared/picker/PickerShell.vue +11 -14
  98. package/src/components/shared/picker/types.ts +7 -3
  99. package/src/composables/useKeyboardShortcut.spec.ts +40 -1
  100. package/src/composables/useKeyboardShortcut.ts +19 -10
  101. package/src/molecules/list/List.cy.ts +146 -11
  102. package/src/molecules/list/ListGroup.vue +5 -5
  103. package/src/molecules/list/ListHeaderCellSort.vue +11 -9
  104. package/src/molecules/list/ListRowBase.vue +2 -2
  105. package/src/molecules/list/ListRows.vue +41 -18
  106. package/src/molecules/list/index.ts +0 -4
  107. package/src/molecules/list/list.api.md +14 -8
  108. package/src/molecules/list/list.md +17 -10
  109. package/src/molecules/list/stories/Columns.vue +3 -3
  110. package/src/molecules/list/stories/Feed.vue +5 -2
  111. package/src/molecules/list/style.css +2 -2
  112. package/src/molecules/list/types.ts +0 -8
  113. package/src/molecules/list/useVirtualRows.ts +46 -27
@@ -27,13 +27,13 @@
27
27
  @update:modelValue="(val) => onMonthYearChange(val)"
28
28
  :clearable="false"
29
29
  >
30
- <template #trigger="{ toggle }">
30
+ <template #trigger="{ open, setOpen }">
31
31
  <Button
32
32
  variant="ghost"
33
33
  class="text-lg-medium text-ink-gray-7"
34
34
  :label="currentMonthYear"
35
35
  iconRight="lucide-chevron-down"
36
- @click="toggle"
36
+ @click="setOpen(!open)"
37
37
  />
38
38
  </template>
39
39
  </DatePicker>
@@ -110,13 +110,13 @@ const events = ref([
110
110
  @update:modelValue="(val) => headerProps.onMonthYearChange(val)"
111
111
  :clearable="false"
112
112
  >
113
- <template #trigger="{ toggle }">
113
+ <template #trigger="{ setOpen, open }">
114
114
  <Button
115
115
  variant="ghost"
116
116
  class="text-lg-medium text-ink-gray-7"
117
117
  :label="headerProps.currentMonthYear"
118
118
  iconRight="lucide-chevron-down"
119
- @click="toggle"
119
+ @click="setOpen(!open)"
120
120
  />
121
121
  </template>
122
122
  </DatePicker>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "1.0.0-beta.68",
3
+ "version": "1.0.0-beta.69",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -20,6 +20,9 @@
20
20
  },
21
21
  "bin": {
22
22
  "base-props-v1": "./scripts/migrate-base-props-v1.js",
23
+ "list-v1": "./scripts/migrate-list-v1.js",
24
+ "navigation-v1": "./scripts/migrate-navigation-v1.js",
25
+ "overlays-v1": "./scripts/migrate-overlays-v1.js",
23
26
  "tokens-v2": "./tailwind/migrate-tokens-v2.js",
24
27
  "shortcuts-v1": "./scripts/migrate-shortcuts-v1.js",
25
28
  "editor-v1": "./scripts/migrate-editor-v1.js",
@@ -107,6 +110,9 @@
107
110
  "scripts/migrate-shortcuts-v1.js",
108
111
  "scripts/migrate-base-props-v1.js",
109
112
  "scripts/migrate-editor-v1.js",
113
+ "scripts/migrate-list-v1.js",
114
+ "scripts/migrate-navigation-v1.js",
115
+ "scripts/migrate-overlays-v1.js",
110
116
  "experimental",
111
117
  "experimental.ts",
112
118
  "tsconfig.base.json"
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Migrate the list family's pre-v1 row selectors and slot names.
4
+ *
5
+ * Usage: list-v1 [--dry-run] <dir-or-file...>
6
+ */
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const USAGE = 'Usage: list-v1 [--dry-run] <dir-or-file...>'
12
+ const EXTENSIONS = new Set([
13
+ '.css',
14
+ '.html',
15
+ '.js',
16
+ '.jsx',
17
+ '.md',
18
+ '.ts',
19
+ '.tsx',
20
+ '.vue',
21
+ ])
22
+ const COMPONENTS = new Set(['ListGroup', 'ListHeaderCellSort', 'ListRow'])
23
+ const VOID_ELEMENTS = new Set([
24
+ 'area',
25
+ 'base',
26
+ 'br',
27
+ 'col',
28
+ 'embed',
29
+ 'hr',
30
+ 'img',
31
+ 'input',
32
+ 'link',
33
+ 'meta',
34
+ 'source',
35
+ 'track',
36
+ 'wbr',
37
+ ])
38
+
39
+ const kebab = (name) =>
40
+ name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
41
+
42
+ function knownComponent(name) {
43
+ return [...COMPONENTS].find(
44
+ (component) => name === component || name === kebab(component),
45
+ )
46
+ }
47
+
48
+ function lineAt(source, index) {
49
+ return source.slice(0, index).split('\n').length
50
+ }
51
+
52
+ // Only the start of an import has to be code. Its module string is expected to
53
+ // be masked, and checking every character would reject every real import.
54
+ function codeMask(source, { templateStrings = true } = {}) {
55
+ const mask = new Uint8Array(source.length)
56
+ let i = 0
57
+ while (i < source.length) {
58
+ const char = source[i]
59
+ const next = source[i + 1]
60
+ if (char === '/' && next === '/') {
61
+ const end = source.indexOf('\n', i + 2)
62
+ const stop = end === -1 ? source.length : end
63
+ mask.fill(1, i, stop)
64
+ i = stop
65
+ continue
66
+ }
67
+ if (char === '/' && next === '*') {
68
+ const end = source.indexOf('*/', i + 2)
69
+ const stop = end === -1 ? source.length : end + 2
70
+ mask.fill(1, i, stop)
71
+ i = stop
72
+ continue
73
+ }
74
+ if (
75
+ char === "'" ||
76
+ char === '"' ||
77
+ (templateStrings && char === '`')
78
+ ) {
79
+ const quote = char
80
+ mask[i] = 1
81
+ i += 1
82
+ while (i < source.length) {
83
+ mask[i] = 1
84
+ if (source[i] === '\\') {
85
+ if (i + 1 < source.length) mask[i + 1] = 1
86
+ i += 2
87
+ continue
88
+ }
89
+ if (source[i] === quote) {
90
+ i += 1
91
+ break
92
+ }
93
+ i += 1
94
+ }
95
+ continue
96
+ }
97
+ i += 1
98
+ }
99
+ return mask
100
+ }
101
+
102
+ function importedBindings(clause) {
103
+ const bindings = []
104
+ const trimmed = clause.trim()
105
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('*')) {
106
+ const name = trimmed.split(',')[0].trim()
107
+ if (name) bindings.push({ imported: 'default', local: name })
108
+ }
109
+ const named = /\{([\s\S]*?)\}/.exec(clause)?.[1]
110
+ if (!named) return bindings
111
+ for (const part of named.split(',')) {
112
+ const match = /^\s*(?:type\s+)?([\w$]+)(?:\s+as\s+([\w$]+))?\s*$/.exec(part)
113
+ if (match)
114
+ bindings.push({ imported: match[1], local: match[2] || match[1] })
115
+ }
116
+ return bindings
117
+ }
118
+
119
+ export function componentAliases(source, { markdown = false } = {}) {
120
+ const aliases = new Map()
121
+
122
+ const mask = codeMask(source, { templateStrings: !markdown })
123
+ const imports = /\bimport\s+([\s\S]*?)\s+from\s*(['"])([^'"]+)\2/g
124
+ for (const match of source.matchAll(imports)) {
125
+ if (mask[match.index]) continue
126
+ const moduleName = match[3]
127
+ for (const binding of importedBindings(match[1])) {
128
+ if (moduleName === 'frappe-ui/list' && COMPONENTS.has(binding.imported)) {
129
+ aliases.set(binding.local, binding.imported)
130
+ aliases.set(kebab(binding.local), binding.imported)
131
+ } else if (binding.local && COMPONENTS.has(binding.local)) {
132
+ // A local component with the same name shadows a global registration.
133
+ aliases.delete(binding.local)
134
+ aliases.delete(kebab(binding.local))
135
+ }
136
+ }
137
+ }
138
+
139
+ for (const name of COMPONENTS) {
140
+ const declaration = new RegExp(
141
+ `\\b(?:const|let|var|class|function)\\s+${name}\\b`,
142
+ 'g',
143
+ )
144
+ let match
145
+ while ((match = declaration.exec(source))) {
146
+ if (mask[match.index]) continue
147
+ aliases.delete(name)
148
+ aliases.delete(kebab(name))
149
+ break
150
+ }
151
+ }
152
+ return aliases
153
+ }
154
+
155
+ function findTagEnd(source, start) {
156
+ let quote
157
+ for (let i = start; i < source.length; i += 1) {
158
+ const char = source[i]
159
+ if (quote) {
160
+ if (char === quote && source[i - 1] !== '\\') quote = undefined
161
+ } else if (char === "'" || char === '"') {
162
+ quote = char
163
+ } else if (char === '>') {
164
+ return i
165
+ }
166
+ }
167
+ return -1
168
+ }
169
+
170
+ function replaceStaticSlot(tag, from, to) {
171
+ let migrated = tag
172
+ .replace(new RegExp(`(^|\\s)#${from}(?=[.\\s=/>])`, 'g'), `$1#${to}`)
173
+ .replace(
174
+ new RegExp(`(^|\\s)v-slot:${from}(?=[.\\s=/>])`, 'g'),
175
+ `$1v-slot:${to}`,
176
+ )
177
+ migrated = migrated.replace(
178
+ new RegExp(`(\\sslot\\s*=\\s*)(['"])${from}\\2`, 'g'),
179
+ `$1$2${to}$2`,
180
+ )
181
+ return migrated
182
+ }
183
+
184
+ function replaceListRowUtilities(tag) {
185
+ return tag
186
+ .replaceAll('data-[state=selected]', 'data-[selected]')
187
+ .replaceAll('data-[active]', 'data-[state=active]')
188
+ }
189
+
190
+ function migrateMarkup(source, aliases) {
191
+ let migrated = ''
192
+ let cursor = 0
193
+ const stack = []
194
+ const refusals = []
195
+
196
+ while (cursor < source.length) {
197
+ const start = source.indexOf('<', cursor)
198
+ if (start === -1) break
199
+ migrated += source.slice(cursor, start)
200
+ if (source.startsWith('<!--', start)) {
201
+ const commentEnd = source.indexOf('-->', start + 4)
202
+ const next = commentEnd === -1 ? source.length : commentEnd + 3
203
+ migrated += source.slice(start, next)
204
+ cursor = next
205
+ continue
206
+ }
207
+ const remainder = source.slice(start)
208
+ const close = /^<\/([A-Za-z][\w.-]*)/.exec(remainder)
209
+ const open = /^<([A-Za-z][\w.-]*)/.exec(remainder)
210
+ if (!close && !open) {
211
+ migrated += '<'
212
+ cursor = start + 1
213
+ continue
214
+ }
215
+ const end = findTagEnd(source, start + 1)
216
+ if (end === -1) {
217
+ migrated += source.slice(start)
218
+ refusals.push({
219
+ line: lineAt(source, start),
220
+ message: `unterminated <${(close || open)[1]}> tag must be checked manually`,
221
+ })
222
+ cursor = source.length
223
+ break
224
+ }
225
+ const tag = source.slice(start, end + 1)
226
+ if (close) {
227
+ const at = stack.map((entry) => entry.name).lastIndexOf(close[1])
228
+ if (at !== -1) stack.splice(at)
229
+ migrated += tag
230
+ cursor = end + 1
231
+ continue
232
+ }
233
+
234
+ const name = open[1]
235
+ if (name === 'script' || name === 'style') {
236
+ const close = new RegExp(`</${name}\\s*>`, 'gi')
237
+ close.lastIndex = end + 1
238
+ const match = close.exec(source)
239
+ const next = match ? close.lastIndex : source.length
240
+ migrated += source.slice(start, next)
241
+ cursor = next
242
+ continue
243
+ }
244
+ const component = aliases.get(name)
245
+ const ambiguousComponent = component ? undefined : knownComponent(name)
246
+ let nextTag = tag
247
+ if (component === 'ListRow') nextTag = replaceListRowUtilities(nextTag)
248
+ if (
249
+ ambiguousComponent === 'ListRow' &&
250
+ /data-\[(?:active|state=selected)\]/.test(tag)
251
+ ) {
252
+ refusals.push({
253
+ line: lineAt(source, start),
254
+ message:
255
+ '<ListRow> state utility found but no frappe-ui/list import; check by hand',
256
+ })
257
+ }
258
+
259
+ const parentEntry = stack.at(-1)
260
+ const parent = parentEntry?.component
261
+ const ambiguousParent = parentEntry?.ambiguousComponent
262
+ if (name === 'template' && (parent || ambiguousParent)) {
263
+ if (/#\[|v-slot:\[/.test(tag)) {
264
+ const owner = parent || ambiguousParent
265
+ if (owner === 'ListGroup' || owner === 'ListHeaderCellSort') {
266
+ refusals.push({
267
+ line: lineAt(source, start),
268
+ message: `dynamic slot under <${owner}> must be checked manually`,
269
+ })
270
+ }
271
+ } else if (parent === 'ListGroup') {
272
+ nextTag = replaceStaticSlot(nextTag, 'header', 'label')
273
+ } else if (parent === 'ListHeaderCellSort') {
274
+ nextTag = replaceStaticSlot(nextTag, 'suffix', 'sort-indicator')
275
+ } else if (
276
+ ambiguousParent === 'ListGroup' &&
277
+ /(?:#|v-slot:)header(?=[.\s=/>])|slot\s*=\s*(['"])header\1/.test(tag)
278
+ ) {
279
+ refusals.push({
280
+ line: lineAt(source, start),
281
+ message:
282
+ '<ListGroup> #header found but no frappe-ui/list import; check by hand',
283
+ })
284
+ } else if (
285
+ ambiguousParent === 'ListHeaderCellSort' &&
286
+ /(?:#|v-slot:)suffix(?=[.\s=/>])|slot\s*=\s*(['"])suffix\1/.test(tag)
287
+ ) {
288
+ refusals.push({
289
+ line: lineAt(source, start),
290
+ message:
291
+ '<ListHeaderCellSort> #suffix found but no frappe-ui/list import; check by hand',
292
+ })
293
+ }
294
+ }
295
+
296
+ migrated += nextTag
297
+ if (
298
+ !/\/\s*>$/.test(tag) &&
299
+ !VOID_ELEMENTS.has(name.toLowerCase())
300
+ ) {
301
+ stack.push({ name, component, ambiguousComponent })
302
+ }
303
+ cursor = end + 1
304
+ }
305
+ migrated += source.slice(cursor)
306
+ return { migrated, refusals }
307
+ }
308
+
309
+ const LIST_ROW_SLOT = String.raw`\[data-slot\s*=\s*(?:["']?list-row["']?)\]`
310
+ const OLD_SELECTED = String.raw`\[data-state\s*=\s*(?:["']?selected["']?)\]`
311
+
312
+ function migrateAnchoredSelectors(source) {
313
+ return source
314
+ .replace(
315
+ new RegExp(`(${LIST_ROW_SLOT})(${OLD_SELECTED})`, 'g'),
316
+ '$1[data-selected]',
317
+ )
318
+ .replace(
319
+ new RegExp(`(${OLD_SELECTED})(${LIST_ROW_SLOT})`, 'g'),
320
+ '[data-selected]$2',
321
+ )
322
+ .replace(
323
+ new RegExp(`(${LIST_ROW_SLOT})\\[data-active\\]`, 'g'),
324
+ "$1[data-state='active']",
325
+ )
326
+ .replace(
327
+ new RegExp(`\\[data-active\\](${LIST_ROW_SLOT})`, 'g'),
328
+ "[data-state='active']$1",
329
+ )
330
+ }
331
+
332
+ export function migrateList(source, options = {}) {
333
+ const aliases = componentAliases(source, options)
334
+ const selectorsMigrated = migrateAnchoredSelectors(source)
335
+ return options.markup === false
336
+ ? { migrated: selectorsMigrated, refusals: [] }
337
+ : migrateMarkup(selectorsMigrated, aliases)
338
+ }
339
+
340
+ function filesIn(target, seen = new Set()) {
341
+ let stat
342
+ try {
343
+ stat = fs.lstatSync(target)
344
+ } catch (error) {
345
+ throw new Error(
346
+ `${target}: ${error.code === 'ENOENT' ? 'not found' : error.message}`,
347
+ )
348
+ }
349
+ if (stat.isSymbolicLink()) return []
350
+ if (stat.isFile()) {
351
+ // Migration guides intentionally contain the old spelling in "before"
352
+ // examples. Rewriting them makes the guide false and prevents idempotent
353
+ // own-tree runs.
354
+ if (path.basename(target).toLowerCase() === 'migration.md') return []
355
+ return EXTENSIONS.has(path.extname(target)) ? [target] : []
356
+ }
357
+ if (!stat.isDirectory()) return []
358
+ const real = fs.realpathSync(target)
359
+ if (seen.has(real)) return []
360
+ seen.add(real)
361
+ return fs
362
+ .readdirSync(target, { withFileTypes: true })
363
+ .sort((a, b) => a.name.localeCompare(b.name))
364
+ .flatMap((entry) => {
365
+ if (['node_modules', '.git', 'dist', 'build'].includes(entry.name))
366
+ return []
367
+ return filesIn(path.join(target, entry.name), seen)
368
+ })
369
+ }
370
+
371
+ export function run(argv) {
372
+ const dryRun = argv.includes('--dry-run')
373
+ const unknown = argv.find((arg) => arg.startsWith('-') && arg !== '--dry-run')
374
+ if (unknown) {
375
+ console.error(`Unknown option: ${unknown}\n${USAGE}`)
376
+ return 1
377
+ }
378
+ const targets = argv.filter((arg) => arg !== '--dry-run')
379
+ if (!targets.length) {
380
+ console.error(USAGE)
381
+ return 1
382
+ }
383
+
384
+ let files
385
+ try {
386
+ files = [
387
+ ...new Set(targets.flatMap((target) => filesIn(path.resolve(target)))),
388
+ ]
389
+ } catch (error) {
390
+ console.error(error.message)
391
+ return 1
392
+ }
393
+
394
+ let changed = 0
395
+ let refusalCount = 0
396
+ for (const file of files) {
397
+ const source = fs.readFileSync(file, 'utf8')
398
+ const extension = path.extname(file).toLowerCase()
399
+ const result = migrateList(source, {
400
+ markdown: extension === '.md',
401
+ markup: ['.html', '.md', '.vue'].includes(extension),
402
+ })
403
+ if (result.refusals.length) {
404
+ refusalCount += result.refusals.length
405
+ for (const refusal of result.refusals) {
406
+ console.error(`${file}:${refusal.line}: ${refusal.message}`)
407
+ }
408
+ continue
409
+ }
410
+ if (result.migrated === source) continue
411
+ changed += 1
412
+ if (!dryRun) fs.writeFileSync(file, result.migrated)
413
+ console.log(`${dryRun ? 'Would migrate' : 'Migrated'} ${file}`)
414
+ }
415
+ console.log(
416
+ `${dryRun ? 'Would migrate' : 'Migrated'} ${changed} file${changed === 1 ? '' : 's'}.`,
417
+ )
418
+ if (refusalCount) {
419
+ console.error(
420
+ `Refused ${refusalCount} dynamic slot${refusalCount === 1 ? '' : 's'}; no refused file was changed.`,
421
+ )
422
+ return 1
423
+ }
424
+ return 0
425
+ }
426
+
427
+ const script = fileURLToPath(import.meta.url)
428
+ let isMain = false
429
+ if (process.argv[1]) {
430
+ try {
431
+ isMain = fs.realpathSync(process.argv[1]) === fs.realpathSync(script)
432
+ } catch {
433
+ isMain = false
434
+ }
435
+ }
436
+ if (isMain) process.exitCode = run(process.argv.slice(2))