@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/README.md +55 -2
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +4 -0
- package/dist-wc/index.js +2047 -841
- package/package.json +20 -5
- package/scripts/codemod/README.md +76 -0
- package/scripts/codemod/cli.ts +206 -0
- package/scripts/codemod/map.ts +118 -0
- package/scripts/codemod/transform.ts +549 -0
- package/scripts/codemod/tsconfig.json +15 -0
- package/scripts/codemod/wc-children.ts +75 -0
- package/scripts/postinstall.mjs +56 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { parse } from 'svelte/compiler';
|
|
3
|
+
import type { AST } from 'svelte/compiler';
|
|
4
|
+
import { directionConfig } from './map.ts';
|
|
5
|
+
import type { Direction, DirectionConfig } from './map.ts';
|
|
6
|
+
|
|
7
|
+
export type TransformWarning = {
|
|
8
|
+
readonly file: string;
|
|
9
|
+
readonly line: number;
|
|
10
|
+
readonly column: number;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type TransformResult = {
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly changed: boolean;
|
|
17
|
+
readonly propsRenamed: number;
|
|
18
|
+
readonly importsRewritten: number;
|
|
19
|
+
readonly warnings: ReadonlyArray<TransformWarning>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Edit = { readonly start: number; readonly end: number; readonly text: string };
|
|
23
|
+
|
|
24
|
+
type Program = AST.Script['content'];
|
|
25
|
+
type LibraryElement = AST.Component | AST.SvelteComponent;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* estree nodes inside `<script>` are not typed with offsets, but the acorn
|
|
29
|
+
* parser svelte uses attaches them at runtime; recover them by narrowing.
|
|
30
|
+
*/
|
|
31
|
+
function rangeOf(node: object): { readonly start: number; readonly end: number } | null {
|
|
32
|
+
if (
|
|
33
|
+
'start' in node &&
|
|
34
|
+
typeof node.start === 'number' &&
|
|
35
|
+
'end' in node &&
|
|
36
|
+
typeof node.end === 'number'
|
|
37
|
+
) {
|
|
38
|
+
return { start: node.start, end: node.end };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function positionOf(source: string, offset: number): { line: number; column: number } {
|
|
44
|
+
const before = source.slice(0, offset);
|
|
45
|
+
const line = (before.match(/\n/g) ?? []).length + 1;
|
|
46
|
+
return { line, column: offset - before.lastIndexOf('\n') };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function applyEdits(source: string, edits: ReadonlyArray<Edit>): string {
|
|
50
|
+
const ordered = [...edits].sort((a, b) => b.start - a.start);
|
|
51
|
+
return ordered.reduce(
|
|
52
|
+
(code, edit) => code.slice(0, edit.start) + edit.text + code.slice(edit.end),
|
|
53
|
+
source
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type Binding =
|
|
58
|
+
| { readonly kind: 'component'; readonly exported: string }
|
|
59
|
+
| { readonly kind: 'namespace' }
|
|
60
|
+
| { readonly kind: 'other' };
|
|
61
|
+
|
|
62
|
+
type Scan = {
|
|
63
|
+
readonly edits: Edit[];
|
|
64
|
+
readonly warnings: TransformWarning[];
|
|
65
|
+
readonly bindings: Map<string, Binding>;
|
|
66
|
+
importsRewritten: number;
|
|
67
|
+
importsFromPackage: boolean;
|
|
68
|
+
propsRenamed: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function packageSubpath(specifier: string, fromPackage: string): string | null {
|
|
72
|
+
if (specifier === fromPackage) {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
if (specifier.startsWith(`${fromPackage}/`)) {
|
|
76
|
+
return specifier.slice(fromPackage.length);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rewriteSpecifierEdit(
|
|
82
|
+
source: string,
|
|
83
|
+
literal: object,
|
|
84
|
+
value: string,
|
|
85
|
+
config: DirectionConfig
|
|
86
|
+
): Edit | null {
|
|
87
|
+
const subpath = packageSubpath(value, config.fromPackage);
|
|
88
|
+
if (subpath === null) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const range = rangeOf(literal);
|
|
92
|
+
if (range === null) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const quote = source.charAt(range.start);
|
|
96
|
+
return {
|
|
97
|
+
start: range.start,
|
|
98
|
+
end: range.end,
|
|
99
|
+
text: `${quote}${config.toPackage}${subpath}${quote}`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Deep-walk an estree subtree for dynamic `import('...')` of the source package. */
|
|
104
|
+
function collectDynamicImportEdits(
|
|
105
|
+
source: string,
|
|
106
|
+
node: unknown,
|
|
107
|
+
config: DirectionConfig,
|
|
108
|
+
scan: Scan
|
|
109
|
+
): void {
|
|
110
|
+
if (Array.isArray(node)) {
|
|
111
|
+
for (const item of node) {
|
|
112
|
+
collectDynamicImportEdits(source, item, config, scan);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (typeof node !== 'object' || node === null) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if ('type' in node && node.type === 'ImportExpression' && 'source' in node) {
|
|
120
|
+
const literal = node.source;
|
|
121
|
+
if (
|
|
122
|
+
typeof literal === 'object' &&
|
|
123
|
+
literal !== null &&
|
|
124
|
+
'type' in literal &&
|
|
125
|
+
literal.type === 'Literal' &&
|
|
126
|
+
'value' in literal &&
|
|
127
|
+
typeof literal.value === 'string'
|
|
128
|
+
) {
|
|
129
|
+
const edit = rewriteSpecifierEdit(source, literal, literal.value, config);
|
|
130
|
+
if (edit !== null) {
|
|
131
|
+
scan.edits.push(edit);
|
|
132
|
+
scan.importsRewritten += 1;
|
|
133
|
+
scan.importsFromPackage = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const value of Object.values(node)) {
|
|
138
|
+
collectDynamicImportEdits(source, value, config, scan);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function scanScript(
|
|
143
|
+
source: string,
|
|
144
|
+
file: string,
|
|
145
|
+
program: Program,
|
|
146
|
+
config: DirectionConfig,
|
|
147
|
+
scan: Scan
|
|
148
|
+
): void {
|
|
149
|
+
for (const statement of program.body) {
|
|
150
|
+
if (
|
|
151
|
+
(statement.type === 'ImportDeclaration' ||
|
|
152
|
+
statement.type === 'ExportNamedDeclaration' ||
|
|
153
|
+
statement.type === 'ExportAllDeclaration') &&
|
|
154
|
+
statement.source != null &&
|
|
155
|
+
typeof statement.source.value === 'string'
|
|
156
|
+
) {
|
|
157
|
+
const edit = rewriteSpecifierEdit(source, statement.source, statement.source.value, config);
|
|
158
|
+
if (edit !== null) {
|
|
159
|
+
scan.edits.push(edit);
|
|
160
|
+
scan.importsRewritten += 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (statement.type !== 'ImportDeclaration') {
|
|
164
|
+
collectDynamicImportEdits(source, statement, config, scan);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const specifier = typeof statement.source.value === 'string' ? statement.source.value : '';
|
|
168
|
+
const fromPackage = packageSubpath(specifier, config.fromPackage) !== null;
|
|
169
|
+
if (fromPackage) {
|
|
170
|
+
scan.importsFromPackage = true;
|
|
171
|
+
}
|
|
172
|
+
for (const spec of statement.specifiers) {
|
|
173
|
+
if (spec.type === 'ImportSpecifier') {
|
|
174
|
+
const exported =
|
|
175
|
+
spec.imported.type === 'Identifier'
|
|
176
|
+
? spec.imported.name
|
|
177
|
+
: typeof spec.imported.value === 'string'
|
|
178
|
+
? spec.imported.value
|
|
179
|
+
: null;
|
|
180
|
+
scan.bindings.set(
|
|
181
|
+
spec.local.name,
|
|
182
|
+
fromPackage && exported !== null ? { kind: 'component', exported } : { kind: 'other' }
|
|
183
|
+
);
|
|
184
|
+
} else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
185
|
+
scan.bindings.set(spec.local.name, fromPackage ? { kind: 'namespace' } : { kind: 'other' });
|
|
186
|
+
} else {
|
|
187
|
+
scan.bindings.set(spec.local.name, { kind: 'other' });
|
|
188
|
+
if (fromPackage) {
|
|
189
|
+
const at = positionOf(source, rangeOf(spec)?.start ?? 0);
|
|
190
|
+
scan.warnings.push({
|
|
191
|
+
file,
|
|
192
|
+
line: at.line,
|
|
193
|
+
column: at.column,
|
|
194
|
+
message: `default import from '${specifier}' cannot be resolved to a component; its usages are not rewritten`
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type Resolution =
|
|
203
|
+
| { readonly kind: 'library'; readonly component: string }
|
|
204
|
+
| { readonly kind: 'other' }
|
|
205
|
+
| { readonly kind: 'unresolved' };
|
|
206
|
+
|
|
207
|
+
function resolveTag(tagName: string, bindings: ReadonlyMap<string, Binding>): Resolution {
|
|
208
|
+
const segments = tagName.split('.');
|
|
209
|
+
const head = segments.at(0);
|
|
210
|
+
if (typeof head !== 'string' || head.length === 0) {
|
|
211
|
+
return { kind: 'unresolved' };
|
|
212
|
+
}
|
|
213
|
+
const binding = bindings.get(head) ?? null;
|
|
214
|
+
if (binding === null) {
|
|
215
|
+
return { kind: 'unresolved' };
|
|
216
|
+
}
|
|
217
|
+
if (segments.length === 1) {
|
|
218
|
+
return binding.kind === 'component'
|
|
219
|
+
? { kind: 'library', component: binding.exported }
|
|
220
|
+
: { kind: 'other' };
|
|
221
|
+
}
|
|
222
|
+
const member = segments.at(1);
|
|
223
|
+
if (segments.length === 2 && binding.kind === 'namespace' && typeof member === 'string') {
|
|
224
|
+
return { kind: 'library', component: member };
|
|
225
|
+
}
|
|
226
|
+
return { kind: 'other' };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveThisExpression(
|
|
230
|
+
expression: AST.SvelteComponent['expression'],
|
|
231
|
+
bindings: ReadonlyMap<string, Binding>
|
|
232
|
+
): Resolution {
|
|
233
|
+
if (expression.type === 'Identifier') {
|
|
234
|
+
return resolveTag(expression.name, bindings);
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
expression.type === 'MemberExpression' &&
|
|
238
|
+
!expression.computed &&
|
|
239
|
+
expression.object.type === 'Identifier' &&
|
|
240
|
+
expression.property.type === 'Identifier'
|
|
241
|
+
) {
|
|
242
|
+
return resolveTag(`${expression.object.name}.${expression.property.name}`, bindings);
|
|
243
|
+
}
|
|
244
|
+
return { kind: 'unresolved' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function renameAttributes(
|
|
248
|
+
source: string,
|
|
249
|
+
file: string,
|
|
250
|
+
tagLabel: string,
|
|
251
|
+
component: string,
|
|
252
|
+
element: LibraryElement,
|
|
253
|
+
config: DirectionConfig,
|
|
254
|
+
scan: Scan
|
|
255
|
+
): void {
|
|
256
|
+
const table = config.renames.get(component) ?? null;
|
|
257
|
+
if (table === null) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const presentNames = new Set<string>();
|
|
261
|
+
for (const attribute of element.attributes) {
|
|
262
|
+
if (attribute.type === 'Attribute') {
|
|
263
|
+
presentNames.add(attribute.name);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const attribute of element.attributes) {
|
|
267
|
+
if (attribute.type === 'SpreadAttribute') {
|
|
268
|
+
const at = positionOf(source, attribute.start);
|
|
269
|
+
const renameable = [...table.keys()].sort().join(', ');
|
|
270
|
+
scan.warnings.push({
|
|
271
|
+
file,
|
|
272
|
+
line: at.line,
|
|
273
|
+
column: at.column,
|
|
274
|
+
message: `spread attribute on <${tagLabel}> (${component}) may carry renamed props (${renameable}) — not rewritten, review manually`
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (attribute.type !== 'Attribute') {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const target = table.get(attribute.name) ?? null;
|
|
282
|
+
if (target === null) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (presentNames.has(target)) {
|
|
286
|
+
const at = positionOf(source, attribute.start);
|
|
287
|
+
scan.warnings.push({
|
|
288
|
+
file,
|
|
289
|
+
line: at.line,
|
|
290
|
+
column: at.column,
|
|
291
|
+
message: `target prop '${target}' already present on <${tagLabel}> (${component}); '${attribute.name}' left as-is — review manually`
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (source.charAt(attribute.start) === '{') {
|
|
296
|
+
// Shorthand `{prop}`: expand to `newName={prop}`, keeping the local identifier.
|
|
297
|
+
scan.edits.push({
|
|
298
|
+
start: attribute.start,
|
|
299
|
+
end: attribute.end,
|
|
300
|
+
text: `${target}={${attribute.name}}`
|
|
301
|
+
});
|
|
302
|
+
scan.propsRenamed += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const nameEnd = attribute.start + attribute.name.length;
|
|
306
|
+
if (source.slice(attribute.start, nameEnd) !== attribute.name) {
|
|
307
|
+
const at = positionOf(source, attribute.start);
|
|
308
|
+
scan.warnings.push({
|
|
309
|
+
file,
|
|
310
|
+
line: at.line,
|
|
311
|
+
column: at.column,
|
|
312
|
+
message: `could not locate attribute name '${attribute.name}' on <${tagLabel}> in source text — not rewritten`
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
scan.edits.push({ start: attribute.start, end: nameEnd, text: target });
|
|
317
|
+
scan.propsRenamed += 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function warnUnresolvedElement(
|
|
322
|
+
source: string,
|
|
323
|
+
file: string,
|
|
324
|
+
tagLabel: string,
|
|
325
|
+
element: LibraryElement,
|
|
326
|
+
config: DirectionConfig,
|
|
327
|
+
scan: Scan
|
|
328
|
+
): void {
|
|
329
|
+
const suspicious = element.attributes
|
|
330
|
+
.filter((attribute) => attribute.type === 'Attribute')
|
|
331
|
+
.map((attribute) => attribute.name)
|
|
332
|
+
.filter((name) => config.allFromProps.has(name));
|
|
333
|
+
if (suspicious.length === 0) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const at = positionOf(source, element.start);
|
|
337
|
+
scan.warnings.push({
|
|
338
|
+
file,
|
|
339
|
+
line: at.line,
|
|
340
|
+
column: at.column,
|
|
341
|
+
message: `<${tagLabel}> is not resolvable to an import but has renameable prop(s) ${suspicious.join(', ')} — not rewritten, review manually`
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function walkFragment(fragment: AST.Fragment, visit: (element: LibraryElement) => void): void {
|
|
346
|
+
for (const node of fragment.nodes) {
|
|
347
|
+
if (node.type === 'Component' || node.type === 'SvelteComponent') {
|
|
348
|
+
visit(node);
|
|
349
|
+
}
|
|
350
|
+
// Structural recursion over every fragment-bearing property covers all
|
|
351
|
+
// element and block kinds (including future ones) without enumerating them.
|
|
352
|
+
if ('fragment' in node && typeof node.fragment === 'object') {
|
|
353
|
+
walkFragment(node.fragment, visit);
|
|
354
|
+
}
|
|
355
|
+
if ('body' in node && typeof node.body === 'object') {
|
|
356
|
+
walkFragment(node.body, visit);
|
|
357
|
+
}
|
|
358
|
+
if ('consequent' in node) {
|
|
359
|
+
walkFragment(node.consequent, visit);
|
|
360
|
+
}
|
|
361
|
+
if ('alternate' in node && node.alternate !== null) {
|
|
362
|
+
walkFragment(node.alternate, visit);
|
|
363
|
+
}
|
|
364
|
+
if ('fallback' in node) {
|
|
365
|
+
const fallback = node.fallback ?? null;
|
|
366
|
+
if (fallback !== null) {
|
|
367
|
+
walkFragment(fallback, visit);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if ('pending' in node && node.pending !== null) {
|
|
371
|
+
walkFragment(node.pending, visit);
|
|
372
|
+
}
|
|
373
|
+
if ('then' in node && node.then !== null) {
|
|
374
|
+
walkFragment(node.then, visit);
|
|
375
|
+
}
|
|
376
|
+
if ('catch' in node && node.catch !== null) {
|
|
377
|
+
walkFragment(node.catch, visit);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function transformSvelte(
|
|
383
|
+
source: string,
|
|
384
|
+
file: string,
|
|
385
|
+
direction: Direction
|
|
386
|
+
): TransformResult {
|
|
387
|
+
const config = directionConfig(direction);
|
|
388
|
+
let root: AST.Root;
|
|
389
|
+
try {
|
|
390
|
+
root = parse(source, { modern: true, filename: file });
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
393
|
+
return {
|
|
394
|
+
code: source,
|
|
395
|
+
changed: false,
|
|
396
|
+
propsRenamed: 0,
|
|
397
|
+
importsRewritten: 0,
|
|
398
|
+
warnings: [{ file, line: 1, column: 1, message: `could not parse file: ${message}` }]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const scan: Scan = {
|
|
402
|
+
edits: [],
|
|
403
|
+
warnings: [],
|
|
404
|
+
bindings: new Map(),
|
|
405
|
+
importsRewritten: 0,
|
|
406
|
+
importsFromPackage: false,
|
|
407
|
+
propsRenamed: 0
|
|
408
|
+
};
|
|
409
|
+
const instance = root.instance ?? null;
|
|
410
|
+
if (instance !== null) {
|
|
411
|
+
scanScript(source, file, instance.content, config, scan);
|
|
412
|
+
}
|
|
413
|
+
const moduleScript = root.module ?? null;
|
|
414
|
+
if (moduleScript !== null) {
|
|
415
|
+
scanScript(source, file, moduleScript.content, config, scan);
|
|
416
|
+
}
|
|
417
|
+
walkFragment(root.fragment, (element) => {
|
|
418
|
+
const resolution =
|
|
419
|
+
element.type === 'Component'
|
|
420
|
+
? resolveTag(element.name, scan.bindings)
|
|
421
|
+
: resolveThisExpression(element.expression, scan.bindings);
|
|
422
|
+
const tagLabel = element.type === 'Component' ? element.name : 'svelte:component';
|
|
423
|
+
if (resolution.kind === 'library') {
|
|
424
|
+
renameAttributes(source, file, tagLabel, resolution.component, element, config, scan);
|
|
425
|
+
} else if (resolution.kind === 'unresolved' && scan.importsFromPackage) {
|
|
426
|
+
warnUnresolvedElement(source, file, tagLabel, element, config, scan);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
const code = applyEdits(source, scan.edits);
|
|
430
|
+
return {
|
|
431
|
+
code,
|
|
432
|
+
changed: code !== source,
|
|
433
|
+
propsRenamed: scan.propsRenamed,
|
|
434
|
+
importsRewritten: scan.importsRewritten,
|
|
435
|
+
warnings: scan.warnings
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function scriptKindFor(file: string): ts.ScriptKind {
|
|
440
|
+
if (file.endsWith('.tsx')) {
|
|
441
|
+
return ts.ScriptKind.TSX;
|
|
442
|
+
}
|
|
443
|
+
if (file.endsWith('.jsx')) {
|
|
444
|
+
return ts.ScriptKind.JSX;
|
|
445
|
+
}
|
|
446
|
+
if (file.endsWith('.js') || file.endsWith('.mjs') || file.endsWith('.cjs')) {
|
|
447
|
+
return ts.ScriptKind.JS;
|
|
448
|
+
}
|
|
449
|
+
return ts.ScriptKind.TS;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** The module specifier a node carries, or null if it is not an import site. */
|
|
453
|
+
function specifierOf(node: ts.Node): ts.StringLiteralLike | null {
|
|
454
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
|
|
455
|
+
return node.moduleSpecifier;
|
|
456
|
+
}
|
|
457
|
+
if (ts.isExportDeclaration(node)) {
|
|
458
|
+
const specifier = node.moduleSpecifier;
|
|
459
|
+
if (typeof specifier !== 'undefined' && ts.isStringLiteralLike(specifier)) {
|
|
460
|
+
return specifier;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
// `import('x')` and `require('x')`.
|
|
465
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
466
|
+
const callee = node.expression;
|
|
467
|
+
const isImport = callee.kind === ts.SyntaxKind.ImportKeyword;
|
|
468
|
+
const isRequire = ts.isIdentifier(callee) && callee.text === 'require';
|
|
469
|
+
const first = node.arguments[0];
|
|
470
|
+
if ((isImport || isRequire) && ts.isStringLiteralLike(first)) {
|
|
471
|
+
return first;
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
// `import X = require('x')`. TS-only syntax whose specifier hangs off an
|
|
476
|
+
// ExternalModuleReference rather than a call expression.
|
|
477
|
+
if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
|
478
|
+
const expression = node.moduleReference.expression;
|
|
479
|
+
if (ts.isStringLiteralLike(expression)) {
|
|
480
|
+
return expression;
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
// `import('x').Foo` in type position.
|
|
485
|
+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
486
|
+
const literal = node.argument.literal;
|
|
487
|
+
if (ts.isStringLiteralLike(literal)) {
|
|
488
|
+
return literal;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Import-specifier rewrite for plain .ts/.js files (barrels, utilities).
|
|
496
|
+
*
|
|
497
|
+
* Parsed rather than matched. This was a context-anchored regex on the
|
|
498
|
+
* reasoning that a specifier only appears after `from`, `import`, `import(` or
|
|
499
|
+
* `require(` -- true, but those words appear just as readily in a comment or
|
|
500
|
+
* inside a quoted string, and the regex rewrote all of them. TypeScript is
|
|
501
|
+
* already a devDependency and already type-checks this directory, so the exact
|
|
502
|
+
* answer costs nothing here. Only the literal's interior is replaced, so the
|
|
503
|
+
* original quote style survives.
|
|
504
|
+
*/
|
|
505
|
+
export function transformModuleSpecifiers(
|
|
506
|
+
source: string,
|
|
507
|
+
file: string,
|
|
508
|
+
direction: Direction
|
|
509
|
+
): TransformResult {
|
|
510
|
+
const config = directionConfig(direction);
|
|
511
|
+
const sourceFile = ts.createSourceFile(
|
|
512
|
+
file,
|
|
513
|
+
source,
|
|
514
|
+
ts.ScriptTarget.ESNext,
|
|
515
|
+
true,
|
|
516
|
+
scriptKindFor(file)
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const edits: { readonly start: number; readonly end: number; readonly text: string }[] = [];
|
|
520
|
+
const visit = (node: ts.Node): void => {
|
|
521
|
+
const specifier = specifierOf(node);
|
|
522
|
+
if (specifier !== null) {
|
|
523
|
+
const subpath = packageSubpath(specifier.text, config.fromPackage);
|
|
524
|
+
if (subpath !== null) {
|
|
525
|
+
edits.push({
|
|
526
|
+
start: specifier.getStart(sourceFile) + 1,
|
|
527
|
+
end: specifier.getEnd() - 1,
|
|
528
|
+
text: `${config.toPackage}${subpath}`
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
ts.forEachChild(node, visit);
|
|
533
|
+
};
|
|
534
|
+
visit(sourceFile);
|
|
535
|
+
|
|
536
|
+
// Applied back to front so earlier offsets stay valid.
|
|
537
|
+
let code = source;
|
|
538
|
+
for (const edit of [...edits].reverse()) {
|
|
539
|
+
code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
code,
|
|
544
|
+
changed: code !== source,
|
|
545
|
+
propsRenamed: 0,
|
|
546
|
+
importsRewritten: edits.length,
|
|
547
|
+
warnings: []
|
|
548
|
+
};
|
|
549
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"target": "ES2023",
|
|
9
|
+
"lib": ["ES2023"],
|
|
10
|
+
"types": ["node"],
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["./**/*.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { TransformWarning } from './transform.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detects the one breaking change in the 4.0.0 custom-element surface:
|
|
5
|
+
* `children` is no longer a declared prop on any `sui-*` element.
|
|
6
|
+
*
|
|
7
|
+
* Three elements shipped it in 3.1.4 — `sui-chat-bubble`, `sui-draggable` and
|
|
8
|
+
* `sui-resizable` — and none of them had a `<slot>`, so assigning the property
|
|
9
|
+
* was the only way to give them content. Declaring it was also what left
|
|
10
|
+
* `element.children` returning undefined instead of the native HTMLCollection,
|
|
11
|
+
* so `el.children.length` threw. Both are fixed together: the declaration is
|
|
12
|
+
* gone and those wrappers forward the default slot instead.
|
|
13
|
+
*
|
|
14
|
+
* This reports rather than rewrites. The fix moves content from a JavaScript
|
|
15
|
+
* assignment into markup, which changes where the content is authored, not just
|
|
16
|
+
* how it is spelled — a person has to decide what the light-DOM children are.
|
|
17
|
+
* Auto-editing that would be guesswork dressed as a codemod.
|
|
18
|
+
*/
|
|
19
|
+
export const CHILDREN_BREAKING_TAGS: ReadonlyArray<string> = [
|
|
20
|
+
'sui-chat-bubble',
|
|
21
|
+
'sui-draggable',
|
|
22
|
+
'sui-resizable'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Matches `<expr>.children =` but not `==`/`===`, and not `.childNodes`.
|
|
27
|
+
* Deliberately loose on the left-hand side: a consumer reaches these elements
|
|
28
|
+
* through any expression at all (`ref.current`, `this.$refs.panel`, a query
|
|
29
|
+
* result), so anchoring on the property and confirming the tag appears in the
|
|
30
|
+
* same file gives far better recall than trying to type the receiver.
|
|
31
|
+
*/
|
|
32
|
+
const CHILDREN_ASSIGNMENT = /(^|[^.\w])([\w$\][().]*?)\.children\s*=(?!=)/gm;
|
|
33
|
+
|
|
34
|
+
const lineAndColumn = (source: string, index: number): { line: number; column: number } => {
|
|
35
|
+
const upTo = source.slice(0, index);
|
|
36
|
+
const lines = upTo.split('\n');
|
|
37
|
+
return { line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns one warning per `.children =` assignment in a file that also mentions
|
|
42
|
+
* an affected tag. Requiring the tag in the same file is what keeps this from
|
|
43
|
+
* flagging every DOM manipulation in a codebase: `element.children` is a common
|
|
44
|
+
* expression, and only these three elements changed.
|
|
45
|
+
*/
|
|
46
|
+
export const findChildrenAssignments = (
|
|
47
|
+
source: string,
|
|
48
|
+
file: string
|
|
49
|
+
): ReadonlyArray<TransformWarning> => {
|
|
50
|
+
const mentioned = CHILDREN_BREAKING_TAGS.filter((tag) => source.includes(tag));
|
|
51
|
+
if (mentioned.length === 0) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const warnings: TransformWarning[] = [];
|
|
56
|
+
for (const match of source.matchAll(CHILDREN_ASSIGNMENT)) {
|
|
57
|
+
if (typeof match.index !== 'number') {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const { line, column } = lineAndColumn(source, match.index + match[1].length);
|
|
61
|
+
warnings.push({
|
|
62
|
+
file,
|
|
63
|
+
line,
|
|
64
|
+
column,
|
|
65
|
+
message:
|
|
66
|
+
`assignment to \`.children\` in a file using ${mentioned.join(', ')}. ` +
|
|
67
|
+
'That property is no longer declared on those elements in 4.0.0; assigning it ' +
|
|
68
|
+
'now sets an inert expando and the content silently disappears. Pass the ' +
|
|
69
|
+
'content as light-DOM children instead — `<sui-draggable><div>…</div></sui-draggable>` ' +
|
|
70
|
+
'— which the wrappers now forward through their default slot. Reading ' +
|
|
71
|
+
'`element.children` is unaffected, and in fact returns a real HTMLCollection again.'
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return warnings;
|
|
75
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Printed on install in a consumer's project. Kept in plain .mjs deliberately:
|
|
2
|
+
// this runs before anything is built and in whatever Node the consumer has, so
|
|
3
|
+
// it must not depend on TypeScript stripping, on a build step, or on any
|
|
4
|
+
// dependency at all.
|
|
5
|
+
//
|
|
6
|
+
// It never fails an install. Every path exits 0, and any unexpected error is
|
|
7
|
+
// swallowed — a dependency that breaks `npm install` over a advisory message
|
|
8
|
+
// would be far worse than a consumer missing the message.
|
|
9
|
+
|
|
10
|
+
const RESET = '[0m';
|
|
11
|
+
const BOLD = '[1m';
|
|
12
|
+
const DIM = '[2m';
|
|
13
|
+
|
|
14
|
+
const isSelfInstall = () => {
|
|
15
|
+
// In a consumer install, INIT_CWD is the consumer's project root and cwd is
|
|
16
|
+
// this package inside node_modules. Developing this repo makes them equal.
|
|
17
|
+
const initCwd = process.env.INIT_CWD;
|
|
18
|
+
return typeof initCwd !== 'string' || initCwd === process.cwd();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const isNonInteractive = () =>
|
|
22
|
+
process.env.CI === 'true' ||
|
|
23
|
+
process.env.CI === '1' ||
|
|
24
|
+
process.env.npm_config_loglevel === 'silent' ||
|
|
25
|
+
process.stdout.isTTY !== true;
|
|
26
|
+
|
|
27
|
+
const notice = () =>
|
|
28
|
+
[
|
|
29
|
+
'',
|
|
30
|
+
`${BOLD}@juspay/svelte-ui-components — one breaking change is coming in 4.0.0${RESET}`,
|
|
31
|
+
'',
|
|
32
|
+
' Nothing has changed yet. In 4.0.0, `children` stops being a settable',
|
|
33
|
+
' property on sui-chat-bubble, sui-draggable and sui-resizable, and',
|
|
34
|
+
' assigning it will silently lose the content.',
|
|
35
|
+
'',
|
|
36
|
+
' Move to light-DOM children ahead of time:',
|
|
37
|
+
` ${DIM}el.children = snippet;${RESET} // before`,
|
|
38
|
+
` ${DIM}<sui-draggable><div>…</div></sui-draggable>${RESET} // after`,
|
|
39
|
+
'',
|
|
40
|
+
' That change is what restores `element.children` on those three:',
|
|
41
|
+
' declaring the prop leaves it undefined today.',
|
|
42
|
+
'',
|
|
43
|
+
` Find affected call sites now (reports only, writes nothing):`,
|
|
44
|
+
` ${BOLD}npx sui-codemod --dry-run ./src${RESET}`,
|
|
45
|
+
'',
|
|
46
|
+
` ${DIM}Nothing else in the custom-element surface breaks.${RESET}`,
|
|
47
|
+
''
|
|
48
|
+
].join('\n');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (!isSelfInstall() && !isNonInteractive()) {
|
|
52
|
+
process.stdout.write(notice());
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Advisory only; never interfere with the install.
|
|
56
|
+
}
|