@juspay/svelte-ui-components 3.2.0 → 3.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",
@@ -27,7 +27,7 @@
27
27
  "package": "svelte-kit sync && svelte-package && publint",
28
28
  "prepublishOnly": "npm run build:wc && npm run package",
29
29
  "test": "npm run test:integration && npm run test:unit",
30
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && npm run check:codemod && npm run check:migrate",
30
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && npm run check:codemod && npm run check:migrate && npm run check:wc-parity",
31
31
  "check:codemod": "tsc -p scripts/codemod/tsconfig.json",
32
32
  "codemod": "node scripts/codemod/cli.ts",
33
33
  "check:wc": "svelte-check --tsconfig ./tsconfig.wc.json --compiler-warnings \"options_missing_custom_element:ignore\"",
@@ -40,7 +40,9 @@
40
40
  "test:visual": "scripts/visual-test.sh",
41
41
  "test:visual:update": "scripts/visual-test.sh --update-snapshots",
42
42
  "migrate": "node scripts/migrate/cli.ts",
43
- "check:migrate": "tsc -p scripts/migrate/tsconfig.json"
43
+ "check:migrate": "tsc -p scripts/migrate/tsconfig.json",
44
+ "check:wc-parity": "tsc -p scripts/wc-parity/tsconfig.json",
45
+ "postinstall": "node scripts/postinstall.mjs"
44
46
  },
45
47
  "exports": {
46
48
  ".": {
@@ -56,13 +58,17 @@
56
58
  "dist",
57
59
  "!dist/**/*.test.*",
58
60
  "!dist/**/*.spec.*",
59
- "dist-wc"
61
+ "dist-wc",
62
+ "scripts/codemod",
63
+ "!scripts/codemod/*.test.ts",
64
+ "scripts/postinstall.mjs"
60
65
  ],
61
66
  "peerDependencies": {
62
67
  "lottie-web": ">=5.0.0",
63
68
  "marked": "^18.0.0",
64
69
  "svelte": "^5.41.2",
65
- "type-decoder": "^2.1.0"
70
+ "type-decoder": "^2.1.0",
71
+ "typescript": ">=5.0.0"
66
72
  },
67
73
  "peerDependenciesMeta": {
68
74
  "lottie-web": {
@@ -70,6 +76,9 @@
70
76
  },
71
77
  "marked": {
72
78
  "optional": true
79
+ },
80
+ "typescript": {
81
+ "optional": true
73
82
  }
74
83
  },
75
84
  "devDependencies": {
@@ -183,5 +192,11 @@
183
192
  "replaceText": {
184
193
  "(ABC-\\d+)": "[`$1`](https://juspay.atlassian.net/browse/$1)"
185
194
  }
195
+ },
196
+ "bin": {
197
+ "sui-codemod": "./scripts/codemod/cli.ts"
198
+ },
199
+ "engines": {
200
+ "node": ">=22.18"
186
201
  }
187
202
  }
@@ -0,0 +1,76 @@
1
+ # polymorph → SUI consumer migration codemod
2
+
3
+ Also reports the one breaking change in the 4.0.0 custom-element surface:
4
+ `children` is no longer a declared property on `sui-chat-bubble`,
5
+ `sui-draggable` or `sui-resizable`, so assigning it silently loses the content.
6
+ That check runs on every forward migration and in dry runs, reports rather than
7
+ rewrites (moving content into markup is a decision, not a rename), and only
8
+ fires in files that also mention one of the three elements. See
9
+ `wc-children.ts`.
10
+
11
+ This package is published, so consumers can run it without a checkout:
12
+
13
+ ```sh
14
+ npx sui-codemod --dry-run ./src
15
+ ```
16
+
17
+ Mechanically migrates a consumer codebase from `polymorph-ui-components` to
18
+ `@juspay/svelte-ui-components`:
19
+
20
+ - rewrites import/export/dynamic-import specifiers (subpaths like `/wc` are
21
+ preserved), in `.svelte`, `.ts` and `.js` files;
22
+ - renames the 28 synthetic event props (see `map.ts`) on library components in
23
+ `.svelte` markup. All but one differ from the fork only in case; Stepper's
24
+ `onhandlestepclick` becomes `onstepclick`, stepping over the deprecated
25
+ `onhandleStepClick` alias that the casing match would otherwise have picked.
26
+ Renaming is component-aware: `onsort` is renamed on a
27
+ `<Table>` imported from the library (through aliases,
28
+ `import { Table as DataTable }`, namespace imports and
29
+ `<svelte:component this={Table}>`), and left alone on your own components
30
+ and on native elements.
31
+
32
+ ## Usage
33
+
34
+ Run from a checkout of this repo (dependencies resolve here), pointing at the
35
+ consumer project. Requires Node >= 22.18 (runs TypeScript directly).
36
+
37
+ ```sh
38
+ # preview
39
+ npm run codemod -- --dry-run ../consumer-app/src
40
+
41
+ # apply
42
+ npm run codemod -- ../consumer-app/src
43
+
44
+ # migrate in the other direction (SUI -> polymorph)
45
+ npm run codemod -- --reverse ../consumer-app/src
46
+ ```
47
+
48
+ Directories are walked recursively; `node_modules`, `.git`, `.svelte-kit`,
49
+ `dist`, `dist-wc`, `build`, `coverage`, `playwright-report` and `test-results`
50
+ are skipped.
51
+
52
+ ## What it will not do (by design)
53
+
54
+ Anything it cannot prove safe is reported as a `WARN` with `file:line:column`
55
+ instead of being rewritten — silent wrong rewrites are worse than reported
56
+ skips:
57
+
58
+ - **Spread attributes.** `<Table {...props} />` may carry a renamed prop
59
+ inside `props`; the object is defined elsewhere, so it is warned about, not
60
+ rewritten.
61
+ - **Unresolvable components.** `const Picked = Table;` then
62
+ `<Picked onsort={...} />` — the tag does not resolve to an import, so a
63
+ warning is emitted when it carries a renameable prop name.
64
+ - **Default imports** from the library cannot be resolved to a component.
65
+ - **Semantic renames** — same event, different word — are not casing pairs
66
+ and need a human: Gallery `onclose`→`onDismiss`, `onchange`→`onIndexChange`;
67
+ MediaUpload `onchange`→`onFilesChange`, `onerror`→`onRejected`.
68
+ - Props typed against library types in `<script>` (e.g. an object literal fed
69
+ to a spread) are not rewritten — the spread warning covers the usage site.
70
+
71
+ Shorthand attributes are expanded when renamed (`{onsort}` →
72
+ `onSort={onsort}`), so a `--reverse` round trip restores the original names
73
+ but not the shorthand form.
74
+
75
+ `map.ts` documents how the pair list was derived and verified against both
76
+ libraries' sources.
@@ -0,0 +1,206 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { parseArgs } from 'node:util';
5
+ import type { Direction } from './map.ts';
6
+ import { transformModuleSpecifiers, transformSvelte } from './transform.ts';
7
+ import type { TransformResult } from './transform.ts';
8
+ import { findChildrenAssignments } from './wc-children.ts';
9
+
10
+ export type CliSummary = {
11
+ readonly exitCode: number;
12
+ readonly filesScanned: number;
13
+ readonly filesChanged: number;
14
+ readonly propsRenamed: number;
15
+ readonly importsRewritten: number;
16
+ readonly warnings: number;
17
+ };
18
+
19
+ const USAGE = [
20
+ 'Usage: node scripts/codemod/cli.ts [--reverse] [--dry-run] <path...>',
21
+ '',
22
+ 'Migrates consumer sources from polymorph-ui-components to',
23
+ '@juspay/svelte-ui-components: rewrites import specifiers and renames the',
24
+ 'casing-only synthetic event props on library components (see map.ts).',
25
+ '',
26
+ ' --reverse migrate @juspay/svelte-ui-components -> polymorph-ui-components',
27
+ ' --dry-run print every change as a diff without writing any file',
28
+ ' --help show this help',
29
+ '',
30
+ 'Paths may be files or directories; directories are walked recursively,',
31
+ 'skipping node_modules, .git, .svelte-kit, dist, dist-wc, build, coverage,',
32
+ 'playwright-report and test-results.'
33
+ ].join('\n');
34
+
35
+ const SKIPPED_DIRECTORIES = new Set([
36
+ 'node_modules',
37
+ '.git',
38
+ '.svelte-kit',
39
+ 'dist',
40
+ 'dist-wc',
41
+ 'build',
42
+ 'coverage',
43
+ 'playwright-report',
44
+ 'test-results'
45
+ ]);
46
+
47
+ const SCRIPT_FILE = /\.(ts|js|mts|mjs|cts|cjs)$/;
48
+
49
+ function collectFiles(path: string, out: Set<string>): void {
50
+ const stats = statSync(path);
51
+ if (stats.isFile()) {
52
+ if (path.endsWith('.svelte') || SCRIPT_FILE.test(path)) {
53
+ out.add(path);
54
+ }
55
+ return;
56
+ }
57
+ if (!stats.isDirectory()) {
58
+ return;
59
+ }
60
+ const entries = [...readdirSync(path, { withFileTypes: true })].sort((a, b) =>
61
+ a.name.localeCompare(b.name)
62
+ );
63
+ for (const entry of entries) {
64
+ if (entry.isDirectory()) {
65
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
66
+ collectFiles(join(path, entry.name), out);
67
+ }
68
+ } else if (entry.isFile()) {
69
+ collectFiles(join(path, entry.name), out);
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Every edit the transform makes stays on a single line, so old and new text
76
+ * always have the same line count and a pairwise comparison is an exact diff.
77
+ */
78
+ function printLineDiff(
79
+ file: string,
80
+ before: string,
81
+ after: string,
82
+ log: (line: string) => void
83
+ ): void {
84
+ const oldLines = before.split('\n');
85
+ const newLines = after.split('\n');
86
+ log(`--- ${file}`);
87
+ if (oldLines.length !== newLines.length) {
88
+ log(' (line counts differ; showing full replacement)');
89
+ for (const line of oldLines) {
90
+ log(`- ${line}`);
91
+ }
92
+ for (const line of newLines) {
93
+ log(`+ ${line}`);
94
+ }
95
+ return;
96
+ }
97
+ oldLines.forEach((oldLine, index) => {
98
+ const newLine = newLines.at(index) ?? '';
99
+ if (oldLine !== newLine) {
100
+ log(`@@ ${file}:${index + 1}`);
101
+ log(`- ${oldLine}`);
102
+ log(`+ ${newLine}`);
103
+ }
104
+ });
105
+ }
106
+
107
+ export function runCodemod(argv: ReadonlyArray<string>, log: (line: string) => void): CliSummary {
108
+ const failure = {
109
+ exitCode: 2,
110
+ filesScanned: 0,
111
+ filesChanged: 0,
112
+ propsRenamed: 0,
113
+ importsRewritten: 0,
114
+ warnings: 0
115
+ };
116
+ let parsed;
117
+ try {
118
+ parsed = parseArgs({
119
+ args: [...argv],
120
+ options: {
121
+ reverse: { type: 'boolean' },
122
+ 'dry-run': { type: 'boolean' },
123
+ help: { type: 'boolean' }
124
+ },
125
+ allowPositionals: true,
126
+ strict: true
127
+ });
128
+ } catch (error) {
129
+ log(error instanceof Error ? error.message : String(error));
130
+ log(USAGE);
131
+ return failure;
132
+ }
133
+ if (parsed.values.help === true) {
134
+ log(USAGE);
135
+ return { ...failure, exitCode: 0 };
136
+ }
137
+ if (parsed.positionals.length === 0) {
138
+ log(USAGE);
139
+ return failure;
140
+ }
141
+ for (const path of parsed.positionals) {
142
+ if (!existsSync(path)) {
143
+ log(`path does not exist: ${path}`);
144
+ return failure;
145
+ }
146
+ }
147
+ const direction: Direction = parsed.values.reverse === true ? 'to-poly' : 'to-sui';
148
+ const dryRun = parsed.values['dry-run'] === true;
149
+
150
+ const files = new Set<string>();
151
+ for (const path of parsed.positionals) {
152
+ collectFiles(resolve(path), files);
153
+ }
154
+
155
+ let filesChanged = 0;
156
+ let propsRenamed = 0;
157
+ let importsRewritten = 0;
158
+ let warnings = 0;
159
+ for (const file of files) {
160
+ const source = readFileSync(file, 'utf8');
161
+ const result: TransformResult = file.endsWith('.svelte')
162
+ ? transformSvelte(source, file, direction)
163
+ : transformModuleSpecifiers(source, file, direction);
164
+ // Reported in both directions and in dry runs: it is a 4.0.0 breaking change
165
+ // to report, not a rename to apply, so it never contributes to filesChanged.
166
+ const childrenWarnings = direction === 'to-sui' ? findChildrenAssignments(source, file) : [];
167
+ for (const warning of [...result.warnings, ...childrenWarnings]) {
168
+ log(`${warning.file}:${warning.line}:${warning.column} WARN ${warning.message}`);
169
+ }
170
+ warnings += result.warnings.length + childrenWarnings.length;
171
+ if (!result.changed) {
172
+ continue;
173
+ }
174
+ filesChanged += 1;
175
+ propsRenamed += result.propsRenamed;
176
+ importsRewritten += result.importsRewritten;
177
+ if (dryRun) {
178
+ printLineDiff(file, source, result.code, log);
179
+ } else {
180
+ writeFileSync(file, result.code);
181
+ log(
182
+ `${file}: ${result.propsRenamed} prop(s) renamed, ${result.importsRewritten} import(s) rewritten`
183
+ );
184
+ }
185
+ }
186
+ log(
187
+ `${dryRun ? '[dry run] ' : ''}scanned ${files.size} file(s), changed ${filesChanged}, ` +
188
+ `renamed ${propsRenamed} prop(s), rewrote ${importsRewritten} import(s), ${warnings} warning(s)`
189
+ );
190
+ return {
191
+ exitCode: 0,
192
+ filesScanned: files.size,
193
+ filesChanged,
194
+ propsRenamed,
195
+ importsRewritten,
196
+ warnings
197
+ };
198
+ }
199
+
200
+ const entryPoint = process.argv.at(1) ?? '';
201
+ if (entryPoint !== '' && import.meta.url === pathToFileURL(entryPoint).href) {
202
+ const summary = runCodemod(process.argv.slice(2), (line) => {
203
+ console.log(line);
204
+ });
205
+ process.exitCode = summary.exitCode;
206
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Casing-only prop pairs between this library (SUI) and its fork
3
+ * `polymorph-ui-components` (poly). Poly renamed synthesized event props to
4
+ * all-lowercase; these are the props whose names differ ONLY in case between
5
+ * the two libraries.
6
+ *
7
+ * Derived mechanically (not copied from docs): the `$props()` destructure of
8
+ * every component exported by both `src/lib/index.ts` (SUI @ release) and
9
+ * `dist/index.js` of polymorph-ui-components@0.7.0 was parsed with
10
+ * `svelte/compiler`, and a pair was accepted only when
11
+ * `lower(suiName) === lower(polyName)`, the names differ, and neither side
12
+ * also declares the other side's spelling (which would make a rewrite
13
+ * ambiguous or unnecessary). Cross-checked against each component's
14
+ * `properties.ts` / `properties.d.ts`.
15
+ *
16
+ * That derivation is deprecation-blind, and one pair had to be corrected by
17
+ * hand afterwards: where a component keeps a deprecated alias whose lowercase
18
+ * spelling matches the fork's, the alias wins the case-insensitive match and
19
+ * outranks the canonical prop. Stepper is the only such pair. `map.test.ts`
20
+ * now asserts no target is `@deprecated`, so a future alias cannot repeat it.
21
+ *
22
+ * Deliberately excluded (same event, different word — a semantic rename, not
23
+ * a casing pair; these need a human):
24
+ * Gallery: poly `onclose` vs SUI `onDismiss`, poly `onchange` vs SUI `onIndexChange`
25
+ * MediaUpload: poly `onchange` vs SUI `onFilesChange`, poly `onerror` vs SUI `onRejected`
26
+ * Props spelled identically on both sides (e.g. `onkeydown` on ListItem or
27
+ * Modal) are intentionally absent: they need no rewrite. `onkeydown` on Input
28
+ * IS listed because SUI's Input only accepts `onKeyDown`.
29
+ */
30
+
31
+ export type PropPair = {
32
+ readonly component: string;
33
+ readonly sui: string;
34
+ readonly poly: string;
35
+ /**
36
+ * Superseded SUI spellings that still work. Never a rewrite target, but
37
+ * recognized as a source when rewriting back to the fork.
38
+ */
39
+ readonly suiDeprecatedAliases?: readonly string[];
40
+ };
41
+
42
+ export const SUI_PACKAGE = '@juspay/svelte-ui-components';
43
+ export const POLY_PACKAGE = 'polymorph-ui-components';
44
+
45
+ export const PROP_PAIRS: ReadonlyArray<PropPair> = [
46
+ { component: 'Gallery', sui: 'onDeleteClick', poly: 'ondeleteclick' },
47
+ { component: 'Gallery', sui: 'onEditClick', poly: 'oneditclick' },
48
+ { component: 'Gallery', sui: 'onImageClick', poly: 'onimageclick' },
49
+ { component: 'Gallery', sui: 'onOpen', poly: 'onopen' },
50
+ { component: 'Input', sui: 'onBlur', poly: 'onblur' },
51
+ { component: 'Input', sui: 'onClick', poly: 'onclick' },
52
+ { component: 'Input', sui: 'onFocus', poly: 'onfocus' },
53
+ { component: 'Input', sui: 'onFocusout', poly: 'onfocusout' },
54
+ { component: 'Input', sui: 'onInput', poly: 'oninput' },
55
+ { component: 'Input', sui: 'onKeyDown', poly: 'onkeydown' },
56
+ { component: 'Input', sui: 'onPaste', poly: 'onpaste' },
57
+ { component: 'Input', sui: 'onStateChange', poly: 'onstatechange' },
58
+ { component: 'ListItem', sui: 'oncenterTextClick', poly: 'oncentertextclick' },
59
+ { component: 'ListItem', sui: 'onitemClick', poly: 'onitemclick' },
60
+ { component: 'ListItem', sui: 'onleftImageClick', poly: 'onleftimageclick' },
61
+ { component: 'ListItem', sui: 'onrightImageClick', poly: 'onrightimageclick' },
62
+ { component: 'ListItem', sui: 'ontopSectionClick', poly: 'ontopsectionclick' },
63
+ { component: 'MediaUpload', sui: 'onRemove', poly: 'onremove' },
64
+ { component: 'Modal', sui: 'onheaderLeftImageClick', poly: 'onheaderleftimageclick' },
65
+ { component: 'Modal', sui: 'onheaderRightImageClick', poly: 'onheaderrightimageclick' },
66
+ { component: 'Modal', sui: 'onoverlayClick', poly: 'onoverlayclick' },
67
+ { component: 'Modal', sui: 'onprimaryButtonClick', poly: 'onprimarybuttonclick' },
68
+ { component: 'Modal', sui: 'onsecondaryButtonClick', poly: 'onsecondarybuttonclick' },
69
+ // The lowercase match here is `onhandleStepClick`, which SUI deprecates in
70
+ // favour of `onstepclick`. Rewriting to the alias would be case-correct and
71
+ // still wrong: it is the one spelling of this event scheduled for removal.
72
+ {
73
+ component: 'Stepper',
74
+ sui: 'onstepclick',
75
+ poly: 'onhandlestepclick',
76
+ suiDeprecatedAliases: ['onhandleStepClick']
77
+ },
78
+ { component: 'Table', sui: 'onRowClick', poly: 'onrowclick' },
79
+ { component: 'Table', sui: 'onSort', poly: 'onsort' },
80
+ { component: 'Toast', sui: 'onToastHide', poly: 'ontoasthide' },
81
+ { component: 'Toolbar', sui: 'onbackClick', poly: 'onbackclick' }
82
+ ];
83
+
84
+ export type Direction = 'to-sui' | 'to-poly';
85
+
86
+ export type DirectionConfig = {
87
+ readonly fromPackage: string;
88
+ readonly toPackage: string;
89
+ /** component name -> (source prop name -> target prop name) */
90
+ readonly renames: ReadonlyMap<string, ReadonlyMap<string, string>>;
91
+ /** every source prop name across all components, for unresolved-tag warnings */
92
+ readonly allFromProps: ReadonlySet<string>;
93
+ };
94
+
95
+ export function directionConfig(direction: Direction): DirectionConfig {
96
+ const renames = new Map<string, Map<string, string>>();
97
+ const allFromProps = new Set<string>();
98
+ for (const pair of PROP_PAIRS) {
99
+ const table = renames.get(pair.component) ?? new Map<string, string>();
100
+ // Deprecated aliases are sources, never targets: a consumer moving to SUI
101
+ // lands on the canonical prop, while one moving back to the fork is
102
+ // recognized whichever spelling they are currently on.
103
+ const sources =
104
+ direction === 'to-sui' ? [pair.poly] : [pair.sui, ...(pair.suiDeprecatedAliases ?? [])];
105
+ const to = direction === 'to-sui' ? pair.sui : pair.poly;
106
+ for (const from of sources) {
107
+ table.set(from, to);
108
+ allFromProps.add(from);
109
+ }
110
+ renames.set(pair.component, table);
111
+ }
112
+ return {
113
+ fromPackage: direction === 'to-sui' ? POLY_PACKAGE : SUI_PACKAGE,
114
+ toPackage: direction === 'to-sui' ? SUI_PACKAGE : POLY_PACKAGE,
115
+ renames,
116
+ allFromProps
117
+ };
118
+ }