@jirawatpyk/aura-react 5.8.0 → 5.10.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.
- package/README.md +36 -0
- package/dist/aura.bundle.js +1528 -1098
- package/dist/cjs/index.cjs +1528 -1098
- package/dist/esm/Accordion.js +2 -1
- package/dist/esm/Alert.js +2 -1
- package/dist/esm/AppShell.js +2 -1
- package/dist/esm/Breadcrumb.js +2 -1
- package/dist/esm/Button.js +2 -1
- package/dist/esm/Checkbox.js +2 -1
- package/dist/esm/Combobox.js +7 -6
- package/dist/esm/Command.js +3 -2
- package/dist/esm/DataTable.js +35 -13
- package/dist/esm/DatePicker.js +8 -7
- package/dist/esm/Dialog.js +38 -5
- package/dist/esm/DropdownMenu.js +1 -1
- package/dist/esm/Field.js +2 -1
- package/dist/esm/FileUpload.js +4 -3
- package/dist/esm/FilterBar.js +3 -2
- package/dist/esm/FormErrorSummary.js +2 -1
- package/dist/esm/Icon.js +50 -31
- package/dist/esm/Menu.js +37 -12
- package/dist/esm/NumberField.js +3 -2
- package/dist/esm/Pagination.js +3 -2
- package/dist/esm/PasswordField.js +2 -1
- package/dist/esm/Popover.js +2 -1
- package/dist/esm/RadioGroup.js +2 -1
- package/dist/esm/Select.js +4 -3
- package/dist/esm/SideNav.js +3 -2
- package/dist/esm/Stat.js +8 -1
- package/dist/esm/Stepper.js +2 -1
- package/dist/esm/Tabs.js +89 -13
- package/dist/esm/Tag.js +3 -2
- package/dist/esm/TimePicker.js +5 -4
- package/dist/esm/Toaster.js +3 -2
- package/dist/esm/classes.js +14 -1
- package/dist/esm/colorScheme.js +8 -1
- package/dist/esm/display.js +22 -12
- package/dist/esm/iconSvg.js +53 -0
- package/dist/esm/icons.js +531 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/internal.js +1 -14
- package/dist/esm/locale.js +7 -0
- package/dist/esm/strings.js +4 -245
- package/dist/esm/strings.sv.js +127 -0
- package/dist/esm/strings.th.js +127 -0
- package/dist/icons/index.cjs +613 -0
- package/dist/icons/index.d.cts +106 -0
- package/dist/icons/index.d.ts +106 -0
- package/dist/index.d.cts +52 -2
- package/dist/index.d.ts +52 -2
- package/dist/locales/sv.cjs +149 -0
- package/dist/locales/sv.d.cts +96 -0
- package/dist/locales/sv.d.ts +96 -0
- package/dist/locales/th.cjs +149 -0
- package/dist/locales/th.d.cts +96 -0
- package/dist/locales/th.d.ts +96 -0
- package/dist/server/index.cjs +445 -314
- package/dist/server/index.d.cts +24 -0
- package/dist/server/index.d.ts +24 -0
- package/dist/server/index.js +445 -314
- package/dist/styles.css +22 -1
- package/dist/styles.layer.css +22 -1
- package/package.json +35 -3
- package/scripts/aura-icons-codemod.mjs +345 -0
package/dist/styles.css
CHANGED
|
@@ -226,7 +226,13 @@ a.aura-btn { text-decoration: none; }
|
|
|
226
226
|
}
|
|
227
227
|
.aura-menu__item:hover, .aura-menu__item:focus { background: var(--aura-bg-surface-hover); outline: none; }
|
|
228
228
|
.aura-menu__item:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
|
|
229
|
-
.aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default;
|
|
229
|
+
.aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; }
|
|
230
|
+
/* 5.10: a disabled item is dimmed but focusable; its reason stays at full strength (it is the information). */
|
|
231
|
+
.aura-menu__item[aria-disabled="true"] { cursor: default; }
|
|
232
|
+
.aura-menu__item[aria-disabled="true"] > :not(.aura-menu__reason) { opacity: var(--aura-disabled-opacity); }
|
|
233
|
+
.aura-menu__reason { padding-left: var(--aura-space-4); }
|
|
234
|
+
.aura-menu__item:disabled, .aura-menu__item[aria-disabled="true"]:hover:not(:focus) { background: transparent; }
|
|
235
|
+
.aura-menu:focus, .aura-menu__list:focus { outline: none; }
|
|
230
236
|
.aura-menu__item .aura-icon { color: var(--aura-fg-secondary); }
|
|
231
237
|
.aura-menu__label { flex: 1; }
|
|
232
238
|
.aura-menu__blank { width: var(--aura-icon-sm); flex: none; }
|
|
@@ -623,6 +629,7 @@ a.aura-tab { text-decoration: none; }
|
|
|
623
629
|
font-size: 11px; line-height: 18px; text-align: center; font-variant-numeric: tabular-nums;
|
|
624
630
|
}
|
|
625
631
|
.aura-tabs__panel { padding-top: var(--aura-space-6); outline: none; }
|
|
632
|
+
.aura-tabs__panel[hidden] { display: none; }
|
|
626
633
|
.aura-tabs__panel:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 2px; border-radius: var(--aura-radius-sm); }
|
|
627
634
|
|
|
628
635
|
/* ---------- SideNav ---------- */
|
|
@@ -1202,6 +1209,16 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
|
|
|
1202
1209
|
.aura-segmented--sm .aura-segmented__option { height: 24px; padding: 0 var(--aura-space-3); font-size: 12px; }
|
|
1203
1210
|
.aura-segmented--sm .aura-segmented__option.is-icon { width: 28px; padding: 0; }
|
|
1204
1211
|
.aura-segmented.is-disabled { opacity: var(--aura-disabled-opacity); }
|
|
1212
|
+
/* 5.10 (Chamber-OS 72): Tabs variant="segmented" — the tablist wears SegmentedControl's track and pills. The focus
|
|
1213
|
+
* ring is drawn inside the pill, so neither the scrolling list (many tabs on a phone) nor the next pill can cover it;
|
|
1214
|
+
* full-width tabs share the track but never shrink below their label (the track scrolls instead). The phone rule's
|
|
1215
|
+
* wider tab gap doesn't apply. */
|
|
1216
|
+
.aura-tabs--segmented .aura-tabs__list.aura-segmented { gap: 2px; }
|
|
1217
|
+
.aura-tabs--segmented .aura-tabs__list.aura-segmented.is-full .aura-tab { min-width: max-content; }
|
|
1218
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option { margin-bottom: 0; }
|
|
1219
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { border-radius: var(--aura-radius-full); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
|
|
1220
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option:disabled,
|
|
1221
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option.is-disabled { opacity: var(--aura-disabled-opacity); cursor: not-allowed; }
|
|
1205
1222
|
|
|
1206
1223
|
/* A field inside a row Stack takes the remaining width (a search box beside its button). */
|
|
1207
1224
|
.aura-stack > .aura-field { flex: 1 1 auto; min-width: 0; }
|
|
@@ -1589,4 +1606,8 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
|
|
|
1589
1606
|
.aura-segmented__option.is-selected { outline: 2px solid Highlight; outline-offset: -2px; }
|
|
1590
1607
|
.aura-tab { border-bottom-color: Canvas; }
|
|
1591
1608
|
.aura-tab.is-active { border-bottom-color: Highlight; }
|
|
1609
|
+
/* 5.10: segmented tabs mark the selection with a border and focus with a dashed ring inside it, so a focused tab
|
|
1610
|
+
* and the selected one never look alike (manual activation moves focus without selecting). */
|
|
1611
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option.is-selected { outline: none; border: 2px solid Highlight; }
|
|
1612
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { outline: 2px dashed CanvasText; outline-offset: -5px; }
|
|
1592
1613
|
}
|
package/dist/styles.layer.css
CHANGED
|
@@ -228,7 +228,13 @@ a.aura-btn { text-decoration: none; }
|
|
|
228
228
|
}
|
|
229
229
|
.aura-menu__item:hover, .aura-menu__item:focus { background: var(--aura-bg-surface-hover); outline: none; }
|
|
230
230
|
.aura-menu__item:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
|
|
231
|
-
.aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default;
|
|
231
|
+
.aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; }
|
|
232
|
+
/* 5.10: a disabled item is dimmed but focusable; its reason stays at full strength (it is the information). */
|
|
233
|
+
.aura-menu__item[aria-disabled="true"] { cursor: default; }
|
|
234
|
+
.aura-menu__item[aria-disabled="true"] > :not(.aura-menu__reason) { opacity: var(--aura-disabled-opacity); }
|
|
235
|
+
.aura-menu__reason { padding-left: var(--aura-space-4); }
|
|
236
|
+
.aura-menu__item:disabled, .aura-menu__item[aria-disabled="true"]:hover:not(:focus) { background: transparent; }
|
|
237
|
+
.aura-menu:focus, .aura-menu__list:focus { outline: none; }
|
|
232
238
|
.aura-menu__item .aura-icon { color: var(--aura-fg-secondary); }
|
|
233
239
|
.aura-menu__label { flex: 1; }
|
|
234
240
|
.aura-menu__blank { width: var(--aura-icon-sm); flex: none; }
|
|
@@ -625,6 +631,7 @@ a.aura-tab { text-decoration: none; }
|
|
|
625
631
|
font-size: 11px; line-height: 18px; text-align: center; font-variant-numeric: tabular-nums;
|
|
626
632
|
}
|
|
627
633
|
.aura-tabs__panel { padding-top: var(--aura-space-6); outline: none; }
|
|
634
|
+
.aura-tabs__panel[hidden] { display: none; }
|
|
628
635
|
.aura-tabs__panel:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 2px; border-radius: var(--aura-radius-sm); }
|
|
629
636
|
|
|
630
637
|
/* ---------- SideNav ---------- */
|
|
@@ -1204,6 +1211,16 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
|
|
|
1204
1211
|
.aura-segmented--sm .aura-segmented__option { height: 24px; padding: 0 var(--aura-space-3); font-size: 12px; }
|
|
1205
1212
|
.aura-segmented--sm .aura-segmented__option.is-icon { width: 28px; padding: 0; }
|
|
1206
1213
|
.aura-segmented.is-disabled { opacity: var(--aura-disabled-opacity); }
|
|
1214
|
+
/* 5.10 (Chamber-OS 72): Tabs variant="segmented" — the tablist wears SegmentedControl's track and pills. The focus
|
|
1215
|
+
* ring is drawn inside the pill, so neither the scrolling list (many tabs on a phone) nor the next pill can cover it;
|
|
1216
|
+
* full-width tabs share the track but never shrink below their label (the track scrolls instead). The phone rule's
|
|
1217
|
+
* wider tab gap doesn't apply. */
|
|
1218
|
+
.aura-tabs--segmented .aura-tabs__list.aura-segmented { gap: 2px; }
|
|
1219
|
+
.aura-tabs--segmented .aura-tabs__list.aura-segmented.is-full .aura-tab { min-width: max-content; }
|
|
1220
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option { margin-bottom: 0; }
|
|
1221
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { border-radius: var(--aura-radius-full); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
|
|
1222
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option:disabled,
|
|
1223
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option.is-disabled { opacity: var(--aura-disabled-opacity); cursor: not-allowed; }
|
|
1207
1224
|
|
|
1208
1225
|
/* A field inside a row Stack takes the remaining width (a search box beside its button). */
|
|
1209
1226
|
.aura-stack > .aura-field { flex: 1 1 auto; min-width: 0; }
|
|
@@ -1591,6 +1608,10 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
|
|
|
1591
1608
|
.aura-segmented__option.is-selected { outline: 2px solid Highlight; outline-offset: -2px; }
|
|
1592
1609
|
.aura-tab { border-bottom-color: Canvas; }
|
|
1593
1610
|
.aura-tab.is-active { border-bottom-color: Highlight; }
|
|
1611
|
+
/* 5.10: segmented tabs mark the selection with a border and focus with a dashed ring inside it, so a focused tab
|
|
1612
|
+
* and the selected one never look alike (manual activation moves focus without selecting). */
|
|
1613
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option.is-selected { outline: none; border: 2px solid Highlight; }
|
|
1614
|
+
.aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { outline: 2px dashed CanvasText; outline-offset: -5px; }
|
|
1594
1615
|
}
|
|
1595
1616
|
|
|
1596
1617
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jirawatpyk/aura-react",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.0",
|
|
4
4
|
"description": "AURA Design System — React components (Enterprise Standard, Human Creative). Thai-first: Buddhist-era dates, Thai collation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,36 @@
|
|
|
28
28
|
"default": "./dist/server/index.cjs"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
|
+
"./icons": {
|
|
32
|
+
"import": {
|
|
33
|
+
"types": "./dist/icons/index.d.ts",
|
|
34
|
+
"default": "./dist/esm/icons.js"
|
|
35
|
+
},
|
|
36
|
+
"require": {
|
|
37
|
+
"types": "./dist/icons/index.d.cts",
|
|
38
|
+
"default": "./dist/icons/index.cjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"./locales/th": {
|
|
42
|
+
"import": {
|
|
43
|
+
"types": "./dist/locales/th.d.ts",
|
|
44
|
+
"default": "./dist/esm/strings.th.js"
|
|
45
|
+
},
|
|
46
|
+
"require": {
|
|
47
|
+
"types": "./dist/locales/th.d.cts",
|
|
48
|
+
"default": "./dist/locales/th.cjs"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"./locales/sv": {
|
|
52
|
+
"import": {
|
|
53
|
+
"types": "./dist/locales/sv.d.ts",
|
|
54
|
+
"default": "./dist/esm/strings.sv.js"
|
|
55
|
+
},
|
|
56
|
+
"require": {
|
|
57
|
+
"types": "./dist/locales/sv.d.cts",
|
|
58
|
+
"default": "./dist/locales/sv.cjs"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
31
61
|
"./styles.css": "./dist/styles.css",
|
|
32
62
|
"./styles.layer.css": "./dist/styles.layer.css",
|
|
33
63
|
"./aura.bundle.js": "./dist/aura.bundle.js",
|
|
@@ -36,6 +66,7 @@
|
|
|
36
66
|
"files": [
|
|
37
67
|
"dist",
|
|
38
68
|
"scripts/aura-theme.mjs",
|
|
69
|
+
"scripts/aura-icons-codemod.mjs",
|
|
39
70
|
"README.md",
|
|
40
71
|
"LICENSE"
|
|
41
72
|
],
|
|
@@ -45,7 +76,7 @@
|
|
|
45
76
|
],
|
|
46
77
|
"scripts": {
|
|
47
78
|
"build": "node scripts/build.ts && node scripts/sync-bundle.ts",
|
|
48
|
-
"test:ssr": "node scripts/ssr-test.ts && node --conditions=react-server scripts/server-entry-test.ts",
|
|
79
|
+
"test:ssr": "node scripts/ssr-test.ts && node scripts/prep60-test.ts && node --conditions=react-server scripts/server-entry-test.ts",
|
|
49
80
|
"typecheck": "tsc -p tsconfig.src.json && tsc -p tsconfig.json && tsc -p tsconfig.exact.json && tsc -p examples/tsconfig.json",
|
|
50
81
|
"examples": "node examples/build.ts",
|
|
51
82
|
"themes": "node scripts/test-themes.ts",
|
|
@@ -76,7 +107,8 @@
|
|
|
76
107
|
"@playwright/test": "^1.45.0"
|
|
77
108
|
},
|
|
78
109
|
"bin": {
|
|
79
|
-
"aura-theme": "scripts/aura-theme.mjs"
|
|
110
|
+
"aura-theme": "scripts/aura-theme.mjs",
|
|
111
|
+
"aura-icons-codemod": "scripts/aura-icons-codemod.mjs"
|
|
80
112
|
},
|
|
81
113
|
"repository": {
|
|
82
114
|
"type": "git",
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/* aura-icons-codemod (5.9) — moves icon names as strings to the per-icon components, ready for 6.0.
|
|
4
|
+
* npx aura-icons-codemod src [more paths…] [--dry]
|
|
5
|
+
* In .tsx and .jsx files that import from @jirawatpyk/aura-react (or /server), for names in AURA's set, it rewrites
|
|
6
|
+
* <Icon name="users" size={20} /> → <IconUsers size={20} />
|
|
7
|
+
* <Button icon="plus"> (icon / iconRight on an AURA component) → <Button icon={<IconPlus />}>
|
|
8
|
+
* { id: 'home', label: 'Home', icon: 'house' } → { id: 'home', label: 'Home', icon: <IconHouse /> }
|
|
9
|
+
* (an object literal with a `label` or `title`: nav, menu, tab and option items)
|
|
10
|
+
* and adds `import { IconHouse, IconPlus, IconUsers } from '@jirawatpyk/aura-react/icons'`. It reads the code with
|
|
11
|
+
* the TypeScript parser (your project's `typescript`), so types, strings and comments are never touched. Everything
|
|
12
|
+
* it leaves — names chosen at run time, items in .ts files, objects it isn't sure are AURA's, a name already taken by
|
|
13
|
+
* another import — is listed at the end: convert those by hand, or keep names with registerIcons(allIcons).
|
|
14
|
+
* --dry prints what would change and writes nothing. */
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const iconsSrc = fs.readFileSync(path.join(here, '../dist/esm/icons.js'), 'utf8');
|
|
22
|
+
const NAMES = new Set(Array.from(iconsSrc.matchAll(/defineIcon\d*\("([a-z0-9-]+)"/g), (m) => m[1]));
|
|
23
|
+
/** @param {string} n */
|
|
24
|
+
export const pascal = (n) =>
|
|
25
|
+
'Icon' +
|
|
26
|
+
n
|
|
27
|
+
.split('-')
|
|
28
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
29
|
+
.join('');
|
|
30
|
+
|
|
31
|
+
/** TypeScript from the project being migrated, else from next to this package. */
|
|
32
|
+
function loadTs() {
|
|
33
|
+
for (const from of [path.join(process.cwd(), 'package.json'), fileURLToPath(import.meta.url)]) {
|
|
34
|
+
try {
|
|
35
|
+
return createRequire(from)('typescript');
|
|
36
|
+
} catch (e) {
|
|
37
|
+
/* try the next */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** @type {any} */
|
|
43
|
+
let ts = null;
|
|
44
|
+
|
|
45
|
+
const ROOT = '@jirawatpyk/aura-react';
|
|
46
|
+
const ICONS = '@jirawatpyk/aura-react/icons';
|
|
47
|
+
const ICON_PROPS = ['icon', 'iconRight'];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Rewrites one file. Returns the new text, the icon names it imported, and notes on what it left.
|
|
51
|
+
* @param {string} src
|
|
52
|
+
* @param {string} [file]
|
|
53
|
+
*/
|
|
54
|
+
export function transform(src, file = 'file.tsx') {
|
|
55
|
+
if (!ts) ts = loadTs();
|
|
56
|
+
if (!ts) throw new Error('aura-icons-codemod needs TypeScript to read your code: npm i -D typescript');
|
|
57
|
+
/** @type {Set<string>} */
|
|
58
|
+
const used = new Set();
|
|
59
|
+
/** @type {string[]} */
|
|
60
|
+
const left = [];
|
|
61
|
+
const none = { code: src, used, left };
|
|
62
|
+
if (!/@jirawatpyk\/aura-react/.test(src)) return none;
|
|
63
|
+
const ext = path.extname(file).toLowerCase();
|
|
64
|
+
/** @type {Record<string, number>} */
|
|
65
|
+
const kinds = { '.tsx': ts.ScriptKind.TSX, '.ts': ts.ScriptKind.TS };
|
|
66
|
+
/* .js / .jsx / .mjs are read as JSX (plain JavaScript parses the same). */
|
|
67
|
+
const kind = kinds[ext] || ts.ScriptKind.JSX;
|
|
68
|
+
const sf = ts.createSourceFile(file, src, ts.ScriptTarget.Latest, true, kind);
|
|
69
|
+
/* An element can go where the file already allows JSX: .tsx/.jsx, or a .js file that has some. */
|
|
70
|
+
let jsx = ext === '.tsx' || ext === '.jsx';
|
|
71
|
+
if (!jsx && kind === ts.ScriptKind.JSX) {
|
|
72
|
+
const find = (/** @type {any} */ n) => {
|
|
73
|
+
if (jsx) return;
|
|
74
|
+
if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) jsx = true;
|
|
75
|
+
else ts.forEachChild(n, find);
|
|
76
|
+
};
|
|
77
|
+
find(sf);
|
|
78
|
+
}
|
|
79
|
+
const nl = src.indexOf('\r\n') >= 0 ? '\r\n' : '\n';
|
|
80
|
+
const line = (/** @type {any} */ node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
81
|
+
|
|
82
|
+
/* AURA's components by local name, the namespace (`import * as Aura`), Icon's local name, the icons import. */
|
|
83
|
+
/** @type {Set<string>} */
|
|
84
|
+
const aura = new Set();
|
|
85
|
+
/** @type {string[]} */
|
|
86
|
+
const nsNames = [];
|
|
87
|
+
let iconLocal = '';
|
|
88
|
+
/** @type {any} */
|
|
89
|
+
let iconsImport = null;
|
|
90
|
+
/** @type {any} */
|
|
91
|
+
let lastImport = null;
|
|
92
|
+
/** Names bound by imports from anywhere else and top-level declarations — a new IconX must not collide. */
|
|
93
|
+
/** @type {Set<string>} */
|
|
94
|
+
const taken = new Set();
|
|
95
|
+
for (const st of sf.statements) {
|
|
96
|
+
if (!ts.isImportDeclaration(st)) continue;
|
|
97
|
+
lastImport = st;
|
|
98
|
+
const from = st.moduleSpecifier.text;
|
|
99
|
+
const cl = st.importClause;
|
|
100
|
+
if (from === ICONS) {
|
|
101
|
+
/* Merge into a value import only: names added to `import type { … }` couldn't be used as JSX. */
|
|
102
|
+
if (cl && !cl.isTypeOnly) iconsImport = st;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const mine = from === ROOT || from === ROOT + '/server';
|
|
106
|
+
if (!cl) continue;
|
|
107
|
+
if (cl.name) (mine ? aura : taken).add(cl.name.text);
|
|
108
|
+
const nb = cl.namedBindings;
|
|
109
|
+
if (nb && ts.isNamespaceImport(nb)) {
|
|
110
|
+
if (mine) nsNames.push(nb.name.text);
|
|
111
|
+
else taken.add(nb.name.text);
|
|
112
|
+
}
|
|
113
|
+
if (nb && ts.isNamedImports(nb))
|
|
114
|
+
for (const el of nb.elements) {
|
|
115
|
+
if (!mine) {
|
|
116
|
+
taken.add(el.name.text);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
aura.add(el.name.text);
|
|
120
|
+
if ((el.propertyName || el.name).text === 'Icon') iconLocal = el.name.text;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!aura.size && !nsNames.length) return none;
|
|
124
|
+
/* Every name declared anywhere in the file (a local `const IconUsers` inside a component too): a new IconX that
|
|
125
|
+
* matches one would silently render the local thing. */
|
|
126
|
+
const declared = (/** @type {any} */ n) => {
|
|
127
|
+
if (
|
|
128
|
+
(ts.isVariableDeclaration(n) ||
|
|
129
|
+
ts.isParameter(n) ||
|
|
130
|
+
ts.isFunctionDeclaration(n) ||
|
|
131
|
+
ts.isClassDeclaration(n) ||
|
|
132
|
+
ts.isBindingElement(n)) &&
|
|
133
|
+
n.name &&
|
|
134
|
+
ts.isIdentifier(n.name)
|
|
135
|
+
)
|
|
136
|
+
taken.add(n.name.text);
|
|
137
|
+
ts.forEachChild(n, declared);
|
|
138
|
+
};
|
|
139
|
+
declared(sf);
|
|
140
|
+
|
|
141
|
+
const isAura = (/** @type {string} */ t) => aura.has(t) || nsNames.some((n) => t.startsWith(n + '.'));
|
|
142
|
+
const isIconTag = (/** @type {string} */ t) =>
|
|
143
|
+
(!!iconLocal && t === iconLocal) || nsNames.some((n) => t === n + '.Icon');
|
|
144
|
+
/** @type {Array<{ start: number, end: number, text: string }>} */
|
|
145
|
+
const edits = [];
|
|
146
|
+
/** A literal icon name in an attribute initializer or a property value, else null.
|
|
147
|
+
* @param {any} init @returns {string | null} */
|
|
148
|
+
const literal = (init) => {
|
|
149
|
+
if (!init) return null;
|
|
150
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) return init.text;
|
|
151
|
+
if (ts.isJsxExpression(init) && init.expression) return literal(init.expression);
|
|
152
|
+
if (ts.isParenthesizedExpression(init)) return literal(init.expression);
|
|
153
|
+
return null;
|
|
154
|
+
};
|
|
155
|
+
/** Can this name be imported as its component here? */
|
|
156
|
+
const usable = (/** @type {string} */ n, /** @type {any} */ node) => {
|
|
157
|
+
if (!NAMES.has(n)) return false;
|
|
158
|
+
if (taken.has(pascal(n))) {
|
|
159
|
+
left.push(line(node) + ': ' + pascal(n) + ' is already a name in this file — "' + n + '" left as a string');
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
const isElement = (/** @type {any} */ e) => !!e && (ts.isJsxElement(e) || ts.isJsxSelfClosingElement(e));
|
|
165
|
+
|
|
166
|
+
/** @param {any} node */
|
|
167
|
+
function visit(node) {
|
|
168
|
+
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
169
|
+
const tag = node.tagName.getText(sf);
|
|
170
|
+
if (isAura(tag)) {
|
|
171
|
+
for (const attr of node.attributes.properties) {
|
|
172
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
173
|
+
const prop = attr.name.getText(sf);
|
|
174
|
+
const isName = prop === 'name' && isIconTag(tag);
|
|
175
|
+
if (!isName && ICON_PROPS.indexOf(prop) < 0) continue;
|
|
176
|
+
const init = attr.initializer;
|
|
177
|
+
const n = literal(init);
|
|
178
|
+
if (n == null) {
|
|
179
|
+
if (init && !(ts.isJsxExpression(init) && isElement(init.expression)))
|
|
180
|
+
left.push(line(attr) + ': ' + attr.getText(sf) + ' (chosen at run time)');
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!usable(n, attr)) continue;
|
|
184
|
+
used.add(n);
|
|
185
|
+
if (isName) {
|
|
186
|
+
/* <Icon name="users" …> → <IconUsers …>: rename the tag (and a closing tag), drop `name`. */
|
|
187
|
+
edits.push({ start: node.tagName.getStart(sf), end: node.tagName.end, text: pascal(n) });
|
|
188
|
+
edits.push({ start: attr.getFullStart(), end: attr.end, text: '' });
|
|
189
|
+
if (ts.isJsxOpeningElement(node)) {
|
|
190
|
+
const close = node.parent.closingElement;
|
|
191
|
+
edits.push({ start: close.tagName.getStart(sf), end: close.tagName.end, text: pascal(n) });
|
|
192
|
+
}
|
|
193
|
+
} else edits.push({ start: init.getStart(sf), end: init.end, text: '{<' + pascal(n) + ' />}' });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
|
|
198
|
+
const key = ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) ? node.name.text : '';
|
|
199
|
+
if (ICON_PROPS.indexOf(key) >= 0) {
|
|
200
|
+
const n = literal(node.initializer);
|
|
201
|
+
/* An item has a label or title that is text (a string, template or JSX), not a config object. */
|
|
202
|
+
const item = node.parent.properties.some(
|
|
203
|
+
(/** @type {any} */ p) =>
|
|
204
|
+
ts.isPropertyAssignment(p) &&
|
|
205
|
+
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
206
|
+
(p.name.text === 'label' || p.name.text === 'title') &&
|
|
207
|
+
(ts.isStringLiteral(p.initializer) ||
|
|
208
|
+
ts.isNoSubstitutionTemplateLiteral(p.initializer) ||
|
|
209
|
+
ts.isTemplateExpression(p.initializer) ||
|
|
210
|
+
ts.isJsxElement(p.initializer) ||
|
|
211
|
+
ts.isJsxSelfClosingElement(p.initializer) ||
|
|
212
|
+
ts.isJsxFragment(p.initializer) ||
|
|
213
|
+
ts.isIdentifier(p.initializer) ||
|
|
214
|
+
ts.isPropertyAccessExpression(p.initializer) ||
|
|
215
|
+
ts.isCallExpression(p.initializer)),
|
|
216
|
+
);
|
|
217
|
+
if (n != null && NAMES.has(n)) {
|
|
218
|
+
if (!item)
|
|
219
|
+
left.push(
|
|
220
|
+
line(node) + ': ' + node.getText(sf) + " (an object with no label or title — AURA's? convert by hand)",
|
|
221
|
+
);
|
|
222
|
+
else if (!jsx)
|
|
223
|
+
left.push(line(node) + ': ' + node.getText(sf) + ' (in a ' + ext + ' file: an element needs JSX)');
|
|
224
|
+
else if (usable(n, node)) {
|
|
225
|
+
used.add(n);
|
|
226
|
+
edits.push({
|
|
227
|
+
start: node.initializer.getStart(sf),
|
|
228
|
+
end: node.initializer.end,
|
|
229
|
+
text: '<' + pascal(n) + ' />',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
} else if (
|
|
233
|
+
n == null &&
|
|
234
|
+
item &&
|
|
235
|
+
(ts.isIdentifier(node.initializer) || ts.isPropertyAccessExpression(node.initializer))
|
|
236
|
+
)
|
|
237
|
+
left.push(line(node) + ': ' + node.getText(sf) + ' (chosen at run time)');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
ts.forEachChild(node, visit);
|
|
241
|
+
}
|
|
242
|
+
visit(sf);
|
|
243
|
+
if (!edits.length) return { code: src, used, left };
|
|
244
|
+
|
|
245
|
+
/* The import: merged into an existing icons import, else after the last import (or after the directives). */
|
|
246
|
+
const names = Array.from(used, pascal);
|
|
247
|
+
const nbIcons =
|
|
248
|
+
iconsImport &&
|
|
249
|
+
iconsImport.importClause &&
|
|
250
|
+
!iconsImport.importClause.isTypeOnly &&
|
|
251
|
+
iconsImport.importClause.namedBindings;
|
|
252
|
+
if (nbIcons && ts.isNamedImports(nbIcons)) {
|
|
253
|
+
const have = nbIcons.elements.map((/** @type {any} */ e) => e.getText(sf));
|
|
254
|
+
const add = names.filter((x) => have.indexOf(x) < 0);
|
|
255
|
+
if (add.length)
|
|
256
|
+
edits.push({
|
|
257
|
+
start: nbIcons.getStart(sf),
|
|
258
|
+
end: nbIcons.end,
|
|
259
|
+
text: '{ ' + have.concat(add).sort().join(', ') + ' }',
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
let at = 0;
|
|
263
|
+
if (lastImport) {
|
|
264
|
+
/* After the import's line, so a trailing comment stays with it. */
|
|
265
|
+
const eol = src.indexOf('\n', lastImport.end);
|
|
266
|
+
at = eol < 0 ? src.length : src[eol - 1] === '\r' ? eol - 1 : eol;
|
|
267
|
+
} else
|
|
268
|
+
for (const st of sf.statements) {
|
|
269
|
+
if (ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)) at = st.end;
|
|
270
|
+
else break;
|
|
271
|
+
}
|
|
272
|
+
const text = 'import { ' + names.sort().join(', ') + " } from '" + ICONS + "';";
|
|
273
|
+
edits.push({ start: at, end: at, text: at ? nl + text : text + nl });
|
|
274
|
+
}
|
|
275
|
+
edits.sort((a, b) => b.start - a.start || b.end - a.end);
|
|
276
|
+
let code = src;
|
|
277
|
+
for (const e of edits) code = code.slice(0, e.start) + e.text + code.slice(e.end);
|
|
278
|
+
|
|
279
|
+
/* Drop AURA's Icon from its import when nothing uses it any more (not when it is the only name there). */
|
|
280
|
+
if (iconLocal) {
|
|
281
|
+
const again = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, kind);
|
|
282
|
+
let refs = 0;
|
|
283
|
+
/** @type {any} */
|
|
284
|
+
let spec = null;
|
|
285
|
+
const count = (/** @type {any} */ n) => {
|
|
286
|
+
if (ts.isImportSpecifier(n) && n.name.text === iconLocal) spec = n;
|
|
287
|
+
else if (ts.isIdentifier(n) && n.text === iconLocal && !ts.isImportSpecifier(n.parent)) refs++;
|
|
288
|
+
ts.forEachChild(n, count);
|
|
289
|
+
};
|
|
290
|
+
count(again);
|
|
291
|
+
if (!refs && spec && spec.parent.elements.length > 1) {
|
|
292
|
+
const list = spec.parent.elements
|
|
293
|
+
.filter((/** @type {any} */ e) => e !== spec)
|
|
294
|
+
.map((/** @type {any} */ e) => e.getText(again));
|
|
295
|
+
code = code.slice(0, spec.parent.getStart(again)) + '{ ' + list.join(', ') + ' }' + code.slice(spec.parent.end);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { code, used, left };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** @param {string} p @param {string[]} out */
|
|
302
|
+
function walk(p, out) {
|
|
303
|
+
const st = fs.statSync(p);
|
|
304
|
+
if (st.isDirectory()) {
|
|
305
|
+
for (const f of fs.readdirSync(p))
|
|
306
|
+
if (!/^(node_modules|dist|build|\.next|\.git)$/.test(f)) walk(path.join(p, f), out);
|
|
307
|
+
} else if (/\.(tsx|jsx|ts|js|mjs)$/.test(p) && !/\.d\.ts$/.test(p)) out.push(p);
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === fs.realpathSync(process.argv[1])) {
|
|
312
|
+
const argv = process.argv.slice(2);
|
|
313
|
+
const dry = argv.includes('--dry');
|
|
314
|
+
const roots = argv.filter((a) => !a.startsWith('--'));
|
|
315
|
+
if (!roots.length || argv.includes('--help')) {
|
|
316
|
+
console.log('Usage: aura-icons-codemod <files or folders…> [--dry]');
|
|
317
|
+
process.exit(roots.length ? 0 : 2);
|
|
318
|
+
}
|
|
319
|
+
if (!NAMES.size) {
|
|
320
|
+
console.error('aura-icons-codemod: no icons found in dist/esm/icons.js — is the package built?');
|
|
321
|
+
process.exit(2);
|
|
322
|
+
}
|
|
323
|
+
ts = loadTs();
|
|
324
|
+
if (!ts) {
|
|
325
|
+
console.error('aura-icons-codemod: needs TypeScript to read your code (it only parses): npm i -D typescript');
|
|
326
|
+
process.exit(2);
|
|
327
|
+
}
|
|
328
|
+
let changed = 0;
|
|
329
|
+
/** @type {string[]} */
|
|
330
|
+
const todo = [];
|
|
331
|
+
for (const f of roots.flatMap((r) => walk(r, []))) {
|
|
332
|
+
const src = fs.readFileSync(f, 'utf8');
|
|
333
|
+
const r = transform(src, f);
|
|
334
|
+
for (const l of r.left) todo.push(f + ':' + l);
|
|
335
|
+
if (r.code === src) continue;
|
|
336
|
+
changed++;
|
|
337
|
+
console.log((dry ? 'would change ' : 'changed ') + f + ' (' + Array.from(r.used, pascal).join(', ') + ')');
|
|
338
|
+
if (!dry) fs.writeFileSync(f, r.code);
|
|
339
|
+
}
|
|
340
|
+
console.log('\n' + changed + (changed === 1 ? ' file' : ' files') + (dry ? ' would change.' : ' changed.'));
|
|
341
|
+
if (todo.length) {
|
|
342
|
+
console.log('\nLeft as names — convert by hand, or keep them with registerIcons(allIcons):');
|
|
343
|
+
for (const t of todo) console.log(' ' + t);
|
|
344
|
+
}
|
|
345
|
+
}
|