Skip to content

Commit ab54212

Browse files
committed
Merge branch 'main' into nested-css-combinator
2 parents 1929e46 + 6c1e462 commit ab54212

File tree

27 files changed

+727
-15
lines changed

27 files changed

+727
-15
lines changed

.changeset/funny-houses-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: migrate `$$Props` without creating non existent props

.changeset/good-vans-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: support migration of `svelte:component`

.changeset/large-rules-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: fix accessors and support migration of accessors

.changeset/many-fishes-warn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: move labeled statements that need reordering after props insertion point

.changeset/real-camels-pay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: support migration of self closing tags

.changeset/real-timers-complain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: various `svelte:component` migration bugs

packages/svelte/src/compiler/migrate/index.js

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
/** @import { VariableDeclarator, Node, Identifier } from 'estree' */
22
/** @import { Visitors } from 'zimmerframe' */
33
/** @import { ComponentAnalysis } from '../phases/types.js' */
4-
/** @import { Scope } from '../phases/scope.js' */
4+
/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
55
/** @import { AST, Binding, SvelteNode, ValidatedCompileOptions } from '#compiler' */
66
import MagicString from 'magic-string';
77
import { walk } from 'zimmerframe';
88
import { parse } from '../phases/1-parse/index.js';
9+
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
910
import { analyze_component } from '../phases/2-analyze/index.js';
1011
import { get_rune } from '../phases/scope.js';
1112
import { reset, reset_warning_filter } from '../state.js';
1213
import { extract_identifiers } from '../utils/ast.js';
1314
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
1415
import { validate_component_options } from '../validate-options.js';
16+
import { is_svg, is_void } from '../../utils.js';
1517

1618
const regex_style_tags = /(<style[^>]+>)([\S\s]*?)(<\/style>)/g;
1719
const style_placeholder = '/*$$__STYLE_CONTENT__$$*/';
@@ -52,6 +54,8 @@ export function migrate(source) {
5254
const analysis = analyze_component(parsed, source, combined_options);
5355
const indent = guess_indent(source);
5456

57+
str.replaceAll(/(<svelte:options\s.*?\s?)accessors\s?/g, (_, $1) => $1);
58+
5559
for (const content of style_contents) {
5660
str.overwrite(content[0], content[0] + style_placeholder.length, content[1]);
5761
}
@@ -85,7 +89,8 @@ export function migrate(source) {
8589
nonpassive: analysis.root.unique('nonpassive').name
8690
},
8791
legacy_imports: new Set(),
88-
script_insertions: new Set()
92+
script_insertions: new Set(),
93+
derived_components: new Map()
8994
};
9095

9196
if (parsed.module) {
@@ -108,6 +113,7 @@ export function migrate(source) {
108113

109114
const need_script =
110115
state.legacy_imports.size > 0 ||
116+
state.derived_components.size > 0 ||
111117
state.script_insertions.size > 0 ||
112118
state.props.length > 0 ||
113119
analysis.uses_rest_props ||
@@ -146,6 +152,7 @@ export function migrate(source) {
146152
props = `...${state.names.props}`;
147153
} else {
148154
props = state.props
155+
.filter((prop) => !prop.type_only)
149156
.map((prop) => {
150157
let prop_str =
151158
prop.local === prop.exported ? prop.local : `${prop.exported}: ${prop.local}`;
@@ -232,7 +239,9 @@ export function migrate(source) {
232239
dependencies.some(
233240
(dep) =>
234241
!ids.includes(dep) &&
235-
/** @type {number} */ (dep.node.start) > /** @type {number} */ (node.start)
242+
(dep.kind === 'prop' || dep.kind === 'bindable_prop'
243+
? state.props_insertion_point
244+
: /** @type {number} */ (dep.node.start)) > /** @type {number} */ (node.start)
236245
)
237246
) {
238247
needs_reordering = true;
@@ -250,6 +259,24 @@ export function migrate(source) {
250259
}
251260
}
252261

262+
insertion_point = parsed.instance
263+
? /** @type {number} */ (parsed.instance.content.end)
264+
: insertion_point;
265+
266+
if (state.derived_components.size > 0) {
267+
str.appendRight(
268+
insertion_point,
269+
`\n${indent}${[...state.derived_components.entries()].map(([init, name]) => `const ${name} = $derived(${init});`).join(`\n${indent}`)}\n`
270+
);
271+
}
272+
273+
if (state.props.length > 0 && state.analysis.accessors) {
274+
str.appendRight(
275+
insertion_point,
276+
`\n${indent}export {${state.props.reduce((acc, prop) => (prop.slot_name ? acc : `${acc}\n${indent}\t${prop.local},`), '')}\n${indent}}\n`
277+
);
278+
}
279+
253280
if (!parsed.instance && need_script) {
254281
str.appendRight(insertion_point, '\n</script>\n\n');
255282
}
@@ -267,13 +294,14 @@ export function migrate(source) {
267294
* str: MagicString;
268295
* analysis: ComponentAnalysis;
269296
* indent: string;
270-
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string }>;
297+
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string, type_only?: boolean }>;
271298
* props_insertion_point: number;
272299
* has_props_rune: boolean;
273300
* end: number;
274301
* names: Record<string, string>;
275302
* legacy_imports: Set<string>;
276-
* script_insertions: Set<string>
303+
* script_insertions: Set<string>;
304+
* derived_components: Map<string, string>
277305
* }} State
278306
*/
279307

@@ -403,6 +431,7 @@ const instance_script = {
403431
: '';
404432
prop.bindable = binding.updated;
405433
prop.exported = binding.prop_alias || name;
434+
prop.type_only = false;
406435
} else {
407436
state.props.push({
408437
local: name,
@@ -560,6 +589,15 @@ const template = {
560589
},
561590
RegularElement(node, { state, next }) {
562591
handle_events(node, state);
592+
// Strip off any namespace from the beginning of the node name.
593+
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
594+
595+
if (state.analysis.source[node.end - 2] === '/' && !is_void(node_name) && !is_svg(node_name)) {
596+
let trimmed_position = node.end - 2;
597+
while (state.str.original.charAt(trimmed_position - 1) === ' ') trimmed_position--;
598+
state.str.remove(trimmed_position, node.end - 1);
599+
state.str.appendRight(node.end, `</${node.name}>`);
600+
}
563601
next();
564602
},
565603
SvelteElement(node, { state, next }) {
@@ -586,6 +624,74 @@ const template = {
586624
handle_events(node, state);
587625
next();
588626
},
627+
SvelteComponent(node, { state, next, path }) {
628+
next();
629+
630+
let expression = state.str
631+
.snip(
632+
/** @type {number} */ (node.expression.start),
633+
/** @type {number} */ (node.expression.end)
634+
)
635+
.toString();
636+
637+
if (
638+
(node.expression.type !== 'Identifier' && node.expression.type !== 'MemberExpression') ||
639+
!regex_valid_component_name.test(expression)
640+
) {
641+
let current_expression = expression;
642+
expression = state.scope.generate('SvelteComponent');
643+
let needs_derived = true;
644+
for (let i = path.length - 1; i >= 0; i--) {
645+
const part = path[i];
646+
if (
647+
part.type === 'EachBlock' ||
648+
part.type === 'AwaitBlock' ||
649+
part.type === 'IfBlock' ||
650+
part.type === 'SnippetBlock' ||
651+
part.type === 'Component' ||
652+
part.type === 'SvelteComponent'
653+
) {
654+
let position = node.start;
655+
if (i !== path.length - 1) {
656+
for (let modifier = 1; modifier < path.length - i; modifier++) {
657+
const path_part = path[i + modifier];
658+
if ('start' in path_part) {
659+
position = /** @type {number} */ (path_part.start);
660+
break;
661+
}
662+
}
663+
}
664+
const indent = state.str.original.substring(
665+
state.str.original.lastIndexOf('\n', position) + 1,
666+
position
667+
);
668+
state.str.prependLeft(
669+
position,
670+
`{@const ${expression} = ${current_expression}}\n${indent}`
671+
);
672+
needs_derived = false;
673+
break;
674+
}
675+
}
676+
if (needs_derived) {
677+
if (state.derived_components.has(current_expression)) {
678+
expression = /** @type {string} */ (state.derived_components.get(current_expression));
679+
} else {
680+
state.derived_components.set(current_expression, expression);
681+
}
682+
}
683+
}
684+
685+
state.str.overwrite(node.start + 1, node.start + node.name.length + 1, expression);
686+
687+
if (state.str.original.substring(node.end - node.name.length - 1, node.end - 1) === node.name) {
688+
state.str.overwrite(node.end - node.name.length - 1, node.end - 1, expression);
689+
}
690+
let this_pos = state.str.original.lastIndexOf('this', node.expression.start);
691+
while (!state.str.original.charAt(this_pos - 1).trim()) this_pos--;
692+
const end_pos = state.str.original.indexOf('}', node.expression.end) + 1;
693+
state.str.remove(this_pos, end_pos);
694+
},
589695
SvelteWindow(node, { state, next }) {
590696
handle_events(node, state);
591697
next();
@@ -914,7 +1020,8 @@ function handle_identifier(node, state, path) {
9141020
bindable: false,
9151021
optional: member.optional,
9161022
type,
917-
comment
1023+
comment,
1024+
type_only: true
9181025
});
9191026
}
9201027
}

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const regex_starts_with_quote_characters = /^["']/;
2323
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
2424
const regex_valid_element_name =
2525
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
26-
const regex_valid_component_name =
26+
export const regex_valid_component_name =
2727
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
2828
// (must start with uppercase letter if no dots, can contain dots)
2929
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;

packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ const css_visitors = {
114114
node.metadata.used ||= node.children.every(
115115
({ metadata }) => metadata.is_global || metadata.is_global_like
116116
);
117+
118+
if (
119+
node.metadata.rule?.metadata.parent_rule &&
120+
node.children[0]?.selectors[0]?.type === 'NestingSelector'
121+
) {
122+
const parent_is_global = node.metadata.rule.metadata.parent_rule.prelude.children.some(
123+
(child) => child.children.length === 1 && child.children[0].metadata.is_global
124+
);
125+
// mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used
126+
if (parent_is_global) {
127+
node.metadata.used = true;
128+
}
129+
}
117130
},
118131
RelativeSelector(node, context) {
119132
const parent = /** @type {Css.ComplexSelector} */ (context.path.at(-1));

packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteComponent.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ export function SvelteComponent(node, context) {
1212
w.svelte_component_deprecated(node);
1313
}
1414

15+
context.visit(node.expression);
16+
1517
visit_component(node, context);
1618
}

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ export function client_component(analysis, options) {
276276
}
277277
}
278278

279+
if (binding?.kind === 'prop' || binding?.kind === 'bindable_prop') {
280+
return [getter, b.set(alias ?? name, [b.stmt(b.call(name, b.id('$$value')))])];
281+
}
282+
279283
if (binding?.kind === 'state' || binding?.kind === 'raw_state') {
280284
const value = binding.kind === 'state' ? b.call('$.proxy', b.id('$$value')) : b.id('$$value');
281285
return [getter, b.set(alias ?? name, [b.stmt(b.call('$.set', b.id(name), value))])];

packages/svelte/src/compiler/phases/scope.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
391391
if (node.expression) {
392392
for (const id of extract_identifiers_from_destructuring(node.expression)) {
393393
const binding = scope.declare(id, 'template', 'const');
394+
scope.reference(id, [context.path[context.path.length - 1], node]);
394395
bindings.push(binding);
395396
}
396397
} else {
@@ -402,6 +403,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
402403
end: node.end
403404
};
404405
const binding = scope.declare(id, 'template', 'const');
406+
scope.reference(id, [context.path[context.path.length - 1], node]);
405407
bindings.push(binding);
406408
}
407409
},

packages/svelte/tests/css/samples/global-nested-block/_config.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,46 @@ import { test } from '../../test';
22

33
export default test({
44
warnings: [
5+
{
6+
code: 'css_unused_selector',
7+
message: 'Unused CSS selector ".unused"',
8+
start: {
9+
line: 19,
10+
column: 3,
11+
character: 204
12+
},
13+
end: {
14+
line: 19,
15+
column: 10,
16+
character: 211
17+
}
18+
},
519
{
620
code: 'css_unused_selector',
721
message: 'Unused CSS selector ".unused :global"',
822
start: {
9-
line: 25,
23+
line: 34,
1024
column: 2,
11-
character: 229
25+
character: 332
1226
},
1327
end: {
14-
line: 25,
28+
line: 34,
1529
column: 17,
16-
character: 244
30+
character: 347
1731
}
1832
},
1933
{
2034
code: 'css_unused_selector',
2135
message: 'Unused CSS selector ".unused :global(.z)"',
2236
start: {
23-
line: 31,
37+
line: 40,
2438
column: 2,
25-
character: 283
39+
character: 386
2640
},
2741
end: {
28-
line: 31,
42+
line: 40,
2943
column: 21,
30-
character: 302
44+
character: 405
3145
}
3246
}
3347
]

packages/svelte/tests/css/samples/global-nested-block/expected.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88

99
.x {
1010
color: green;
11+
&:hover {
12+
color: green;
13+
}
14+
&div {
15+
color: green;
16+
}
17+
/* (unused) .unused {
18+
color: red;
19+
}*/
1120
}
1221

1322
p:where(.svelte-xyz) {

0 commit comments

Comments
 (0)