Skip to content

Commit 5c6d111

Browse files
tanhauhaugtm-nayan
andauthored
Do not expose default slot let bindings to named slots (#6049)
* should not extend scope for across slots * disallow named slots inheriting let: scope from default slots * fix tests * fix test * fix * add runtime tests * rename test since it doesn't inherit anymore * fix lint * remove warnings * add compile script * document script * improve warning * fix test * handle renames * fix lint * gather names from all parents instead of just the nearest * remove unused import * add reminder --------- Co-authored-by: gtmnayan <[email protected]>
1 parent 198dbcf commit 5c6d111

File tree

29 files changed

+228
-110
lines changed

29 files changed

+228
-110
lines changed

scripts/compile-test.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Compile all Svelte files in a directory to JS and CSS files
2+
// Usage: node scripts/compile-test.js <directory>
3+
4+
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
5+
import path from 'path';
6+
import glob from 'tiny-glob/sync.js';
7+
import { compile } from '../src/compiler/index.js';
8+
9+
const cwd = path.resolve(process.argv[2]);
10+
11+
const options = [
12+
['normal', {}],
13+
['hydrate', { hydratable: true }],
14+
['ssr', { generate: 'ssr' }]
15+
];
16+
for (const file of glob('**/*.svelte', { cwd })) {
17+
const contents = readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r/g, '');
18+
let w;
19+
for (const [name, opts] of options) {
20+
const dir = `${cwd}/_output/${name}`;
21+
22+
const { js, css, warnings } = compile(contents, {
23+
...opts,
24+
filename: file
25+
});
26+
27+
if (warnings.length) {
28+
w = warnings;
29+
}
30+
31+
mkdirSync(dir, { recursive: true });
32+
js.code && writeFileSync(`${dir}/${file.replace(/\.svelte$/, '.js')}`, js.code);
33+
css.code && writeFileSync(`${dir}/${file.replace(/\.svelte$/, '.css')}`, css.code);
34+
}
35+
36+
if (w) {
37+
console.log(`Warnings for ${file}:`);
38+
console.log(w);
39+
}
40+
}

src/compiler/compile/Component.js

+30-1
Original file line numberDiff line numberDiff line change
@@ -1643,8 +1643,9 @@ export default class Component {
16431643
* @param {string} name
16441644
* @param {any} node
16451645
* @param {import('./nodes/shared/TemplateScope.js').default} template_scope
1646+
* @param {import("./nodes/shared/Node.js").default} [owner]
16461647
*/
1647-
warn_if_undefined(name, node, template_scope) {
1648+
warn_if_undefined(name, node, template_scope, owner) {
16481649
if (name[0] === '$') {
16491650
if (name === '$' || (name[1] === '$' && !is_reserved_keyword(name))) {
16501651
return this.error(node, compiler_errors.illegal_global(name));
@@ -1656,6 +1657,34 @@ export default class Component {
16561657
if (this.var_lookup.has(name) && !this.var_lookup.get(name).global) return;
16571658
if (template_scope && template_scope.names.has(name)) return;
16581659
if (globals.has(name) && node.type !== 'InlineComponent') return;
1660+
1661+
function has_out_of_scope_let() {
1662+
for (let parent = owner.parent; parent; parent = parent.parent) {
1663+
if (parent.type === 'InlineComponent') {
1664+
const { let_attributes } = parent;
1665+
1666+
for (const attr of let_attributes) {
1667+
if (
1668+
// @ts-expect-error
1669+
// TODO extract_names only considers patterns but let attributes return expressions
1670+
(attr.expression && extract_names(attr.expression).includes(name)) ||
1671+
attr.name === name
1672+
)
1673+
return true;
1674+
}
1675+
}
1676+
}
1677+
1678+
return false;
1679+
}
1680+
1681+
if (owner && has_out_of_scope_let()) {
1682+
return this.warn(node, {
1683+
code: 'missing-declaration',
1684+
message: `let:${name} declared on parent component cannot be used inside named slot`
1685+
});
1686+
}
1687+
16591688
this.warn(node, compiler_warnings.missing_declaration(name, !!this.ast.instance));
16601689
}
16611690

src/compiler/compile/nodes/InlineComponent.js

+20-20
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import map_children from './shared/map_children.js';
44
import Binding from './Binding.js';
55
import EventHandler from './EventHandler.js';
66
import Expression from './shared/Expression.js';
7-
import Let from './Let.js';
87
import compiler_errors from '../compiler_errors.js';
98
import { regex_only_whitespaces } from '../../utils/patterns.js';
109

@@ -22,9 +21,6 @@ export default class InlineComponent extends Node {
2221
/** @type {import('./EventHandler.js').default[]} */
2322
handlers = [];
2423

25-
/** @type {import('./Let.js').default[]} */
26-
lets = [];
27-
2824
/** @type {import('./Attribute.js').default[]} */
2925
css_custom_properties = [];
3026

@@ -37,6 +33,8 @@ export default class InlineComponent extends Node {
3733
/** @type {string} */
3834
namespace;
3935

36+
/** @type {Attribute[]} */
37+
let_attributes;
4038
/**
4139
* @param {import('../Component.js').default} component
4240
* @param {import('./shared/Node.js').default} parent
@@ -58,6 +56,8 @@ export default class InlineComponent extends Node {
5856
this.name === 'svelte:component'
5957
? new Expression(component, this, scope, info.expression)
6058
: null;
59+
60+
const let_attributes = (this.let_attributes = []);
6161
info.attributes.forEach(
6262
/** @param {import('../../interfaces.js').BaseDirective | import('../../interfaces.js').Attribute | import('../../interfaces.js').SpreadAttribute} node */ (
6363
node
@@ -84,7 +84,7 @@ export default class InlineComponent extends Node {
8484
this.handlers.push(new EventHandler(component, this, scope, node));
8585
break;
8686
case 'Let':
87-
this.lets.push(new Let(component, this, scope, node));
87+
let_attributes.push(node);
8888
break;
8989
case 'Transition':
9090
return component.error(node, compiler_errors.invalid_transition);
@@ -98,21 +98,9 @@ export default class InlineComponent extends Node {
9898
/* eslint-enable no-fallthrough */
9999
}
100100
);
101-
if (this.lets.length > 0) {
102-
this.scope = scope.child();
103-
this.lets.forEach(
104-
/** @param {any} l */ (l) => {
105-
const dependencies = new Set([l.name.name]);
106-
l.names.forEach(
107-
/** @param {any} name */ (name) => {
108-
this.scope.add(name, dependencies, this);
109-
}
110-
);
111-
}
112-
);
113-
} else {
114-
this.scope = scope;
115-
}
101+
102+
this.scope = scope;
103+
116104
this.handlers.forEach(
117105
/** @param {any} handler */ (handler) => {
118106
handler.modifiers.forEach(
@@ -178,6 +166,18 @@ export default class InlineComponent extends Node {
178166
children: info.children
179167
});
180168
}
169+
170+
if (let_attributes.length) {
171+
// copy let: attribute from <Component /> to <svelte:fragment slot="default" />
172+
// as they are for `slot="default"` only
173+
children.forEach((child) => {
174+
const slot = child.attributes.find((attribute) => attribute.name === 'slot');
175+
if (!slot || slot.value[0].data === 'default') {
176+
child.attributes.push(...let_attributes);
177+
}
178+
});
179+
}
180+
181181
this.children = map_children(component, this, this.scope, children);
182182
}
183183
get slot_template_name() {

src/compiler/compile/nodes/Slot.js

+1-33
Original file line numberDiff line numberDiff line change
@@ -40,39 +40,7 @@ export default class Slot extends Element {
4040
}
4141
);
4242
if (!this.slot_name) this.slot_name = 'default';
43-
if (this.slot_name === 'default') {
44-
// if this is the default slot, add our dependencies to any
45-
// other slots (which inherit our slot values) that were
46-
// previously encountered
47-
component.slots.forEach(
48-
/** @param {any} slot */ (slot) => {
49-
this.values.forEach(
50-
/**
51-
* @param {any} attribute
52-
* @param {any} name
53-
*/ (attribute, name) => {
54-
if (!slot.values.has(name)) {
55-
slot.values.set(name, attribute);
56-
}
57-
}
58-
);
59-
}
60-
);
61-
} else if (component.slots.has('default')) {
62-
// otherwise, go the other way — inherit values from
63-
// a previously encountered default slot
64-
const default_slot = component.slots.get('default');
65-
default_slot.values.forEach(
66-
/**
67-
* @param {any} attribute
68-
* @param {any} name
69-
*/ (attribute, name) => {
70-
if (!this.values.has(name)) {
71-
this.values.set(name, attribute);
72-
}
73-
}
74-
);
75-
}
43+
7644
component.slots.set(this.slot_name, this);
7745
this.cannot_use_innerhtml();
7846
this.not_static_content();

src/compiler/compile/nodes/shared/Expression.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export default class Expression {
7777
this.scope_map = map;
7878
const expression = this;
7979
let function_expression;
80+
8081
// discover dependencies, but don't change the code yet
8182
walk(info, {
8283
/**
@@ -125,7 +126,7 @@ export default class Expression {
125126
dependencies.add(name);
126127
}
127128
component.add_reference(node, name);
128-
component.warn_if_undefined(name, nodes[0], template_scope);
129+
component.warn_if_undefined(name, nodes[0], template_scope, owner);
129130
}
130131
this.skip();
131132
}
@@ -376,8 +377,9 @@ export default class Expression {
376377
node = node.parent;
377378
}
378379
const func_expression = func_declaration[0];
379-
if (node.type === 'InlineComponent' || node.type === 'SlotTemplate') {
380-
// <Comp let:data />
380+
381+
if (node.type === 'SlotTemplate') {
382+
// <svelte:fragment let:data />
381383
this.replace(func_expression);
382384
} else {
383385
// {#each}, {#await}
@@ -458,6 +460,7 @@ export default class Expression {
458460
}
459461
}
460462
});
463+
461464
if (declarations.length > 0) {
462465
block.maintain_context = true;
463466
declarations.forEach(

src/compiler/compile/render_dom/wrappers/InlineComponent/index.js

+11-17
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1-
import Wrapper from '../shared/Wrapper.js';
2-
import BindingWrapper from '../Element/Binding.js';
3-
import SlotTemplateWrapper from '../SlotTemplate.js';
1+
import { b, p, x } from 'code-red';
2+
import { extract_ignores_above_node } from '../../../../utils/extract_svelte_ignore.js';
43
import { sanitize } from '../../../../utils/names.js';
4+
import { namespaces } from '../../../../utils/namespaces.js';
5+
import compiler_warnings from '../../../compiler_warnings.js';
56
import add_to_set from '../../../utils/add_to_set.js';
6-
import { b, x, p } from 'code-red';
7-
import is_dynamic from '../shared/is_dynamic.js';
8-
import bind_this from '../shared/bind_this.js';
9-
import EventHandler from '../Element/EventHandler.js';
10-
import { extract_names } from 'periscopic';
11-
import mark_each_block_bindings from '../shared/mark_each_block_bindings.js';
127
import { string_to_member_expression } from '../../../utils/string_to_member_expression.js';
8+
import BindingWrapper from '../Element/Binding.js';
9+
import EventHandler from '../Element/EventHandler.js';
10+
import SlotTemplateWrapper from '../SlotTemplate.js';
11+
import Wrapper from '../shared/Wrapper.js';
12+
import bind_this from '../shared/bind_this.js';
13+
import is_dynamic from '../shared/is_dynamic.js';
1314
import { is_head } from '../shared/is_head.js';
14-
import compiler_warnings from '../../../compiler_warnings.js';
15-
import { namespaces } from '../../../../utils/namespaces.js';
16-
import { extract_ignores_above_node } from '../../../../utils/extract_svelte_ignore.js';
15+
import mark_each_block_bindings from '../shared/mark_each_block_bindings.js';
1716

1817
const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g;
1918

@@ -77,11 +76,6 @@ export default class InlineComponentWrapper extends Wrapper {
7776
).toLowerCase()
7877
};
7978
if (this.node.children.length) {
80-
this.node.lets.forEach((l) => {
81-
extract_names(l.value || l.name).forEach((name) => {
82-
renderer.add_to_context(name, true);
83-
});
84-
});
8579
this.children = this.node.children.map(
8680
(child) =>
8781
new SlotTemplateWrapper(

src/compiler/compile/render_dom/wrappers/SlotTemplate.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ export default class SlotTemplateWrapper extends Wrapper {
3838
type: 'slot'
3939
});
4040
this.renderer.blocks.push(this.block);
41-
const seen = new Set(lets.map((l) => l.name.name));
42-
this.parent.node.lets.forEach((l) => {
43-
if (!seen.has(l.name.name)) lets.push(l);
44-
});
41+
4542
/** @type {import('./InlineComponent/index.js').default} */ (this.parent).set_slot(
4643
slot_template_name,
4744
get_slot_definition(this.block, scope, lets)

src/compiler/compile/render_ssr/handlers/Slot.js

-5
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@ export default function (node, renderer, options) {
2525
: ${result}
2626
`);
2727
if (slot && nearest_inline_component) {
28-
const lets = node.lets;
29-
const seen = new Set(lets.map((l) => l.name.name));
30-
nearest_inline_component.lets.forEach((l) => {
31-
if (!seen.has(l.name.name)) lets.push(l);
32-
});
3328
options.slot_scopes.set(slot, {
3429
input: get_slot_scope(node.lets),
3530
output: renderer.pop()

src/compiler/compile/render_ssr/handlers/SlotTemplate.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@ export default function (node, renderer, options) {
2020
);
2121
renderer.push();
2222
renderer.render(children, options);
23-
const lets = node.lets;
24-
const seen = new Set(lets.map((l) => l.name.name));
25-
parent_inline_component.lets.forEach((l) => {
26-
if (!seen.has(l.name.name)) lets.push(l);
27-
});
23+
2824
const slot_fragment_content = renderer.pop();
2925
if (!is_empty_template_literal(slot_fragment_content)) {
3026
if (options.slot_scopes.has(node.slot_template_name)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
let promise = new Promise(resolve => resolve(10));
3+
</script>
4+
5+
{#each {length: 3} as _, i}
6+
<slot item={i}/>
7+
{/each}
8+
9+
{#await promise then value}
10+
<slot {value}/>
11+
{/await}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
html: `
3+
<div>0 - undefined</div>
4+
<div>1 - undefined</div>
5+
<div>2 - undefined</div>
6+
`
7+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import Nested from './Nested.svelte';
3+
</script>
4+
5+
<Nested let:item let:value>
6+
<div>{item} - {value}</div>
7+
</Nested>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<slot />
2+
<slot thing={2} name="thing"/>
3+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
html: '<span>undefined</span><span>2</span>'
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
import Nested from './Nested.svelte';
3+
</script>
4+
5+
<Nested let:thing>
6+
<span>{thing}</span>
7+
<svelte:fragment slot="thing" let:thing><span>{thing}</span></svelte:fragment>
8+
</Nested>

test/runtime/samples/component-slot-named-inherits-default-lets/Nested.svelte renamed to test/runtime/samples/component-slot-let-scope-3/Nested.svelte

-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
<script>
2-
import { onDestroy } from 'svelte';
3-
42
let count = 0;
53
64
function increment() {

0 commit comments

Comments
 (0)