diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e04386fcc..12819d3e2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,14 @@ jobs: - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - id: npm-cache-dir + run: echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v2 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- - run: npm install - run: npm test env: @@ -23,6 +31,14 @@ jobs: steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 + - id: npm-cache-dir + run: echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v2 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- - run: 'npm i && npm run lint' Unit: runs-on: ${{ matrix.os }} @@ -33,4 +49,12 @@ jobs: steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 + - id: npm-cache-dir + run: echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v2 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- - run: 'npm i && npm run test:unit' diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c8414dff..09e9808546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Svelte changelog -## Unreleased +## 3.36.0 * Add `this: void` typing to store functions ([#6094](https://github.com/sveltejs/svelte/pull/6094)) * Export `Spring`, `Tweened` and `EasingFunction` interfaces ([#6070](https://github.com/sveltejs/svelte/issues/6070), [#6056](https://github.com/sveltejs/svelte/pull/6056)) * Export interfaces for transition parameters ([#5207](https://github.com/sveltejs/svelte/issues/5207)) * Export store's useful TypeScript definitions ([#5864](https://github.com/sveltejs/svelte/issues/5864)) +* Fix previous breaking change to `svelte/preprocess` types location ([#6100](https://github.com/sveltejs/svelte/pull/6100)) +* Fix missing slotted elements in AST ([#6066](https://github.com/sveltejs/svelte/issues/6066)) ## 3.35.0 diff --git a/package-lock.json b/package-lock.json index d8a594416c..e20509316a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "3.35.0", + "version": "3.36.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4797,9 +4797,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { diff --git a/package.json b/package.json index 2223b84771..aa81e982eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "3.35.0", + "version": "3.36.0", "description": "Cybernetically enhanced web apps", "module": "index.mjs", "main": "index", diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md index 2c665376dc..0e0e8e4008 100644 --- a/site/content/docs/02-template-syntax.md +++ b/site/content/docs/02-template-syntax.md @@ -1328,6 +1328,28 @@ Named slots allow consumers to target specific areas. They can also have fallbac ``` +Components can be placed in a named slot using the syntax ``. +In order to place content in a slot without using a wrapper element, you can use the special element ``. + +```sv + +
+ No header was provided +

Some content between header and footer

+ +
+ + + + + +

All rights reserved.

+

Copyright (c) 2019 Svelte Industries

+
+
+``` + + #### [`$$slots`](slots_object) --- @@ -1537,3 +1559,25 @@ The `` element provides a place to specify per-component compile ```sv ``` + +### `` + +The `` element allows you to place content in a [named slot](docs#slot_name) without wrapping it in a container DOM element. This keeps the flow layout of your document intact. + +```sv + +
+ No header was provided +

Some content between header and footer

+ +
+ + + +

Hello

+ +

All rights reserved.

+

Copyright (c) 2019 Svelte Industries

+
+
+``` diff --git a/site/content/faq/800-how-do-i-test-svelte-apps.md b/site/content/faq/800-how-do-i-test-svelte-apps.md index 8949327305..5e59d5812d 100644 --- a/site/content/faq/800-how-do-i-test-svelte-apps.md +++ b/site/content/faq/800-how-do-i-test-svelte-apps.md @@ -2,7 +2,7 @@ question: How do I test Svelte apps? --- -We recommend trying to seperate your view logic from your business logic. Data transformation or cross component state management is best kept outside of Svelte components. You can test those parts like you would test any JavaScript functionality that way. When it comes to testing the components, it is best to test the logic of the component and remember that the Svelte library has its own tests and you do not need to test implementation details provided by Svelte. +We recommend trying to separate your view logic from your business logic. Data transformation or cross component state management is best kept outside of Svelte components. You can test those parts like you would test any JavaScript functionality that way. When it comes to testing the components, it is best to test the logic of the component and remember that the Svelte library has its own tests and you do not need to test implementation details provided by Svelte. There are a few approaches that people take when testing, but it generally involves compiling the component and mounting it to something and then performing the tests. You essentially need to create a bundle for each component you're testing (since svelte is a compiler and not a normal library) and then mount them. You can mount to a JSDOM instance. Or you can use a real browser powered by a library like Playwright, Puppeteer, or Cypress. diff --git a/site/content/tutorial/07-lifecycle/03-update/text.md b/site/content/tutorial/07-lifecycle/03-update/text.md index 40e071630a..0255e845a8 100644 --- a/site/content/tutorial/07-lifecycle/03-update/text.md +++ b/site/content/tutorial/07-lifecycle/03-update/text.md @@ -2,7 +2,7 @@ title: beforeUpdate and afterUpdate --- -The `beforeUpdate` function schedules work to happen immediately before the DOM has been updated. `afterUpdate` is its counterpart, used for running code once the DOM is in sync with your data. +The `beforeUpdate` function schedules work to happen immediately before the DOM is updated. `afterUpdate` is its counterpart, used for running code once the DOM is in sync with your data. Together, they're useful for doing things imperatively that are difficult to achieve in a purely state-driven way, like updating the scroll position of an element. @@ -21,4 +21,4 @@ afterUpdate(() => { }); ``` -Note that `beforeUpdate` will first run before the component has mounted, so we need to check for the existence of `div` before reading its properties. \ No newline at end of file +Note that `beforeUpdate` will first run before the component has mounted, so we need to check for the existence of `div` before reading its properties. diff --git a/site/content/tutorial/16-special-elements/08-svelte-fragment/app-a/App.svelte b/site/content/tutorial/16-special-elements/08-svelte-fragment/app-a/App.svelte new file mode 100644 index 0000000000..35f2586857 --- /dev/null +++ b/site/content/tutorial/16-special-elements/08-svelte-fragment/app-a/App.svelte @@ -0,0 +1,10 @@ + + + +
+

All rights reserved.

+

Copyright (c) 2019 Svelte Industries

+
+
diff --git a/site/content/tutorial/16-special-elements/08-svelte-fragment/app-a/Box.svelte b/site/content/tutorial/16-special-elements/08-svelte-fragment/app-a/Box.svelte new file mode 100644 index 0000000000..16c17f90ef --- /dev/null +++ b/site/content/tutorial/16-special-elements/08-svelte-fragment/app-a/Box.svelte @@ -0,0 +1,20 @@ + + +
+ No header was provided +

Some content between header and footer

+ +
diff --git a/site/content/tutorial/16-special-elements/08-svelte-fragment/app-b/App.svelte b/site/content/tutorial/16-special-elements/08-svelte-fragment/app-b/App.svelte new file mode 100644 index 0000000000..6f55903ece --- /dev/null +++ b/site/content/tutorial/16-special-elements/08-svelte-fragment/app-b/App.svelte @@ -0,0 +1,10 @@ + + + + +

All rights reserved.

+

Copyright (c) 2019 Svelte Industries

+
+
diff --git a/site/content/tutorial/16-special-elements/08-svelte-fragment/app-b/Box.svelte b/site/content/tutorial/16-special-elements/08-svelte-fragment/app-b/Box.svelte new file mode 100644 index 0000000000..16c17f90ef --- /dev/null +++ b/site/content/tutorial/16-special-elements/08-svelte-fragment/app-b/Box.svelte @@ -0,0 +1,20 @@ + + +
+ No header was provided +

Some content between header and footer

+ +
diff --git a/site/content/tutorial/16-special-elements/08-svelte-fragment/text.md b/site/content/tutorial/16-special-elements/08-svelte-fragment/text.md new file mode 100644 index 0000000000..fca7aeb844 --- /dev/null +++ b/site/content/tutorial/16-special-elements/08-svelte-fragment/text.md @@ -0,0 +1,35 @@ +--- +title: +--- + +The `` element allows you to place content in a named slot without wrapping it in a container DOM element. This keeps the flow layout of your document intact. + +In the example notice how we applied a flex layout with a gap of `1em` to the box. + +```sv + + + +
+ No header was provided +

Some content between header and footer

+ +
+``` + +However, the content in the footer is not spaced out according to this rhythm because wrapping it in a div created a new flow layout. + +We can solve this by changing `
` in the `App` component. Replace the `
` with ``: + +```sv + +

All rights reserved.

+

Copyright (c) 2019 Svelte Industries

+
+``` diff --git a/site/src/routes/tutorial/[slug]/index.svelte b/site/src/routes/tutorial/[slug]/index.svelte index be59665970..bcb9a9b24a 100644 --- a/site/src/routes/tutorial/[slug]/index.svelte +++ b/site/src/routes/tutorial/[slug]/index.svelte @@ -198,7 +198,6 @@ .chapter-markup::-webkit-scrollbar-thumb { background-color: rgba(255,255,255,.7); border-radius: 1em; - outline: 1px solid green; } .chapter-markup :global(p) > :global(code), diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 70c25460b5..155b64778b 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -32,6 +32,7 @@ import { is_reserved_keyword } from './utils/reserved_keywords'; import { apply_preprocessor_sourcemap } from '../utils/mapped_code'; import Element from './nodes/Element'; import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types'; +import { clone } from '../utils/clone'; interface ComponentOptions { namespace?: string; @@ -116,12 +117,12 @@ export default class Component { // the instance JS gets mutated, so we park // a copy here for later. TODO this feels gross - this.original_ast = { + this.original_ast = clone({ html: ast.html, css: ast.css, - instance: ast.instance && JSON.parse(JSON.stringify(ast.instance)), + instance: ast.instance, module: ast.module - }; + }); this.file = compile_options.filename && diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index 6d0e7c5003..66caea3988 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -223,7 +223,7 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]): const siblings = get_possible_element_siblings(node, block.combinator.name === '+'); let has_match = false; - // NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the + // NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the // css-tree limitation that does not parse the inner selector of :global // so unless we are sure there will be no sibling to match, we will consider it as matched const has_global = blocks.some(block => block.global); @@ -449,7 +449,7 @@ function get_possible_element_siblings(node: INode, adjacent_only: boolean): Map while ((parent = parent.parent) && (parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'ElseBlock' || parent.type === 'AwaitBlock')) { const possible_siblings = get_possible_element_siblings(parent, adjacent_only); add_to_map(possible_siblings, result); - + if (parent.type === 'EachBlock') { // first child of each block can select the last child of each block as previous sibling if (skip_each_for_last_child) { @@ -477,7 +477,7 @@ function get_possible_last_child(block: EachBlock | IfBlock | AwaitBlock, adjace if (block.type === 'EachBlock') { const each_result: Map = loop_child(block.children, adjacent_only); const else_result: Map = block.else ? loop_child(block.else.children, adjacent_only) : new Map(); - + const not_exhaustive = !has_definite_elements(else_result); if (not_exhaustive) { diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index 3a9aa943e9..8fcc70ded9 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -9,6 +9,7 @@ import { TemplateNode } from '../../interfaces'; import Element from './Element'; import InlineComponent from './InlineComponent'; import Window from './Window'; +import { clone } from '../../utils/clone'; // TODO this should live in a specific binding const read_only_media_attributes = new Set([ @@ -42,7 +43,7 @@ export default class Binding extends Node { this.name = info.name; this.expression = new Expression(component, this, scope, info.expression); - this.raw_expression = JSON.parse(JSON.stringify(info.expression)); + this.raw_expression = clone(info.expression); const { name } = get_object(this.expression.node); diff --git a/src/compiler/compile/nodes/shared/Context.ts b/src/compiler/compile/nodes/shared/Context.ts index c6ad2c2893..dbcdada587 100644 --- a/src/compiler/compile/nodes/shared/Context.ts +++ b/src/compiler/compile/nodes/shared/Context.ts @@ -2,6 +2,7 @@ import { x } from 'code-red'; import { Node, Identifier, Expression } from 'estree'; import { walk } from 'estree-walker'; import is_reference from 'is-reference'; +import { clone } from '../../../utils/clone'; export interface Context { key: Identifier; @@ -81,7 +82,7 @@ function update_reference(contexts: Context[], n: number, expression: Expression } // NOTE: avoid unnecessary deep clone? - expression = JSON.parse(JSON.stringify(expression)) as Expression; + expression = clone(expression) as Expression; walk(expression, { enter(node, parent: Node) { if (is_reference(node, parent)) { diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index caece14f00..cca81b6372 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -16,6 +16,7 @@ import { is_reserved_keyword } from '../../utils/reserved_keywords'; import replace_object from '../../utils/replace_object'; import is_contextual from './is_contextual'; import EachBlock from '../EachBlock'; +import { clone } from '../../../utils/clone'; type Owner = INode; @@ -195,7 +196,7 @@ export default class Expression { const node = walk(this.node, { enter(node: any, parent: any) { if (node.type === 'Property' && node.shorthand) { - node.value = JSON.parse(JSON.stringify(node.value)); + node.value = clone(node.value); node.shorthand = false; } diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index 93b48996c1..16bcdf1687 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -5,6 +5,8 @@ import { decode_map } from './decode_sourcemap'; import { replace_in_code, slice_source } from './replace_in_code'; import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types'; +export * from './types'; + interface SourceUpdate { string?: string; map?: DecodedSourceMap; diff --git a/src/compiler/utils/clone.ts b/src/compiler/utils/clone.ts new file mode 100644 index 0000000000..74b6cfa0a1 --- /dev/null +++ b/src/compiler/utils/clone.ts @@ -0,0 +1,33 @@ +// adapted from klona v2.0.4 - https://github.com/lukeed/klona +// (c) Luke Edwards, under MIT License + +// The sole modification is to skip function values in objects when cloning, so we don't break tests. + +export function clone(val) { + let k, out, tmp; + + if (Array.isArray(val)) { + out = Array(k=val.length); + while (k--) out[k] = (tmp=val[k]) && typeof tmp === 'object' ? clone(tmp) : tmp; + return out; + } + + if (Object.prototype.toString.call(val) === '[object Object]') { + out = {}; // null + for (k in val) { + if (k === '__proto__') { + Object.defineProperty(out, k, { + value: clone(val[k]), + configurable: true, + enumerable: true, + writable: true + }); + } else if (typeof val[k] !== 'function') { // MODIFICATION: skip functions + out[k] = (tmp=val[k]) && typeof tmp === 'object' ? clone(tmp) : tmp; + } + } + return out; + } + + return val; +} diff --git a/test/parser/samples/slotted-element/input.svelte b/test/parser/samples/slotted-element/input.svelte new file mode 100644 index 0000000000..709dede9a5 --- /dev/null +++ b/test/parser/samples/slotted-element/input.svelte @@ -0,0 +1 @@ +
diff --git a/test/parser/samples/slotted-element/output.json b/test/parser/samples/slotted-element/output.json new file mode 100644 index 0000000000..7028a367ff --- /dev/null +++ b/test/parser/samples/slotted-element/output.json @@ -0,0 +1,42 @@ +{ + "html": { + "start": 0, + "end": 45, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 45, + "type": "InlineComponent", + "name": "Component", + "attributes": [], + "children": [ + { + "start": 11, + "end": 33, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 16, + "end": 26, + "type": "Attribute", + "name": "slot", + "value": [ + { + "start": 22, + "end": 25, + "type": "Text", + "raw": "foo", + "data": "foo" + } + ] + } + ], + "children": [] + } + ] + } + ] + } +} diff --git a/test/parser/samples/textarea-children/output.json b/test/parser/samples/textarea-children/output.json index 919fa33fde..0ebbfd38fc 100644 --- a/test/parser/samples/textarea-children/output.json +++ b/test/parser/samples/textarea-children/output.json @@ -9,51 +9,45 @@ "end": 61, "type": "Element", "name": "textarea", - "attributes": [ + "attributes": [], + "children": [ { - "type": "Attribute", - "name": "value", - "value": [ - { - "start": 10, - "end": 41, - "type": "Text", - "raw": "\n\t

not actually an element. ", - "data": "\n\t

not actually an element. " - }, - { - "start": 40, - "end": 45, - "type": "MustacheTag", - "expression": { - "type": "Identifier", - "start": 41, - "end": 44, - "loc": { - "start": { - "line": 2, - "column": 30 - }, - "end": { - "line": 2, - "column": 33 - } - }, - "name": "foo" + "start": 10, + "end": 41, + "type": "Text", + "raw": "\n\t

not actually an element. ", + "data": "\n\t

not actually an element. " + }, + { + "start": 40, + "end": 45, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 41, + "end": 44, + "loc": { + "start": { + "line": 2, + "column": 30 + }, + "end": { + "line": 2, + "column": 33 } }, - { - "start": 45, - "end": 50, - "type": "Text", - "raw": "

\n", - "data": "

\n" - } - ] + "name": "foo" + } + }, + { + "start": 45, + "end": 50, + "type": "Text", + "raw": "

\n", + "data": "

\n" } - ], - "children": [] + ] } ] } -} \ No newline at end of file +}