@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.
@@ -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 = '';
11
+ const BOLD = '';
12
+ const DIM = '';
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
+ }