@wise/wds-codemods 0.0.1-experimental-731cdc7 → 0.0.1-experimental-cbae00f

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 (60) hide show
  1. package/.changeset/better-impalas-drop.md +5 -0
  2. package/.changeset/config.json +13 -0
  3. package/.changeset/quick-mails-joke.md +128 -0
  4. package/.github/CODEOWNERS +1 -0
  5. package/.github/actions/bootstrap/action.yml +49 -0
  6. package/.github/actions/commitlint/action.yml +27 -0
  7. package/.github/actions/test/action.yml +23 -0
  8. package/.github/workflows/cd-cd.yml +127 -0
  9. package/.github/workflows/renovate.yml +16 -0
  10. package/.husky/commit-msg +1 -0
  11. package/.husky/pre-commit +1 -0
  12. package/.nvmrc +1 -0
  13. package/.prettierignore +1 -0
  14. package/.prettierrc.js +5 -0
  15. package/DEVELOPER.md +783 -0
  16. package/babel.config.js +28 -0
  17. package/commitlint.config.js +3 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +135 -133
  20. package/dist/index.js.map +1 -1
  21. package/dist/transforms/button.d.ts +16 -0
  22. package/dist/transforms/button.js +566 -493
  23. package/dist/transforms/button.js.map +1 -1
  24. package/eslint.config.js +15 -0
  25. package/jest.config.js +9 -0
  26. package/mkdocs.yml +4 -0
  27. package/package.json +14 -19
  28. package/renovate.json +9 -0
  29. package/scripts/build.sh +10 -0
  30. package/src/__tests__/runCodemod.test.ts +96 -0
  31. package/src/index.ts +4 -0
  32. package/src/runCodemod.ts +88 -0
  33. package/src/transforms/button/__tests__/button.test.tsx +153 -0
  34. package/src/transforms/button/button.ts +418 -0
  35. package/src/transforms/helpers/__tests__/createTestTransform.test.ts +27 -0
  36. package/src/transforms/helpers/__tests__/hasImport.test.ts +52 -0
  37. package/src/transforms/helpers/__tests__/iconUtils.test.ts +207 -0
  38. package/src/transforms/helpers/__tests__/jsxElementUtils.test.ts +130 -0
  39. package/src/transforms/helpers/__tests__/jsxReportingUtils.test.ts +265 -0
  40. package/src/transforms/helpers/createTestTransform.ts +18 -0
  41. package/src/transforms/helpers/hasImport.ts +60 -0
  42. package/src/transforms/helpers/iconUtils.ts +87 -0
  43. package/src/transforms/helpers/index.ts +5 -0
  44. package/src/transforms/helpers/jsxElementUtils.ts +67 -0
  45. package/src/transforms/helpers/jsxReportingUtils.ts +224 -0
  46. package/src/utils/__tests__/getOptions.test.ts +170 -0
  47. package/src/utils/__tests__/handleError.test.ts +18 -0
  48. package/src/utils/__tests__/loadTransformModules.test.ts +51 -0
  49. package/src/utils/__tests__/reportManualReview.test.ts +42 -0
  50. package/src/utils/getOptions.ts +63 -0
  51. package/src/utils/handleError.ts +6 -0
  52. package/src/utils/index.ts +4 -0
  53. package/src/utils/loadTransformModules.ts +28 -0
  54. package/src/utils/reportManualReview.ts +17 -0
  55. package/test-button.tsx +230 -0
  56. package/test-file.js +2 -0
  57. package/tsconfig.json +14 -0
  58. package/tsup.config.js +13 -0
  59. package/dist/reportManualReview-DQ00-OKx.js +0 -50
  60. package/dist/reportManualReview-DQ00-OKx.js.map +0 -1
@@ -0,0 +1,63 @@
1
+ import { confirm, input, select as list } from '@inquirer/prompts';
2
+
3
+ async function getOptions(transformFiles: string[]) {
4
+ const args = process.argv.slice(2);
5
+ if (args.length > 0) {
6
+ const [transformFile, targetPath] = args;
7
+ const dry = args.includes('--dry') || args.includes('--dry-run');
8
+ const print = args.includes('--print');
9
+ const ignorePatternIndex = args.findIndex((arg) => arg === '--ignore-pattern');
10
+ let ignorePattern: string | undefined;
11
+ if (ignorePatternIndex !== -1 && args.length > ignorePatternIndex + 1) {
12
+ ignorePattern = args[ignorePatternIndex + 1];
13
+ }
14
+ const gitignore = args.includes('--gitignore');
15
+ const noGitignore = args.includes('--no-gitignore');
16
+
17
+ if (!transformFile || !transformFiles.includes(transformFile)) {
18
+ throw new Error('Invalid transform file specified.');
19
+ }
20
+ if (!targetPath) {
21
+ throw new Error('Target path cannot be empty.');
22
+ }
23
+
24
+ // If both --gitignore and --no-gitignore are specified, prioritize --gitignore
25
+ const useGitignore = !!(gitignore || (!gitignore && !noGitignore));
26
+
27
+ return { transformFile, targetPath, dry, print, ignorePattern, gitignore: useGitignore };
28
+ }
29
+
30
+ const transformFile = await list({
31
+ message: 'Select a codemod transform to run:',
32
+ choices: transformFiles.map((file) => ({ name: file, value: file })),
33
+ });
34
+
35
+ const targetPath = await input({
36
+ message: 'Enter the target directory or file path to run codemod on:',
37
+ validate: (value) => value.trim() !== '' || 'Target path cannot be empty',
38
+ });
39
+
40
+ const dry = await confirm({
41
+ message: 'Run in dry mode (no changes written to files)?',
42
+ default: true,
43
+ });
44
+
45
+ const print = await confirm({
46
+ message: 'Print transformed source to console?',
47
+ default: false,
48
+ });
49
+
50
+ const ignorePattern = await input({
51
+ message: 'Enter ignore pattern(s) (comma separated) or leave empty:',
52
+ validate: (value) => true,
53
+ });
54
+
55
+ const gitignore = await confirm({
56
+ message: 'Respect .gitignore files?',
57
+ default: true,
58
+ });
59
+
60
+ return { transformFile, targetPath, dry, print, ignorePattern, gitignore };
61
+ }
62
+
63
+ export default getOptions;
@@ -0,0 +1,6 @@
1
+ function handleError(message: string): void {
2
+ console.error(message);
3
+ process.exit(1);
4
+ }
5
+
6
+ export default handleError;
@@ -0,0 +1,4 @@
1
+ export { default as getOptions } from './getOptions';
2
+ export { default as handleError } from './handleError';
3
+ export { default as loadTransformModules } from './loadTransformModules';
4
+ export { default as reportManualReview } from './reportManualReview';
@@ -0,0 +1,28 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+
4
+ interface TransformModule {
5
+ default: {
6
+ default: unknown;
7
+ };
8
+ }
9
+
10
+ async function loadTransformModules(transformsDir: string) {
11
+ let transformModules: Record<string, unknown> = {};
12
+
13
+ const files = await fs.readdir(transformsDir);
14
+ const transformFiles = await Promise.all(
15
+ files
16
+ .filter((file) => file.endsWith('.js'))
17
+ .map(async (file) => {
18
+ const transformPath = path.join(transformsDir, file);
19
+ const transformModule = (await import(transformPath)) as TransformModule;
20
+ transformModules = { ...transformModules, [file]: transformModule.default.default };
21
+ return file.replace('.js', '');
22
+ }),
23
+ );
24
+
25
+ return { transformModules, transformFiles };
26
+ }
27
+
28
+ export default loadTransformModules;
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ import path from 'path';
4
+
5
+ const REPORT_PATH = path.resolve(process.cwd(), 'codemod-report.txt');
6
+
7
+ const reportManualReview = async (filePath: string, message: string): Promise<void> => {
8
+ const lineMatch = /at line (\d+)/u.exec(message);
9
+ const lineNumber = lineMatch?.[1];
10
+
11
+ const cleanMessage = message.replace(/ at line \d+/u, '');
12
+ const lineInfo = lineNumber ? `:${lineNumber}` : '';
13
+
14
+ await fs.appendFile(REPORT_PATH, `[${filePath}${lineInfo}] ${cleanMessage}\n`, 'utf8');
15
+ };
16
+
17
+ export default reportManualReview;
@@ -0,0 +1,230 @@
1
+ import { ActionButton, Button } from '@transferwise/components';
2
+ import { Settings, ArrowRight, ArrowLeft } from '@transferwise/icons';
3
+
4
+ export default function Test() {
5
+ return (
6
+ <>
7
+ {/* Supported priorities (should NOT warn) */}
8
+ <ActionButton priority="primary">Primary</ActionButton>
9
+ <ActionButton priority="secondary">Secondary</ActionButton>
10
+ <ActionButton priority="tertiary">Tertiary</ActionButton>
11
+ {/* Supported priorities with icon (should NOT warn) */}
12
+ <ActionButton priority="primary">
13
+ <Settings />
14
+ Primary
15
+ </ActionButton>
16
+ <ActionButton priority="secondary">
17
+ <Settings />
18
+ Secondary
19
+ </ActionButton>
20
+ <ActionButton priority="tertiary">
21
+ <Settings />
22
+ Tertiary
23
+ </ActionButton>
24
+ {/* No priority (should NOT warn, default is primary) */}
25
+ <ActionButton>Default</ActionButton>
26
+ {/* Spread props (should warn) */}
27
+ <ActionButton {...props}>Spread</ActionButton>
28
+ <ActionButton priority="primary" {...otherProps}>
29
+ Spread
30
+ </ActionButton>
31
+ {/* Unsupported string value (should warn) */}
32
+ <ActionButton priority="super-primary">Unsupported</ActionButton>
33
+ {/* Enum values (should NOT warn if mapped, should warn if not) */}
34
+ <ActionButton priority={Priority.PRIMARY}>Enum Primary</ActionButton>
35
+ <ActionButton priority={Priority.SECONDARY}>Enum Secondary</ActionButton>
36
+ <ActionButton priority={Priority.TERTIARY}>Enum Tertiary</ActionButton>
37
+ <ActionButton priority={Priority.UNKNOWN}>Enum Unknown</ActionButton> {/* Should warn */}
38
+ {/* Identifier (should warn) */}
39
+ <ActionButton priority={somePriority}>Identifier</ActionButton>
40
+ {/* Function call (should warn) */}
41
+ <ActionButton priority={getPriority()}>Function Call</ActionButton>
42
+ {/* Conditional expression (should warn) */}
43
+ <ActionButton priority={isPrimary ? 'primary' : 'secondary'}>Conditional</ActionButton>
44
+ {/* Member expression (should warn if not mapped) */}
45
+ <ActionButton priority={props.priority}>Member Expression</ActionButton>
46
+ {/* With text prop (should NOT warn, but should be migrated to children) */}
47
+ <ActionButton text="Text prop" />
48
+ {/* With text prop and children (should NOT warn, but children take precedence) */}
49
+ <ActionButton text="Text prop">Child</ActionButton>
50
+ {/* Disabled (should NOT warn) */}
51
+ <ActionButton disabled>Disabled</ActionButton>
52
+ {/* With className and onClick (should NOT warn) */}
53
+ <ActionButton className="foo" onClick={() => {}}>
54
+ With className and onClick
55
+ </ActionButton>
56
+ {/* All ambiguous (should warn for each) */}
57
+ <ActionButton priority={foo} text={bar} {...baz}>
58
+ Ambiguous
59
+ </ActionButton>
60
+ {/* Ambiguous icon usage: conditional expression (should warn) */}
61
+ <ActionButton>
62
+ {isActive ? <Settings /> : <ArrowRight />}
63
+ Action
64
+ </ActionButton>
65
+ {/* Ambiguous icon usage: function call (should warn) */}
66
+ <ActionButton>
67
+ {getIcon()}
68
+ Action
69
+ </ActionButton>
70
+ {/* Ambiguous icon usage: identifier (should warn) */}
71
+ <ActionButton>
72
+ {iconComponent}
73
+ Action
74
+ </ActionButton>
75
+ {/* Ambiguous icon usage: member expression (should warn) */}
76
+ <ActionButton>
77
+ {props.icon}
78
+ Action
79
+ </ActionButton>
80
+ {/* Button with icon as first child (should map to addonStart, no warning) */}
81
+ <Button size="sm" priority="primary" type="accent">
82
+ <Settings />
83
+ Continue
84
+ </Button>
85
+ {/* Button with icon as last child (should map to addonEnd, no warning) */}
86
+ <Button size="sm" priority="primary" type="accent">
87
+ Continue
88
+ <ArrowRight />
89
+ </Button>
90
+ {/* Button with icon in the middle (should map to addonStart or addonEnd, depending on codemod logic) */}
91
+ <Button size="sm" priority="primary" type="accent">
92
+ Continue
93
+ <ArrowRight />
94
+ Now
95
+ </Button>
96
+ {/* Button with two icons (should map first to addonStart, second to addonEnd) */}
97
+ <Button size="sm" priority="primary" type="accent">
98
+ <ArrowLeft />
99
+ Continue
100
+ <ArrowRight />
101
+ </Button>
102
+ {/* Button with icon wrapped in JSX expression (should still map, no warning) */}
103
+ <Button size="sm" priority="primary" type="accent">
104
+ {<Settings />}
105
+ Continue
106
+ </Button>
107
+ {/* Button with icon and ambiguous prop (should warn for prop, not for icon) */}
108
+ <Button size={getSize()}>
109
+ <Settings />
110
+ Continue
111
+ </Button>
112
+ {/* ActionButton with icon as first child (should map to addonStart, no warning) */}
113
+ <ActionButton priority="primary">
114
+ <Settings />
115
+ Action
116
+ </ActionButton>
117
+ {/* ActionButton with icon as last child (should map to addonEnd, no warning) */}
118
+ <ActionButton priority="primary">
119
+ Action
120
+ <ArrowRight />
121
+ </ActionButton>
122
+ {/* ActionButton with icon and ambiguous prop (should warn for prop, not for icon) */}
123
+ <ActionButton priority={somePriority}>
124
+ <Settings />
125
+ Action
126
+ </ActionButton>
127
+ {/* ActionButton with icon and spread props (should warn for spread) */}
128
+ <ActionButton {...props}>
129
+ <Settings />
130
+ Action
131
+ </ActionButton>
132
+ {/* Supported values (should NOT warn) */}
133
+ <Button size="sm" priority="primary" type="accent" />
134
+ <Button size="md" priority="secondary" type="submit" />
135
+ <Button sentiment="negative" />
136
+ {/* Spread props (should warn) */}
137
+ <Button {...props} />
138
+ <Button size="sm" {...otherProps} />
139
+ {/* Unsupported string values (should warn) */}
140
+ <Button size="huge" />
141
+ <Button priority="super-primary" />
142
+ <Button type="unknown" />
143
+ <Button sentiment="neutral" />
144
+ {/* Enum values (should NOT warn if mapped, should warn if not) */}
145
+ <Button size={Size.EXTRA_SMALL} priority={Priority.SECONDARY} type={ControlType.NEGATIVE} />
146
+ <Button size={Size.UNKNOWN} /> {/* Should warn: unknown enum */}
147
+ <Button priority={Priority.UNKNOWN} /> {/* Should warn: unknown enum */}
148
+ <Button type={ControlType.UNKNOWN} /> {/* Should warn: unknown enum */}
149
+ {/* Identifiers (should warn) */}
150
+ <Button size={dynamicSize} />
151
+ <Button priority={somePriority} />
152
+ <Button type={someType} />
153
+ {/* Function calls (should warn) */}
154
+ <Button size={getSize()} />
155
+ <Button priority={getPriority()} />
156
+ <Button type={getType()} />
157
+ {/* Conditional expressions (should warn) */}
158
+ <Button size={isBig ? 'lg' : 'sm'} />
159
+ <Button priority={isPrimary ? 'primary' : 'secondary'} />
160
+ <Button type={isAccent ? 'accent' : 'primary'} />
161
+ {/* Member expressions (should warn if not mapped) */}
162
+ <Button size={props.size} />
163
+ <Button priority={props.priority} />
164
+ <Button type={props.type} />
165
+ {/* htmlType prop (should warn if unsupported) */}
166
+ <Button htmlType="magic" />
167
+ <Button htmlType={getHtmlType()} />
168
+ {/* Multiple ambiguous props (should warn for each) */}
169
+ <Button size={getSize()} priority={somePriority} type="unknown" />
170
+ {/* as="a" without href (should not warn, but should add href) */}
171
+ <Button as="a" />
172
+ {/* as="a" with href (should not warn) */}
173
+ <Button as="a" href="/foo" />
174
+ {/* as="a" with spread (should warn for spread) */}
175
+ <Button as="a" {...props} />
176
+ {/* All legacy props as ambiguous */}
177
+ <Button size={foo} priority={bar} type={baz} htmlType={qux} sentiment={quux} />
178
+ {/* No legacy props (should NOT warn) */}
179
+ <Button block />
180
+ {/* Supported string literals */}
181
+ <Button size="sm" priority="primary" type="accent" />
182
+ <Button size="md" priority="secondary" type="submit" />
183
+ <Button size="lg" priority="tertiary" type="danger" />
184
+ <Button size="xl" priority="secondary-neutral" type="link" />
185
+ <Button size="xs" priority="primary" type="pay" />
186
+ <Button sentiment="negative" />
187
+ <Button type="button" />
188
+ <Button type="reset" />
189
+ <Button htmlType="submit" />
190
+ <Button htmlType="button" />
191
+ <Button htmlType="reset" />
192
+ {/* Supported enum values (assuming your codemod maps these correctly) */}
193
+ <Button size={Size.SMALL} priority={Priority.PRIMARY} type={ControlType.ACCENT} />
194
+ <Button size={Size.MEDIUM} priority={Priority.SECONDARY} type={ControlType.POSITIVE} />
195
+ <Button size={Size.LARGE} priority={Priority.TERTIARY} type={ControlType.DANGER} />
196
+ <Button
197
+ size={Size.EXTRA_SMALL}
198
+ priority={Priority.SECONDARY_NEUTRAL}
199
+ type={ControlType.LINK}
200
+ />
201
+ <Button size={Size.EXTRA_LARGE} priority={Priority.PRIMARY} type={ControlType.PAY} />
202
+ <Button sentiment={Sentiment.NEGATIVE} />
203
+ <Button type={ControlType.BUTTON} />
204
+ <Button type={ControlType.RESET} />
205
+ <Button htmlType={HtmlType.SUBMIT} />
206
+ <Button htmlType={HtmlType.BUTTON} />
207
+ <Button htmlType={HtmlType.RESET} />
208
+ {/* No legacy props */}
209
+ <Button block />
210
+ <Button disabled />
211
+ <Button style={{ color: 'red' }} />
212
+ <Button href="/foo" />
213
+ <Button v2 />
214
+ <Button />
215
+ {/* as="a" with href */}
216
+ <Button as="a" href="/account" />
217
+ <Button as="a" href="#" />
218
+ {/* Combination of supported props */}
219
+ <Button size="sm" priority="primary" type="accent" block disabled />
220
+ <Button
221
+ size={Size.SMALL}
222
+ priority={Priority.PRIMARY}
223
+ type={ControlType.ACCENT}
224
+ block
225
+ disabled
226
+ />
227
+ <Button size="md" priority="secondary" type="submit" style={{ color: 'blue' }} />
228
+ </>
229
+ );
230
+ }
package/test-file.js ADDED
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2
+ const foo = 123;
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "outDir": "dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["node_modules", "dist", "src/utils/createTestTransform.ts"]
14
+ }
package/tsup.config.js ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ target: 'es2022',
11
+ outDir: 'dist',
12
+ platform: 'node',
13
+ });
@@ -1,50 +0,0 @@
1
- //#region rolldown:runtime
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
- key = keys[i];
11
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
- get: ((k) => from[k]).bind(null, key),
13
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
- });
15
- }
16
- return to;
17
- };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
- value: mod,
20
- enumerable: true
21
- }) : target, mod));
22
-
23
- //#endregion
24
- const node_fs_promises = __toESM(require("node:fs/promises"));
25
- const node_path = __toESM(require("node:path"));
26
-
27
- //#region src/utils/reportManualReview.ts
28
- const REPORT_PATH = node_path.default.resolve(process.cwd(), "codemod-report.txt");
29
- const reportManualReview = async (filePath, message) => {
30
- const lineMatch = /at line (\d+)/u.exec(message);
31
- const lineNumber = lineMatch?.[1];
32
- const cleanMessage = message.replace(/ at line \d+/u, "");
33
- const lineInfo = lineNumber ? `:${lineNumber}` : "";
34
- await node_fs_promises.default.appendFile(REPORT_PATH, `[${filePath}${lineInfo}] ${cleanMessage}\n`, "utf8");
35
- };
36
-
37
- //#endregion
38
- Object.defineProperty(exports, '__toESM', {
39
- enumerable: true,
40
- get: function () {
41
- return __toESM;
42
- }
43
- });
44
- Object.defineProperty(exports, 'reportManualReview', {
45
- enumerable: true,
46
- get: function () {
47
- return reportManualReview;
48
- }
49
- });
50
- //# sourceMappingURL=reportManualReview-DQ00-OKx.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"reportManualReview-DQ00-OKx.js","names":["path","fs"],"sources":["../src/utils/reportManualReview.ts"],"sourcesContent":["import fs from 'node:fs/promises';\n\nimport path from 'path';\n\nconst REPORT_PATH = path.resolve(process.cwd(), 'codemod-report.txt');\n\nconst reportManualReview = async (filePath: string, message: string): Promise<void> => {\n const lineMatch = /at line (\\d+)/u.exec(message);\n const lineNumber = lineMatch?.[1];\n\n const cleanMessage = message.replace(/ at line \\d+/u, '');\n const lineInfo = lineNumber ? `:${lineNumber}` : '';\n\n await fs.appendFile(REPORT_PATH, `[${filePath}${lineInfo}] ${cleanMessage}\\n`, 'utf8');\n};\n\nexport default reportManualReview;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,MAAM,cAAcA,kBAAK,QAAQ,QAAQ,OAAO;AAEhD,MAAM,qBAAqB,OAAO,UAAkB,YAAmC;CACrF,MAAM,YAAY,iBAAiB,KAAK;CACxC,MAAM,aAAa,YAAY;CAE/B,MAAM,eAAe,QAAQ,QAAQ,iBAAiB;CACtD,MAAM,WAAW,aAAa,IAAI,eAAe;AAEjD,OAAMC,yBAAG,WAAW,aAAa,IAAI,WAAW,SAAS,IAAI,aAAa,KAAK;AAChF"}