Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions scripts/compile-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Compile all Svelte files in a directory to JS and CSS files
// Usage: node scripts/compile-test.js <directory>

import { mkdirSync, readFileSync, writeFileSync } from 'fs';
import path from 'path';
import glob from 'tiny-glob/sync.js';
import { compile } from '../src/compiler/index.js';

const cwd = path.resolve(process.argv[2]);

const options = [
['normal', {}],
['hydrate', { hydratable: true }],
['ssr', { generate: 'ssr' }]
];
for (const file of glob('**/*.svelte', { cwd })) {
const contents = readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r/g, '');
let w;
for (const [name, opts] of options) {
const dir = `${cwd}/_output/${name}`;

const { js, css, warnings } = compile(contents, {
...opts,
filename: file
});

if (warnings.length) {
w = warnings;
}

mkdirSync(dir, { recursive: true });
js.code && writeFileSync(`${dir}/${file.replace(/\.svelte$/, '.js')}`, js.code);
css.code && writeFileSync(`${dir}/${file.replace(/\.svelte$/, '.css')}`, css.code);
}

if (w) {
console.log(`Warnings for ${file}:`);
console.log(w);
}
}
31 changes: 30 additions & 1 deletion src/compiler/compile/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -1644,8 +1644,9 @@ export default class Component {
* @param {string} name
* @param {any} node
* @param {import('./nodes/shared/TemplateScope.js').default} template_scope
* @param {import("./nodes/shared/Node.js").default} [owner]
*/
warn_if_undefined(name, node, template_scope) {
warn_if_undefined(name, node, template_scope, owner) {
if (name[0] === '$') {
if (name === '$' || (name[1] === '$' && !is_reserved_keyword(name))) {
return this.error(node, compiler_errors.illegal_global(name));
Expand All @@ -1657,6 +1658,34 @@ export default class Component {
if (this.var_lookup.has(name) && !this.var_lookup.get(name).global) return;
if (template_scope && template_scope.names.has(name)) return;
if (globals.has(name) && node.type !== 'InlineComponent') return;

function has_out_of_scope_let() {
for (let parent = owner.parent; parent; parent = parent.parent) {
if (parent.type === 'InlineComponent') {
const { let_attributes } = parent;

for (const attr of let_attributes) {
if (
// @ts-expect-error
// TODO extract_names only considers patterns but let attributes return expressions
(attr.expression && extract_names(attr.expression).includes(name)) ||
attr.name === name
)
return true;
}
}
}

return false;
}

if (owner && has_out_of_scope_let()) {
return this.warn(node, {
code: 'missing-declaration',
message: `let:${name} declared on parent component cannot be used inside named slot`
});
}

this.warn(node, compiler_warnings.missing_declaration(name, !!this.ast.instance));
}

Expand Down
40 changes: 20 additions & 20 deletions src/compiler/compile/nodes/InlineComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import map_children from './shared/map_children.js';
import Binding from './Binding.js';
import EventHandler from './EventHandler.js';
import Expression from './shared/Expression.js';
import Let from './Let.js';
import compiler_errors from '../compiler_errors.js';
import { regex_only_whitespaces } from '../../utils/patterns.js';

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

/** @type {import('./Let.js').default[]} */
lets = [];

/** @type {import('./Attribute.js').default[]} */
css_custom_properties = [];

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

/** @type {Attribute[]} */
let_attributes;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
Expand All @@ -58,6 +56,8 @@ export default class InlineComponent extends Node {
this.name === 'svelte:component'
? new Expression(component, this, scope, info.expression)
: null;

const let_attributes = (this.let_attributes = []);
info.attributes.forEach(
/** @param {any} node */ (node) => {
/* eslint-disable no-fallthrough */
Expand All @@ -82,7 +82,7 @@ export default class InlineComponent extends Node {
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Let':
this.lets.push(new Let(component, this, scope, node));
let_attributes.push(node);
break;
case 'Transition':
return component.error(node, compiler_errors.invalid_transition);
Expand All @@ -94,21 +94,9 @@ export default class InlineComponent extends Node {
/* eslint-enable no-fallthrough */
}
);
if (this.lets.length > 0) {
this.scope = scope.child();
this.lets.forEach(
/** @param {any} l */ (l) => {
const dependencies = new Set([l.name.name]);
l.names.forEach(
/** @param {any} name */ (name) => {
this.scope.add(name, dependencies, this);
}
);
}
);
} else {
this.scope = scope;
}

this.scope = scope;

this.handlers.forEach(
/** @param {any} handler */ (handler) => {
handler.modifiers.forEach(
Expand Down Expand Up @@ -174,6 +162,18 @@ export default class InlineComponent extends Node {
children: info.children
});
}

if (let_attributes.length) {
// copy let: attribute from <Component /> to <svelte:fragment slot="default" />
// as they are for `slot="default"` only
children.forEach((child) => {
const slot = child.attributes.find((attribute) => attribute.name === 'slot');
if (!slot || slot.value[0].data === 'default') {
child.attributes.push(...let_attributes);
}
});
}

this.children = map_children(component, this, this.scope, children);
}
get slot_template_name() {
Expand Down
34 changes: 1 addition & 33 deletions src/compiler/compile/nodes/Slot.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,39 +40,7 @@ export default class Slot extends Element {
}
);
if (!this.slot_name) this.slot_name = 'default';
if (this.slot_name === 'default') {
// if this is the default slot, add our dependencies to any
// other slots (which inherit our slot values) that were
// previously encountered
component.slots.forEach(
/** @param {any} slot */ (slot) => {
this.values.forEach(
/**
* @param {any} attribute
* @param {any} name
*/ (attribute, name) => {
if (!slot.values.has(name)) {
slot.values.set(name, attribute);
}
}
);
}
);
} else if (component.slots.has('default')) {
// otherwise, go the other way — inherit values from
// a previously encountered default slot
const default_slot = component.slots.get('default');
default_slot.values.forEach(
/**
* @param {any} attribute
* @param {any} name
*/ (attribute, name) => {
if (!this.values.has(name)) {
this.values.set(name, attribute);
}
}
);
}

component.slots.set(this.slot_name, this);
this.cannot_use_innerhtml();
this.not_static_content();
Expand Down
9 changes: 6 additions & 3 deletions src/compiler/compile/nodes/shared/Expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default class Expression {
this.scope_map = map;
const expression = this;
let function_expression;

// discover dependencies, but don't change the code yet
walk(info, {
/**
Expand Down Expand Up @@ -125,7 +126,7 @@ export default class Expression {
dependencies.add(name);
}
component.add_reference(node, name);
component.warn_if_undefined(name, nodes[0], template_scope);
component.warn_if_undefined(name, nodes[0], template_scope, owner);
}
this.skip();
}
Expand Down Expand Up @@ -376,8 +377,9 @@ export default class Expression {
node = node.parent;
}
const func_expression = func_declaration[0];
if (node.type === 'InlineComponent' || node.type === 'SlotTemplate') {
// <Comp let:data />

if (node.type === 'SlotTemplate') {
// <svelte:fragment let:data />
this.replace(func_expression);
} else {
// {#each}, {#await}
Expand Down Expand Up @@ -458,6 +460,7 @@ export default class Expression {
}
}
});

if (declarations.length > 0) {
block.maintain_context = true;
declarations.forEach(
Expand Down
28 changes: 11 additions & 17 deletions src/compiler/compile/render_dom/wrappers/InlineComponent/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import Wrapper from '../shared/Wrapper.js';
import BindingWrapper from '../Element/Binding.js';
import SlotTemplateWrapper from '../SlotTemplate.js';
import { b, p, x } from 'code-red';
import { extract_ignores_above_node } from '../../../../utils/extract_svelte_ignore.js';
import { sanitize } from '../../../../utils/names.js';
import { namespaces } from '../../../../utils/namespaces.js';
import compiler_warnings from '../../../compiler_warnings.js';
import add_to_set from '../../../utils/add_to_set.js';
import { b, x, p } from 'code-red';
import is_dynamic from '../shared/is_dynamic.js';
import bind_this from '../shared/bind_this.js';
import EventHandler from '../Element/EventHandler.js';
import { extract_names } from 'periscopic';
import mark_each_block_bindings from '../shared/mark_each_block_bindings.js';
import { string_to_member_expression } from '../../../utils/string_to_member_expression.js';
import BindingWrapper from '../Element/Binding.js';
import EventHandler from '../Element/EventHandler.js';
import SlotTemplateWrapper from '../SlotTemplate.js';
import Wrapper from '../shared/Wrapper.js';
import bind_this from '../shared/bind_this.js';
import is_dynamic from '../shared/is_dynamic.js';
import { is_head } from '../shared/is_head.js';
import compiler_warnings from '../../../compiler_warnings.js';
import { namespaces } from '../../../../utils/namespaces.js';
import { extract_ignores_above_node } from '../../../../utils/extract_svelte_ignore.js';
import mark_each_block_bindings from '../shared/mark_each_block_bindings.js';

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

Expand Down Expand Up @@ -77,11 +76,6 @@ export default class InlineComponentWrapper extends Wrapper {
).toLowerCase()
};
if (this.node.children.length) {
this.node.lets.forEach((l) => {
extract_names(l.value || l.name).forEach((name) => {
renderer.add_to_context(name, true);
});
});
this.children = this.node.children.map(
(child) =>
new SlotTemplateWrapper(
Expand Down
5 changes: 1 addition & 4 deletions src/compiler/compile/render_dom/wrappers/SlotTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ export default class SlotTemplateWrapper extends Wrapper {
type: 'slot'
});
this.renderer.blocks.push(this.block);
const seen = new Set(lets.map((l) => l.name.name));
this.parent.node.lets.forEach((l) => {
if (!seen.has(l.name.name)) lets.push(l);
});

/** @type {import('./InlineComponent/index.js').default} */ (this.parent).set_slot(
slot_template_name,
get_slot_definition(this.block, scope, lets)
Expand Down
5 changes: 0 additions & 5 deletions src/compiler/compile/render_ssr/handlers/Slot.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ export default function (node, renderer, options) {
: ${result}
`);
if (slot && nearest_inline_component) {
const lets = node.lets;
const seen = new Set(lets.map((l) => l.name.name));
nearest_inline_component.lets.forEach((l) => {
if (!seen.has(l.name.name)) lets.push(l);
});
options.slot_scopes.set(slot, {
input: get_slot_scope(node.lets),
output: renderer.pop()
Expand Down
6 changes: 1 addition & 5 deletions src/compiler/compile/render_ssr/handlers/SlotTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ export default function (node, renderer, options) {
);
renderer.push();
renderer.render(children, options);
const lets = node.lets;
const seen = new Set(lets.map((l) => l.name.name));
parent_inline_component.lets.forEach((l) => {
if (!seen.has(l.name.name)) lets.push(l);
});

const slot_fragment_content = renderer.pop();
if (!is_empty_template_literal(slot_fragment_content)) {
if (options.slot_scopes.has(node.slot_template_name)) {
Expand Down
11 changes: 11 additions & 0 deletions test/runtime/samples/component-slot-default-in-each/Nested.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
let promise = new Promise(resolve => resolve(10));
</script>

{#each {length: 3} as _, i}
<slot item={i}/>
{/each}

{#await promise then value}
<slot {value}/>
{/await}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
html: `
<div>0 - undefined</div>
<div>1 - undefined</div>
<div>2 - undefined</div>
`
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import Nested from './Nested.svelte';
</script>

<Nested let:item let:value>
<div>{item} - {value}</div>
</Nested>
3 changes: 3 additions & 0 deletions test/runtime/samples/component-slot-let-scope-2/Nested.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<slot />
<slot thing={2} name="thing"/>

3 changes: 3 additions & 0 deletions test/runtime/samples/component-slot-let-scope-2/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
html: '<span>undefined</span><span>2</span>'
};
8 changes: 8 additions & 0 deletions test/runtime/samples/component-slot-let-scope-2/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
</script>

<Nested let:thing>
<span>{thing}</span>
<svelte:fragment slot="thing" let:thing><span>{thing}</span></svelte:fragment>
</Nested>
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script>
import { onDestroy } from 'svelte';

let count = 0;

function increment() {
Expand Down
Loading