merge master -> gh-938

pull/1382/head
Rich Harris 6 years ago
commit 86fd8f3e16

@ -8,6 +8,9 @@ indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
[test/**/expected.css]
insert_final_newline = false
[{package.json,.travis.yml,.eslintrc.json}]
indent_style = space
indent_size = 2

5
.gitignore vendored

@ -1,16 +1,19 @@
.DS_Store
.nyc_output
node_modules
/cli/
/compiler/
/ssr/
/shared.js
/scratch/
/coverage/
/coverage.lcov/
/test/cli/samples/*/actual
/test/sourcemaps/samples/*/output.js
/test/sourcemaps/samples/*/output.js.map
/test/sourcemaps/samples/*/output.css
/test/sourcemaps/samples/*/output.css.map
/src/compile/shared.ts
/package-lock.json
/store.umd.js
/yarn-error.log
_actual*.*

@ -5,7 +5,7 @@ node_js:
env:
global:
- BUILD_TIMEOUT=10000
- BUILD_TIMEOUT=20000
addons:
apt:
@ -15,6 +15,6 @@ addons:
install:
- export DISPLAY=':99.0'
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- npm install
- npm ci || npm install
after_success: npm run codecov

@ -1,5 +1,263 @@
# Svelte changelog
## 2.14.3
* Account for directive dependencies ([#1793](https://github.com/sveltejs/svelte/issues/1793))
* Detach each block iterations in each blocks with no update method ([#1795](https://github.com/sveltejs/svelte/issues/1795))
## 2.14.2
* Fix issue with nested `{#if}` blocks ([#1780](https://github.com/sveltejs/svelte/issues/1780))
## 2.14.1
* Fix block insertion order regression ([#1778](https://github.com/sveltejs/svelte/issues/1778))
* Fix blocks inside `<svelte:head>` ([#1774](https://github.com/sveltejs/svelte/issues/1774))
* Better attribute parsing ([#1772](https://github.com/sveltejs/svelte/issues/1772))
* Fix parse errors inside directives ([#1788](https://github.com/sveltejs/svelte/issues/1788))
## 2.14.0
* Refactor internals ([#1678](https://github.com/sveltejs/svelte/issues/1678))
* Deprecate `onerror` option ([#1745](https://github.com/sveltejs/svelte/issues/1745))
* Handle edge cases where `destroy` is called before `mount` ([#1653](https://github.com/sveltejs/svelte/pull/1653))
* Make `scroll` binding more efficient ([#1579](https://github.com/sveltejs/svelte/pull/1770))
* Make 'readonly property' store error more informative ([#1761](https://github.com/sveltejs/svelte/pull/1761))
## 2.13.5
* Fix missing dependencies in shorthand class directives ([#1739](https://github.com/sveltejs/svelte/issues/1739))
## 2.13.4
* Support dynamic `import()` in template expressions
## 2.13.3
* Fix bug with keyed each blocks and nested components ([#1706](https://github.com/sveltejs/svelte/issues/1706))
## 2.13.2
* Coalesce simultaneous store/component updates ([#1520](https://github.com/sveltejs/svelte/issues/1520))
* Fix nested transitions preventing each block item removal ([#1617](https://github.com/sveltejs/svelte/issues/1617))
* Add `class` directive shorthand and encapsulate styles ([#1695](https://github.com/sveltejs/svelte/pull/1695))
* Prevent erroneous updates of bound inputs ([#1699](https://github.com/sveltejs/svelte/issues/1699))
## 2.13.1
* Coerce second argument to `toggleClass` ([#1685](https://github.com/sveltejs/svelte/issues/1685))
## 2.13.0
* Add `class` directive ([#890](https://github.com/sveltejs/svelte/issues/890))
* Remove sourcemaps from npm package ([#1690](https://github.com/sveltejs/svelte/pull/1690))
## 2.12.1
* Allow actions to take any expression ([#1676](https://github.com/sveltejs/svelte/issues/1676))
* Run transitions in component context ([#1675](https://github.com/sveltejs/svelte/issues/1675))
* Correctly set select value on mount ([#1666](https://github.com/sveltejs/svelte/issues/1666))
* Support `{@debug}` in SSR ([#1659](https://github.com/sveltejs/svelte/issues/1659))
* Don't treat `&nbsp;` as empty whitespace ([#1658](https://github.com/sveltejs/svelte/issues/1658))
* Fix outros for if blocks with no else ([#1688](https://github.com/sveltejs/svelte/pull/1688))
* Set `style.cssText` in spread attributes ([#1684](https://github.com/sveltejs/svelte/pull/1684))
## 2.12.0
* Initialise actions on mount rather than hydrate ([#1653](https://github.com/sveltejs/svelte/pull/1653))
* Allow non-existent components to be destroyed ([#1677](https://github.com/sveltejs/svelte/pull/1677))
* Pass AMD ID from CLI correctly ([#1672](https://github.com/sveltejs/svelte/pull/1672))
* Minor AST tweaks ([#1673](https://github.com/sveltejs/svelte/pull/1673), [#1674](https://github.com/sveltejs/svelte/pull/1674))
* Reduce code duplication in component initialisation ([#1670](https://github.com/sveltejs/svelte/pull/1670))
## 2.11.0
* Add `--shared` CLI option ([#1649](https://github.com/sveltejs/svelte/pull/1649))
* Run first `onstate` *before* fragment is rendered ([#1522](https://github.com/sveltejs/svelte/issues/1522))
* Exclude current computed prop from state object ([#1544](https://github.com/sveltejs/svelte/issues/1544))
## 2.10.1
* Add sourcemaps to `{@debug}` tags ([#1647](https://github.com/sveltejs/svelte/pull/1647))
## 2.10.0
* Add a `{@debug}` tag, for inspecting values in templates in dev mode ([#1635](https://github.com/sveltejs/svelte/issues/1635))
* Fix dimension bindings in iOS ([#1642](https://github.com/sveltejs/svelte/pull/1642))
## 2.9.11
* Pass props to custom elements rather than setting attributes, where appropriate ([#875](https://github.com/sveltejs/svelte/issues/875))
* Handle whitespace in lists consistently between SSR and DOM renderers ([#1637](https://github.com/sveltejs/svelte/pull/1637))
* Improve error for invalid `ref` names ([#1613](https://github.com/sveltejs/svelte/issues/1613))
## 2.9.10
* Handle `null` consistently in tags ([#1598](https://github.com/sveltejs/svelte/issues/1598))
* Support object rest in computed properties ([#1540](https://github.com/sveltejs/svelte/issues/1540))
* Always update dynamic components when expression changes ([#1621](https://github.com/sveltejs/svelte/issues/1621))
* Encapsulate local styles inside global styles ([#1618](https://github.com/sveltejs/svelte/issues/1618))
## 2.9.9
* Fix attribute name regex ([#1623](https://github.com/sveltejs/svelte/pull/1623))
## 2.9.8
* Sanitize spread attributes in SSR — fixes vulnerability CVE-2018-6341 ([#1623](https://github.com/sveltejs/svelte/pull/1623))
## 2.9.7
* Allow `<input type=file bind:files>` ([#1608](https://github.com/sveltejs/svelte/issues/1608))
* Ensure child window exists before removing listener in `addResizeHandler` ([#1600](https://github.com/sveltejs/svelte/issues/1600))
* Handle transitions in `else` block ([#1589](https://github.com/sveltejs/svelte/issues/1589))
## 2.9.6
* Provide more useful error if SSR component attempts to render non-SSR component ([#1605](https://github.com/sveltejs/svelte/issues/1605))
## 2.9.5
* Null out refs to dynamic components ([#1596](https://github.com/sveltejs/svelte/issues/1596))
## 2.9.4
* Make identifier optional for `then` and `catch` blocks ([#1507](https://github.com/sveltejs/svelte/issues/1507))
* Group outros correctly ([#1575](https://github.com/sveltejs/svelte/issues/1575))
## 2.9.3
* Fix bug when an each block contains transitions but its else branch does not ([#1559](https://github.com/sveltejs/svelte/issues/1559))
* If an event handler throws an exception, don't block all future calls to that handler ([#1573](https://github.com/sveltejs/svelte/issues/1573))
## 2.9.2
* Fix conflict when using multiple if-else blocks, some of which use outros and some of which do not ([#1580](https://github.com/sveltejs/svelte/issues/1580))
* Fix some cases where `.innerHTML` was being used to create child elements when it shouldn't ([#1581](https://github.com/sveltejs/svelte/issues/1581))
## 2.9.1
* Use `template.content` instead of `template` where appropriate ([#1571](https://github.com/sveltejs/svelte/issues/1571))
## 2.9.0
* Play outro transitions on `<svelte:component>` if `nestedTransitions` is true ([#1568](https://github.com/sveltejs/svelte/issues/1568))
* Allow illegal identifiers to be component prop names, for e.g. spreading `data-foo` props ([#887](https://github.com/sveltejs/svelte/issues/887))
* Abort transition when node is detached ([#1561](https://github.com/sveltejs/svelte/issues/1561))
* Only include `transitionManager` when necessary ([#1514](https://github.com/sveltejs/svelte/issues/1514))
## 2.8.1
* Fix prefixed animation name replacement ([#1556](https://github.com/sveltejs/svelte/pull/1556))
## 2.8.0
* Correctly set store on nested components (to parent store, not root store) ([#1538](https://github.com/sveltejs/svelte/issues/1538))
## 2.7.2
* Prevent unnecessary remounts ([#1527](https://github.com/sveltejs/svelte/issues/1527))
* Allow `refs.*` as callee ([#1526](https://github.com/sveltejs/svelte/pull/1526))
* Handle empty lists when outroing ([#1532](https://github.com/sveltejs/svelte/issues/1532))
## 2.7.1
* Fix spread props with multiple dependencies ([#1515](https://github.com/sveltejs/svelte/issues/1515))
## 2.7.0
* Add `__svelte_meta` object to elements in dev mode, containing source info ([#1499](https://github.com/sveltejs/svelte/issues/1499))
* Fix `bind:online` in dev mode ([#1502](https://github.com/sveltejs/svelte/issues/1502))
* Update v1 warnings/errors ([#1508](https://github.com/sveltejs/svelte/pull/1508))
* Transform prefixed keyframes ([#1504](https://github.com/sveltejs/svelte/issues/1504))
## 2.6.6
* Fix nested transition bug ([#1497](https://github.com/sveltejs/svelte/issues/1497))
## 2.6.5
* Handle cases where only some `if` block branches have outros ([#1492](https://github.com/sveltejs/svelte/issues/1492))
## 2.6.4
* Web worker support ([#1487](https://github.com/sveltejs/svelte/issues/1487))
* Update dynamic component bindings when component changes ([#1489](https://github.com/sveltejs/svelte/issues/1489))
## 2.6.3
* Nested transitions respect `skipIntroByDefault` ([#1460](https://github.com/sveltejs/svelte/issues/1460))
* Always create outro for top-level block ([#1470](https://github.com/sveltejs/svelte/issues/1470))
## 2.6.2
* Fix spread+bindings on dynamic components ([#1433](https://github.com/sveltejs/svelte/issues/1433))
* Abort in-progress animations, if a new one starts ([#1458](https://github.com/sveltejs/svelte/issues/1458))
* Allow animations to be parameterised ([#1462](https://github.com/sveltejs/svelte/issues/1462))
## 2.6.1
* Absolutely position outroing animated nodes ([#1457](https://github.com/sveltejs/svelte/pull/1457))
## 2.6.0
* Add `animate` directive ([#1454](https://github.com/sveltejs/svelte/pull/1454))
* Add `skipIntroByDefault` compiler option and `intro: true` init option ([#1448](https://github.com/sveltejs/svelte/pull/1448))
* Add `nestedTransitions` compiler option ([#1451](https://github.com/sveltejs/svelte/pull/1451))
* Component outros, if `nestedTransitions` is true ([#1211](https://github.com/sveltejs/svelte/issues/1211))
* Allow transition functions to return a function, for inter-transition coordination ([#1453](https://github.com/sveltejs/svelte/pull/1453))
* Pass `1 - t` as second argument to transition functions ([#1452](https://github.com/sveltejs/svelte/pull/1452))
## 2.5.1
* Add new ARIA attributes ([#1436](https://github.com/sveltejs/svelte/pull/1436))
* Add `Promise` to whitelisted globals ([#1441](https://github.com/sveltejs/svelte/issues/1441))
* Allow spaces around reserved keyword attributes ([#1445](https://github.com/sveltejs/svelte/issues/1445))
## 2.5.0
* Support transitions in `await` blocks ([#956](https://github.com/sveltejs/svelte/issues/956))
* Abort outros if block is recreated ([#1425](https://github.com/sveltejs/svelte/issues/1425))
* Wait until transitions have completed before removing styles ([#648](https://github.com/sveltejs/svelte/issues/648))
* Support event shorthand on dynamic components ([#1427](https://github.com/sveltejs/svelte/pull/1427))
* Various codegen improvements ([#1419](https://github.com/sveltejs/svelte/pull/1419), [#1421](https://github.com/sveltejs/svelte/pull/1421), [#1422](https://github.com/sveltejs/svelte/pull/1422), [#1424](https://github.com/sveltejs/svelte/pull/1424))
* Correctly handle `await` blocks with no dynamic content ([#1417](https://github.com/sveltejs/svelte/issues/1417))
* Allow spread props on elements with static attribute tests ([#1429](https://github.com/sveltejs/svelte/pull/1429))
## 2.4.4
* Declare missing variable in Store ([#1415](https://github.com/sveltejs/svelte/issues/1415))
* ALways declare spread levels ([#1413](https://github.com/sveltejs/svelte/issues/1413))
## 2.4.3
* `ref` directives prevent HTMLified content ([#1407](https://github.com/sveltejs/svelte/issues/1407))
* Store computed properties update components immediately upon declaration ([#1327](https://github.com/sveltejs/svelte/issues/1327))
## 2.4.2
* Evaluate `each` key in child scope ([#1397](https://github.com/sveltejs/svelte/issues/1397))
* Prevent false negatives and positives when detecting cyclical computed store properties ([#1399](https://github.com/sveltejs/svelte/issues/1399))
* Only update dynamic component props ([#1394](https://github.com/sveltejs/svelte/issues/1394))
## 2.4.1
* Fix DOM event context ([#1390](https://github.com/sveltejs/svelte/issues/1390))
## 2.4.0
* Integrate CLI ([#1360](https://github.com/sveltejs/svelte/issues/1360))
* Allow arbitrary destructuring for each block items, with binding ([#1385](https://github.com/sveltejs/svelte/pull/1385))
* Each block keys can use arbitrary expressions ([#703](https://github.com/sveltejs/svelte/issues/703))
* `bind:offsetWidth`, `bind:offsetHeight`, `bind:clientWidth` and `bind:clientHeight` ([#984](https://github.com/sveltejs/svelte/issues/984))
* Leaner generated code for `each` blocks ([#1287](https://github.com/sveltejs/svelte/issues/1287))
## 2.3.0
* Allow computed properties to have entire state object as dependency ([#1303](https://github.com/sveltejs/svelte/issues/1303))

@ -5,31 +5,44 @@ The magical disappearing UI framework.
* [Read the introductory blog post](https://svelte.technology/blog/frameworks-without-the-framework)
* [Read the guide](https://svelte.technology/guide)
* [Try it out](https://svelte.technology/repl)
* [Chat on Gitter](https://gitter.im/sveltejs/svelte)
* [Chat on Discord](https://discord.gg/yy75DKs)
---
## Tooling
This is the Svelte compiler, which is primarily intended for authors of tooling that integrates Svelte with different build systems. If you just want to write Svelte components and use them in your app, you probably want one of those tools:
* [svelte-cli](https://github.com/sveltejs/svelte-cli) Command line interface for compiling components
* [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) Rollup plugin
* [sveltify](https://github.com/tehshrike/sveltify) - Browserify transform
### Build Systems
* [gulp-svelte](https://github.com/shinnn/gulp-svelte) - gulp plugin
* [metalsmith-svelte](https://github.com/shinnn/metalsmith-svelte) - Metalsmith plugin
* [system-svelte](https://github.com/CanopyTax/system-svelte)  System.js loader
* [svelte-loader](https://github.com/sveltejs/svelte-loader) Webpack loader
* [meteor-svelte](https://github.com/klaussner/meteor-svelte) Meteor build plugin
* [sveltejs-brunch](https://github.com/StarpTech/sveltejs-brunch) Brunch build plugin
* [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) Rollup plugin
* [parcel-plugin-svelte](https://github.com/DeMoorJasper/parcel-plugin-svelte) - Parcel build plugin
* [sveltify](https://github.com/tehshrike/sveltify) - Browserify transform
### CSS Preprocessors
* [Less](https://github.com/ls-age/svelte-preprocess-less)
* [modular-css](https://github.com/tivac/modular-css/tree/master/packages/svelte)
* [PostCSS](https://github.com/TehShrike/svelte-preprocess-postcss)
* [Sass](https://github.com/ls-age/svelte-preprocess-sass)
### Additional tools
* [svelte-dev-store](https://github.com/GarethOates/svelte-dev-store) - Use Redux tools to visualise Svelte store
* More to come!
## Example usage
```js
import * as svelte from 'svelte';
const { code, map } = svelte.compile(source, {
const { js, css, ast } = svelte.compile(source, {
// the target module format defaults to 'es' (ES2015 modules), can
// also be 'amd', 'cjs', 'umd', 'iife' or 'eval'
format: 'umd',
@ -126,14 +139,60 @@ The `style` and `script` preprocessors will run *after* the `markup` preprocesso
## Example/starter repos
* [sveltejs/template](https://github.com/sveltejs/template) — the 'official' starter template
* [sveltejs/template-webpack](https://github.com/sveltejs/template-webpack) — using webpack for bundling
* [charpeni/svelte-example](https://github.com/charpeni/svelte-example) - Some Svelte examples with configured Rollup, Babel, ESLint, directives, Two-Way binding, and nested components
* [EmilTholin/svelte-test](https://github.com/EmilTholin/svelte-test)
* [lukechinworth/codenames](https://github.com/lukechinworth/codenames/tree/svelte)  example integration with Redux
* [khtdr/svelte-redux-shopping-cart](https://github.com/khtdr/svelte-redux-shopping-cart) Redux Shopping Cart example (with devtools and hot-reloading)
## BrowserStack
<img src="https://cdn.worldvectorlogo.com/logos/browserstack.svg" height="80" width="80" align="left">
<p>To keep Svelte's performance in check, we use BrowserStack to quickly run benchmarks for each PR that immediately give feedback to the contributor. You can see how we use BrowserStack in the <a href="https://github.com/sveltejs/svelte-bench">svelte-bench</a> project and check out BrowserStack's services on their <a href="https://www.browserstack.com/">website</a>.</p>
## Development
Pull requests are encouraged and always welcome. [Pick an issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and help us out!
To install and work on Svelte locally:
```bash
git clone git@github.com:sveltejs/svelte.git
cd svelte
npm install
npm run dev
```
The compiler is written in [TypeScript](https://www.typescriptlang.org/), but don't let that put you off — it's basically just JavaScript with type annotations. You'll pick it up in no time. If you're using an editor other than [VSCode](https://code.visualstudio.com/) you may need to install a plugin in order to get syntax highlighting and code hints etc.
### Linking to a Live Project
You can make changes locally to Svelte and test it against any Svelte project. You can also use a [default template](https://github.com/sveltejs/template) for development. Instruction on setup are found in that project repository.
From your project:
```bash
cd ~/path/to/your-svelte-project
npm install ~/path/to/svelte
```
And you should be good to test changes locally.
To undo this and link to the official latest Svelte release, just run:
```bash
npm install svelte@latest
```
### Running Tests
```bash
npm run test
```
For running single tests, you can use pattern matching:
```bash
npm run test -- -g "includes AST in svelte.compile output"
```
Alternately, you can add `solo: true` to any given `test/../_config.js` file, but **remember never to commit that setting.**
## License

6830
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,14 +1,20 @@
{
"name": "svelte",
"version": "2.3.0",
"version": "2.14.3",
"description": "The magical disappearing UI framework",
"main": "compiler/svelte.js",
"bin": {
"svelte": "svelte"
},
"files": [
"compiler",
"ssr",
"cli",
"compiler/*.js",
"ssr/*.js",
"shared.js",
"store.js",
"store.umd.js",
"store.d.ts",
"svelte",
"README.md"
],
"scripts": {
@ -23,6 +29,7 @@
"prepare": "npm run build",
"dev": "node src/shared/_build.js && rollup -c -w",
"pretest": "npm run build",
"posttest": "agadoo shared.js",
"prepublishOnly": "npm run lint && npm test",
"prettier": "prettier --write \"src/**/*.ts\""
},
@ -44,42 +51,46 @@
"homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": {
"@types/mocha": "^5.2.0",
"@types/node": "^9.6.6",
"@types/node": "^10.5.5",
"acorn": "^5.4.1",
"acorn-dynamic-import": "^3.0.0",
"agadoo": "^1.0.1",
"chalk": "^2.4.0",
"codecov": "^3.0.0",
"console-group": "^0.3.2",
"css-tree": "1.0.0-alpha22",
"eslint": "^4.19.1",
"eslint": "^5.3.0",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-import": "^2.11.0",
"estree-walker": "^0.5.1",
"glob": "^7.1.1",
"is-reference": "^1.1.0",
"jsdom": "^11.8.0",
"kleur": "^2.0.1",
"locate-character": "^2.0.5",
"magic-string": "^0.24.0",
"mocha": "3",
"magic-string": "^0.25.0",
"mocha": "^5.2.0",
"nightmare": "^3.0.1",
"node-resolve": "^1.3.3",
"nyc": "^11.7.1",
"nyc": "^12.0.2",
"prettier": "^1.12.1",
"reify": "^0.15.1",
"rollup": "^0.58.1",
"rollup": "^0.63.5",
"rollup-plugin-buble": "^0.19.2",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-json": "^2.1.0",
"rollup-plugin-json": "^3.0.0",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-typescript": "^0.8.1",
"rollup-plugin-virtual": "^1.0.1",
"rollup-watch": "^4.3.1",
"sade": "^1.4.0",
"sander": "^0.6.0",
"shelljs": "^0.8.2",
"source-map": "0.6",
"source-map-support": "^0.5.4",
"ts-node": "^6.0.0",
"tiny-glob": "^0.2.1",
"ts-node": "^7.0.0",
"tslib": "^1.8.0",
"typescript": "^2.8.3"
"typescript": "^3.0.1"
},
"nyc": {
"include": [

@ -57,6 +57,28 @@ export default [
}
},
/* cli/*.js */
{
input: ['src/cli/index.ts'],
output: {
dir: 'cli',
format: 'cjs',
paths: {
svelte: '../compiler/svelte.js'
}
},
external: ['fs', 'path', 'os', 'svelte'],
plugins: [
json(),
commonjs(),
resolve(),
typescript({
typescript: require('typescript')
})
],
experimentalCodeSplitting: true
},
/* shared.js */
{
input: 'src/shared/index.js',

@ -1,12 +1,12 @@
import { Node, Warning } from './interfaces';
import Compiler from './compile/Compiler';
import Component from './compile/Component';
const now = (typeof process !== 'undefined' && process.hrtime)
? () => {
const t = process.hrtime();
return t[0] * 1e3 + t[1] / 1e6;
}
: () => window.performance.now();
: () => self.performance.now();
type Timing = {
label: string;
@ -64,7 +64,7 @@ export default class Stats {
stop(label) {
if (label !== this.currentTiming.label) {
throw new Error(`Mismatched timing labels`);
throw new Error(`Mismatched timing labels (expected ${this.currentTiming.label}, got ${label})`);
}
this.currentTiming.end = now();
@ -73,14 +73,14 @@ export default class Stats {
this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings;
}
render(compiler: Compiler) {
render(component: Component) {
const timings = Object.assign({
total: now() - this.startTime
}, collapseTimings(this.timings));
// TODO would be good to have this info even
// if options.generate is false
const imports = compiler && compiler.imports.map(node => {
const imports = component && component.imports.map(node => {
return {
source: node.source.value,
specifiers: node.specifiers.map(specifier => {
@ -96,11 +96,11 @@ export default class Stats {
}
});
const hooks: Record<string, boolean> = compiler && {
oncreate: !!compiler.templateProperties.oncreate,
ondestroy: !!compiler.templateProperties.ondestroy,
onstate: !!compiler.templateProperties.onstate,
onupdate: !!compiler.templateProperties.onupdate
const hooks: Record<string, boolean> = component && {
oncreate: !!component.templateProperties.oncreate,
ondestroy: !!component.templateProperties.ondestroy,
onstate: !!component.templateProperties.onstate,
onupdate: !!component.templateProperties.onupdate
};
return {

@ -0,0 +1,145 @@
import * as path from 'path';
import * as fs from 'fs';
import * as svelte from 'svelte';
import error from './error.js';
function mkdirp(dir) {
const parent = path.dirname(dir);
if (dir === parent) return;
mkdirp(parent);
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
}
export function compile(input, opts) {
if (opts._.length > 0) {
error(`Can only compile a single file or directory`);
}
const output = opts.output;
const stats = fs.statSync(input);
const isDir = stats.isDirectory();
if (isDir) {
if (!output) {
error(`You must specify an --output (-o) option when compiling a directory of files`);
}
if (opts.name || opts.amdId) {
error(`Cannot specify --${opts.name ? 'name' : 'amdId'} when compiling a directory`);
}
}
const globals = {};
if (opts.globals) {
opts.globals.split(',').forEach(pair => {
const [key, value] = pair.split(':');
globals[key] = value;
});
}
const options = {
name: opts.name,
format: opts.format,
sourceMap: opts.sourcemap,
globals,
amd: opts.amdId
? {
id: opts.amdId,
}
: undefined,
css: opts.css !== false,
dev: opts.dev,
immutable: opts.immutable,
generate: opts.generate || 'dom',
customElement: opts.customElement,
store: opts.store,
shared: opts.shared
};
if (isDir) {
mkdirp(output);
compileDirectory(input, output, options);
} else {
compileFile(input, output, options);
}
}
function compileDirectory(input, output, options) {
fs.readdirSync(input).forEach(file => {
const src = path.resolve(input, file);
const dest = path.resolve(output, file);
if (path.extname(file) === '.html') {
compileFile(
src,
dest.substring(0, dest.lastIndexOf('.html')) + '.js',
options
);
} else {
const stats = fs.statSync(src);
if (stats.isDirectory()) {
compileDirectory(src, dest, options);
}
}
});
}
let SOURCEMAPPING_URL = 'sourceMa';
SOURCEMAPPING_URL += 'ppingURL';
function compileFile(input, output, options) {
console.error(`compiling ${path.relative(process.cwd(), input)}...`); // eslint-disable-line no-console
options = Object.assign({}, options);
if (!options.name) options.name = getName(input);
options.filename = input;
options.outputFilename = output;
const { sourceMap } = options;
const inline = sourceMap === 'inline';
let source = fs.readFileSync(input, 'utf-8');
if (source[0] === 0xfeff) source = source.slice(1);
let compiled;
try {
compiled = svelte.compile(source, options);
} catch (err) {
error(err);
}
const { js } = compiled;
if (sourceMap) {
js.code += `\n//# ${SOURCEMAPPING_URL}=${inline || !output
? js.map.toUrl()
: `${path.basename(output)}.map`}\n`;
}
if (output) {
const outputDir = path.dirname(output);
mkdirp(outputDir);
fs.writeFileSync(output, js.code);
console.error(`wrote ${path.relative(process.cwd(), output)}`); // eslint-disable-line no-console
if (sourceMap && !inline) {
fs.writeFileSync(`${output}.map`, js.map);
console.error(`wrote ${path.relative(process.cwd(), `${output}.map`)}`); // eslint-disable-line no-console
}
} else {
process.stdout.write(js.code);
}
}
function getName(input) {
return path
.basename(input)
.replace(path.extname(input), '')
.replace(/[^a-zA-Z_$0-9]+/g, '_')
.replace(/^_/, '')
.replace(/_$/, '')
.replace(/^(\d)/, '_$1');
}

@ -0,0 +1,17 @@
import c from 'kleur';
function stderr(msg) {
console.error(msg); // eslint-disable-line no-console
}
export default function error(err) {
stderr(c.red(err.message || err));
if (err.frame) {
stderr(err.frame);
} else if (err.stack) {
stderr(c.gray(err.stack));
}
process.exit(1);
}

@ -0,0 +1,31 @@
import sade from 'sade';
import * as pkg from '../../package.json';
const prog = sade('svelte').version(pkg.version);
prog
.command('compile <input>')
.option('-o, --output', 'Output (if absent, prints to stdout)')
.option('-f, --format', 'Type of output (amd, cjs, es, iife, umd)')
.option('-g, --globals', 'Comma-separate list of `module ID:Global` pairs')
.option('-n, --name', 'Name for IIFE/UMD export (inferred from filename by default)')
.option('-m, --sourcemap', 'Generate sourcemap (`-m inline` for inline map)')
.option('-d, --dev', 'Add dev mode warnings and errors')
.option('--amdId', 'ID for AMD module (default is anonymous)')
.option('--generate', 'Change generate format between `dom` and `ssr`')
.option('--no-css', `Don't include CSS (useful with SSR)`)
.option('--immutable', 'Support immutable data structures')
.option('--shared', 'Don\'t include shared helpers')
.example('compile App.html > App.js')
.example('compile src -o dest')
.example('compile -f umd MyComponent.html > MyComponent.js')
.action((input, opts) => {
import('./compile').then(({ compile }) => {
compile(input, opts);
});
})
.parse(process.argv);

@ -1,720 +0,0 @@
import { parseExpressionAt } from 'acorn';
import MagicString, { Bundle } from 'magic-string';
import isReference from 'is-reference';
import { walk, childKeys } from 'estree-walker';
import { getLocator } from 'locate-character';
import Stats from '../Stats';
import deindent from '../utils/deindent';
import CodeBuilder from '../utils/CodeBuilder';
import flattenReference from '../utils/flattenReference';
import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces';
import { removeNode, removeObjectKey } from '../utils/removeNode';
import nodeToString from '../utils/nodeToString';
import wrapModule from './wrapModule';
import annotateWithScopes, { Scope } from '../utils/annotateWithScopes';
import getName from '../utils/getName';
import Stylesheet from '../css/Stylesheet';
import { test } from '../config';
import Fragment from './nodes/Fragment';
import shared from './shared';
import { DomTarget } from './dom/index';
import { SsrTarget } from './ssr/index';
import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
interface Computation {
key: string;
deps: string[]
}
function detectIndentation(str: string) {
const pattern = /^[\t\s]{1,4}/gm;
let match;
while (match = pattern.exec(str)) {
if (match[0][0] === '\t') return '\t';
if (match[0].length === 2) return ' ';
}
return ' ';
}
function getIndentationLevel(str: string, b: number) {
let a = b;
while (a > 0 && str[a - 1] !== '\n') a -= 1;
return /^\s*/.exec(str.slice(a, b))[0];
}
function getIndentExclusionRanges(node: Node) {
const ranges: Node[] = [];
walk(node, {
enter(node: Node) {
if (node.type === 'TemplateElement') ranges.push(node);
}
});
return ranges;
}
function removeIndentation(
code: MagicString,
start: number,
end: number,
indentationLevel: string,
ranges: Node[]
) {
const str = code.original.slice(start, end);
const pattern = new RegExp(`^${indentationLevel}`, 'gm');
let match;
while (match = pattern.exec(str)) {
// TODO bail if we're inside an exclusion range
code.remove(start + match.index, start + match.index + indentationLevel.length);
}
}
// We need to tell estree-walker that it should always
// look for an `else` block, otherwise it might get
// the wrong idea about the shape of each/if blocks
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
childKeys.Attribute = ['value'];
export default class Compiler {
stats: Stats;
ast: Ast;
source: string;
name: string;
options: CompileOptions;
fragment: Fragment;
target: DomTarget | SsrTarget;
customElement: CustomElementOptions;
tag: string;
props: string[];
defaultExport: Node[];
imports: Node[];
shorthandImports: ShorthandImport[];
helpers: Set<string>;
components: Set<string>;
events: Set<string>;
methods: Set<string>;
transitions: Set<string>;
actions: Set<string>;
importedComponents: Map<string, string>;
namespace: string;
hasComponents: boolean;
computations: Computation[];
templateProperties: Record<string, Node>;
slots: Set<string>;
javascript: string;
code: MagicString;
bindingGroups: string[];
indirectDependencies: Map<string, Set<string>>;
expectedProperties: Set<string>;
usesRefs: boolean;
locate: (c: number) => { line: number, column: number };
stylesheet: Stylesheet;
userVars: Set<string>;
templateVars: Map<string, string>;
aliases: Map<string, string>;
usedNames: Set<string>;
constructor(
ast: Ast,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats,
dom: boolean,
target: DomTarget | SsrTarget
) {
stats.start('compile');
this.stats = stats;
this.ast = ast;
this.source = source;
this.options = options;
this.target = target;
this.imports = [];
this.shorthandImports = [];
this.helpers = new Set();
this.components = new Set();
this.events = new Set();
this.methods = new Set();
this.transitions = new Set();
this.actions = new Set();
this.importedComponents = new Map();
this.slots = new Set();
this.bindingGroups = [];
this.indirectDependencies = new Map();
this.locate = getLocator(this.source);
// track which properties are needed, so we can provide useful info
// in dev mode
this.expectedProperties = new Set();
this.code = new MagicString(source);
this.usesRefs = false;
// styles
this.stylesheet = stylesheet;
// allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
this.userVars = new Set();
this.templateVars = new Map();
this.aliases = new Map();
this.usedNames = new Set();
this.computations = [];
this.templateProperties = {};
this.walkJs(dom);
this.name = this.alias(name);
if (options.customElement === true) {
this.customElement = {
tag: this.tag,
props: this.props
}
} else {
this.customElement = options.customElement;
}
if (this.customElement && !this.customElement.tag) {
throw new Error(`No tag name specified`); // TODO better error
}
this.fragment = new Fragment(this, ast.html);
// this.walkTemplate();
if (!this.customElement) this.stylesheet.reify();
stylesheet.warnOnUnusedSelectors(options.onwarn);
}
addSourcemapLocations(node: Node) {
walk(node, {
enter: (node: Node) => {
this.code.addSourcemapLocation(node.start);
this.code.addSourcemapLocation(node.end);
},
});
}
alias(name: string) {
if (!this.aliases.has(name)) {
this.aliases.set(name, this.getUniqueName(name));
}
return this.aliases.get(name);
}
generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
const helpers = new Set();
// TODO use same regex for both
result = result.replace(options.generate === 'ssr' ? /(@+|#+|%+)(\w*(?:-\w*)?)/g : /(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') {
if (name in shared) {
if (options.dev && `${name}Dev` in shared) name = `${name}Dev`;
helpers.add(name);
}
return this.alias(name);
}
if (sigil === '%') {
return this.templateVars.get(name);
}
return sigil.slice(1) + name;
});
let importedHelpers;
if (options.shared) {
if (format !== 'es' && format !== 'cjs') {
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
}
importedHelpers = Array.from(helpers).sort().map(name => {
const alias = this.alias(name);
return { name, alias };
});
} else {
let inlineHelpers = '';
const compiler = this;
importedHelpers = [];
helpers.forEach(name => {
const str = shared[name];
const code = new MagicString(str);
const expression = parseExpressionAt(str, 0);
let { scope } = annotateWithScopes(expression);
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) scope = node._scope;
if (
node.type === 'Identifier' &&
isReference(node, parent) &&
!scope.has(node.name)
) {
if (node.name in shared) {
// this helper function depends on another one
const dependency = node.name;
helpers.add(dependency);
const alias = compiler.alias(dependency);
if (alias !== node.name) {
code.overwrite(node.start, node.end, alias);
}
}
}
},
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
if (name === 'transitionManager') {
// special case
const global = `_svelteTransitionManager`;
inlineHelpers += `\n\nvar ${this.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`;
} else if (name === 'escaped' || name === 'missingComponent') {
// vars are an awkward special case... would be nice to avoid this
const alias = this.alias(name);
inlineHelpers += `\n\nconst ${alias} = ${code};`
} else {
const alias = this.alias(expression.id.name);
if (alias !== expression.id.name) {
code.overwrite(expression.id.start, expression.id.end, alias);
}
inlineHelpers += `\n\n${code}`;
}
});
result += inlineHelpers;
}
const sharedPath = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source);
const parts = module.split('✂]');
const finalChunk = parts.pop();
const compiled = new Bundle({ separator: '' });
function addString(str: string) {
compiled.addSource({
content: new MagicString(str),
});
}
const { filename } = options;
// special case — the source file doesn't actually get used anywhere. we need
// to add an empty file to populate map.sources and map.sourcesContent
if (!parts.length) {
compiled.addSource({
filename,
content: new MagicString(this.source).remove(0, this.source.length),
});
}
parts.forEach((str: string) => {
const chunk = str.replace(pattern, '');
if (chunk) addString(chunk);
const match = pattern.exec(str);
const snippet = this.code.snip(+match[1], +match[2]);
compiled.addSource({
filename,
content: snippet,
});
});
addString(finalChunk);
const css = this.customElement ?
{ code: null, map: null } :
this.stylesheet.render(options.cssOutputFilename, true);
const js = {
code: compiled.toString(),
map: compiled.generateMap({
includeContent: true,
file: options.outputFilename,
})
};
this.stats.stop('compile');
return {
ast: this.ast,
js,
css,
stats: this.stats.render(this)
};
}
getUniqueName(name: string) {
if (test) name = `${name}$`;
let alias = name;
for (
let i = 1;
reservedNames.has(alias) ||
this.userVars.has(alias) ||
this.usedNames.has(alias);
alias = `${name}_${i++}`
);
this.usedNames.add(alias);
return alias;
}
getUniqueNameMaker() {
const localUsedNames = new Set();
function add(name: string) {
localUsedNames.add(name);
}
reservedNames.forEach(add);
this.userVars.forEach(add);
return (name: string) => {
if (test) name = `${name}$`;
let alias = name;
for (
let i = 1;
this.usedNames.has(alias) ||
localUsedNames.has(alias);
alias = `${name}_${i++}`
);
localUsedNames.add(alias);
return alias;
};
}
walkJs(dom: boolean) {
const {
code,
source,
computations,
methods,
templateProperties,
imports
} = this;
const { js } = this.ast;
const componentDefinition = new CodeBuilder();
if (js) {
this.addSourcemapLocations(js.content);
const indentation = detectIndentation(source.slice(js.start, js.end));
const indentationLevel = getIndentationLevel(source, js.content.body[0].start);
const indentExclusionRanges = getIndentExclusionRanges(js.content);
const { scope, globals } = annotateWithScopes(js.content);
scope.declarations.forEach(name => {
this.userVars.add(name);
});
globals.forEach(name => {
this.userVars.add(name);
});
const body = js.content.body.slice(); // slice, because we're going to be mutating the original
// imports need to be hoisted out of the IIFE
for (let i = 0; i < body.length; i += 1) {
const node = body[i];
if (node.type === 'ImportDeclaration') {
removeNode(code, js.content, node);
imports.push(node);
node.specifiers.forEach((specifier: Node) => {
this.userVars.add(specifier.local.name);
});
}
}
const defaultExport = this.defaultExport = body.find(
(node: Node) => node.type === 'ExportDefaultDeclaration'
);
if (defaultExport) {
defaultExport.declaration.properties.forEach((prop: Node) => {
templateProperties[getName(prop.key)] = prop;
});
['helpers', 'events', 'components', 'transitions', 'actions'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: Node) => {
this[key].add(getName(prop.key));
});
}
});
const addArrowFunctionExpression = (name: string, node: Node) => {
const { body, params, async } = node;
const fnKeyword = async ? 'async function' : 'function';
const paramString = params.length ?
`[✂${params[0].start}-${params[params.length - 1].end}✂]` :
``;
if (body.type === 'BlockStatement') {
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}(${paramString}) [${body.start}-${body.end}]
`);
} else {
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}(${paramString}) {
return [${body.start}-${body.end}];
}
`);
}
};
const addFunctionExpression = (name: string, node: Node) => {
const { async } = node;
const fnKeyword = async ? 'async function' : 'function';
let c = node.start;
while (this.source[c] !== '(') c += 1;
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}[${c}-${node.end}];
`);
};
const addValue = (name: string, node: Node) => {
componentDefinition.addBlock(deindent`
var ${name} = [${node.start}-${node.end}];
`);
};
const addDeclaration = (key: string, node: Node, allowShorthandImport?: boolean, disambiguator?: string, conflicts?: Record<string, boolean>) => {
const qualified = disambiguator ? `${disambiguator}-${key}` : key;
if (node.type === 'Identifier' && node.name === key) {
this.templateVars.set(qualified, key);
return;
}
let deconflicted = key;
if (conflicts) while (deconflicted in conflicts) deconflicted += '_'
let name = this.getUniqueName(deconflicted);
this.templateVars.set(qualified, name);
if (allowShorthandImport && node.type === 'Literal' && typeof node.value === 'string') {
this.shorthandImports.push({ name, source: node.value });
return;
}
// deindent
const indentationLevel = getIndentationLevel(source, node.start);
if (indentationLevel) {
removeIndentation(code, node.start, node.end, indentationLevel, indentExclusionRanges);
}
if (node.type === 'ArrowFunctionExpression') {
addArrowFunctionExpression(name, node);
} else if (node.type === 'FunctionExpression') {
addFunctionExpression(name, node);
} else {
addValue(name, node);
}
};
if (templateProperties.components) {
templateProperties.components.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, true, 'components');
});
}
if (templateProperties.computed) {
const dependencies = new Map();
const fullStateComputations = [];
templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = getName(prop.key);
const value = prop.value;
addDeclaration(key, value, false, 'computed', {
state: true,
changed: true
});
const param = value.params[0];
if (param.type === 'ObjectPattern') {
const deps = param.properties.map(prop => prop.key.name);
deps.forEach(dep => {
this.expectedProperties.add(dep);
});
dependencies.set(key, deps);
} else {
fullStateComputations.push({ key, deps: null })
}
});
const visited = new Set();
const visit = (key: string) => {
if (!dependencies.has(key)) return; // not a computation
if (visited.has(key)) return;
visited.add(key);
const deps = dependencies.get(key);
deps.forEach(visit);
computations.push({ key, deps });
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
};
templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(getName(prop.key))
);
if (fullStateComputations.length > 0) {
computations.push(...fullStateComputations);
}
}
if (templateProperties.data) {
addDeclaration('data', templateProperties.data.value);
}
if (templateProperties.events && dom) {
templateProperties.events.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'events');
});
}
if (templateProperties.helpers) {
templateProperties.helpers.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'helpers');
});
}
if (templateProperties.methods && dom) {
addDeclaration('methods', templateProperties.methods.value);
templateProperties.methods.value.properties.forEach(prop => {
this.methods.add(prop.key.name);
});
}
if (templateProperties.namespace) {
const ns = nodeToString(templateProperties.namespace.value);
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.oncreate && dom) {
addDeclaration('oncreate', templateProperties.oncreate.value);
}
if (templateProperties.ondestroy && dom) {
addDeclaration('ondestroy', templateProperties.ondestroy.value);
}
if (templateProperties.onstate && dom) {
addDeclaration('onstate', templateProperties.onstate.value);
}
if (templateProperties.onupdate && dom) {
addDeclaration('onupdate', templateProperties.onupdate.value);
}
if (templateProperties.preload) {
addDeclaration('preload', templateProperties.preload.value);
}
if (templateProperties.props) {
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
}
if (templateProperties.setup) {
addDeclaration('setup', templateProperties.setup.value);
}
if (templateProperties.store) {
addDeclaration('store', templateProperties.store.value);
}
if (templateProperties.tag) {
this.tag = nodeToString(templateProperties.tag.value);
}
if (templateProperties.transitions) {
templateProperties.transitions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'transitions');
});
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'actions');
});
}
}
if (indentationLevel) {
if (defaultExport) {
removeIndentation(code, js.content.start, defaultExport.start, indentationLevel, indentExclusionRanges);
removeIndentation(code, defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
} else {
removeIndentation(code, js.content.start, js.content.end, indentationLevel, indentExclusionRanges);
}
}
let a = js.content.start;
while (/\s/.test(source[a])) a += 1;
let b = js.content.end;
while (/\s/.test(source[b - 1])) b -= 1;
if (defaultExport) {
this.javascript = '';
if (a !== defaultExport.start) this.javascript += `[✂${a}-${defaultExport.start}✂]`;
if (!componentDefinition.isEmpty()) this.javascript += componentDefinition;
if (defaultExport.end !== b) this.javascript += `[✂${defaultExport.end}-${b}✂]`;
} else {
this.javascript = a === b ? null : `[✂${a}-${b}✂]`;
}
}
}
}

@ -0,0 +1,937 @@
import { parseExpressionAt } from 'acorn';
import MagicString, { Bundle } from 'magic-string';
import isReference from 'is-reference';
import { walk, childKeys } from 'estree-walker';
import { getLocator } from 'locate-character';
import Stats from '../Stats';
import deindent from '../utils/deindent';
import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces';
import { removeNode } from '../utils/removeNode';
import nodeToString from '../utils/nodeToString';
import wrapModule from './wrapModule';
import annotateWithScopes from '../utils/annotateWithScopes';
import getName from '../utils/getName';
import Stylesheet from './css/Stylesheet';
import { test } from '../config';
import Fragment from './nodes/Fragment';
import shared from './shared';
import { Node, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
import error from '../utils/error';
import getCodeFrame from '../utils/getCodeFrame';
import checkForComputedKeys from './validate/js/utils/checkForComputedKeys';
import checkForDupes from './validate/js/utils/checkForDupes';
import propValidators from './validate/js/propValidators';
import fuzzymatch from './validate/utils/fuzzymatch';
import flattenReference from '../utils/flattenReference';
interface Computation {
key: string;
deps: string[];
hasRestParam: boolean;
}
interface Declaration {
type: string;
name: string;
node: Node;
block: string;
}
function detectIndentation(str: string) {
const pattern = /^[\t\s]{1,4}/gm;
let match;
while (match = pattern.exec(str)) {
if (match[0][0] === '\t') return '\t';
if (match[0].length === 2) return ' ';
}
return ' ';
}
function getIndentationLevel(str: string, b: number) {
let a = b;
while (a > 0 && str[a - 1] !== '\n') a -= 1;
return /^\s*/.exec(str.slice(a, b))[0];
}
function getIndentExclusionRanges(node: Node) {
// TODO can we fold this into a different pass?
const ranges: Node[] = [];
walk(node, {
enter(node: Node) {
if (node.type === 'TemplateElement') ranges.push(node);
}
});
return ranges;
}
function removeIndentation(
code: MagicString,
start: number,
end: number,
indentationLevel: string,
ranges: Node[]
) {
const str = code.original.slice(start, end);
const pattern = new RegExp(`^${indentationLevel}`, 'gm');
let match;
while (match = pattern.exec(str)) {
// TODO bail if we're inside an exclusion range
code.remove(start + match.index, start + match.index + indentationLevel.length);
}
}
// We need to tell estree-walker that it should always
// look for an `else` block, otherwise it might get
// the wrong idea about the shape of each/if blocks
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
childKeys.Attribute = ['value'];
export default class Component {
stats: Stats;
ast: Ast;
source: string;
name: string;
options: CompileOptions;
fragment: Fragment;
customElement: CustomElementOptions;
tag: string;
props: string[];
properties: Map<string, Node>;
defaultExport: Node;
imports: Node[];
shorthandImports: ShorthandImport[];
helpers: Set<string>;
components: Set<string>;
events: Set<string>;
methods: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
importedComponents: Map<string, string>;
namespace: string;
hasComponents: boolean;
computations: Computation[];
templateProperties: Record<string, Node>;
javascript: [string, string];
used: {
components: Set<string>;
helpers: Set<string>;
events: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
};
declarations: Declaration[];
refCallees: Node[];
code: MagicString;
indirectDependencies: Map<string, Set<string>>;
expectedProperties: Set<string>;
refs: Set<string>;
file: string;
locate: (c: number) => { line: number, column: number };
stylesheet: Stylesheet;
userVars: Set<string>;
templateVars: Map<string, string>;
aliases: Map<string, string>;
usedNames: Set<string>;
locator: (search: number, startIndex?: number) => {
line: number,
column: number
};
constructor(
ast: Ast,
source: string,
name: string,
options: CompileOptions,
stats: Stats
) {
this.stats = stats;
this.ast = ast;
this.source = source;
this.options = options;
this.imports = [];
this.shorthandImports = [];
this.helpers = new Set();
this.components = new Set();
this.events = new Set();
this.methods = new Set();
this.animations = new Set();
this.transitions = new Set();
this.actions = new Set();
this.importedComponents = new Map();
this.used = {
components: new Set(),
helpers: new Set(),
events: new Set(),
animations: new Set(),
transitions: new Set(),
actions: new Set(),
};
this.declarations = [];
this.refs = new Set();
this.refCallees = [];
this.indirectDependencies = new Map();
this.file = options.filename && (
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename
);
this.locate = getLocator(this.source);
// track which properties are needed, so we can provide useful info
// in dev mode
this.expectedProperties = new Set();
this.code = new MagicString(source);
// styles
this.stylesheet = new Stylesheet(source, ast, options.filename, options.dev);
this.stylesheet.validate(this);
// allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
this.userVars = new Set();
this.templateVars = new Map();
this.aliases = new Map();
this.usedNames = new Set();
this.computations = [];
this.templateProperties = {};
this.properties = new Map();
this.walkJs();
this.name = this.alias(name);
if (options.customElement === true) {
this.customElement = {
tag: this.tag,
props: this.props
}
} else {
this.customElement = options.customElement;
}
if (this.customElement && !this.customElement.tag) {
throw new Error(`No tag name specified`); // TODO better error
}
this.fragment = new Fragment(this, ast.html);
// this.walkTemplate();
if (!this.customElement) this.stylesheet.reify();
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
if (this.defaultExport) {
const categories = {
components: 'component',
helpers: 'helper',
events: 'event definition',
transitions: 'transition',
actions: 'actions',
};
Object.keys(categories).forEach(category => {
const definitions = this.defaultExport.declaration.properties.find(prop => prop.key.name === category);
if (definitions) {
definitions.value.properties.forEach(prop => {
const { name } = prop.key;
if (!this.used[category].has(name)) {
this.warn(prop, {
code: `unused-${category.slice(0, -1)}`,
message: `The '${name}' ${categories[category]} is unused`
});
}
});
}
});
}
this.refCallees.forEach(callee => {
const { parts } = flattenReference(callee);
const ref = parts[1];
if (this.refs.has(ref)) {
// TODO check method is valid, e.g. `audio.stop()` should be `audio.pause()`
} else {
const match = fuzzymatch(ref, Array.from(this.refs.keys()));
let message = `'refs.${ref}' does not exist`;
if (match) message += ` (did you mean 'refs.${match}'?)`;
this.error(callee, {
code: `missing-ref`,
message
});
}
});
}
addSourcemapLocations(node: Node) {
walk(node, {
enter: (node: Node) => {
this.code.addSourcemapLocation(node.start);
this.code.addSourcemapLocation(node.end);
},
});
}
alias(name: string) {
if (!this.aliases.has(name)) {
this.aliases.set(name, this.getUniqueName(name));
}
return this.aliases.get(name);
}
generate(result: string, options: CompileOptions, {
banner = '',
name,
format
}) {
const pattern = /\[✂(\d+)-(\d+)$/;
const helpers = new Set();
// TODO use same regex for both
result = result.replace(options.generate === 'ssr' ? /(@+|#+|%+)(\w*(?:-\w*)?)/g : /(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') {
if (name in shared) {
if (options.dev && `${name}Dev` in shared) name = `${name}Dev`;
helpers.add(name);
}
return this.alias(name);
}
if (sigil === '%') {
return this.templateVars.get(name);
}
return sigil.slice(1) + name;
});
let importedHelpers;
if (options.shared) {
if (format !== 'es' && format !== 'cjs') {
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
}
importedHelpers = Array.from(helpers).sort().map(name => {
const alias = this.alias(name);
return { name, alias };
});
} else {
let inlineHelpers = '';
const component = this;
importedHelpers = [];
helpers.forEach(name => {
const str = shared[name];
const code = new MagicString(str);
const expression = parseExpressionAt(str, 0);
let { scope } = annotateWithScopes(expression);
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) scope = node._scope;
if (
node.type === 'Identifier' &&
isReference(node, parent) &&
!scope.has(node.name)
) {
if (node.name in shared) {
// this helper function depends on another one
const dependency = node.name;
helpers.add(dependency);
const alias = component.alias(dependency);
if (alias !== node.name) {
code.overwrite(node.start, node.end, alias);
}
}
}
},
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
if (name === 'transitionManager' || name === 'outros') {
// special case
const global = name === 'outros'
? `_svelteOutros`
: `_svelteTransitionManager`;
inlineHelpers += `\n\nvar ${this.alias(name)} = window.${global} || (window.${global} = ${code});\n\n`;
} else if (name === 'escaped' || name === 'missingComponent' || name === 'invalidAttributeNameCharacter') {
// vars are an awkward special case... would be nice to avoid this
const alias = this.alias(name);
inlineHelpers += `\n\nconst ${alias} = ${code};`
} else {
const alias = this.alias(expression.id.name);
if (alias !== expression.id.name) {
code.overwrite(expression.id.start, expression.id.end, alias);
}
inlineHelpers += `\n\n${code}`;
}
});
result += inlineHelpers;
}
const sharedPath = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source);
const parts = module.split('✂]');
const finalChunk = parts.pop();
const compiled = new Bundle({ separator: '' });
function addString(str: string) {
compiled.addSource({
content: new MagicString(str),
});
}
const { filename } = options;
// special case — the source file doesn't actually get used anywhere. we need
// to add an empty file to populate map.sources and map.sourcesContent
if (!parts.length) {
compiled.addSource({
filename,
content: new MagicString(this.source).remove(0, this.source.length),
});
}
parts.forEach((str: string) => {
const chunk = str.replace(pattern, '');
if (chunk) addString(chunk);
const match = pattern.exec(str);
const snippet = this.code.snip(+match[1], +match[2]);
compiled.addSource({
filename,
content: snippet,
});
});
addString(finalChunk);
const css = this.customElement ?
{ code: null, map: null } :
this.stylesheet.render(options.cssOutputFilename, true);
const js = {
code: compiled.toString(),
map: compiled.generateMap({
includeContent: true,
file: options.outputFilename,
})
};
return {
ast: this.ast,
js,
css,
stats: this.stats.render(this)
};
}
getUniqueName(name: string) {
if (test) name = `${name}$`;
let alias = name;
for (
let i = 1;
reservedNames.has(alias) ||
this.userVars.has(alias) ||
this.usedNames.has(alias);
alias = `${name}_${i++}`
);
this.usedNames.add(alias);
return alias;
}
getUniqueNameMaker() {
const localUsedNames = new Set();
function add(name: string) {
localUsedNames.add(name);
}
reservedNames.forEach(add);
this.userVars.forEach(add);
return (name: string) => {
if (test) name = `${name}$`;
let alias = name;
for (
let i = 1;
this.usedNames.has(alias) ||
localUsedNames.has(alias);
alias = `${name}_${i++}`
);
localUsedNames.add(alias);
return alias;
};
}
error(
pos: {
start: number,
end: number
},
e : {
code: string,
message: string
}
) {
error(e.message, {
name: 'ValidationError',
code: e.code,
source: this.source,
start: pos.start,
end: pos.end,
filename: this.options.filename
});
}
warn(
pos: {
start: number,
end: number
},
warning: {
code: string,
message: string
}
) {
if (!this.locator) {
this.locator = getLocator(this.source, { offsetLine: 1 });
}
const start = this.locator(pos.start);
const end = this.locator(pos.end);
const frame = getCodeFrame(this.source, start.line - 1, start.column);
this.stats.warn({
code: warning.code,
message: warning.message,
frame,
start,
end,
pos: pos.start,
filename: this.options.filename,
toString: () => `${warning.message} (${start.line + 1}:${start.column})\n${frame}`,
});
}
processDefaultExport(node, indentExclusionRanges) {
const { templateProperties, source, code } = this;
if (node.declaration.type !== 'ObjectExpression') {
this.error(node.declaration, {
code: `invalid-default-export`,
message: `Default export must be an object literal`
});
}
checkForComputedKeys(this, node.declaration.properties);
checkForDupes(this, node.declaration.properties);
const props = this.properties;
node.declaration.properties.forEach((prop: Node) => {
props.set(getName(prop.key), prop);
});
const validPropList = Object.keys(propValidators);
// ensure all exported props are valid
node.declaration.properties.forEach((prop: Node) => {
const name = getName(prop.key);
const propValidator = propValidators[name];
if (propValidator) {
propValidator(this, prop);
} else {
const match = fuzzymatch(name, validPropList);
if (match) {
this.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}' (did you mean '${match}'?)`
});
} else if (/FunctionExpression/.test(prop.value.type)) {
this.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}' (did you mean to include it in 'methods'?)`
});
} else {
this.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}'`
});
}
}
});
if (props.has('namespace')) {
const ns = nodeToString(props.get('namespace').value);
this.namespace = namespaces[ns] || ns;
}
node.declaration.properties.forEach((prop: Node) => {
templateProperties[getName(prop.key)] = prop;
});
['helpers', 'events', 'components', 'transitions', 'actions', 'animations'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: Node) => {
this[key].add(getName(prop.key));
});
}
});
const addArrowFunctionExpression = (type: string, name: string, node: Node) => {
const { body, params, async } = node;
const fnKeyword = async ? 'async function' : 'function';
const paramString = params.length ?
`[✂${params[0].start}-${params[params.length - 1].end}✂]` :
``;
const block = body.type === 'BlockStatement'
? deindent`
${fnKeyword} ${name}(${paramString}) [${body.start}-${body.end}]
`
: deindent`
${fnKeyword} ${name}(${paramString}) {
return [${body.start}-${body.end}];
}
`;
this.declarations.push({ type, name, block, node });
};
const addFunctionExpression = (type: string, name: string, node: Node) => {
const { async } = node;
const fnKeyword = async ? 'async function' : 'function';
let c = node.start;
while (this.source[c] !== '(') c += 1;
const block = deindent`
${fnKeyword} ${name}[${c}-${node.end}];
`;
this.declarations.push({ type, name, block, node });
};
const addValue = (type: string, name: string, node: Node) => {
const block = deindent`
var ${name} = [${node.start}-${node.end}];
`;
this.declarations.push({ type, name, block, node });
};
const addDeclaration = (
type: string,
key: string,
node: Node,
allowShorthandImport?: boolean,
disambiguator?: string,
conflicts?: Record<string, boolean>
) => {
const qualified = disambiguator ? `${disambiguator}-${key}` : key;
if (node.type === 'Identifier' && node.name === key) {
this.templateVars.set(qualified, key);
return;
}
let deconflicted = key;
if (conflicts) while (deconflicted in conflicts) deconflicted += '_'
let name = this.getUniqueName(deconflicted);
this.templateVars.set(qualified, name);
if (allowShorthandImport && node.type === 'Literal' && typeof node.value === 'string') {
this.shorthandImports.push({ name, source: node.value });
return;
}
// deindent
const indentationLevel = getIndentationLevel(source, node.start);
if (indentationLevel) {
removeIndentation(code, node.start, node.end, indentationLevel, indentExclusionRanges);
}
if (node.type === 'ArrowFunctionExpression') {
addArrowFunctionExpression(type, name, node);
} else if (node.type === 'FunctionExpression') {
addFunctionExpression(type, name, node);
} else {
addValue(type, name, node);
}
};
if (templateProperties.components) {
templateProperties.components.value.properties.forEach((property: Node) => {
addDeclaration('components', getName(property.key), property.value, true, 'components');
});
}
if (templateProperties.computed) {
const dependencies = new Map();
const fullStateComputations = [];
templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = getName(prop.key);
const value = prop.value;
addDeclaration('computed', key, value, false, 'computed', {
state: true,
changed: true
});
const param = value.params[0];
const hasRestParam = (
param.properties &&
param.properties.some(prop => prop.type === 'RestElement')
);
if (param.type !== 'ObjectPattern' || hasRestParam) {
fullStateComputations.push({ key, deps: null, hasRestParam });
} else {
const deps = param.properties.map(prop => prop.key.name);
deps.forEach(dep => {
this.expectedProperties.add(dep);
});
dependencies.set(key, deps);
}
});
const visited = new Set();
const visit = (key: string) => {
if (!dependencies.has(key)) return; // not a computation
if (visited.has(key)) return;
visited.add(key);
const deps = dependencies.get(key);
deps.forEach(visit);
this.computations.push({ key, deps, hasRestParam: false });
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
};
templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(getName(prop.key))
);
if (fullStateComputations.length > 0) {
this.computations.push(...fullStateComputations);
}
}
if (templateProperties.data) {
addDeclaration('data', 'data', templateProperties.data.value);
}
if (templateProperties.events) {
templateProperties.events.value.properties.forEach((property: Node) => {
addDeclaration('events', getName(property.key), property.value, false, 'events');
});
}
if (templateProperties.helpers) {
templateProperties.helpers.value.properties.forEach((property: Node) => {
addDeclaration('helpers', getName(property.key), property.value, false, 'helpers');
});
}
if (templateProperties.methods) {
addDeclaration('methods', 'methods', templateProperties.methods.value);
templateProperties.methods.value.properties.forEach(property => {
this.methods.add(getName(property.key));
});
}
if (templateProperties.namespace) {
const ns = nodeToString(templateProperties.namespace.value);
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.oncreate) {
addDeclaration('oncreate', 'oncreate', templateProperties.oncreate.value);
}
if (templateProperties.ondestroy) {
addDeclaration('ondestroy', 'ondestroy', templateProperties.ondestroy.value);
}
if (templateProperties.onstate) {
addDeclaration('onstate', 'onstate', templateProperties.onstate.value);
}
if (templateProperties.onupdate) {
addDeclaration('onupdate', 'onupdate', templateProperties.onupdate.value);
}
if (templateProperties.preload) {
addDeclaration('preload', 'preload', templateProperties.preload.value);
}
if (templateProperties.props) {
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
}
if (templateProperties.setup) {
addDeclaration('setup', 'setup', templateProperties.setup.value);
}
if (templateProperties.store) {
addDeclaration('store', 'store', templateProperties.store.value);
}
if (templateProperties.tag) {
this.tag = nodeToString(templateProperties.tag.value);
}
if (templateProperties.transitions) {
templateProperties.transitions.value.properties.forEach((property: Node) => {
addDeclaration('transitions', getName(property.key), property.value, false, 'transitions');
});
}
if (templateProperties.animations) {
templateProperties.animations.value.properties.forEach((property: Node) => {
addDeclaration('animations', getName(property.key), property.value, false, 'animations');
});
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration('actions', getName(property.key), property.value, false, 'actions');
});
}
this.defaultExport = node;
}
walkJs() {
const { js } = this.ast;
if (!js) return;
this.addSourcemapLocations(js.content);
const { code, source, imports } = this;
const indentationLevel = getIndentationLevel(source, js.content.body[0].start);
const indentExclusionRanges = getIndentExclusionRanges(js.content);
const { scope, globals } = annotateWithScopes(js.content);
scope.declarations.forEach(name => {
this.userVars.add(name);
});
globals.forEach(name => {
this.userVars.add(name);
});
const body = js.content.body.slice(); // slice, because we're going to be mutating the original
body.forEach(node => {
// check there are no named exports
if (node.type === 'ExportNamedDeclaration') {
this.error(node, {
code: `named-export`,
message: `A component can only have a default export`
});
}
if (node.type === 'ExportDefaultDeclaration') {
this.processDefaultExport(node, indentExclusionRanges);
}
// imports need to be hoisted out of the IIFE
else if (node.type === 'ImportDeclaration') {
removeNode(code, js.content, node);
imports.push(node);
node.specifiers.forEach((specifier: Node) => {
this.userVars.add(specifier.local.name);
});
}
});
if (indentationLevel) {
if (this.defaultExport) {
removeIndentation(code, js.content.start, this.defaultExport.start, indentationLevel, indentExclusionRanges);
removeIndentation(code, this.defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
} else {
removeIndentation(code, js.content.start, js.content.end, indentationLevel, indentExclusionRanges);
}
}
let a = js.content.start;
while (/\s/.test(source[a])) a += 1;
let b = js.content.end;
while (/\s/.test(source[b - 1])) b -= 1;
this.javascript = this.defaultExport
? [
a !== this.defaultExport.start ? `[✂${a}-${this.defaultExport.start}✂]` : '',
b !== this.defaultExport.end ?`[✂${this.defaultExport.end}-${b}✂]` : ''
]
: [
a !== b ? `[✂${a}-${b}✂]` : '',
''
];
}
}

@ -1,8 +1,8 @@
import MagicString from 'magic-string';
import Stylesheet from './Stylesheet';
import { gatherPossibleValues, UNKNOWN } from './gatherPossibleValues';
import { Validator } from '../validate/index';
import { Node } from '../interfaces';
import Component from '../compile/Component';
export default class Selector {
node: Node;
@ -30,7 +30,8 @@ export default class Selector {
apply(node: Node, stack: Node[]) {
const toEncapsulate: Node[] = [];
applySelector(this.localBlocks.slice(), node, stack.slice(), toEncapsulate);
applySelector(this.stylesheet, this.localBlocks.slice(), node, stack.slice(), toEncapsulate);
if (toEncapsulate.length > 0) {
toEncapsulate.filter((_, i) => i === 0 || i === toEncapsulate.length - 1).forEach(({ node, block }) => {
@ -76,7 +77,7 @@ export default class Selector {
const selector = block.selectors[i];
if (selector.type === 'RefSelector') {
code.overwrite(selector.start, selector.end, `[svelte-ref-${selector.name}]`, {
code.overwrite(selector.start, selector.end, `.svelte-ref-${selector.name}`, {
contentOnly: true,
storeName: false
});
@ -96,13 +97,13 @@ export default class Selector {
});
}
validate(validator: Validator) {
validate(component: Component) {
this.blocks.forEach((block) => {
let i = block.selectors.length;
while (i-- > 1) {
const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
validator.error(selector, {
component.error(selector, {
code: `css-invalid-global`,
message: `:global(...) must be the first element in a compound selector`
});
@ -123,7 +124,7 @@ export default class Selector {
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
validator.error(this.blocks[i].selectors[0], {
component.error(this.blocks[i].selectors[0], {
code: `css-invalid-global`,
message: `:global(...) can be at the start or end of a selector sequence, but not in the middle`
});
@ -132,11 +133,7 @@ export default class Selector {
}
}
function isDescendantSelector(selector: Node) {
return selector.type === 'WhiteSpace' || selector.type === 'Combinator';
}
function applySelector(blocks: Block[], node: Node, stack: Node[], toEncapsulate: any[]): boolean {
function applySelector(stylesheet: Stylesheet, blocks: Block[], node: Node, stack: Node[], toEncapsulate: any[]): boolean {
const block = blocks.pop();
if (!block) return false;
@ -145,7 +142,6 @@ function applySelector(blocks: Block[], node: Node, stack: Node[], toEncapsulate
}
let i = block.selectors.length;
let j = stack.length;
while (i--) {
const selector = block.selectors[i];
@ -161,7 +157,7 @@ function applySelector(blocks: Block[], node: Node, stack: Node[], toEncapsulate
}
if (selector.type === 'ClassSelector') {
if (!attributeMatches(node, 'class', selector.name, '~=', false)) return false;
if (!attributeMatches(node, 'class', selector.name, '~=', false) && !classMatches(node, selector.name)) return false;
}
else if (selector.type === 'IdSelector') {
@ -178,8 +174,8 @@ function applySelector(blocks: Block[], node: Node, stack: Node[], toEncapsulate
}
else if (selector.type === 'RefSelector') {
if (node.ref === selector.name) {
node._cssRefAttribute = selector.name;
if (node.ref && node.ref.name === selector.name) {
stylesheet.nodesWithRefCssClass.set(selector.name, node);
toEncapsulate.push({ node, block });
return true;
}
@ -196,18 +192,24 @@ function applySelector(blocks: Block[], node: Node, stack: Node[], toEncapsulate
if (block.combinator) {
if (block.combinator.type === 'WhiteSpace') {
while (stack.length) {
if (applySelector(blocks.slice(), stack.pop(), stack, toEncapsulate)) {
if (applySelector(stylesheet, blocks.slice(), stack.pop(), stack, toEncapsulate)) {
toEncapsulate.push({ node, block });
return true;
}
}
if (blocks.every(block => block.global)) {
toEncapsulate.push({ node, block });
return true;
}
return false;
} else if (block.combinator.name === '>') {
if (applySelector(blocks, stack.pop(), stack, toEncapsulate)) {
if (applySelector(stylesheet, blocks, stack.pop(), stack, toEncapsulate)) {
toEncapsulate.push({ node, block });
return true;
}
return false;
}
@ -256,6 +258,12 @@ function attributeMatches(node: Node, name: string, expectedValue: string, opera
return false;
}
function classMatches(node, name: string) {
return node.classes.some(function(classDir) {
return classDir.name === name;
});
}
function isDynamic(value: Node) {
return value.length > 1 || value[0].type !== 'Text';
}

@ -2,11 +2,14 @@ import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import { getLocator } from 'locate-character';
import Selector from './Selector';
import getCodeFrame from '../utils/getCodeFrame';
import hash from '../utils/hash';
import Element from '../compile/nodes/Element';
import { Validator } from '../validate/index';
import { Node, Ast, Warning } from '../interfaces';
import getCodeFrame from '../../utils/getCodeFrame';
import hash from '../../utils/hash';
import removeCSSPrefix from '../../utils/removeCSSPrefix';
import Element from '../nodes/Element';
import { Node, Ast, Warning } from '../../interfaces';
import Component from '../Component';
const isKeyframesNode = (node: Node) => removeCSSPrefix(node.name) === 'keyframes'
class Rule {
selectors: Selector[];
@ -26,7 +29,7 @@ class Rule {
}
isUsed(dev: boolean) {
if (this.parent && this.parent.node.type === 'Atrule' && this.parent.node.name === 'keyframes') return true;
if (this.parent && this.parent.node.type === 'Atrule' && isKeyframesNode(this.parent.node)) return true;
if (this.declarations.length === 0) return dev;
return this.selectors.some(s => s.used);
}
@ -67,7 +70,7 @@ class Rule {
}
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (this.parent && this.parent.node.type === 'Atrule' && this.parent.node.name === 'keyframes') return true;
if (this.parent && this.parent.node.type === 'Atrule' && isKeyframesNode(this.parent.node)) return true;
const attr = `.${id}`;
@ -75,9 +78,9 @@ class Rule {
this.declarations.forEach(declaration => declaration.transform(code, keyframes));
}
validate(validator: Validator) {
validate(component: Component) {
this.selectors.forEach(selector => {
selector.validate(validator);
selector.validate(component);
});
}
@ -96,7 +99,7 @@ class Declaration {
}
transform(code: MagicString, keyframes: Map<string, string>) {
const property = this.node.property && this.node.property.toLowerCase();
const property = this.node.property && removeCSSPrefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
this.node.value.children.forEach((block: Node) => {
if (block.type === 'Identifier') {
@ -142,7 +145,7 @@ class Atrule {
});
}
else if (this.node.name === 'keyframes') {
else if (isKeyframesNode(this.node)) {
this.children.forEach((rule: Rule) => {
rule.selectors.forEach(selector => {
selector.used = true;
@ -167,8 +170,8 @@ class Atrule {
});
code.remove(c, this.node.block.start);
} else if (this.node.name === 'keyframes') {
let c = this.node.start + 10;
} else if (isKeyframesNode(this.node)) {
let c = this.node.start + this.node.name.length + 1;
if (this.node.expression.start - c > 1) code.overwrite(c, this.node.expression.start, ' ');
c = this.node.expression.end;
if (this.node.block.start - c > 0) code.remove(c, this.node.block.start);
@ -200,7 +203,7 @@ class Atrule {
}
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (this.node.name === 'keyframes') {
if (isKeyframesNode(this.node)) {
this.node.expression.children.forEach(({ type, name, start, end }: Node) => {
if (type === 'Identifier') {
if (name.startsWith('-global-')) {
@ -217,9 +220,9 @@ class Atrule {
})
}
validate(validator: Validator) {
validate(component: Component) {
this.children.forEach(child => {
child.validate(validator);
child.validate(component);
});
}
@ -247,6 +250,7 @@ export default class Stylesheet {
keyframes: Map<string, string>;
nodesWithCssClass: Set<Node>;
nodesWithRefCssClass: Map<String, Node>;
constructor(source: string, ast: Ast, filename: string, dev: boolean) {
this.source = source;
@ -258,6 +262,7 @@ export default class Stylesheet {
this.keyframes = new Map();
this.nodesWithCssClass = new Set();
this.nodesWithRefCssClass = new Map();
if (ast.css && ast.css.children.length) {
this.id = `svelte-${hash(ast.css.content.styles)}`;
@ -285,7 +290,7 @@ export default class Stylesheet {
this.children.push(atrule);
}
if (node.name === 'keyframes') {
if (isKeyframesNode(node)) {
node.expression.children.forEach((expression: Node) => {
if (expression.type === 'Identifier' && !expression.name.startsWith('-global-')) {
this.keyframes.set(expression.name, `${this.id}-${expression.name}`);
@ -337,6 +342,9 @@ export default class Stylesheet {
this.nodesWithCssClass.forEach((node: Node) => {
node.addCssClass();
});
this.nodesWithRefCssClass.forEach((node: Node, name: String) => {
node.addCssClass(`svelte-ref-${name}`);
})
}
render(cssOutputFilename: string, shouldTransformSelectors: boolean) {
@ -380,9 +388,9 @@ export default class Stylesheet {
};
}
validate(validator: Validator) {
validate(component: Component) {
this.children.forEach(child => {
child.validate(validator);
child.validate(component);
});
}

@ -0,0 +1,96 @@
import { assign } from '../shared';
import Stats from '../Stats';
import parse from '../parse/index';
import renderDOM from './render-dom/index';
import renderSSR from './render-ssr/index';
import { CompileOptions, Warning, Ast } from '../interfaces';
import Component from './Component';
import deprecate from '../utils/deprecate';
function normalize_options(options: CompileOptions): CompileOptions {
let normalized = assign({ generate: 'dom', dev: false }, options);
const { onwarn } = normalized;
normalized.onwarn = onwarn
? (warning: Warning) => onwarn(warning, default_onwarn)
: default_onwarn;
return normalized;
}
function default_onwarn({ start, message }: Warning) {
if (start) {
console.warn(`(${start.line}:${start.column}) ${message}`);
} else {
console.warn(message);
}
}
function validate_options(options: CompileOptions, stats: Stats) {
const { name, filename } = options;
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
const error = new Error(`options.name must be a valid identifier (got '${name}')`);
throw error;
}
if (name && /^[a-z]/.test(name)) {
const message = `options.name should be capitalised`;
stats.warn({
code: `options-lowercase-name`,
message,
filename,
toString: () => message,
});
}
}
export default function compile(source: string, options: CompileOptions = {}) {
const onerror = options.onerror || (err => {
throw err;
});
if (options.onerror) {
// TODO remove in v3
deprecate(`Instead of using options.onerror, wrap svelte.compile in a try-catch block`);
delete options.onerror;
}
options = normalize_options(options);
const stats = new Stats({
onwarn: options.onwarn
});
let ast: Ast;
try {
validate_options(options, stats);
stats.start('parse');
ast = parse(source, options);
stats.stop('parse');
stats.start('create component');
const component = new Component(
ast,
source,
options.name || 'SvelteComponent',
options,
stats
);
stats.stop('create component');
if (options.generate === false) {
return { ast, stats: stats.render(null), js: null, css: null };
}
if (options.generate === 'ssr') {
return renderSSR(component, options);
}
return renderDOM(component, options);
} catch (err) {
onerror(err);
}
}

@ -6,13 +6,22 @@ export default class Action extends Node {
name: string;
expression: Expression;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
component.used.actions.add(this.name);
if (!component.actions.has(this.name)) {
component.error(this, {
code: `missing-action`,
message: `Missing action '${this.name}'`
});
}
this.expression = info.expression
? new Expression(compiler, this, scope, info.expression)
? new Expression(component, this, scope, info.expression)
: null;
}
}

@ -0,0 +1,45 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
export default class Animation extends Node {
type: 'Animation';
name: string;
expression: Expression;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
component.used.animations.add(this.name);
if (parent.animation) {
component.error(this, {
code: `duplicate-animation`,
message: `An element can only have one 'animate' directive`
});
}
if (!component.animations.has(this.name)) {
component.error(this, {
code: `missing-animation`,
message: `Missing animation '${this.name}'`
});
}
const block = parent.parent;
if (!block || block.type !== 'EachBlock' || !block.key) {
// TODO can we relax the 'immediate child' rule?
component.error(this, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the immediate child of a keyed each block`
});
}
block.hasAnimation = true;
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
: null;
}
}

@ -1,25 +1,17 @@
import deindent from '../../utils/deindent';
import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import addToSet from '../../utils/addToSet';
import Compiler from '../Compiler';
import Component from '../Component';
import Node from './shared/Node';
import Element from './Element';
import Text from './Text';
import Block from '../dom/Block';
import Expression from './shared/Expression';
export interface StyleProp {
key: string;
value: Node[];
}
export default class Attribute extends Node {
type: 'Attribute';
start: number;
end: number;
compiler: Compiler;
component: Component;
parent: Element;
name: string;
isSpread: boolean;
@ -31,8 +23,8 @@ export default class Attribute extends Node {
chunks: (Text | Expression)[];
dependencies: Set<string>;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (info.type === 'Spread') {
this.name = null;
@ -40,7 +32,7 @@ export default class Attribute extends Node {
this.isTrue = false;
this.isSynthetic = false;
this.expression = new Expression(compiler, this, scope, info.expression);
this.expression = new Expression(component, this, scope, info.expression);
this.dependencies = this.expression.dependencies;
this.chunks = null;
@ -60,17 +52,13 @@ export default class Attribute extends Node {
: info.value.map(node => {
if (node.type === 'Text') return node;
const expression = new Expression(compiler, this, scope, node.expression);
const expression = new Expression(component, this, scope, node.expression);
addToSet(this.dependencies, expression.dependencies);
return expression;
});
// TODO this would be better, but it breaks some stuff
// this.isDynamic = this.dependencies.size > 0;
this.isDynamic = this.chunks.length === 1
? this.chunks[0].type !== 'Text'
: this.chunks.length > 1;
this.isDynamic = this.dependencies.size > 0;
this.shouldCache = this.isDynamic
? this.chunks.length === 1
@ -82,7 +70,7 @@ export default class Attribute extends Node {
getValue() {
if (this.isTrue) return true;
if (this.chunks.length === 0) return `''`;
if (this.chunks.length === 0) return `""`;
if (this.chunks.length === 1) {
return this.chunks[0].type === 'Text'
@ -102,235 +90,14 @@ export default class Attribute extends Node {
.join(' + ');
}
render(block: Block) {
const node = this.parent;
const name = fixAttributeCasing(this.name);
if (name === 'style') {
const styleProps = optimizeStyle(this.chunks);
if (styleProps) {
this.renderStyle(block, styleProps);
return;
}
}
let metadata = node.namespace ? null : attributeLookup[name];
if (metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf(node.name))
metadata = null;
const isIndirectlyBoundValue =
name === 'value' &&
(node.name === 'option' || // TODO check it's actually bound
(node.name === 'input' &&
node.bindings.find(
(binding: Binding) =>
/checked|group/.test(binding.name)
)));
const propertyName = isIndirectlyBoundValue
? '__value'
: metadata && metadata.propertyName;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = name.slice(0, 6) === 'xlink:'
? '@setXlinkAttribute'
: '@setAttribute';
const isLegacyInputType = this.compiler.options.legacy && name === 'type' && this.parent.name === 'input';
const isDataSet = /^data-/.test(name) && !this.compiler.options.legacy && !node.namespace;
const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
return m[1].toUpperCase();
}) : name;
if (this.isDynamic) {
let value;
getStaticValue() {
if (this.isSpread || this.isDynamic) return null;
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.chunks.length === 1) {
// single {tag} — may be a non-string
value = this.chunks[0].snippet;
} else {
// '{foo} {bar}' — treat as string concatenation
value =
(this.chunks[0].type === 'Text' ? '' : `"" + `) +
this.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
return chunk.getPrecedence() <= 13
? `(${chunk.snippet})`
: chunk.snippet;
}
})
.join(' + ');
}
const isSelectValueAttribute =
name === 'value' && node.name === 'select';
const shouldCache = this.shouldCache || isSelectValueAttribute;
const last = shouldCache && block.getUniqueName(
`${node.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
if (shouldCache) block.addVariable(last);
let updater;
const init = shouldCache ? `${last} = ${value}` : value;
if (isLegacyInputType) {
block.builders.hydrate.addLine(
`@setInputType(${node.var}, ${init});`
);
updater = `@setInputType(${node.var}, ${shouldCache ? last : value});`;
} else if (isSelectValueAttribute) {
// annoying special case
const isMultipleSelect = node.getStaticAttributeValue('multiple');
const i = block.getUniqueName('i');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${last}.indexOf(${option}.__value);`
: deindent`
if (${option}.__value === ${last}) {
${option}.selected = true;
break;
}`;
updater = deindent`
for (var ${i} = 0; ${i} < ${node.var}.options.length; ${i} += 1) {
var ${option} = ${node.var}.options[${i}];
${ifStatement}
}
`;
block.builders.hydrate.addBlock(deindent`
${last} = ${value};
${updater}
`);
} else if (propertyName) {
block.builders.hydrate.addLine(
`${node.var}.${propertyName} = ${init};`
);
updater = `${node.var}.${propertyName} = ${shouldCache ? last : value};`;
} else if (isDataSet) {
block.builders.hydrate.addLine(
`${node.var}.dataset.${camelCaseName} = ${init};`
);
updater = `${node.var}.dataset.${camelCaseName} = ${shouldCache ? last : value};`;
} else {
block.builders.hydrate.addLine(
`${method}(${node.var}, "${name}", ${init});`
);
updater = `${method}(${node.var}, "${name}", ${shouldCache ? last : value});`;
}
if (this.dependencies.size || isSelectValueAttribute) {
const dependencies = Array.from(this.dependencies);
const changedCheck = (
( block.hasOutroMethod ? `#outroing || ` : '' ) +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
block.builders.update.addConditional(
condition,
updater
);
}
} else {
const value = this.isTrue
? 'true'
: this.chunks.length === 0 ? `""` : stringify(this.chunks[0].data);
const statement = (
isLegacyInputType
? `@setInputType(${node.var}, ${value});`
: propertyName
? `${node.var}.${propertyName} = ${value};`
: isDataSet
? `${node.var}.dataset.${camelCaseName} = ${value};`
: `${method}(${node.var}, "${name}", ${value});`
);
block.builders.hydrate.addLine(statement);
// special case autofocus. has to be handled in a bit of a weird way
if (this.isTrue && name === 'autofocus') {
block.autofocus = node.var;
}
}
if (isIndirectlyBoundValue) {
const updateValue = `${node.var}.value = ${node.var}.__value;`;
block.builders.hydrate.addLine(updateValue);
if (this.isDynamic) block.builders.update.addLine(updateValue);
}
}
renderStyle(
block: Block,
styleProps: StyleProp[]
) {
styleProps.forEach((prop: StyleProp) => {
let value;
if (isDynamic(prop.value)) {
const propDependencies = new Set();
let shouldCache;
value =
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
prop.value
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = chunk;
dependencies.forEach(d => {
propDependencies.add(d);
});
return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
if (propDependencies.size) {
const dependencies = Array.from(propDependencies);
const condition = (
(block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
block.builders.update.addConditional(
condition,
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
);
}
} else {
value = stringify(prop.value[0].data);
}
block.builders.hydrate.addLine(
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
);
});
return this.isTrue
? true
: this.chunks[0]
? this.chunks[0].data
: '';
}
stringifyForSsr() {
@ -344,353 +111,4 @@ export default class Attribute extends Node {
})
.join('');
}
}
// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
const attributeLookup = {
accept: { appliesTo: ['form', 'input'] },
'accept-charset': { propertyName: 'acceptCharset', appliesTo: ['form'] },
accesskey: { propertyName: 'accessKey' },
action: { appliesTo: ['form'] },
align: {
appliesTo: [
'applet',
'caption',
'col',
'colgroup',
'hr',
'iframe',
'img',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
],
},
allowfullscreen: { propertyName: 'allowFullscreen', appliesTo: ['iframe'] },
alt: { appliesTo: ['applet', 'area', 'img', 'input'] },
async: { appliesTo: ['script'] },
autocomplete: { appliesTo: ['form', 'input'] },
autofocus: { appliesTo: ['button', 'input', 'keygen', 'select', 'textarea'] },
autoplay: { appliesTo: ['audio', 'video'] },
autosave: { appliesTo: ['input'] },
bgcolor: {
propertyName: 'bgColor',
appliesTo: [
'body',
'col',
'colgroup',
'marquee',
'table',
'tbody',
'tfoot',
'td',
'th',
'tr',
],
},
border: { appliesTo: ['img', 'object', 'table'] },
buffered: { appliesTo: ['audio', 'video'] },
challenge: { appliesTo: ['keygen'] },
charset: { appliesTo: ['meta', 'script'] },
checked: { appliesTo: ['command', 'input'] },
cite: { appliesTo: ['blockquote', 'del', 'ins', 'q'] },
class: { propertyName: 'className' },
code: { appliesTo: ['applet'] },
codebase: { propertyName: 'codeBase', appliesTo: ['applet'] },
color: { appliesTo: ['basefont', 'font', 'hr'] },
cols: { appliesTo: ['textarea'] },
colspan: { propertyName: 'colSpan', appliesTo: ['td', 'th'] },
content: { appliesTo: ['meta'] },
contenteditable: { propertyName: 'contentEditable' },
contextmenu: {},
controls: { appliesTo: ['audio', 'video'] },
coords: { appliesTo: ['area'] },
data: { appliesTo: ['object'] },
datetime: { propertyName: 'dateTime', appliesTo: ['del', 'ins', 'time'] },
default: { appliesTo: ['track'] },
defer: { appliesTo: ['script'] },
dir: {},
dirname: { propertyName: 'dirName', appliesTo: ['input', 'textarea'] },
disabled: {
appliesTo: [
'button',
'command',
'fieldset',
'input',
'keygen',
'optgroup',
'option',
'select',
'textarea',
],
},
download: { appliesTo: ['a', 'area'] },
draggable: {},
dropzone: {},
enctype: { appliesTo: ['form'] },
for: { propertyName: 'htmlFor', appliesTo: ['label', 'output'] },
form: {
appliesTo: [
'button',
'fieldset',
'input',
'keygen',
'label',
'meter',
'object',
'output',
'progress',
'select',
'textarea',
],
},
formaction: { appliesTo: ['input', 'button'] },
headers: { appliesTo: ['td', 'th'] },
height: {
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
},
hidden: {},
high: { appliesTo: ['meter'] },
href: { appliesTo: ['a', 'area', 'base', 'link'] },
hreflang: { appliesTo: ['a', 'area', 'link'] },
'http-equiv': { propertyName: 'httpEquiv', appliesTo: ['meta'] },
icon: { appliesTo: ['command'] },
id: {},
indeterminate: { appliesTo: ['input'] },
ismap: { propertyName: 'isMap', appliesTo: ['img'] },
itemprop: {},
keytype: { appliesTo: ['keygen'] },
kind: { appliesTo: ['track'] },
label: { appliesTo: ['track'] },
lang: {},
language: { appliesTo: ['script'] },
loop: { appliesTo: ['audio', 'bgsound', 'marquee', 'video'] },
low: { appliesTo: ['meter'] },
manifest: { appliesTo: ['html'] },
max: { appliesTo: ['input', 'meter', 'progress'] },
maxlength: { propertyName: 'maxLength', appliesTo: ['input', 'textarea'] },
media: { appliesTo: ['a', 'area', 'link', 'source', 'style'] },
method: { appliesTo: ['form'] },
min: { appliesTo: ['input', 'meter'] },
multiple: { appliesTo: ['input', 'select'] },
muted: { appliesTo: ['audio', 'video'] },
name: {
appliesTo: [
'button',
'form',
'fieldset',
'iframe',
'input',
'keygen',
'object',
'output',
'select',
'textarea',
'map',
'meta',
'param',
],
},
novalidate: { propertyName: 'noValidate', appliesTo: ['form'] },
open: { appliesTo: ['details'] },
optimum: { appliesTo: ['meter'] },
pattern: { appliesTo: ['input'] },
ping: { appliesTo: ['a', 'area'] },
placeholder: { appliesTo: ['input', 'textarea'] },
poster: { appliesTo: ['video'] },
preload: { appliesTo: ['audio', 'video'] },
radiogroup: { appliesTo: ['command'] },
readonly: { propertyName: 'readOnly', appliesTo: ['input', 'textarea'] },
rel: { appliesTo: ['a', 'area', 'link'] },
required: { appliesTo: ['input', 'select', 'textarea'] },
reversed: { appliesTo: ['ol'] },
rows: { appliesTo: ['textarea'] },
rowspan: { propertyName: 'rowSpan', appliesTo: ['td', 'th'] },
sandbox: { appliesTo: ['iframe'] },
scope: { appliesTo: ['th'] },
scoped: { appliesTo: ['style'] },
seamless: { appliesTo: ['iframe'] },
selected: { appliesTo: ['option'] },
shape: { appliesTo: ['a', 'area'] },
size: { appliesTo: ['input', 'select'] },
sizes: { appliesTo: ['link', 'img', 'source'] },
span: { appliesTo: ['col', 'colgroup'] },
spellcheck: {},
src: {
appliesTo: [
'audio',
'embed',
'iframe',
'img',
'input',
'script',
'source',
'track',
'video',
],
},
srcdoc: { appliesTo: ['iframe'] },
srclang: { appliesTo: ['track'] },
srcset: { appliesTo: ['img'] },
start: { appliesTo: ['ol'] },
step: { appliesTo: ['input'] },
style: { propertyName: 'style.cssText' },
summary: { appliesTo: ['table'] },
tabindex: { propertyName: 'tabIndex' },
target: { appliesTo: ['a', 'area', 'base', 'form'] },
title: {},
type: {
appliesTo: [
'button',
'command',
'embed',
'object',
'script',
'source',
'style',
'menu',
],
},
usemap: { propertyName: 'useMap', appliesTo: ['img', 'input', 'object'] },
value: {
appliesTo: [
'button',
'option',
'input',
'li',
'meter',
'progress',
'param',
'select',
'textarea',
],
},
volume: { appliesTo: ['audio', 'video'] },
width: {
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
},
wrap: { appliesTo: ['textarea'] },
};
Object.keys(attributeLookup).forEach(name => {
const metadata = attributeLookup[name];
if (!metadata.propertyName) metadata.propertyName = name;
});
function optimizeStyle(value: Node[]) {
let expectingKey = true;
let i = 0;
const props: { key: string, value: Node[] }[] = [];
let chunks = value.slice();
while (chunks.length) {
const chunk = chunks[0];
if (chunk.type !== 'Text') return null;
const keyMatch = /^\s*([\w-]+):\s*/.exec(chunk.data);
if (!keyMatch) return null;
const key = keyMatch[1];
const offset = keyMatch.index + keyMatch[0].length;
const remainingData = chunk.data.slice(offset);
if (remainingData) {
chunks[0] = {
start: chunk.start + offset,
end: chunk.end,
type: 'Text',
data: remainingData
};
} else {
chunks.shift();
}
const result = getStyleValue(chunks);
if (!result) return null;
props.push({ key, value: result.value });
chunks = result.chunks;
}
return props;
}
function getStyleValue(chunks: Node[]) {
const value: Node[] = [];
let inUrl = false;
let quoteMark = null;
let escaped = false;
while (chunks.length) {
const chunk = chunks.shift();
if (chunk.type === 'Text') {
let c = 0;
while (c < chunk.data.length) {
const char = chunk.data[c];
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quoteMark) {
quoteMark === null;
} else if (char === '"' || char === "'") {
quoteMark = char;
} else if (char === ')' && inUrl) {
inUrl = false;
} else if (char === 'u' && chunk.data.slice(c, c + 4) === 'url(') {
inUrl = true;
} else if (char === ';' && !inUrl && !quoteMark) {
break;
}
c += 1;
}
if (c > 0) {
value.push({
type: 'Text',
start: chunk.start,
end: chunk.start + c,
data: chunk.data.slice(0, c)
});
}
while (/[;\s]/.test(chunk.data[c])) c += 1;
const remainingData = chunk.data.slice(c);
if (remainingData) {
chunks.unshift({
start: chunk.start + c,
end: chunk.end,
type: 'Text',
data: remainingData
});
break;
}
}
else {
value.push(chunk);
}
}
return {
chunks,
value
};
}
function isDynamic(value: Node[]) {
return value.length > 1 || value[0].type !== 'Text';
}
}

@ -1,12 +1,8 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import Block from '../dom/Block';
import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import { SsrTarget } from '../ssr';
export default class AwaitBlock extends Node {
expression: Expression;
@ -17,225 +13,17 @@ export default class AwaitBlock extends Node {
then: ThenBlock;
catch: CatchBlock;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.expression = new Expression(component, this, scope, info.expression);
const deps = this.expression.dependencies;
this.value = info.value;
this.error = info.error;
this.pending = new PendingBlock(compiler, this, scope, info.pending);
this.then = new ThenBlock(compiler, this, scope.add(this.value, deps), info.then);
this.catch = new CatchBlock(compiler, this, scope.add(this.error, deps), info.catch);
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('await_block');
block.addDependencies(this.expression.dependencies);
let isDynamic = false;
['pending', 'then', 'catch'].forEach(status => {
const child = this[status];
child.block = block.child({
comment: createDebuggingComment(child, this.compiler),
name: this.compiler.getUniqueName(`create_${status}_block`)
});
child.initChildren(child.block, stripWhitespace, nextSibling);
this.compiler.target.blocks.push(child.block);
if (child.block.dependencies.size > 0) {
isDynamic = true;
block.addDependencies(child.block.dependencies);
}
});
this.pending.block.hasUpdateMethod = isDynamic;
this.then.block.hasUpdateMethod = isDynamic;
this.catch.block.hasUpdateMethod = isDynamic;
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const name = this.var;
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor);
const { snippet } = this.expression;
const promise = block.getUniqueName(`promise`);
const resolved = block.getUniqueName(`resolved`);
const await_block = block.getUniqueName(`await_block`);
const await_block_type = block.getUniqueName(`await_block_type`);
const token = block.getUniqueName(`token`);
const await_token = block.getUniqueName(`await_token`);
const handle_promise = block.getUniqueName(`handle_promise`);
const replace_await_block = block.getUniqueName(`replace_await_block`);
const old_block = block.getUniqueName(`old_block`);
const value = block.getUniqueName(`value`);
const error = block.getUniqueName(`error`);
const create_pending_block = this.pending.block.name;
const create_then_block = this.then.block.name;
const create_catch_block = this.catch.block.name;
block.addVariable(await_block);
block.addVariable(await_block_type);
block.addVariable(await_token);
block.addVariable(promise);
block.addVariable(resolved);
block.maintainContext = true;
// the `#component.root.set({})` below is just a cheap way to flush
// any oncreate handlers. We could have a dedicated `flush()` method
// but it's probably not worth it
block.builders.init.addBlock(deindent`
function ${replace_await_block}(${token}, type, ctx) {
if (${token} !== ${await_token}) return;
var ${old_block} = ${await_block};
${await_block} = type && (${await_block_type} = type)(#component, ctx);
if (${old_block}) {
${old_block}.u();
${old_block}.d();
${await_block}.c();
${await_block}.m(${updateMountNode}, ${anchor});
#component.root.set({});
}
}
function ${handle_promise}(${promise}) {
var ${token} = ${await_token} = {};
if (@isPromise(${promise})) {
${promise}.then(function(${value}) {
${this.value ? deindent`
${resolved} = { ${this.value}: ${value} };
${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, ctx), ${resolved}));
` : deindent`
${replace_await_block}(${token}, null, null);
`}
}, function (${error}) {
${this.error ? deindent`
${resolved} = { ${this.error}: ${error} };
${replace_await_block}(${token}, ${create_catch_block}, @assign(@assign({}, ctx), ${resolved}));
` : deindent`
${replace_await_block}(${token}, null, null);
`}
});
// if we previously had a then/catch block, destroy it
if (${await_block_type} !== ${create_pending_block}) {
${replace_await_block}(${token}, ${create_pending_block}, ctx);
return true;
}
} else {
${resolved} = { ${this.value}: ${promise} };
if (${await_block_type} !== ${create_then_block}) {
${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, ctx), ${resolved}));
return true;
}
}
}
${handle_promise}(${promise} = ${snippet});
`);
block.builders.create.addBlock(deindent`
${await_block}.c();
`);
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${await_block}.l(${parentNodes});
`);
}
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addBlock(deindent`
${await_block}.m(${initialMountNode}, ${anchorNode});
`);
const conditions = [];
if (this.expression.dependencies.size > 0) {
conditions.push(
`(${[...this.expression.dependencies].map(dep => `'${dep}' in changed`).join(' || ')})`
);
}
conditions.push(
`${promise} !== (${promise} = ${snippet})`,
`${handle_promise}(${promise}, ctx)`
);
if (this.pending.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
// nothing
} else {
${await_block}.p(changed, @assign(@assign({}, ctx), ${resolved}));
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
${await_block}.c();
${await_block}.m(${anchor}.parentNode, ${anchor});
}
`);
}
block.builders.unmount.addBlock(deindent`
${await_block}.u();
`);
block.builders.destroy.addBlock(deindent`
${await_token} = null;
${await_block}.d();
`);
[this.pending, this.then, this.catch].forEach(status => {
status.children.forEach(child => {
child.build(status.block, null,'nodes');
});
});
}
ssr() {
const target: SsrTarget = <SsrTarget>this.compiler.target;
const { snippet } = this.expression;
target.append('${(function(__value) { if(@isPromise(__value)) return `');
this.pending.children.forEach((child: Node) => {
child.ssr();
});
target.append('`; return function(ctx) { return `');
this.then.children.forEach((child: Node) => {
child.ssr();
});
target.append(`\`;}(Object.assign({}, ctx, { ${this.value}: __value }));}(${snippet})) }`);
this.pending = new PendingBlock(component, this, scope, info.pending);
this.then = new ThenBlock(component, this, scope.add(this.value, deps), info.then);
this.catch = new CatchBlock(component, this, scope.add(this.error, deps), info.catch);
}
}

@ -1,19 +1,7 @@
import Node from './shared/Node';
import Element from './Element';
import getObject from '../../utils/getObject';
import getTailSnippet from '../../utils/getTailSnippet';
import flattenReference from '../../utils/flattenReference';
import Compiler from '../Compiler';
import Block from '../dom/Block';
import Expression from './shared/Expression';
const readOnlyMediaAttributes = new Set([
'duration',
'buffered',
'seekable',
'played'
]);
export default class Binding extends Node {
name: string;
value: Expression;
@ -22,11 +10,11 @@ export default class Binding extends Node {
obj: string;
prop: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.value = new Expression(compiler, this, scope, info.value);
this.value = new Expression(component, this, scope, info.value);
let obj;
let prop;
@ -50,247 +38,4 @@ export default class Binding extends Node {
this.obj = obj;
this.prop = prop;
}
munge(
block: Block
) {
const node: Element = this.parent;
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type'));
const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name);
let updateCondition: string;
const { name } = getObject(this.value.node);
const { snippet } = this.value;
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
// and `selected` is an object chosen with a <select>, then when `checked` changes,
// we need to tell the component to update all the values `selected` might be
// pointing to
// TODO should this happen in preprocess?
const dependencies = new Set(this.value.dependencies);
this.value.dependencies.forEach((prop: string) => {
const indirectDependencies = this.compiler.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
dependencies.add(indirectDependency);
});
}
});
// view to model
const valueFromDom = getValueFromDom(this.compiler, node, this);
const handler = getEventHandler(this, this.compiler, block, name, snippet, dependencies, valueFromDom);
// model to view
let updateDom = getDomUpdater(node, this, snippet);
let initialUpdate = updateDom;
// special cases
if (this.name === 'group') {
const bindingGroup = getBindingGroup(this.compiler, this.value.node);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
);
block.builders.destroy.addLine(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
);
}
if (this.name === 'currentTime' || this.name === 'volume') {
updateCondition = `!isNaN(${snippet})`;
if (this.name === 'currentTime')
initialUpdate = null;
}
if (this.name === 'paused') {
// this is necessary to prevent audio restarting by itself
const last = block.getUniqueName(`${node.var}_is_paused`);
block.addVariable(last, 'true');
updateCondition = `${last} !== (${last} = ${snippet})`;
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
initialUpdate = null;
}
return {
name: this.name,
object: name,
handler,
updateDom,
initialUpdate,
needsLock: !isReadOnly && needsLock,
updateCondition,
isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute()
};
}
isReadOnlyMediaAttribute() {
return readOnlyMediaAttributes.has(this.name);
}
}
function getDomUpdater(
node: Element,
binding: Binding,
snippet: string
) {
if (binding.isReadOnlyMediaAttribute()) {
return null;
}
if (node.name === 'select') {
return node.getStaticAttributeValue('multiple') === true ?
`@selectOptions(${node.var}, ${snippet})` :
`@selectOption(${node.var}, ${snippet})`;
}
if (binding.name === 'group') {
const type = node.getStaticAttributeValue('type');
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${node.var}.__value)`
: `${node.var}.__value === ${snippet}`;
return `${node.var}.checked = ${condition};`
}
return `${node.var}.${binding.name} = ${snippet};`;
}
function getBindingGroup(compiler: Compiler, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = compiler.bindingGroups.indexOf(keypath);
if (index === -1) {
index = compiler.bindingGroups.length;
compiler.bindingGroups.push(keypath);
}
return index;
}
function getEventHandler(
binding: Binding,
compiler: Compiler,
block: Block,
name: string,
snippet: string,
dependencies: string[],
value: string,
isContextual: boolean
) {
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = [...dependencies].filter(prop => prop[0] !== '$');
if (binding.isContextual) {
const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(binding.value.node)
: '';
const list = `ctx.${block.listNames.get(name)}`;
const index = `ctx.${block.indexNames.get(name)}`;
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${list}[${index}]${tail} = ${value};`,
props: dependencies.map(prop => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
if (binding.value.node.type === 'MemberExpression') {
// This is a little confusing, and should probably be tidied up
// at some point. It addresses a tricky bug (#893), wherein
// Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b)
// we should probably populate `compiler.target.readonly` sooner so
// that we don't have to do the `.some()` here
dependencies = dependencies.filter(prop => !compiler.computations.some(computation => computation.key === prop));
return {
usesContext: false,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${snippet} = ${value}`,
props: dependencies.map((prop: string) => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
let props;
let storeProps;
if (name[0] === '$') {
props = [];
storeProps = [`${name.slice(1)}: ${value}`];
} else {
props = [`${name}: ${value}`];
storeProps = [];
}
return {
usesContext: false,
usesState: false,
usesStore: false,
mutation: null,
props,
storeProps
};
}
function getValueFromDom(
compiler: Compiler,
node: Element,
binding: Node
) {
// <select bind:value='selected>
if (node.name === 'select') {
return node.getStaticAttributeValue('multiple') === true ?
`@selectMultipleValue(${node.var})` :
`@selectValue(${node.var})`;
}
const type = node.getStaticAttributeValue('type');
// <input type='checkbox' bind:group='foo'>
if (binding.name === 'group') {
const bindingGroup = getBindingGroup(compiler, binding.value.node);
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}
return `${node.var}.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@toNumber(${node.var}.${binding.name})`;
}
if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) {
return `@timeRangesToArray(${node.var}.${binding.name})`
}
// everything else
return `${node.var}.${binding.name}`;
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -1,13 +1,15 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class CatchBlock extends Node {
block: Block;
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -0,0 +1,18 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
export default class Class extends Node {
type: 'Class';
name: string;
expression: Expression;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
: null;
}
}

@ -4,15 +4,8 @@ export default class Comment extends Node {
type: 'Comment';
data: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.data = info.data;
}
ssr() {
// Allow option to preserve comments, otherwise ignore
if (this.compiler.options.preserveComments) {
this.compiler.target.append(`<!--${this.data}-->`);
}
}
}

@ -1,616 +0,0 @@
import deindent from '../../utils/deindent';
import flattenReference from '../../utils/flattenReference';
import validCalleeObjects from '../../utils/validCalleeObjects';
import stringifyProps from '../../utils/stringifyProps';
import CodeBuilder from '../../utils/CodeBuilder';
import getTailSnippet from '../../utils/getTailSnippet';
import getObject from '../../utils/getObject';
import quoteIfNecessary from '../../utils/quoteIfNecessary';
import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments';
import mapChildren from './shared/mapChildren';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Expression from './shared/Expression';
import { AppendTarget } from '../../interfaces';
export default class Component extends Node {
type: 'Component';
name: string;
expression: Expression;
attributes: Attribute[];
bindings: Binding[];
handlers: EventHandler[];
children: Node[];
ref: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
compiler.hasComponents = true;
this.name = info.name;
this.expression = this.name === 'svelte:component'
? new Expression(compiler, this, scope, info.expression)
: null;
this.attributes = [];
this.bindings = [];
this.handlers = [];
info.attributes.forEach(node => {
switch (node.type) {
case 'Attribute':
case 'Spread':
this.attributes.push(new Attribute(compiler, this, scope, node));
break;
case 'Binding':
this.bindings.push(new Binding(compiler, this, scope, node));
break;
case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, scope, node));
break;
case 'Ref':
// TODO catch this in validation
if (this.ref) throw new Error(`Duplicate refs`);
compiler.usesRefs = true
this.ref = node.name;
break;
default:
throw new Error(`Not implemented: ${node.type}`);
}
});
this.children = mapChildren(compiler, this, scope, info.children);
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.attributes.forEach(attr => {
block.addDependencies(attr.dependencies);
});
this.bindings.forEach(binding => {
block.addDependencies(binding.value.dependencies);
});
this.handlers.forEach(handler => {
block.addDependencies(handler.dependencies);
});
this.var = block.getUniqueName(
(
this.name === 'svelte:self' ? this.compiler.name :
this.name === 'svelte:component' ? 'switch_instance' :
this.name
).toLowerCase()
);
if (this.children.length) {
this._slots = new Set(['default']);
this.children.forEach(child => {
child.init(block, stripWhitespace, nextSibling);
});
}
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { compiler } = this;
const name = this.var;
const componentInitProperties = [`root: #component.root`];
if (this.children.length > 0) {
const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name)}: @createFragment()`);
componentInitProperties.push(`slots: { ${slots.join(', ')} }`);
this.children.forEach((child: Node) => {
child.build(block, `${this.var}._slotted.default`, 'nodes');
});
}
const statements: string[] = [];
const name_initial_data = block.getUniqueName(`${name}_initial_data`);
const name_changes = block.getUniqueName(`${name}_changes`);
let name_updating: string;
let beforecreate: string = null;
const updates: string[] = [];
const usesSpread = !!this.attributes.find(a => a.isSpread);
const attributeObject = usesSpread
? '{}'
: stringifyProps(
this.attributes.map(attr => `${attr.name}: ${attr.getValue()}`)
);
if (this.attributes.length || this.bindings.length) {
componentInitProperties.push(`data: ${name_initial_data}`);
}
if ((!usesSpread && this.attributes.filter(a => a.isDynamic).length) || this.bindings.length) {
updates.push(`var ${name_changes} = {};`);
}
if (this.attributes.length) {
if (usesSpread) {
const levels = block.getUniqueName(`${this.var}_spread_levels`);
const initialProps = [];
const changes = [];
this.attributes.forEach(attr => {
const { name, dependencies } = attr;
const condition = dependencies.size > 0
? [...dependencies].map(d => `changed.${d}`).join(' || ')
: null;
if (attr.isSpread) {
const value = attr.expression.snippet;
initialProps.push(value);
changes.push(condition ? `${condition} && ${value}` : value);
} else {
const obj = `{ ${quoteIfNecessary(name)}: ${attr.getValue()} }`;
initialProps.push(obj);
changes.push(condition ? `${condition} && ${obj}` : obj);
}
});
block.addVariable(levels);
statements.push(deindent`
${levels} = [
${initialProps.join(',\n')}
];
for (var #i = 0; #i < ${levels}.length; #i += 1) {
${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]);
}
`);
updates.push(deindent`
var ${name_changes} = @getSpreadUpdate(${levels}, [
${changes.join(',\n')}
]);
`);
} else {
this.attributes
.filter((attribute: Attribute) => attribute.isDynamic)
.forEach((attribute: Attribute) => {
if (attribute.dependencies.size > 0) {
updates.push(deindent`
if (${[...attribute.dependencies]
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.getValue()};
`);
}
else {
// TODO this is an odd situation to encounter I *think* it should only happen with
// each block indices, in which case it may be possible to optimise this
updates.push(`${name_changes}.${attribute.name} = ${attribute.getValue()};`);
}
});
}
}
if (this.bindings.length) {
compiler.target.hasComplexBindings = true;
name_updating = block.alias(`${name}_updating`);
block.addVariable(name_updating, '{}');
let hasLocalBindings = false;
let hasStoreBindings = false;
const builder = new CodeBuilder();
this.bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value.node);
let setFromChild;
if (binding.isContextual) {
const computed = isComputed(binding.value.node);
const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : '';
const list = block.listNames.get(key);
const index = block.indexNames.get(key);
const lhs = binding.value.node.type === 'MemberExpression'
? binding.value.snippet
: `ctx.${list}[ctx.${index}]${tail} = childState.${binding.name}`;
setFromChild = deindent`
${lhs} = childState.${binding.name};
${[...binding.value.dependencies]
.map((name: string) => {
const isStoreProp = name[0] === '$';
const prop = isStoreProp ? name.slice(1) : name;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
return `${newState}.${prop} = ctx.${name};`;
})}
`;
}
else {
const isStoreProp = key[0] === '$';
const prop = isStoreProp ? key.slice(1) : key;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
if (binding.value.node.type === 'MemberExpression') {
setFromChild = deindent`
${binding.value.snippet} = childState.${binding.name};
${newState}.${prop} = ctx.${key};
`;
}
else {
setFromChild = `${newState}.${prop} = childState.${binding.name};`;
}
}
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.value.snippet};
${name_updating}.${binding.name} = true;
}`
);
builder.addConditional(
`!${name_updating}.${binding.name} && changed.${binding.name}`,
setFromChild
);
updates.push(deindent`
if (!${name_updating}.${binding.name} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}.${binding.name} = ${binding.value.snippet};
${name_updating}.${binding.name} = true;
}
`);
});
block.maintainContext = true; // TODO put this somewhere more logical
const initialisers = [
hasLocalBindings && 'newState = {}',
hasStoreBindings && 'newStoreState = {}',
].filter(Boolean).join(', ');
// TODO use component.on('state', ...) instead of _bind
componentInitProperties.push(deindent`
_bind: function(changed, childState) {
var ${initialisers};
${builder}
${hasStoreBindings && `#component.store.set(newStoreState);`}
${hasLocalBindings && `#component._set(newState);`}
${name_updating} = {};
}
`);
beforecreate = deindent`
#component.root._beforecreate.push(function() {
${name}._bind({ ${this.bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
});
`;
}
this.handlers.forEach(handler => {
handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky
handler.render(compiler, block, false); // TODO hoist when possible
if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this?
});
if (this.name === 'svelte:component') {
const switch_value = block.getUniqueName('switch_value');
const switch_props = block.getUniqueName('switch_props');
const { dependencies, snippet } = this.expression;
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
block.builders.init.addBlock(deindent`
var ${switch_value} = ${snippet};
function ${switch_props}(ctx) {
${(this.attributes.length || this.bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
return {
${componentInitProperties.join(',\n')}
};
}
if (${switch_value}) {
var ${name} = new ${switch_value}(${switch_props}(ctx));
${beforecreate}
}
${this.handlers.map(handler => deindent`
function ${handler.var}(event) {
${handler.snippet}
}
if (${name}) ${name}.on("${handler.name}", ${handler.var});
`)}
`);
block.builders.create.addLine(
`if (${name}) ${name}._fragment.c();`
);
if (parentNodes) {
block.builders.claim.addLine(
`if (${name}) ${name}._fragment.l(${parentNodes});`
);
}
block.builders.mount.addBlock(deindent`
if (${name}) {
${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});
${this.ref && `#component.refs.${this.ref} = ${name};`}
}
`);
const updateMountNode = this.getUpdateMountNode(anchor);
block.builders.update.addBlock(deindent`
if (${switch_value} !== (${switch_value} = ${snippet})) {
if (${name}) ${name}.destroy();
if (${switch_value}) {
${name} = new ${switch_value}(${switch_props}(ctx));
${name}._fragment.c();
${this.children.map(child => child.remount(name))}
${name}._mount(${updateMountNode}, ${anchor});
${this.handlers.map(handler => deindent`
${name}.on("${handler.name}", ${handler.var});
`)}
${this.ref && `#component.refs.${this.ref} = ${name};`}
}
${this.ref && deindent`
else if (#component.refs.${this.ref} === ${name}) {
#component.refs.${this.ref} = null;
}`}
}
`);
if (updates.length) {
block.builders.update.addBlock(deindent`
else {
${updates}
${name}._set(${name_changes});
${this.bindings.length && `${name_updating} = {};`}
}
`);
}
if (!parentNode) block.builders.unmount.addLine(`if (${name}) ${name}._unmount();`);
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`);
} else {
const expression = this.name === 'svelte:self'
? compiler.name
: `%components-${this.name}`;
block.builders.init.addBlock(deindent`
${(this.attributes.length || this.bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
});
${beforecreate}
${this.handlers.map(handler => deindent`
${name}.on("${handler.name}", function(event) {
${handler.snippet || `#component.fire("${handler.name}", event);`}
});
`)}
${this.ref && `#component.refs.${this.ref} = ${name};`}
`);
block.builders.create.addLine(`${name}._fragment.c();`);
if (parentNodes) {
block.builders.claim.addLine(
`${name}._fragment.l(${parentNodes});`
);
}
block.builders.mount.addLine(
`${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
);
if (updates.length) {
block.builders.update.addBlock(deindent`
${updates}
${name}._set(${name_changes});
${this.bindings.length && `${name_updating} = {};`}
`);
}
if (!parentNode) block.builders.unmount.addLine(`${name}._unmount();`);
block.builders.destroy.addLine(deindent`
${name}.destroy(false);
${this.ref && `if (#component.refs.${this.ref} === ${name}) #component.refs.${this.ref} = null;`}
`);
}
}
remount(name: string) {
return `${this.var}._mount(${name}._slotted.default, null);`;
}
ssr() {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data));
}
return '${@escape( ' + chunk.snippet + ')}';
}
const bindingProps = this.bindings.map(binding => {
const { name } = getObject(binding.value.node);
const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(binding.value.node)
: '';
return `${binding.name}: ctx.${name}${tail}`;
});
function getAttributeValue(attribute) {
if (attribute.isTrue) return `true`;
if (attribute.chunks.length === 0) return `''`;
if (attribute.chunks.length === 1) {
const chunk = attribute.chunks[0];
if (chunk.type === 'Text') {
return stringify(chunk.data);
}
return chunk.snippet;
}
return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
}
const usesSpread = this.attributes.find(attr => attr.isSpread);
const props = usesSpread
? `Object.assign(${
this.attributes
.map(attribute => {
if (attribute.isSpread) {
return attribute.expression.snippet;
} else {
return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`;
}
})
.concat(bindingProps.map(p => `{ ${p} }`))
.join(', ')
})`
: `{ ${this.attributes
.map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`)
.concat(bindingProps)
.join(', ')} }`;
const isDynamicComponent = this.name === 'svelte:component';
const expression = (
this.name === 'svelte:self' ? this.compiler.name :
isDynamicComponent ? `((${this.expression.snippet}) || @missingComponent)` :
`%components-${this.name}`
);
this.bindings.forEach(binding => {
const conditions = [];
let node = this;
while (node = node.parent) {
if (node.type === 'IfBlock') {
// TODO handle contextual bindings...
conditions.push(`(${node.expression.snippet})`);
}
}
conditions.push(`!('${binding.name}' in ctx)`);
const { name } = getObject(binding.value.node);
this.compiler.target.bindings.push(deindent`
if (${conditions.reverse().join('&&')}) {
tmp = ${expression}.data();
if ('${name}' in tmp) {
ctx.${binding.name} = tmp.${name};
settled = false;
}
}
`);
});
let open = `\${${expression}._render(__result, ${props}`;
const options = [];
options.push(`store: options.store`);
if (this.children.length) {
const appendTarget: AppendTarget = {
slots: { default: '' },
slotStack: ['default']
};
this.compiler.target.appendTargets.push(appendTarget);
this.children.forEach((child: Node) => {
child.ssr();
});
const slotted = Object.keys(appendTarget.slots)
.map(name => `${name}: () => \`${appendTarget.slots[name]}\``)
.join(', ');
options.push(`slotted: { ${slotted} }`);
this.compiler.target.appendTargets.pop();
}
if (options.length) {
open += `, { ${options.join(', ')} }`;
}
this.compiler.target.append(open);
this.compiler.target.append(')}');
}
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -0,0 +1,14 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
export default class DebugTag extends Node {
expressions: Expression[];
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expressions = info.identifiers.map(node => {
return new Expression(component, parent, scope, node);
});
}
}

@ -1,11 +1,10 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Block from '../render-dom/Block';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
import TemplateScope from './shared/TemplateScope';
import unpackDestructuring from '../../utils/unpackDestructuring';
export default class EachBlock extends Node {
type: 'EachBlock';
@ -16,492 +15,65 @@ export default class EachBlock extends Node {
iterations: string;
index: string;
context: string;
key: string;
key: Expression;
scope: TemplateScope;
destructuredContexts: string[];
contexts: Array<{ name: string, tail: string }>;
hasAnimation: boolean;
children: Node[];
else?: ElseBlock;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.context = info.context;
this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring
this.index = info.index;
this.key = info.key;
this.scope = scope.child();
this.scope.add(this.context, this.expression.dependencies);
this.contexts = [];
unpackDestructuring(this.contexts, info.context, '');
if (this.index) {
// index can only change if this is a keyed each block
const dependencies = this.key ? this.expression.dependencies : [];
this.scope.add(this.index, dependencies);
}
this.contexts.forEach(context => {
if (component.helpers.has(context.key.name)) {
component.warn(context.key, {
code: `each-context-clash`,
message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity`
});
}
// TODO more general approach to destructuring
this.destructuredContexts = info.destructuredContexts || [];
this.destructuredContexts.forEach(name => {
this.scope.add(name, this.expression.dependencies);
this.scope.add(context.key.name, this.expression.dependencies);
});
this.children = mapChildren(compiler, this, this.scope, info.children);
this.else = info.else
? new ElseBlock(compiler, this, this.scope, info.else)
this.key = info.key
? new Expression(component, this, this.scope, info.key)
: null;
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName(`each`);
this.iterations = block.getUniqueName(`${this.var}_blocks`);
this.each_context = block.getUniqueName(`${this.var}_context`);
const { dependencies } = this.expression;
block.addDependencies(dependencies);
this.block = block.child({
comment: createDebuggingComment(this, this.compiler),
name: this.compiler.getUniqueName('create_each_block'),
key: this.key,
indexNames: new Map(block.indexNames),
listNames: new Map(block.listNames)
});
const listName = this.compiler.getUniqueName('each_value');
const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`);
this.block.indexNames.set(this.context, indexName);
this.block.listNames.set(this.context, listName);
if (this.index) {
this.block.getUniqueName(this.index); // this prevents name collisions (#1254)
}
this.contextProps = [
`${listName}: ${listName}`,
`${this.context}: ${listName}[#i]`,
`${indexName}: #i`
];
if (this.destructuredContexts) {
for (let i = 0; i < this.destructuredContexts.length; i += 1) {
this.contextProps.push(`${this.destructuredContexts[i]}: ${listName}[#i][${i}]`);
}
}
this.compiler.target.blocks.push(this.block);
this.initChildren(this.block, stripWhitespace, nextSibling);
block.addDependencies(this.block.dependencies);
this.block.hasUpdateMethod = this.block.dependencies.size > 0;
if (this.else) {
this.else.block = block.child({
comment: createDebuggingComment(this.else, this.compiler),
name: this.compiler.getUniqueName(`${this.block.name}_else`),
});
this.compiler.target.blocks.push(this.else.block);
this.else.initChildren(
this.else.block,
stripWhitespace,
nextSibling
);
this.else.block.hasUpdateMethod = this.else.block.dependencies.size > 0;
}
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
if (this.children.length === 0) return;
const { compiler } = this;
const each = this.var;
const create_each_block = this.block.name;
const each_block_value = this.block.listNames.get(this.context);
const iterations = this.iterations;
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${each}_anchor`)
: (this.next && this.next.var) || 'null';
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = this.start + 2;
while (compiler.source[c] !== 'e') c += 1;
compiler.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
const vars = {
each,
create_each_block,
each_block_value,
length,
iterations,
anchor,
mountOrIntro,
};
const { snippet } = this.expression;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
if (this.key) {
this.buildKeyed(block, parentNode, parentNodes, snippet, vars);
} else {
this.buildUnkeyed(block, parentNode, parentNodes, snippet, vars);
}
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
if (this.else) {
const each_block_else = compiler.getUniqueName(`${each}_else`);
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
}
`);
block.builders.mount.addBlock(deindent`
if (${each_block_else}) {
${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null);
}
`);
const initialMountNode = parentNode || `${anchor}.parentNode`;
if (this.else.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (!${each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p(changed, ctx);
} else if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
} else if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
${each_block_else} = null;
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${each_block_value}.${length}) {
if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
${each_block_else} = null;
}
} else if (!${each_block_else}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
}
`);
}
block.builders.unmount.addLine(
`if (${each_block_else}) ${each_block_else}.u()`
);
block.builders.destroy.addBlock(deindent`
if (${each_block_else}) ${each_block_else}.d();
`);
}
this.children.forEach((child: Node) => {
child.build(this.block, null, 'nodes');
});
if (this.else) {
this.else.children.forEach((child: Node) => {
child.build(this.else.block, null, 'nodes');
});
}
}
buildKeyed(
block: Block,
parentNode: string,
parentNodes: string,
snippet: string,
{
each,
create_each_block,
each_block_value,
length,
anchor,
mountOrIntro,
}
) {
const key = block.getUniqueName('key');
const blocks = block.getUniqueName(`${each}_blocks`);
const lookup = block.getUniqueName(`${each}_lookup`);
block.addVariable(blocks, '[]');
block.addVariable(lookup, `@blankObject()`);
if (this.children[0].isDomNode()) {
this.block.first = this.children[0].var;
} else {
this.block.first = this.block.getUniqueName('first');
this.block.addElement(
this.block.first,
`@createComment()`,
parentNodes && `@createComment()`,
null
);
}
block.builders.init.addBlock(deindent`
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${this.key};
${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')}
}));
}
`);
const initialMountNode = parentNode || '#target';
const updateMountNode = this.getUpdateMountNode(anchor);
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c();
`);
if (parentNodes) {
block.builders.claim.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes});
`);
// index can only change if this is a keyed each block
const dependencies = this.key ? this.expression.dependencies : [];
this.scope.add(this.index, dependencies);
}
block.builders.mount.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
`);
const dynamic = this.block.hasUpdateMethod;
this.hasAnimation = false;
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
this.children = mapChildren(component, this, this.scope, info.children);
${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) {
return @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')}
if (this.hasAnimation) {
if (this.children.length !== 1) {
const child = this.children.find(child => !!child.animation);
component.error(child.animation, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the sole child of a keyed each block`
});
});
`);
if (!parentNode) {
block.builders.unmount.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].u();
`);
}
block.builders.destroy.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d();
`);
}
buildUnkeyed(
block: Block,
parentNode: string,
parentNodes: string,
snippet: string,
{
create_each_block,
each_block_value,
length,
iterations,
anchor,
mountOrIntro,
}
) {
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')}
}));
}
`);
const initialMountNode = parentNode || '#target';
const updateMountNode = this.getUpdateMountNode(anchor);
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].c();
}
`);
if (parentNodes) {
block.builders.claim.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].l(${parentNodes});
}
`);
}
block.builders.mount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
}
`);
const allDependencies = new Set(this.block.dependencies);
const { dependencies } = this.expression;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
this.warnIfEmptyBlock(); // TODO would be better if EachBlock, IfBlock etc extended an abstract Block class
// TODO do this for keyed blocks as well
const condition = Array.from(allDependencies)
.map(dependency => `changed.${dependency}`)
.join(' || ');
if (condition !== '') {
const forLoopBody = this.block.hasUpdateMethod
? this.block.hasIntroMethod
? deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, ${this.each_context});
} else {
${iterations}[#i] = ${create_each_block}(#component, ${this.each_context});
${iterations}[#i].c();
}
${iterations}[#i].i(${updateMountNode}, ${anchor});
`
: deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, ${this.each_context});
} else {
${iterations}[#i] = ${create_each_block}(#component, ${this.each_context});
${iterations}[#i].c();
${iterations}[#i].m(${updateMountNode}, ${anchor});
}
`
: deindent`
${iterations}[#i] = ${create_each_block}(#component, ${this.each_context});
${iterations}[#i].c();
${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor});
`;
const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`;
const outro = block.getUniqueName('outro');
const destroy = this.block.hasOutroMethod
? deindent`
function ${outro}(i) {
if (${iterations}[i]) {
${iterations}[i].o(function() {
${iterations}[i].u();
${iterations}[i].d();
${iterations}[i] = null;
});
}
}
for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
`
: deindent`
for (; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].u();
${iterations}[#i].d();
}
${iterations}.length = ${each_block_value}.${length};
`;
block.builders.update.addBlock(deindent`
if (${condition}) {
${each_block_value} = ${snippet};
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
var ${this.each_context} = @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')}
});
${forLoopBody}
}
${destroy}
}
`);
}
block.builders.unmount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].u();
}
`);
block.builders.destroy.addBlock(`@destroyEach(${iterations});`);
}
remount(name: string) {
// TODO consider keyed blocks
return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`;
}
ssr() {
const { compiler } = this;
const { snippet } = this.expression;
const props = [`${this.context}: item`]
.concat(this.destructuredContexts.map((name, i) => `${name}: item[${i}]`));
const getContext = this.index
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${this.index}: i })`
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
const open = `\${ ${this.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
compiler.target.append(open);
this.children.forEach((child: Node) => {
child.ssr();
});
const close = `\`)`;
compiler.target.append(close);
if (this.else) {
compiler.target.append(` : \``);
this.else.children.forEach((child: Node) => {
child.ssr();
});
compiler.target.append(`\``);
}
compiler.target.append('}');
this.else = info.else
? new ElseBlock(component, this, this.scope, info.else)
: null;
}
}

File diff suppressed because it is too large Load Diff

@ -1,13 +1,16 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class ElseBlock extends Node {
type: 'ElseBlock';
children: Node[];
block: Block;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, this, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, this, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -3,6 +3,9 @@ import Expression from './shared/Expression';
import addToSet from '../../utils/addToSet';
import flattenReference from '../../utils/flattenReference';
import validCalleeObjects from '../../utils/validCalleeObjects';
import list from '../../utils/list';
const validBuiltins = new Set(['set', 'fire', 'destroy']);
export default class EventHandler extends Node {
name: string;
@ -19,21 +22,26 @@ export default class EventHandler extends Node {
args: Expression[];
snippet: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
component.used.events.add(this.name);
this.dependencies = new Set();
if (info.expression) {
this.validateExpression(info.expression);
this.callee = flattenReference(info.expression.callee);
this.insertionPoint = info.expression.start;
this.usesComponent = !validCalleeObjects.has(this.callee.name);
this.usesContext = false;
this.args = info.expression.arguments.map(param => {
const expression = new Expression(compiler, this, scope, param);
const expression = new Expression(component, this, scope, param);
addToSet(this.dependencies, expression.dependencies);
if (expression.usesContext) this.usesContext = true;
return expression;
@ -51,42 +59,99 @@ export default class EventHandler extends Node {
this.snippet = null; // TODO handle shorthand events here?
}
this.isCustomEvent = compiler.events.has(this.name);
this.isCustomEvent = component.events.has(this.name);
this.shouldHoist = !this.isCustomEvent && parent.hasAncestor('EachBlock');
}
render(compiler, block, hoisted) { // TODO hoist more event handlers
render(component, block, context, hoisted) { // TODO hoist more event handlers
if (this.insertionPoint === null) return; // TODO handle shorthand events here?
if (!validCalleeObjects.has(this.callee.name)) {
const component = hoisted ? `component` : block.alias(`component`);
const component_name = hoisted ? `component` : block.alias(`component`);
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (this.callee.name[0] === '$' && !compiler.methods.has(this.callee.name)) {
compiler.code.overwrite(
if (this.callee.name[0] === '$' && !component.methods.has(this.callee.name)) {
component.code.overwrite(
this.insertionPoint,
this.insertionPoint + 1,
`${component}.store.`
`${component_name}.store.`
);
} else {
compiler.code.prependRight(
component.code.prependRight(
this.insertionPoint,
`${component}.`
`${component_name}.`
);
}
}
this.args.forEach(arg => {
arg.overwriteThis(this.parent.var);
});
if (this.isCustomEvent) {
this.args.forEach(arg => {
arg.overwriteThis(context);
});
if (this.callee && this.callee.name === 'this') {
const node = this.callee.nodes[0];
component.code.overwrite(node.start, node.end, context, {
storeName: true,
contentOnly: true
});
}
}
}
validateExpression(expression) {
const { callee, type } = expression;
if (this.isCustomEvent && this.callee && this.callee.name === 'this') {
const node = this.callee.nodes[0];
compiler.code.overwrite(node.start, node.end, this.parent.var, {
storeName: true,
contentOnly: true
if (type !== 'CallExpression') {
this.component.error(expression, {
code: `invalid-event-handler`,
message: `Expected a call expression`
});
}
const { component } = this;
const { name } = flattenReference(callee);
if (validCalleeObjects.has(name) || name === 'options') return;
if (name === 'refs') {
this.component.refCallees.push(callee);
return;
}
if (
(callee.type === 'Identifier' && validBuiltins.has(name)) ||
this.component.methods.has(name)
) {
return;
}
if (name[0] === '$') {
// assume it's a store method
return;
}
const validCallees = ['this.*', 'refs.*', 'event.*', 'options.*', 'console.*'].concat(
Array.from(validBuiltins),
Array.from(this.component.methods.keys())
);
let message = `'${component.source.slice(callee.start, callee.end)}' is an invalid callee ` ;
if (name === 'store') {
message += `(did you mean '$${component.source.slice(callee.start + 6, callee.end)}(...)'?)`;
} else {
message += `(should be one of ${list(validCallees)})`;
if (callee.type === 'Identifier' && component.helpers.has(callee.name)) {
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
}
}
component.warn(expression, {
code: `invalid-callee`,
message
});
}
}

@ -1,7 +1,7 @@
import Node from './shared/Node';
import Compiler from '../Compiler';
import Component from '../Component';
import mapChildren from './shared/mapChildren';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import TemplateScope from './shared/TemplateScope';
export default class Fragment extends Node {
@ -9,37 +9,11 @@ export default class Fragment extends Node {
children: Node[];
scope: TemplateScope;
constructor(compiler: Compiler, info: any) {
constructor(component: Component, info: any) {
const scope = new TemplateScope();
super(compiler, null, scope, info);
super(component, null, scope, info);
this.scope = scope;
this.children = mapChildren(compiler, this, scope, info.children);
}
init() {
this.block = new Block({
compiler: this.compiler,
name: '@create_main_fragment',
key: null,
indexNames: new Map(),
listNames: new Map(),
dependencies: new Set(),
});
this.compiler.target.blocks.push(this.block);
this.initChildren(this.block, true, null);
this.block.hasUpdateMethod = true;
}
build() {
this.init();
this.children.forEach(child => {
child.build(this.block, null, 'nodes');
});
this.children = mapChildren(component, this, scope, info.children);
}
}

@ -1,48 +1,23 @@
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class Head extends Node {
type: 'Head';
children: any[]; // TODO
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children.filter(child => {
return (child.type !== 'Text' || /\S/.test(child.data));
}));
}
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.initChildren(block, true, null);
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
this.var = 'document.head';
if (info.attributes.length) {
component.error(info.attributes[0], {
code: `invalid-attribute`,
message: `<svelte:head> should not have any attributes or directives`
});
}
this.children.forEach((child: Node) => {
child.build(block, 'document.head', null);
});
}
ssr() {
this.compiler.target.append('${(__result.head += `');
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append('`, "")}');
this.children = mapChildren(component, parent, scope, info.children.filter(child => {
return (child.type !== 'Text' || /\S/.test(child.data));
}));
}
}

@ -1,22 +1,9 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Compiler from '../Compiler';
import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Block from '../render-dom/Block';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
function isElseIf(node: ElseBlock) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
function isElseBranch(branch) {
return branch.block && !branch.condition;
}
export default class IfBlock extends Node {
type: 'IfBlock';
expression: Expression;
@ -25,481 +12,16 @@ export default class IfBlock extends Node {
block: Block;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.children = mapChildren(compiler, this, scope, info.children);
this.expression = new Expression(component, this, scope, info.expression);
this.children = mapChildren(component, this, scope, info.children);
this.else = info.else
? new ElseBlock(compiler, this, scope, info.else)
? new ElseBlock(component, this, scope, info.else)
: null;
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
const { compiler } = this;
this.cannotUseInnerHTML();
const blocks: Block[] = [];
let dynamic = false;
let hasIntros = false;
let hasOutros = false;
function attachBlocks(node: IfBlock) {
node.var = block.getUniqueName(`if_block`);
block.addDependencies(node.expression.dependencies);
node.block = block.child({
comment: createDebuggingComment(node, compiler),
name: compiler.getUniqueName(`create_if_block`),
});
blocks.push(node.block);
node.initChildren(node.block, stripWhitespace, nextSibling);
if (node.block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(node.block.dependencies);
}
if (node.block.hasIntroMethod) hasIntros = true;
if (node.block.hasOutroMethod) hasOutros = true;
if (isElseIf(node.else)) {
attachBlocks(node.else.children[0]);
} else if (node.else) {
node.else.block = block.child({
comment: createDebuggingComment(node.else, compiler),
name: compiler.getUniqueName(`create_if_block`),
});
blocks.push(node.else.block);
node.else.initChildren(
node.else.block,
stripWhitespace,
nextSibling
);
if (node.else.block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(node.else.block.dependencies);
}
}
}
attachBlocks(this);
blocks.forEach(block => {
block.hasUpdateMethod = dynamic;
block.hasIntroMethod = hasIntros;
block.hasOutroMethod = hasOutros;
});
compiler.target.blocks.push(...blocks);
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const name = this.var;
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (this.next && this.next.var) || 'null';
const branches = this.getBranches(block, parentNode, parentNodes, this);
const hasElse = isElseBranch(branches[branches.length - 1]);
const if_name = hasElse ? '' : `if (${name}) `;
const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
const hasOutros = branches[0].hasOutroMethod;
const vars = { name, anchor, if_name, hasElse };
if (this.else) {
if (hasOutros) {
this.buildCompoundWithOutros(block, parentNode, parentNodes, branches, dynamic, vars);
} else {
this.buildCompound(block, parentNode, parentNodes, branches, dynamic, vars);
}
} else {
this.buildSimple(block, parentNode, parentNodes, branches[0], dynamic, vars);
}
block.builders.create.addLine(`${if_name}${name}.c();`);
if (parentNodes) {
block.builders.claim.addLine(
`${if_name}${name}.l(${parentNodes});`
);
}
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
}
buildCompound(
block: Block,
parentNode: string,
parentNodes: string,
branches,
dynamic,
{ name, anchor, hasElse, if_name }
) {
const select_block_type = this.compiler.getUniqueName(`select_block_type`);
const current_block_type = block.getUniqueName(`current_block_type`);
const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
block.builders.init.addBlock(deindent`
function ${select_block_type}(ctx) {
${branches
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`)
.join('\n')}
}
`);
block.builders.init.addBlock(deindent`
var ${current_block_type} = ${select_block_type}(ctx);
var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
`);
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_name}${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const changeBlock = deindent`
${hasElse
? deindent`
${name}.u();
${name}.d();
`
: deindent`
if (${name}) {
${name}.u();
${name}.d();
}`}
${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
${if_name}${name}.c();
${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
`;
if (dynamic) {
block.builders.update.addBlock(deindent`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) {
${name}.p(changed, ctx);
} else {
${changeBlock}
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) {
${changeBlock}
}
`);
}
block.builders.unmount.addLine(`${if_name}${name}.u();`);
block.builders.destroy.addLine(`${if_name}${name}.d();`);
}
// if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?)
buildCompoundWithOutros(
block: Block,
parentNode: string,
parentNodes: string,
branches,
dynamic,
{ name, anchor, hasElse }
) {
const select_block_type = block.getUniqueName(`select_block_type`);
const current_block_type_index = block.getUniqueName(`current_block_type_index`);
const previous_block_index = block.getUniqueName(`previous_block_index`);
const if_block_creators = block.getUniqueName(`if_block_creators`);
const if_blocks = block.getUniqueName(`if_blocks`);
const if_current_block_type_index = hasElse
? ''
: `if (~${current_block_type_index}) `;
block.addVariable(current_block_type_index);
block.addVariable(name);
block.builders.init.addBlock(deindent`
var ${if_block_creators} = [
${branches.map(branch => branch.block).join(',\n')}
];
var ${if_blocks} = [];
function ${select_block_type}(ctx) {
${branches
.map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`)
.join('\n')}
}
`);
if (hasElse) {
block.builders.init.addBlock(deindent`
${current_block_type_index} = ${select_block_type}(ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
`);
} else {
block.builders.init.addBlock(deindent`
if (~(${current_block_type_index} = ${select_block_type}(ctx))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
}
`);
}
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_current_block_type_index}${if_blocks}[${current_block_type_index}].${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const destroyOldBlock = deindent`
${name}.o(function() {
${if_blocks}[ ${previous_block_index} ].u();
${if_blocks}[ ${previous_block_index} ].d();
${if_blocks}[ ${previous_block_index} ] = null;
});
`;
const createNewBlock = deindent`
${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
${name}.c();
}
${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
`;
const changeBlock = hasElse
? deindent`
${destroyOldBlock}
${createNewBlock}
`
: deindent`
if (${name}) {
${destroyOldBlock}
}
if (~${current_block_type_index}) {
${createNewBlock}
} else {
${name} = null;
}
`;
if (dynamic) {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
} else {
${changeBlock}
}
`);
} else {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} !== ${previous_block_index}) {
${changeBlock}
}
`);
}
block.builders.destroy.addLine(deindent`
${if_current_block_type_index}{
${if_blocks}[${current_block_type_index}].u();
${if_blocks}[${current_block_type_index}].d();
}
`);
}
buildSimple(
block: Block,
parentNode: string,
parentNodes: string,
branch,
dynamic,
{ name, anchor, if_name }
) {
block.builders.init.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block}(#component, ctx);
`);
const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`if (${name}) ${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const enter = dynamic
? branch.hasIntroMethod
? deindent`
if (${name}) {
${name}.p(changed, ctx);
} else {
${name} = ${branch.block}(#component, ctx);
if (${name}) ${name}.c();
}
${name}.i(${updateMountNode}, ${anchor});
`
: deindent`
if (${name}) {
${name}.p(changed, ctx);
} else {
${name} = ${branch.block}(#component, ctx);
${name}.c();
${name}.m(${updateMountNode}, ${anchor});
}
`
: branch.hasIntroMethod
? deindent`
if (!${name}) {
${name} = ${branch.block}(#component, ctx);
${name}.c();
}
${name}.i(${updateMountNode}, ${anchor});
`
: deindent`
if (!${name}) {
${name} = ${branch.block}(#component, ctx);
${name}.c();
${name}.m(${updateMountNode}, ${anchor});
}
`;
// no `p()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
const exit = branch.hasOutroMethod
? deindent`
${name}.o(function() {
${name}.u();
${name}.d();
${name} = null;
});
`
: deindent`
${name}.u();
${name}.d();
${name} = null;
`;
block.builders.update.addBlock(deindent`
if (${branch.condition}) {
${enter}
} else if (${name}) {
${exit}
}
`);
block.builders.unmount.addLine(`${if_name}${name}.u();`);
block.builders.destroy.addLine(`${if_name}${name}.d();`);
}
getBranches(
block: Block,
parentNode: string,
parentNodes: string,
node: IfBlock
) {
const branches = [
{
condition: node.expression.snippet,
block: node.block.name,
hasUpdateMethod: node.block.hasUpdateMethod,
hasIntroMethod: node.block.hasIntroMethod,
hasOutroMethod: node.block.hasOutroMethod,
},
];
this.visitChildren(block, node);
if (isElseIf(node.else)) {
branches.push(
...this.getBranches(block, parentNode, parentNodes, node.else.children[0])
);
} else {
branches.push({
condition: null,
block: node.else ? node.else.block.name : null,
hasUpdateMethod: node.else ? node.else.block.hasUpdateMethod : false,
hasIntroMethod: node.else ? node.else.block.hasIntroMethod : false,
hasOutroMethod: node.else ? node.else.block.hasOutroMethod : false,
});
if (node.else) {
this.visitChildren(block, node.else);
}
}
return branches;
}
ssr() {
const { compiler } = this;
const { snippet } = this.expression;
compiler.target.append('${ ' + snippet + ' ? `');
this.children.forEach((child: Node) => {
child.ssr();
});
compiler.target.append('` : `');
if (this.else) {
this.else.children.forEach((child: Node) => {
child.ssr();
});
}
compiler.target.append('` }');
}
visitChildren(block: Block, node: Node) {
node.children.forEach((child: Node) => {
child.build(node.block, null, 'nodes');
});
this.warnIfEmptyBlock();
}
}

@ -0,0 +1,90 @@
import Node from './shared/Node';
import Attribute from './Attribute';
import mapChildren from './shared/mapChildren';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Expression from './shared/Expression';
import Component from '../Component';
import Ref from './Ref';
export default class InlineComponent extends Node {
type: 'InlineComponent';
name: string;
expression: Expression;
attributes: Attribute[];
bindings: Binding[];
handlers: EventHandler[];
children: Node[];
ref: Ref;
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
component.hasComponents = true;
this.name = info.name;
if (this.name !== 'svelte:self' && this.name !== 'svelte:component') {
if (!component.components.has(this.name)) {
component.error(this, {
code: `missing-component`,
message: `${this.name} component is not defined`
});
}
component.used.components.add(this.name);
}
this.expression = this.name === 'svelte:component'
? new Expression(component, this, scope, info.expression)
: null;
this.attributes = [];
this.bindings = [];
this.handlers = [];
info.attributes.forEach(node => {
switch (node.type) {
case 'Action':
component.error(node, {
code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components`
});
case 'Attribute':
case 'Spread':
this.attributes.push(new Attribute(component, this, scope, node));
break;
case 'Binding':
this.bindings.push(new Binding(component, this, scope, node));
break;
case 'Class':
component.error(node, {
code: `invalid-class`,
message: `Classes can only be applied to DOM elements, not components`
});
case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Ref':
this.ref = new Ref(component, this, scope, node);
break;
case 'Transition':
component.error(node, {
code: `invalid-transition`,
message: `Transitions can only be applied to DOM elements, not components`
});
default:
throw new Error(`Not implemented: ${node.type}`);
}
});
this.children = mapChildren(component, this, scope, info.children);
}
}

@ -1,37 +1,3 @@
import Node from './shared/Node';
import Tag from './shared/Tag';
import Block from '../dom/Block';
export default class MustacheTag extends Tag {
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { init } = this.renameThisMethod(
block,
value => `${this.var}.data = ${value};`
);
block.addElement(
this.var,
`@createText(${init})`,
parentNodes && `@claimText(${parentNodes}, ${init})`,
parentNode
);
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
ssr() {
this.compiler.target.append(
this.parent &&
this.parent.type === 'Element' &&
this.parent.name === 'style'
? '${' + this.expression.snippet + '}'
: '${@escape(' + this.expression.snippet + ')}'
);
}
}
export default class MustacheTag extends Tag {}

@ -1,13 +1,15 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class PendingBlock extends Node {
block: Block;
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -1,94 +1,3 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import Tag from './shared/Tag';
import Block from '../dom/Block';
export default class RawMustacheTag extends Tag {
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const name = this.var;
const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !parentNode;
const needsAnchorAfter = this.next ? this.next.type !== 'Element' : !parentNode;
const anchorBefore = needsAnchorBefore
? block.getUniqueName(`${name}_before`)
: (this.prev && this.prev.var) || 'null';
const anchorAfter = needsAnchorAfter
? block.getUniqueName(`${name}_after`)
: (this.next && this.next.var) || 'null';
let detach: string;
let insert: (content: string) => string;
let useInnerHTML = false;
if (anchorBefore === 'null' && anchorAfter === 'null') {
useInnerHTML = true;
detach = `${parentNode}.innerHTML = '';`;
insert = content => `${parentNode}.innerHTML = ${content};`;
} else if (anchorBefore === 'null') {
detach = `@detachBefore(${anchorAfter});`;
insert = content => `${anchorAfter}.insertAdjacentHTML("beforebegin", ${content});`;
} else if (anchorAfter === 'null') {
detach = `@detachAfter(${anchorBefore});`;
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
} else {
detach = `@detachBetween(${anchorBefore}, ${anchorAfter});`;
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
}
const { init } = this.renameThisMethod(
block,
content => deindent`
${!useInnerHTML && detach}
${insert(content)}
`
);
// we would have used comments here, but the `insertAdjacentHTML` api only
// exists for `Element`s.
if (needsAnchorBefore) {
block.addElement(
anchorBefore,
`@createElement('noscript')`,
parentNodes && `@createElement('noscript')`,
parentNode
);
}
function addAnchorAfter() {
block.addElement(
anchorAfter,
`@createElement('noscript')`,
parentNodes && `@createElement('noscript')`,
parentNode
);
}
if (needsAnchorAfter && anchorBefore === 'null') {
// anchorAfter needs to be in the DOM before we
// insert the HTML...
addAnchorAfter();
}
block.builders.mount.addLine(insert(init));
block.builders.detachRaw.addBlock(detach);
if (needsAnchorAfter && anchorBefore !== 'null') {
// ...otherwise it should go afterwards
addAnchorAfter();
}
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
ssr() {
this.compiler.target.append('${' + this.expression.snippet + '}');
}
}
export default class RawMustacheTag extends Tag {}

@ -0,0 +1,31 @@
import Node from './shared/Node';
import isValidIdentifier from '../../utils/isValidIdentifier';
export default class Ref extends Node {
type: 'Ref';
name: string;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (parent.ref) {
component.error({
code: 'duplicate-refs',
message: `Duplicate refs`
});
}
if (!isValidIdentifier(info.name)) {
const suggestion = info.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
component.error(info, {
code: `invalid-reference-name`,
message: `Reference name '${info.name}' is invalid — must be a valid identifier such as ${suggestion}`
});
} else {
component.refs.add(info.name);
}
this.name = info.name;
}
}

@ -1,10 +1,6 @@
import deindent from '../../utils/deindent';
import isValidIdentifier from '../../utils/isValidIdentifier';
import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node';
import Element from './Element';
import Attribute from './Attribute';
import Block from '../dom/Block';
export default class Slot extends Element {
type: 'Element';
@ -12,126 +8,55 @@ export default class Slot extends Element {
attributes: Attribute[];
children: Node[];
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('slot');
if (this.children.length) {
this.initChildren(block, stripWhitespace, nextSibling);
}
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { compiler } = this;
const slotName = this.getStaticAttributeValue('name') || 'default';
compiler.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${slotName}`);
const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`;
block.addVariable(content_name, `#component._slotted${prop}`);
const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !parentNode;
const needsAnchorAfter = this.next ? this.next.type !== 'Element' : !parentNode;
const anchorBefore = needsAnchorBefore
? block.getUniqueName(`${content_name}_before`)
: (this.prev && this.prev.var) || 'null';
const anchorAfter = needsAnchorAfter
? block.getUniqueName(`${content_name}_after`)
: (this.next && this.next.var) || 'null';
if (needsAnchorBefore) block.addVariable(anchorBefore);
if (needsAnchorAfter) block.addVariable(anchorAfter);
let mountBefore = block.builders.mount.toString();
let unmountBefore = block.builders.unmount.toString();
block.builders.create.pushCondition(`!${content_name}`);
block.builders.hydrate.pushCondition(`!${content_name}`);
block.builders.mount.pushCondition(`!${content_name}`);
block.builders.update.pushCondition(`!${content_name}`);
block.builders.unmount.pushCondition(`!${content_name}`);
block.builders.destroy.pushCondition(`!${content_name}`);
this.children.forEach((child: Node) => {
child.build(block, parentNode, parentNodes);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
info.attributes.forEach(attr => {
if (attr.type !== 'Attribute') {
component.error(attr, {
code: `invalid-slot-directive`,
message: `<slot> cannot have directives`
});
}
if (attr.name !== 'name') {
component.error(attr, {
code: `invalid-slot-attribute`,
message: `"name" is the only attribute permitted on <slot> elements`
});
}
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
component.error(attr, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
}
const slotName = attr.value[0].data;
if (slotName === 'default') {
component.error(attr, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
// bug than anything. Perhaps it should be a warning
// if (validator.slots.has(slotName)) {
// validator.error(`duplicate '${slotName}' <slot> element`, nameAttribute.start);
// }
// validator.slots.add(slotName);
});
block.builders.create.popCondition();
block.builders.hydrate.popCondition();
block.builders.mount.popCondition();
block.builders.update.popCondition();
block.builders.unmount.popCondition();
block.builders.destroy.popCondition();
const mountLeadin = block.builders.mount.toString() !== mountBefore
? `else`
: `if (${content_name})`;
if (parentNode) {
block.builders.mount.addBlock(deindent`
${mountLeadin} {
${needsAnchorBefore && `@appendNode(${anchorBefore} || (${anchorBefore} = @createComment()), ${parentNode});`}
@appendNode(${content_name}, ${parentNode});
${needsAnchorAfter && `@appendNode(${anchorAfter} || (${anchorAfter} = @createComment()), ${parentNode});`}
}
`);
} else {
block.builders.mount.addBlock(deindent`
${mountLeadin} {
${needsAnchorBefore && `@insertNode(${anchorBefore} || (${anchorBefore} = @createComment()), #target, anchor);`}
@insertNode(${content_name}, #target, anchor);
${needsAnchorAfter && `@insertNode(${anchorAfter} || (${anchorAfter} = @createComment()), #target, anchor);`}
}
`);
}
// if the slot is unmounted, move nodes back into the document fragment,
// so that it can be reinserted later
// TODO so that this can work with public API, component._slotted should
// be all fragments, derived from options.slots. Not === options.slots
const unmountLeadin = block.builders.unmount.toString() !== unmountBefore
? `else`
: `if (${content_name})`;
if (anchorBefore === 'null' && anchorAfter === 'null') {
block.builders.unmount.addBlock(deindent`
${unmountLeadin} {
@reinsertChildren(${parentNode}, ${content_name});
}
`);
} else if (anchorBefore === 'null') {
block.builders.unmount.addBlock(deindent`
${unmountLeadin} {
@reinsertBefore(${anchorAfter}, ${content_name});
}
`);
} else if (anchorAfter === 'null') {
block.builders.unmount.addBlock(deindent`
${unmountLeadin} {
@reinsertAfter(${anchorBefore}, ${content_name});
}
`);
} else {
block.builders.unmount.addBlock(deindent`
${unmountLeadin} {
@reinsertBetween(${anchorBefore}, ${anchorAfter}, ${content_name});
@detachNode(${anchorBefore});
@detachNode(${anchorAfter});
}
`);
}
// if (node.attributes.length === 0) && validator.slots.has('default')) {
// validator.error(node, {
// code: `duplicate-slot`,
// message: `duplicate default <slot> element`
// });
// }
}
getStaticAttributeValue(name: string) {
@ -150,17 +75,4 @@ export default class Slot extends Element {
return null;
}
ssr() {
const name = this.attributes.find(attribute => attribute.name === 'name');
const slotName = name && name.chunks[0].data || 'default';
this.compiler.target.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : \``);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`\`}`);
}
}

@ -1,83 +1,12 @@
import { escape, escapeHTML, escapeTemplate, stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This
// list is almost certainly very incomplete)
const elementsWithoutText = new Set([
'audio',
'datalist',
'dl',
'ol',
'optgroup',
'select',
'ul',
'video',
]);
function shouldSkip(node: Text) {
if (/\S/.test(node.data)) return false;
const parentElement = node.findNearest(/(?:Element|Component|Head)/);
if (!parentElement) return false;
if (parentElement.type === 'Head') return true;
if (parentElement.type === 'Component') return parentElement.children.length === 1 && node === parentElement.children[0];
return parentElement.namespace || elementsWithoutText.has(parentElement.name);
}
export default class Text extends Node {
type: 'Text';
data: string;
shouldSkip: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.data = info.data;
}
init(block: Block) {
const parentElement = this.findNearest(/(?:Element|Component)/);
if (shouldSkip(this)) {
this.shouldSkip = true;
return;
}
this.var = block.getUniqueName(`text`);
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
if (this.shouldSkip) return;
block.addElement(
this.var,
`@createText(${stringify(this.data)})`,
parentNodes && `@claimText(${parentNodes}, ${stringify(this.data)})`,
parentNode
);
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
ssr() {
let text = this.data;
if (
!this.parent ||
this.parent.type !== 'Element' ||
(this.parent.name !== 'script' && this.parent.name !== 'style')
) {
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
this.compiler.target.append(escape(escapeTemplate(text)));
}
}

@ -1,13 +1,15 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class ThenBlock extends Node {
block: Block;
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -1,6 +1,4 @@
import { stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
export default class Title extends Node {
@ -8,9 +6,25 @@ export default class Title extends Node {
children: any[]; // TODO
shouldCache: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
if (info.attributes.length > 0) {
component.error(info.attributes[0], {
code: `illegal-attribute`,
message: `<title> cannot have attributes`
});
}
info.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
component.error(child, {
code: 'illegal-structure',
message: `<title> can only contain text and {tags}`
});
}
});
this.shouldCache = info.children.length === 1
? (
@ -19,96 +33,4 @@ export default class Title extends Node {
)
: true;
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const isDynamic = !!this.children.find(node => node.type !== 'Text');
if (isDynamic) {
let value;
const allDependencies = new Set();
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.children.length === 1) {
// single {{tag}} — may be a non-string
const { expression } = this.children[0];
const { dependencies, snippet } = this.children[0].expression;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
});
} else {
// '{foo} {bar}' — treat as string concatenation
value =
(this.children[0].type === 'Text' ? '' : `"" + `) +
this.children
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = chunk.expression;
dependencies.forEach(d => {
allDependencies.add(d);
});
return chunk.expression.getPrecedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
}
const last = this.shouldCache && block.getUniqueName(
`title_value`
);
if (this.shouldCache) block.addVariable(last);
let updater;
const init = this.shouldCache ? `${last} = ${value}` : value;
block.builders.init.addLine(
`document.title = ${init};`
);
updater = `document.title = ${this.shouldCache ? last : value};`;
if (allDependencies.size) {
const dependencies = Array.from(allDependencies);
const changedCheck = (
( block.hasOutroMethod ? `#outroing || ` : '' ) +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = this.shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
block.builders.update.addConditional(
condition,
updater
);
}
} else {
const value = stringify(this.children[0].data);
block.builders.hydrate.addLine(`document.title = ${value};`);
}
}
ssr() {
this.compiler.target.append(`<title>`);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`</title>`);
}
}

@ -4,15 +4,45 @@ import Expression from './shared/Expression';
export default class Transition extends Node {
type: 'Transition';
name: string;
directive: string;
expression: Expression;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (!component.transitions.has(info.name)) {
component.error(info, {
code: `missing-transition`,
message: `Missing transition '${info.name}'`
});
}
this.name = info.name;
this.directive = info.intro && info.outro ? 'transition' : info.intro ? 'in' : 'out';
if ((info.intro && parent.intro) || (info.outro && parent.outro)) {
const parentTransition = (parent.intro || parent.outro);
const message = this.directive === parentTransition.directive
? `An element can only have one '${this.directive}' directive`
: `An element cannot have both ${describe(parentTransition)} directive and ${describe(this)} directive`;
component.error(info, {
code: `duplicate-transition`,
message
});
}
this.component.used.transitions.add(this.name);
this.expression = info.expression
? new Expression(compiler, this, scope, info.expression)
? new Expression(component, this, scope, info.expression)
: null;
}
}
function describe(transition: Transition) {
return transition.directive === 'transition'
? `a 'transition'`
: `an '${transition.directive}'`;
}

@ -1,244 +1,74 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import flattenReference from '../../utils/flattenReference';
import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node';
import Block from '../dom/Block';
import Binding from './Binding';
import EventHandler from './EventHandler';
import flattenReference from '../../utils/flattenReference';
import fuzzymatch from '../validate/utils/fuzzymatch';
import list from '../../utils/list';
const associatedEvents = {
innerWidth: 'resize',
innerHeight: 'resize',
outerWidth: 'resize',
outerHeight: 'resize',
scrollX: 'scroll',
scrollY: 'scroll',
};
const properties = {
scrollX: 'pageXOffset',
scrollY: 'pageYOffset'
};
const readonly = new Set([
const validBindings = [
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'online',
]);
'scrollX',
'scrollY',
'online'
];
export default class Window extends Node {
type: 'Window';
handlers: EventHandler[];
bindings: Binding[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.handlers = [];
this.bindings = [];
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(compiler, this, scope, node));
} else if (node.type === 'Binding') {
this.bindings.push(new Binding(compiler, this, scope, node));
this.handlers.push(new EventHandler(component, this, scope, node));
}
});
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { compiler } = this;
const events = {};
const bindings: Record<string, string> = {};
this.handlers.forEach(handler => {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
compiler.addSourcemapLocations(handler.expression);
const isCustomEvent = compiler.events.has(handler.name);
let usesState = handler.dependencies.size > 0;
handler.render(compiler, block, false); // TODO hoist?
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent`
${usesState && `var ctx = #component.get();`}
${handler.snippet};
`;
else if (node.type === 'Binding') {
if (node.value.type !== 'Identifier') {
const { parts } = flattenReference(node.value);
if (isCustomEvent) {
// TODO dry this out
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(#component, window, function(event) {
${handlerBody}
component.error(node.value, {
code: `invalid-binding`,
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.destroy();
`);
} else {
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${handler.name}", ${handlerName});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${handler.name}", ${handlerName});
`);
}
});
this.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
compiler.target.readonly.add(binding.value.node.name);
}
bindings[binding.name] = binding.value.node.name;
// bind:online is a special case, we need to listen for two separate events
if (binding.name === 'online') return;
const associatedEvent = associatedEvents[binding.name];
const property = properties[binding.name] || binding.name;
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push({
name: binding.value.node.name,
value: property
});
});
const lock = block.getUniqueName(`window_updating`);
const clear = block.getUniqueName(`clear_window_updating`);
const timeout = block.getUniqueName(`window_updating_timeout`);
Object.keys(events).forEach(event => {
const handlerName = block.getUniqueName(`onwindow${event}`);
const props = events[event];
if (event === 'scroll') {
// TODO other bidirectional bindings...
block.addVariable(lock, 'false');
block.addVariable(clear, `function() { ${lock} = false; }`);
block.addVariable(timeout);
const condition = [
bindings.scrollX && `"${bindings.scrollX}" in this._state`,
bindings.scrollY && `"${bindings.scrollY}" in this._state`
].filter(Boolean).join(' || ');
const x = bindings.scrollX && `this._state.${bindings.scrollX}`;
const y = bindings.scrollY && `this._state.${bindings.scrollY}`;
compiler.target.metaBindings.addBlock(deindent`
if (${condition}) {
window.scrollTo(${x || 'window.pageXOffset'}, ${y || 'window.pageYOffset'});
}
${x && `${x} = window.pageXOffset;`}
${y && `${y} = window.pageYOffset;`}
`);
} else {
props.forEach(prop => {
compiler.target.metaBindings.addLine(
`this._state.${prop.name} = window.${prop.value};`
);
});
}
const handlerBody = deindent`
${event === 'scroll' && deindent`
if (${lock}) return;
${lock} = true;
`}
${compiler.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({
${props.map(prop => `${prop.name}: this.${prop.value}`)}
});
${compiler.options.dev && `component._updatingReadonlyProperty = false;`}
${event === 'scroll' && `${lock} = false;`}
`;
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${event}", ${handlerName});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${event}", ${handlerName});
`);
});
if (!~validBindings.indexOf(node.name)) {
const match = node.name === 'width'
? 'innerWidth'
: node.name === 'height'
? 'innerHeight'
: fuzzymatch(node.name, validBindings);
const message = `'${node.name}' is not a valid binding on <svelte:window>`;
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX || bindings.scrollY) {
block.builders.init.addBlock(deindent`
#component.on("state", ({ changed, current }) => {
if (${
[bindings.scrollX, bindings.scrollY].map(
binding => binding && `changed["${binding}"]`
).filter(Boolean).join(' || ')
}) {
${lock} = true;
clearTimeout(${timeout});
window.scrollTo(${
bindings.scrollX ? `current["${bindings.scrollX}"]` : `window.pageXOffset`
}, ${
bindings.scrollY ? `current["${bindings.scrollY}"]` : `window.pageYOffset`
if (match) {
component.error(node, {
code: `invalid-binding`,
message: `${message} (did you mean '${match}'?)`
});
} else {
component.error(node, {
code: `invalid-binding`,
message: `${message} — valid bindings are ${list(validBindings)}`
});
${timeout} = setTimeout(${clear}, 100);
}
});
`);
}
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
#component.set({ ${bindings.online}: navigator.onLine });
}
window.addEventListener("online", ${handlerName});
window.addEventListener("offline", ${handlerName});
`);
// add initial value
compiler.target.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;`
);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("online", ${handlerName});
window.removeEventListener("offline", ${handlerName});
`);
}
}
this.bindings.push(new Binding(component, this, scope, node));
}
ssr() {
// noop
else {
// TODO there shouldn't be anything else here...
}
});
}
}

@ -1,4 +1,4 @@
import Compiler from '../../Compiler';
import Component from '../../Component';
import { walk } from 'estree-walker';
import isReference from 'is-reference';
import flattenReference from '../../../utils/flattenReference';
@ -54,7 +54,7 @@ const precedence: Record<string, (node?: Node) => number> = {
};
export default class Expression {
compiler: Compiler;
component: Component;
node: any;
snippet: string;
@ -64,11 +64,11 @@ export default class Expression {
thisReferences: Array<{ start: number, end: number }>;
constructor(compiler, parent, scope, info) {
constructor(component, parent, scope, info) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
compiler: {
value: compiler
component: {
value: component
}
});
@ -81,7 +81,7 @@ export default class Expression {
const dependencies = new Set();
const { code, helpers } = compiler;
const { code, helpers } = component;
let { map, scope: currentScope } = createScopes(info);
@ -111,11 +111,13 @@ export default class Expression {
if (currentScope.has(name) || (name === 'event' && isEventHandler)) return;
if (compiler.helpers.has(name)) {
if (component.helpers.has(name)) {
let object = node;
while (object.type === 'MemberExpression') object = object.object;
const alias = compiler.templateVars.get(`helpers-${name}`);
component.used.helpers.add(name);
const alias = component.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias);
return;
}
@ -135,7 +137,7 @@ export default class Expression {
});
} else {
dependencies.add(name);
compiler.expectedProperties.add(name);
component.expectedProperties.add(name);
}
if (node.type === 'MemberExpression') {
@ -163,7 +165,7 @@ export default class Expression {
overwriteThis(name) {
this.thisReferences.forEach(ref => {
this.compiler.code.overwrite(ref.start, ref.end, name, {
this.component.code.overwrite(ref.start, ref.end, name, {
storeName: true
});
});

@ -1,11 +1,11 @@
import Compiler from './../../Compiler';
import Block from '../../dom/Block';
import Component from './../../Component';
import Block from '../../render-dom/Block';
import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node {
readonly start: number;
readonly end: number;
readonly compiler: Compiler;
readonly component: Component;
readonly parent: Node;
readonly type: string;
@ -15,7 +15,7 @@ export default class Node {
canUseInnerHTML: boolean;
var: string;
constructor(compiler: Compiler, parent, scope, info: any) {
constructor(component: Component, parent, scope, info: any) {
this.start = info.start;
this.end = info.end;
this.type = info.type;
@ -23,8 +23,8 @@ export default class Node {
// this makes properties non-enumerable, which makes logging
// bearable. might have a performance cost. TODO remove in prod?
Object.defineProperties(this, {
compiler: {
value: compiler
component: {
value: component
},
parent: {
value: parent
@ -39,98 +39,6 @@ export default class Node {
}
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
// implemented by subclasses
}
initChildren(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
// glue text nodes together
const cleaned: Node[] = [];
let lastChild: Node;
let windowComponent;
this.children.forEach((child: Node) => {
if (child.type === 'Comment') return;
// special case — this is an easy way to remove whitespace surrounding
// <svelte:window/>. lil hacky but it works
if (child.type === 'Window') {
windowComponent = child;
return;
}
if (child.type === 'Text' && lastChild && lastChild.type === 'Text') {
lastChild.data += child.data;
lastChild.end = child.end;
} else {
if (child.type === 'Text' && stripWhitespace && cleaned.length === 0) {
child.data = trimStart(child.data);
if (child.data) cleaned.push(child);
} else {
cleaned.push(child);
}
}
lastChild = child;
});
lastChild = null;
cleaned.forEach((child: Node, i: number) => {
child.canUseInnerHTML = !this.compiler.options.hydratable;
child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);
if (child.shouldSkip) return;
if (lastChild) lastChild.next = child;
child.prev = lastChild;
lastChild = child;
});
// We want to remove trailing whitespace inside an element/component/block,
// *unless* there is no whitespace between this node and its next sibling
if (stripWhitespace && lastChild && lastChild.type === 'Text') {
const shouldTrim = (
nextSibling ? (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)) : !this.hasAncestor('EachBlock')
);
if (shouldTrim) {
lastChild.data = trimEnd(lastChild.data);
if (!lastChild.data) {
cleaned.pop();
lastChild = cleaned[cleaned.length - 1];
lastChild.next = null;
}
}
}
this.children = cleaned;
if (windowComponent) cleaned.unshift(windowComponent);
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
// implemented by subclasses
}
isDomNode() {
return this.type === 'Element' || this.type === 'Text' || this.type === 'MustacheTag';
}
hasAncestor(type: string) {
return this.parent ?
this.parent.type === type || this.parent.hasAncestor(type) :
@ -142,31 +50,22 @@ export default class Node {
if (this.parent) return this.parent.findNearest(selector);
}
getOrCreateAnchor(block: Block, parentNode: string, parentNodes: string) {
// TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${this.var}_anchor`)
: (this.next && this.next.var) || 'null';
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
return anchor;
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
}
getUpdateMountNode(anchor: string) {
return this.parent.isDomNode() ? this.parent.var : `${anchor}.parentNode`;
}
warnIfEmptyBlock() {
if (!this.component.options.dev) return;
if (!/Block$/.test(this.type) || !this.children) return;
if (this.children.length > 1) return;
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
const child = this.children[0];
if (!child || (child.type === 'Text' && !/[^ \r\n\f\v\t]/.test(child.data))) {
this.component.warn(this, {
code: 'empty-block',
message: 'Empty block'
});
}
}
}

@ -1,56 +1,17 @@
import Node from './Node';
import Expression from './Expression';
import Block from '../../dom/Block';
export default class Tag extends Node {
expression: Expression;
shouldCache: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);
this.shouldCache = (
info.expression.type !== 'Identifier' ||
(this.expression.dependencies.size && scope.names.has(info.expression.name))
);
}
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName(this.type === 'MustacheTag' ? 'text' : 'raw');
block.addDependencies(this.expression.dependencies);
}
renameThisMethod(
block: Block,
update: ((value: string) => string)
) {
const { snippet, dependencies } = this.expression;
const value = this.shouldCache && block.getUniqueName(`${this.var}_value`);
const content = this.shouldCache ? value : snippet;
if (this.shouldCache) block.addVariable(value, snippet);
if (dependencies.size) {
const changedCheck = (
(block.hasOutroMethod ? `#outroing || ` : '') +
[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
const condition = this.shouldCache ?
(dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
changedCheck;
block.builders.update.addConditional(
condition,
update(content)
);
}
return { init: content };
}
}

@ -1,12 +1,13 @@
import AwaitBlock from '../AwaitBlock';
import Comment from '../Comment';
import Component from '../Component';
import EachBlock from '../EachBlock';
import Element from '../Element';
import Head from '../Head';
import IfBlock from '../IfBlock';
import InlineComponent from '../InlineComponent';
import MustacheTag from '../MustacheTag';
import RawMustacheTag from '../RawMustacheTag';
import DebugTag from '../DebugTag';
import Slot from '../Slot';
import Text from '../Text';
import Title from '../Title';
@ -17,13 +18,14 @@ function getConstructor(type): typeof Node {
switch (type) {
case 'AwaitBlock': return AwaitBlock;
case 'Comment': return Comment;
case 'Component': return Component;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
case 'Head': return Head;
case 'IfBlock': return IfBlock;
case 'InlineComponent': return InlineComponent;
case 'MustacheTag': return MustacheTag;
case 'RawMustacheTag': return RawMustacheTag;
case 'DebugTag': return DebugTag;
case 'Slot': return Slot;
case 'Text': return Text;
case 'Title': return Title;
@ -32,11 +34,11 @@ function getConstructor(type): typeof Node {
}
}
export default function mapChildren(compiler, parent, scope, children: any[]) {
export default function mapChildren(component, parent, scope, children: any[]) {
let last = null;
return children.map(child => {
const constructor = getConstructor(child.type);
const node = new constructor(compiler, parent, scope, child);
const node = new constructor(component, parent, scope, child);
if (last) last.next = node;
node.prev = last;

@ -1,30 +1,33 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { escape } from '../../utils/stringify';
import Compiler from '../Compiler';
import { Node } from '../../interfaces';
import Renderer from './Renderer';
import Wrapper from './wrappers/shared/Wrapper';
export interface BlockOptions {
parent?: Block;
name: string;
compiler?: Compiler;
renderer?: Renderer;
comment?: string;
key?: string;
indexNames?: Map<string, string>;
listNames?: Map<string, string>;
bindings?: Map<string, () => string>;
dependencies?: Set<string>;
}
export default class Block {
compiler: Compiler;
parent?: Block;
renderer: Renderer;
name: string;
comment?: string;
wrappers: Wrapper[];
key: string;
first: string;
dependencies: Set<string>;
indexNames: Map<string, string>;
listNames: Map<string, string>;
bindings: Map<string, () => string>;
builders: {
init: CodeBuilder;
@ -32,15 +35,20 @@ export default class Block {
claim: CodeBuilder;
hydrate: CodeBuilder;
mount: CodeBuilder;
measure: CodeBuilder;
fix: CodeBuilder;
animate: CodeBuilder;
intro: CodeBuilder;
update: CodeBuilder;
outro: CodeBuilder;
unmount: CodeBuilder;
detachRaw: CodeBuilder;
destroy: CodeBuilder;
};
hasIntroMethod: boolean;
maintainContext: boolean;
hasAnimation: boolean;
hasIntros: boolean;
hasOutros: boolean;
hasIntroMethod: boolean; // could have the method without the transition, due to siblings
hasOutroMethod: boolean;
outros: number;
@ -52,18 +60,20 @@ export default class Block {
autofocus: string;
constructor(options: BlockOptions) {
this.compiler = options.compiler;
this.parent = options.parent;
this.renderer = options.renderer;
this.name = options.name;
this.comment = options.comment;
this.wrappers = [];
// for keyed each blocks
this.key = options.key;
this.first = null;
this.dependencies = new Set();
this.indexNames = options.indexNames;
this.listNames = options.listNames;
this.bindings = options.bindings;
this.builders = {
init: new CodeBuilder(),
@ -71,19 +81,21 @@ export default class Block {
claim: new CodeBuilder(),
hydrate: new CodeBuilder(),
mount: new CodeBuilder(),
measure: new CodeBuilder(),
fix: new CodeBuilder(),
animate: new CodeBuilder(),
intro: new CodeBuilder(),
update: new CodeBuilder(),
outro: new CodeBuilder(),
unmount: new CodeBuilder(),
detachRaw: new CodeBuilder(),
destroy: new CodeBuilder(),
};
this.hasAnimation = false;
this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
this.hasOutroMethod = false;
this.outros = 0;
this.getUniqueName = this.compiler.getUniqueNameMaker();
this.getUniqueName = this.renderer.component.getUniqueNameMaker();
this.variables = new Map();
this.aliases = new Map()
@ -94,6 +106,43 @@ export default class Block {
this.hasUpdateMethod = false; // determined later
}
assignVariableNames() {
const seen = new Set();
const dupes = new Set();
let i = this.wrappers.length;
while (i--) {
const wrapper = this.wrappers[i];
if (!wrapper.var) continue;
if (wrapper.parent && wrapper.parent.canUseInnerHTML) continue;
if (seen.has(wrapper.var)) {
dupes.add(wrapper.var);
}
seen.add(wrapper.var);
}
const counts = new Map();
i = this.wrappers.length;
while (i--) {
const wrapper = this.wrappers[i];
if (!wrapper.var) continue;
if (dupes.has(wrapper.var)) {
const i = counts.get(wrapper.var) || 0;
counts.set(wrapper.var, i + 1);
wrapper.var = this.getUniqueName(wrapper.var + i);
} else {
wrapper.var = this.getUniqueName(wrapper.var);
}
}
}
addDependencies(dependencies: Set<string>) {
dependencies.forEach(dependency => {
this.dependencies.add(dependency);
@ -104,22 +153,40 @@ export default class Block {
name: string,
renderStatement: string,
claimStatement: string,
parentNode: string
parentNode: string,
noDetach?: boolean
) {
this.addVariable(name);
this.builders.create.addLine(`${name} = ${renderStatement};`);
this.builders.claim.addLine(`${name} = ${claimStatement || renderStatement};`);
if (parentNode) {
this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`);
if (parentNode === 'document.head') this.builders.unmount.addLine(`@detachNode(${name});`);
this.builders.mount.addLine(`@append(${parentNode}, ${name});`);
if (parentNode === 'document.head') this.builders.destroy.addLine(`@detachNode(${name});`);
} else {
this.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`);
this.builders.unmount.addLine(`@detachNode(${name});`);
this.builders.mount.addLine(`@insert(#target, ${name}, anchor);`);
if (!noDetach) this.builders.destroy.addConditional('detach', `@detachNode(${name});`);
}
}
addIntro() {
this.hasIntros = this.hasIntroMethod = this.renderer.hasIntroTransitions = true;
}
addOutro() {
this.hasOutros = this.hasOutroMethod = this.renderer.hasOutroTransitions = true;
this.outros += 1;
}
addAnimation() {
this.hasAnimation = true;
}
addVariable(name: string, init?: string) {
if (name[0] === '#') {
name = this.alias(name.slice(1));
}
if (this.variables.has(name) && this.variables.get(name) !== init) {
throw new Error(
`Variable '${name}' already initialised with a different value`
@ -142,27 +209,24 @@ export default class Block {
}
toString() {
let introing;
const hasIntros = !this.builders.intro.isEmpty();
if (hasIntros) {
introing = this.getUniqueName('introing');
this.addVariable(introing);
}
const { dev } = this.renderer.options;
if (this.hasIntroMethod || this.hasOutroMethod) {
this.addVariable('#current');
let outroing;
const hasOutros = !this.builders.outro.isEmpty();
if (hasOutros) {
outroing = this.alias('outroing');
this.addVariable(outroing);
if (!this.builders.mount.isEmpty()) {
this.builders.mount.addLine(`#current = true;`);
}
if (!this.builders.outro.isEmpty()) {
this.builders.outro.addLine(`#current = false;`);
}
}
if (this.autofocus) {
this.builders.mount.addLine(`${this.autofocus}.focus();`);
}
// minor hack we need to ensure that any {{{triples}}} are detached first
this.builders.unmount.addBlockAtStart(this.builders.detachRaw.toString());
const properties = new CodeBuilder();
let localKey;
@ -179,20 +243,26 @@ export default class Block {
if (this.builders.create.isEmpty() && this.builders.hydrate.isEmpty()) {
properties.addBlock(`c: @noop,`);
} else {
const hydrate = !this.builders.hydrate.isEmpty() && (
this.renderer.options.hydratable
? `this.h()`
: this.builders.hydrate
);
properties.addBlock(deindent`
c: function create() {
${dev ? 'c: function create' : 'c'}() {
${this.builders.create}
${!this.builders.hydrate.isEmpty() && `this.h();`}
${hydrate}
},
`);
}
if (this.compiler.options.hydratable) {
if (this.renderer.options.hydratable) {
if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) {
properties.addBlock(`l: @noop,`);
} else {
properties.addBlock(deindent`
l: function claim(nodes) {
${dev ? 'l: function claim' : 'l'}(nodes) {
${this.builders.claim}
${!this.builders.hydrate.isEmpty() && `this.h();`}
},
@ -200,9 +270,9 @@ export default class Block {
}
}
if (!this.builders.hydrate.isEmpty()) {
if (this.renderer.options.hydratable && !this.builders.hydrate.isEmpty()) {
properties.addBlock(deindent`
h: function hydrate() {
${dev ? 'h: function hydrate' : 'h'}() {
${this.builders.hydrate}
},
`);
@ -212,7 +282,7 @@ export default class Block {
properties.addBlock(`m: @noop,`);
} else {
properties.addBlock(deindent`
m: function mount(#target, anchor) {
${dev ? 'm: function mount' : 'm'}(#target, anchor) {
${this.builders.mount}
},
`);
@ -223,7 +293,7 @@ export default class Block {
properties.addBlock(`p: @noop,`);
} else {
properties.addBlock(deindent`
p: function update(changed, ${this.maintainContext ? '_ctx' : 'ctx'}) {
${dev ? 'p: function update' : 'p'}(changed, ${this.maintainContext ? '_ctx' : 'ctx'}) {
${this.maintainContext && `ctx = _ctx;`}
${this.builders.update}
},
@ -231,63 +301,55 @@ export default class Block {
}
}
if (this.hasIntroMethod) {
if (hasIntros) {
properties.addBlock(deindent`
i: function intro(#target, anchor) {
if (${introing}) return;
${introing} = true;
${hasOutros && `${outroing} = false;`}
if (this.hasAnimation) {
properties.addBlock(deindent`
${dev ? `r: function measure` : `r`}() {
${this.builders.measure}
},
${this.builders.intro}
${dev ? `f: function fix` : `f`}() {
${this.builders.fix}
},
this.m(#target, anchor);
},
`);
${dev ? `a: function animate` : `a`}() {
${this.builders.animate}
},
`);
}
if (this.hasIntroMethod || this.hasOutroMethod) {
if (this.builders.mount.isEmpty()) {
properties.addBlock(`i: @noop,`);
} else {
properties.addBlock(deindent`
i: function intro(#target, anchor) {
${dev ? 'i: function intro' : 'i'}(#target, anchor) {
if (#current) return;
${this.builders.intro}
this.m(#target, anchor);
},
`);
}
}
if (this.hasOutroMethod) {
if (hasOutros) {
if (this.builders.outro.isEmpty()) {
properties.addBlock(`o: @run,`);
} else {
properties.addBlock(deindent`
o: function outro(#outrocallback) {
if (${outroing}) return;
${outroing} = true;
${hasIntros && `${introing} = false;`}
${dev ? 'o: function outro' : 'o'}(#outrocallback) {
if (!#current) return;
var #outros = ${this.outros};
${this.outros > 1 && `#outrocallback = @callAfter(#outrocallback, ${this.outros});`}
${this.builders.outro}
},
`);
} else {
properties.addBlock(deindent`
o: @run,
`);
}
}
if (this.builders.unmount.isEmpty()) {
properties.addBlock(`u: @noop,`);
} else {
properties.addBlock(deindent`
u: function unmount() {
${this.builders.unmount}
},
`);
}
if (this.builders.destroy.isEmpty()) {
properties.addBlock(`d: @noop`);
} else {
properties.addBlock(deindent`
d: function destroy() {
${dev ? 'd: function destroy' : 'd'}(detach) {
${this.builders.destroy}
}
`);

@ -0,0 +1,75 @@
import Block from './Block';
import { CompileOptions } from '../../interfaces';
import Component from '../Component';
import FragmentWrapper from './wrappers/Fragment';
export default class Renderer {
component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions;
blocks: (Block | string)[];
readonly: Set<string>;
slots: Set<string>;
metaBindings: string[];
bindingGroups: string[];
block: Block;
fragment: FragmentWrapper;
usedNames: Set<string>;
fileVar: string;
hasIntroTransitions: boolean;
hasOutroTransitions: boolean;
hasComplexBindings: boolean;
constructor(component: Component, options: CompileOptions) {
this.component = component;
this.options = options;
this.locate = component.locate; // TODO messy
this.readonly = new Set();
this.slots = new Set();
this.usedNames = new Set();
this.fileVar = options.dev && this.component.getUniqueName('file');
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
this.metaBindings = [];
this.bindingGroups = [];
// main block
this.block = new Block({
renderer: this,
name: '@create_main_fragment',
key: null,
bindings: new Map(),
dependencies: new Set(),
});
this.block.hasUpdateMethod = true;
this.blocks = [];
this.fragment = new FragmentWrapper(
this,
this.block,
component.fragment.children,
null,
true,
null
);
this.blocks.push(this.block);
this.blocks.forEach(block => {
if (typeof block !== 'string') {
block.assignVariableNames();
}
});
this.fragment.render(this.block, null, 'nodes');
}
}

@ -1,59 +1,30 @@
import MagicString from 'magic-string';
import isReference from 'is-reference';
import { parseExpressionAt } from 'acorn';
import annotateWithScopes from '../../utils/annotateWithScopes';
import { walk } from 'estree-walker';
import deindent from '../../utils/deindent';
import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist';
import reservedNames from '../../utils/reservedNames';
import Compiler from '../Compiler';
import Stylesheet from '../../css/Stylesheet';
import Stats from '../../Stats';
import Block from './Block';
import { test } from '../../config';
import { Ast, CompileOptions, Node } from '../../interfaces';
export class DomTarget {
blocks: (Block|string)[];
readonly: Set<string>;
metaBindings: CodeBuilder;
hasIntroTransitions: boolean;
hasOutroTransitions: boolean;
hasComplexBindings: boolean;
constructor() {
this.blocks = [];
this.readonly = new Set();
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
this.metaBindings = new CodeBuilder();
}
}
import Component from '../Component';
import Renderer from './Renderer';
import { CompileOptions } from '../../interfaces';
export default function dom(
ast: Ast,
source: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
component: Component,
options: CompileOptions
) {
const format = options.format || 'es';
const target = new DomTarget();
const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, true, target);
const {
computations,
name,
templateProperties,
namespace,
} = compiler;
templateProperties
} = component;
compiler.fragment.build();
const { block } = compiler.fragment;
const renderer = new Renderer(component, options);
const { block } = renderer;
if (component.options.nestedTransitions) {
block.hasOutroMethod = true;
}
// prevent fragment being created twice (#1063)
if (options.customElement) block.builders.create.addLine(`this.c = @noop;`);
@ -63,15 +34,15 @@ export default function dom(
const computationDeps = new Set();
if (computations.length) {
computations.forEach(({ key, deps }) => {
if (target.readonly.has(key)) {
computations.forEach(({ key, deps, hasRestParam }) => {
if (renderer.readonly.has(key)) {
// <svelte:window> bindings
throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property`
);
}
target.readonly.add(key);
renderer.readonly.add(key);
if (deps) {
deps.forEach(dep => {
@ -86,33 +57,52 @@ export default function dom(
// computed property depends on entire state object —
// these must go at the end
computationBuilder.addLine(
`if (this._differs(state.${key}, (state.${key} = %computed-${key}(state)))) changed.${key} = true;`
`if (this._differs(state.${key}, (state.${key} = %computed-${key}(@exclude(state, "${key}"))))) changed.${key} = true;`
);
}
});
}
if (compiler.javascript) {
builder.addBlock(compiler.javascript);
if (component.javascript) {
const componentDefinition = new CodeBuilder();
component.declarations.forEach(declaration => {
componentDefinition.addBlock(declaration.block);
});
const js = (
component.javascript[0] +
componentDefinition +
component.javascript[1]
);
builder.addBlock(js);
}
if (component.options.dev) {
builder.addLine(`const ${renderer.fileVar} = ${JSON.stringify(component.file)};`);
}
const css = compiler.stylesheet.render(options.filename, !compiler.customElement);
const styles = compiler.stylesheet.hasStyles && stringify(options.dev ?
const css = component.stylesheet.render(options.filename, !component.customElement);
const styles = component.stylesheet.hasStyles && stringify(options.dev ?
`${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` :
css.code, { onlyEscapeAtSymbol: true });
if (styles && compiler.options.css !== false && !compiler.customElement) {
if (styles && component.options.css !== false && !component.customElement) {
builder.addBlock(deindent`
function @add_css() {
var style = @createElement("style");
style.id = '${compiler.stylesheet.id}-style';
style.id = '${component.stylesheet.id}-style';
style.textContent = ${styles};
@appendNode(style, document.head);
@append(document.head, style);
}
`);
}
target.blocks.forEach(block => {
// fix order
// TODO the deconflicted names of blocks are reversed... should set them here
const blocks = renderer.blocks.slice().reverse();
blocks.forEach(block => {
builder.addBlock(block.toString());
});
@ -124,15 +114,15 @@ export default function dom(
? `@proto`
: deindent`
{
${['destroy', 'get', 'fire', 'on', 'set', '_set', '_mount', '_unmount', '_differs']
${['destroy', 'get', 'fire', 'on', 'set', '_set', '_stage', '_mount', '_differs']
.map(n => `${n}: @${n}`)
.join(',\n')}
}`;
const debugName = `<${compiler.customElement ? compiler.tag : name}>`;
const debugName = `<${component.customElement ? component.tag : name}>`;
// generate initial state object
const expectedProperties = Array.from(compiler.expectedProperties);
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
@ -156,32 +146,42 @@ export default function dom(
const hasInitHooks = !!(templateProperties.oncreate || templateProperties.onstate || templateProperties.onupdate);
const constructorBody = deindent`
${options.dev && `this._debugName = '${debugName}';`}
${options.dev && !compiler.customElement &&
`if (!options || (!options.target && !options.root)) throw new Error("'target' is a required option");`}
${options.dev && deindent`
this._debugName = '${debugName}';
${!component.customElement && deindent`
if (!options || (!options.target && !options.root)) {
throw new Error("'target' is a required option");
}`}
${storeProps.length > 0 && deindent`
if (!options.store) {
throw new Error("${debugName} references store properties, but no store was provided");
}`}
`}
@init(this, options);
${templateProperties.store && `this.store = %store();`}
${compiler.usesRefs && `this.refs = {};`}
${component.refs.size > 0 && `this.refs = {};`}
this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)};
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
${target.metaBindings}
${renderer.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
Array.from(compiler.expectedProperties).map(prop => {
Array.from(component.expectedProperties).map(prop => {
if (globalWhitelist.has(prop)) return;
if (computations.find(c => c.key === prop)) return;
const message = compiler.components.has(prop) ?
const message = component.components.has(prop) ?
`${debugName} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` :
`${debugName} was created without expected data property '${prop}'`;
const conditions = [`!('${prop}' in this._state)`];
if (compiler.customElement) conditions.push(`!('${prop}' in this.attributes)`);
if (component.customElement) conditions.push(`!('${prop}' in this.attributes)`);
return `if (${conditions.join(' && ')}) console.warn("${message}");`
})}
${compiler.bindingGroups.length &&
`this._bindingGroups = [${Array(compiler.bindingGroups.length).fill('[]').join(', ')}];`}
${renderer.bindingGroups.length &&
`this._bindingGroups = [${Array(renderer.bindingGroups.length).fill('[]').join(', ')}];`}
this._intro = ${component.options.skipIntroByDefault ? '!!options.intro' : 'true'};
${templateProperties.onstate && `this._handlers.state = [%onstate];`}
${templateProperties.onupdate && `this._handlers.update = [%onupdate];`}
@ -192,69 +192,56 @@ export default function dom(
}];`
)}
${compiler.slots.size && `this._slotted = options.slots || {};`}
${renderer.slots.size && `this._slotted = options.slots || {};`}
${compiler.customElement ?
${component.customElement ?
deindent`
this.attachShadow({ mode: 'open' });
${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { onlyEscapeAtSymbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
` :
(compiler.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${compiler.stylesheet.id}-style")) @add_css();`)
(component.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`)
}
${(hasInitHooks || compiler.hasComponents || target.hasComplexBindings || target.hasIntroTransitions) && deindent`
if (!options.root) {
this._oncreate = [];
${(compiler.hasComponents || target.hasComplexBindings) && `this._beforecreate = [];`}
${(compiler.hasComponents || target.hasIntroTransitions) && `this._aftercreate = [];`}
}
`}
${compiler.slots.size && `this.slots = {};`}
${templateProperties.onstate && `%onstate.call(this, { changed: @assignTrue({}, this._state), current: this._state });`}
this._fragment = @create_main_fragment(this, this._state);
${hasInitHooks && deindent`
this.root._oncreate.push(() => {
${templateProperties.onstate && `%onstate.call(this, { changed: @assignTrue({}, this._state), current: this._state });`}
${templateProperties.oncreate && `%oncreate.call(this);`}
this.fire("update", { changed: @assignTrue({}, this._state), current: this._state });
});
`}
${compiler.customElement ? deindent`
${component.customElement ? deindent`
this._fragment.c();
this._fragment.${block.hasIntroMethod ? 'i' : 'm'}(this.shadowRoot, null);
if (options.target) this._mount(options.target, options.anchor);
` : deindent`
if (options.target) {
${compiler.options.hydratable
? deindent`
var nodes = @children(options.target);
options.hydrate ? this._fragment.l(nodes) : this._fragment.c();
nodes.forEach(@detachNode);
` :
deindent`
${options.dev && `if (options.hydrate) throw new Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");`}
this._fragment.c();
`}
${component.options.hydratable
? deindent`
var nodes = @children(options.target);
options.hydrate ? this._fragment.l(nodes) : this._fragment.c();
nodes.forEach(@detachNode);` :
deindent`
${options.dev &&
`if (options.hydrate) throw new Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");`}
this._fragment.c();`}
this._mount(options.target, options.anchor);
${(compiler.hasComponents || target.hasComplexBindings || hasInitHooks || target.hasIntroTransitions) && deindent`
${compiler.hasComponents && `this._lock = true;`}
${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(compiler.hasComponents || hasInitHooks) && `@callAll(this._oncreate);`}
${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${compiler.hasComponents && `this._lock = false;`}
`}
${(component.hasComponents || renderer.hasComplexBindings || hasInitHooks || renderer.hasIntroTransitions) &&
`@flush(this);`}
}
`}
${component.options.skipIntroByDefault && `this._intro = true;`}
`;
if (compiler.customElement) {
const props = compiler.props || Array.from(compiler.expectedProperties);
if (component.customElement) {
const props = component.props || Array.from(component.expectedProperties);
builder.addBlock(deindent`
class ${name} extends HTMLElement {
@ -277,7 +264,7 @@ export default function dom(
}
`).join('\n\n')}
${compiler.slots.size && deindent`
${renderer.slots.size && deindent`
connectedCallback() {
Object.keys(this._slotted).forEach(key => {
this.appendChild(this._slotted[key]);
@ -288,13 +275,9 @@ export default function dom(
this.set({ [attr]: newValue });
}
${(compiler.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent`
${(component.hasComponents || renderer.hasComplexBindings || templateProperties.oncreate || renderer.hasIntroTransitions) && deindent`
connectedCallback() {
${compiler.hasComponents && `this._lock = true;`}
${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(compiler.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`}
${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${compiler.hasComponents && `this._lock = false;`}
@flush(this);
}
`}
}
@ -304,14 +287,10 @@ export default function dom(
@assign(${name}.prototype, {
_mount(target, anchor) {
target.insertBefore(this, anchor);
},
_unmount() {
this.parentNode.removeChild(this);
}
});
customElements.define("${compiler.tag}", ${name});
customElements.define("${component.tag}", ${name});
`);
} else {
builder.addBlock(deindent`
@ -329,7 +308,7 @@ export default function dom(
builder.addBlock(deindent`
${options.dev && deindent`
${name}.prototype._checkReadOnly = function _checkReadOnly(newState) {
${Array.from(target.readonly).map(
${Array.from(renderer.readonly).map(
prop =>
`if ('${prop}' in newState && !this._updatingReadonlyProperty) throw new Error("${debugName}: Cannot set read-only property '${prop}'");`
)}
@ -351,12 +330,8 @@ export default function dom(
let result = builder.toString();
const filename = options.filename && (
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename
);
return compiler.generate(result, options, {
banner: `/* ${filename ? `${filename} ` : ``}generated by Svelte v${"__VERSION__"} */`,
return component.generate(result, options, {
banner: `/* ${component.file ? `${component.file} ` : ``}generated by Svelte v${"__VERSION__"} */`,
sharedPath,
name,
format,

@ -0,0 +1,230 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import AwaitBlock from '../../nodes/AwaitBlock';
import createDebuggingComment from '../../../utils/createDebuggingComment';
import deindent from '../../../utils/deindent';
import FragmentWrapper from './Fragment';
import PendingBlock from '../../nodes/PendingBlock';
import ThenBlock from '../../nodes/ThenBlock';
import CatchBlock from '../../nodes/CatchBlock';
class AwaitBlockBranch extends Wrapper {
node: PendingBlock | ThenBlock | CatchBlock;
block: Block;
fragment: FragmentWrapper;
isDynamic: boolean;
var = null;
constructor(
status: string,
renderer: Renderer,
block: Block,
parent: Wrapper,
node: AwaitBlock,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.block = block.child({
comment: createDebuggingComment(node, this.renderer.component),
name: this.renderer.component.getUniqueName(`create_${status}_block`)
});
this.fragment = new FragmentWrapper(
renderer,
this.block,
this.node.children,
parent,
stripWhitespace,
nextSibling
);
this.isDynamic = this.block.dependencies.size > 0;
}
}
export default class AwaitBlockWrapper extends Wrapper {
node: AwaitBlock;
pending: AwaitBlockBranch;
then: AwaitBlockBranch;
catch: AwaitBlockBranch;
var = 'await_block';
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: AwaitBlock,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
block.addDependencies(this.node.expression.dependencies);
let isDynamic = false;
let hasIntros = false;
let hasOutros = false;
['pending', 'then', 'catch'].forEach(status => {
const child = this.node[status];
const branch = new AwaitBlockBranch(
status,
renderer,
block,
parent,
child,
stripWhitespace,
nextSibling
);
renderer.blocks.push(branch.block);
if (branch.isDynamic) {
isDynamic = true;
// TODO should blocks update their own parents?
block.addDependencies(branch.block.dependencies);
}
if (branch.block.hasIntros) hasIntros = true;
if (branch.block.hasOutros) hasOutros = true;
this[status] = branch;
});
this.pending.block.hasUpdateMethod = isDynamic;
this.then.block.hasUpdateMethod = isDynamic;
this.catch.block.hasUpdateMethod = isDynamic;
this.pending.block.hasIntroMethod = hasIntros;
this.then.block.hasIntroMethod = hasIntros;
this.catch.block.hasIntroMethod = hasIntros;
this.pending.block.hasOutroMethod = hasOutros;
this.then.block.hasOutroMethod = hasOutros;
this.catch.block.hasOutroMethod = hasOutros;
if (hasOutros && this.renderer.options.nestedTransitions) {
block.addOutro();
}
}
render(
block: Block,
parentNode: string,
parentNodes: string
) {
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor);
const { snippet } = this.node.expression;
const info = block.getUniqueName(`info`);
const promise = block.getUniqueName(`promise`);
block.addVariable(promise);
block.maintainContext = true;
const infoProps = [
block.alias('component') === 'component' ? 'component' : `component: #component`,
'ctx',
'current: null',
this.pending.block.name && `pending: ${this.pending.block.name}`,
this.then.block.name && `then: ${this.then.block.name}`,
this.catch.block.name && `catch: ${this.catch.block.name}`,
this.then.block.name && `value: '${this.node.value}'`,
this.catch.block.name && `error: '${this.node.error}'`,
this.pending.block.hasOutroMethod && `blocks: Array(3)`
].filter(Boolean);
block.builders.init.addBlock(deindent`
let ${info} = {
${infoProps.join(',\n')}
};
`);
block.builders.init.addBlock(deindent`
@handlePromise(${promise} = ${snippet}, ${info});
`);
block.builders.create.addBlock(deindent`
${info}.block.c();
`);
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${info}.block.l(${parentNodes});
`);
}
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
const hasTransitions = this.pending.block.hasIntroMethod || this.pending.block.hasOutroMethod;
block.builders.mount.addBlock(deindent`
${info}.block.${hasTransitions ? 'i' : 'm'}(${initialMountNode}, ${info}.anchor = ${anchorNode});
${info}.mount = () => ${updateMountNode};
`);
const conditions = [];
if (this.node.expression.dependencies.size > 0) {
conditions.push(
`(${[...this.node.expression.dependencies].map(dep => `'${dep}' in changed`).join(' || ')})`
);
}
conditions.push(
`${promise} !== (${promise} = ${snippet})`,
`@handlePromise(${promise}, ${info})`
);
block.builders.update.addLine(
`${info}.ctx = ctx;`
);
if (this.pending.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
// nothing
} else {
${info}.block.p(changed, @assign(@assign({}, ctx), ${info}.resolved));
}
`);
} else {
block.builders.update.addBlock(deindent`
${conditions.join(' && ')}
`);
}
if (this.pending.block.hasOutroMethod && this.renderer.options.nestedTransitions) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
const ${countdown} = @callAfter(#outrocallback, 3);
for (let #i = 0; #i < 3; #i += 1) {
const block = ${info}.blocks[#i];
if (block) block.o(${countdown});
else ${countdown}();
}
`);
}
block.builders.destroy.addBlock(deindent`
${info}.block.d(${parentNode ? '' : 'detach'});
${info} = null;
`);
[this.pending, this.then, this.catch].forEach(branch => {
branch.fragment.render(branch.block, null, 'nodes');
});
}
}

@ -0,0 +1,72 @@
import Renderer from '../Renderer';
import Wrapper from './shared/Wrapper';
import Block from '../Block';
import DebugTag from '../../nodes/DebugTag';
import addToSet from '../../../utils/addToSet';
import deindent from '../../../utils/deindent';
export default class DebugTagWrapper extends Wrapper {
node: DebugTag;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: DebugTag,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
}
render(block: Block, parentNode: string, parentNodes: string) {
const { renderer } = this;
const { component } = renderer;
if (!renderer.options.dev) return;
const { code } = component;
if (this.node.expressions.length === 0) {
// Debug all
code.overwrite(this.node.start + 1, this.node.start + 7, 'debugger', {
storeName: true
});
const statement = `[✂${this.node.start + 1}-${this.node.start + 7}✂];`;
block.builders.create.addLine(statement);
block.builders.update.addLine(statement);
} else {
const { code } = component;
code.overwrite(this.node.start + 1, this.node.start + 7, 'log', {
storeName: true
});
const log = `[✂${this.node.start + 1}-${this.node.start + 7}✂]`;
const dependencies = new Set();
this.node.expressions.forEach(expression => {
addToSet(dependencies, expression.dependencies);
});
const condition = [...dependencies].map(d => `changed.${d}`).join(' || ');
const identifiers = this.node.expressions.map(e => e.node.name).join(', ');
block.builders.update.addBlock(deindent`
if (${condition}) {
const { ${identifiers} } = ctx;
console.${log}({ ${identifiers} });
debugger;
}
`);
block.builders.create.addBlock(deindent`
{
const { ${identifiers} } = ctx;
console.${log}({ ${identifiers} });
debugger;
}
`);
}
}
}

@ -0,0 +1,503 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Wrapper from './shared/Wrapper';
import createDebuggingComment from '../../../utils/createDebuggingComment';
import EachBlock from '../../nodes/EachBlock';
import FragmentWrapper from './Fragment';
import deindent from '../../../utils/deindent';
import ElseBlock from '../../nodes/ElseBlock';
class ElseBlockWrapper extends Wrapper {
node: ElseBlock;
block: Block;
fragment: FragmentWrapper;
isDynamic: boolean;
var = null;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: ElseBlock,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.block = block.child({
comment: createDebuggingComment(node, this.renderer.component),
name: this.renderer.component.getUniqueName(`create_else_block`)
});
this.fragment = new FragmentWrapper(
renderer,
this.block,
this.node.children,
parent,
stripWhitespace,
nextSibling
);
this.isDynamic = this.block.dependencies.size > 0;
if (this.isDynamic) {
// TODO this can't be right
this.block.hasUpdateMethod = true;
}
}
}
export default class EachBlockWrapper extends Wrapper {
block: Block;
node: EachBlock;
fragment: FragmentWrapper;
else?: ElseBlockWrapper;
var: string;
vars: {
anchor: string;
create_each_block: string;
each_block_value: string;
get_each_context: string;
iterations: string;
length: string;
mountOrIntro: string;
}
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EachBlock,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
this.var = 'each';
const { dependencies } = node.expression;
block.addDependencies(dependencies);
this.block = block.child({
comment: createDebuggingComment(this.node, this.renderer.component),
name: renderer.component.getUniqueName('create_each_block'),
key: <string>node.key, // TODO...
bindings: new Map(block.bindings)
});
// TODO this seems messy
this.block.hasAnimation = this.node.hasAnimation;
this.indexName = this.node.index || renderer.component.getUniqueName(`${this.node.context}_index`);
node.contexts.forEach(prop => {
// TODO this doesn't feel great
this.block.bindings.set(prop.key.name, () => `ctx.${this.vars.each_block_value}[ctx.${this.indexName}]${prop.tail}`);
});
if (this.node.index) {
this.block.getUniqueName(this.node.index); // this prevents name collisions (#1254)
}
renderer.blocks.push(this.block);
this.fragment = new FragmentWrapper(renderer, this.block, node.children, this, stripWhitespace, nextSibling);
if (this.node.else) {
this.else = new ElseBlockWrapper(
renderer,
block,
this,
this.node.else,
stripWhitespace,
nextSibling
);
renderer.blocks.push(this.else.block);
if (this.else.isDynamic) {
this.block.addDependencies(this.else.block.dependencies);
}
}
block.addDependencies(this.block.dependencies);
this.block.hasUpdateMethod = this.block.dependencies.size > 0; // TODO should this logic be in Block?
if (this.block.hasOutros || (this.else && this.else.block.hasOutros)) {
block.addOutro();
}
}
render(block: Block, parentNode: string, parentNodes: string) {
if (this.fragment.nodes.length === 0) return;
const { renderer } = this;
const { component } = renderer;
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = this.node.start + 2;
while (component.source[c] !== 'e') c += 1;
component.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`;
const needsAnchor = this.next
? !this.next.isDomNode() :
!parentNode || !this.parent.isDomNode();
this.vars = {
anchor: needsAnchor
? block.getUniqueName(`${this.var}_anchor`)
: (this.next && this.next.var) || 'null',
create_each_block: this.block.name,
each_block_value: renderer.component.getUniqueName(`${this.var}_value`),
get_each_context: renderer.component.getUniqueName(`get_${this.var}_context`),
iterations: block.getUniqueName(`${this.var}_blocks`),
length: `[✂${c}-${c+4}✂]`,
mountOrIntro: (this.block.hasIntroMethod || this.block.hasOutroMethod)
? 'i'
: 'm'
};
this.contextProps = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = list[i]${prop.tail};`);
// TODO only add these if necessary
this.contextProps.push(
`child_ctx.${this.vars.each_block_value} = list;`,
`child_ctx.${this.indexName} = i;`
);
const { snippet } = this.node.expression;
block.builders.init.addLine(`var ${this.vars.each_block_value} = ${snippet};`);
renderer.blocks.push(deindent`
function ${this.vars.get_each_context}(ctx, list, i) {
const child_ctx = Object.create(ctx);
${this.contextProps}
return child_ctx;
}
`);
if (this.node.key) {
this.renderKeyed(block, parentNode, parentNodes, snippet);
} else {
this.renderUnkeyed(block, parentNode, parentNodes, snippet);
}
if (needsAnchor) {
block.addElement(
this.vars.anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
if (this.else) {
const each_block_else = component.getUniqueName(`${this.var}_else`);
const mountOrIntro = (this.else.block.hasIntroMethod || this.else.block.hasOutroMethod) ? 'i' : 'm';
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if (!${this.vars.each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
}
`);
block.builders.mount.addBlock(deindent`
if (${each_block_else}) {
${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null);
}
`);
const initialMountNode = parentNode || `${this.vars.anchor}.parentNode`;
if (this.else.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (!${this.vars.each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p(changed, ctx);
} else if (!${this.vars.each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${this.vars.anchor});
} else if (${each_block_else}) {
${each_block_else}.d(1);
${each_block_else} = null;
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${this.vars.each_block_value}.${length}) {
if (${each_block_else}) {
${each_block_else}.d(1);
${each_block_else} = null;
}
} else if (!${each_block_else}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${this.vars.anchor});
}
`);
}
block.builders.destroy.addBlock(deindent`
if (${each_block_else}) ${each_block_else}.d(${parentNode ? '' : 'detach'});
`);
}
this.fragment.render(this.block, null, 'nodes');
if (this.else) {
this.else.fragment.render(this.else.block, null, 'nodes');
}
}
renderKeyed(
block: Block,
parentNode: string,
parentNodes: string,
snippet: string
) {
const {
create_each_block,
length,
anchor,
mountOrIntro,
} = this.vars;
const get_key = block.getUniqueName('get_key');
const blocks = block.getUniqueName(`${this.var}_blocks`);
const lookup = block.getUniqueName(`${this.var}_lookup`);
block.addVariable(blocks, '[]');
block.addVariable(lookup, `@blankObject()`);
if (this.fragment.nodes[0].isDomNode()) {
this.block.first = this.fragment.nodes[0].var;
} else {
this.block.first = this.block.getUniqueName('first');
this.block.addElement(
this.block.first,
`@createComment()`,
parentNodes && `@createComment()`,
null
);
}
block.builders.init.addBlock(deindent`
const ${get_key} = ctx => ${this.node.key.snippet};
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
let child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
let key = ${get_key}(child_ctx);
${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx);
}
`);
const initialMountNode = parentNode || '#target';
const updateMountNode = this.getUpdateMountNode(anchor);
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c();
`);
if (parentNodes) {
block.builders.claim.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes});
`);
}
block.builders.mount.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
`);
const dynamic = this.block.hasUpdateMethod;
const rects = block.getUniqueName('rects');
const destroy = this.node.hasAnimation
? `@fixAndOutroAndDestroyBlock`
: this.block.hasOutros
? `@outroAndDestroyBlock`
: `@destroyBlock`;
block.builders.update.addBlock(deindent`
const ${this.vars.each_block_value} = ${snippet};
${this.block.hasOutros && `@groupOutros();`}
${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].r();`}
${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.vars.each_block_value}, ${lookup}, ${updateMountNode}, ${destroy}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.vars.get_each_context});
${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`}
`);
if (this.block.hasOutros && this.renderer.component.options.nestedTransitions) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
const ${countdown} = @callAfter(#outrocallback, ${blocks}.length);
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].o(${countdown});
`);
}
block.builders.destroy.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d(${parentNode ? '' : 'detach'});
`);
}
renderUnkeyed(
block: Block,
parentNode: string,
parentNodes: string,
snippet: string
) {
const {
create_each_block,
length,
iterations,
anchor,
mountOrIntro,
} = this.vars;
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(#component, ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i));
}
`);
const initialMountNode = parentNode || '#target';
const updateMountNode = this.getUpdateMountNode(anchor);
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].c();
}
`);
if (parentNodes) {
block.builders.claim.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].l(${parentNodes});
}
`);
}
block.builders.mount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
}
`);
const allDependencies = new Set(this.block.dependencies);
const { dependencies } = this.node.expression;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
const outroBlock = this.block.hasOutros && block.getUniqueName('outroBlock')
if (outroBlock) {
block.builders.init.addBlock(deindent`
function ${outroBlock}(i, detach, fn) {
if (${iterations}[i]) {
${iterations}[i].o(() => {
if (detach) {
${iterations}[i].d(detach);
${iterations}[i] = null;
}
if (fn) fn();
});
}
}
`);
}
// TODO do this for keyed blocks as well
const condition = Array.from(allDependencies)
.map(dependency => `changed.${dependency}`)
.join(' || ');
if (condition !== '') {
const forLoopBody = this.block.hasUpdateMethod
? (this.block.hasIntros || this.block.hasOutros)
? deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, child_ctx);
} else {
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
${iterations}[#i].c();
}
${iterations}[#i].i(${updateMountNode}, ${anchor});
`
: deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, child_ctx);
} else {
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
${iterations}[#i].c();
${iterations}[#i].m(${updateMountNode}, ${anchor});
}
`
: deindent`
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
${iterations}[#i].c();
${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor});
`;
const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`;
let destroy;
if (this.block.hasOutros) {
destroy = deindent`
@groupOutros();
for (; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 1);
`;
} else {
destroy = deindent`
for (${this.block.hasUpdateMethod ? `` : `#i = ${this.vars.each_block_value}.${length}`}; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].d(1);
}
${iterations}.length = ${this.vars.each_block_value}.${length};
`;
}
block.builders.update.addBlock(deindent`
if (${condition}) {
${this.vars.each_block_value} = ${snippet};
for (var #i = ${start}; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
const child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
${forLoopBody}
}
${destroy}
}
`);
}
if (outroBlock && this.renderer.component.options.nestedTransitions) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
${iterations} = ${iterations}.filter(Boolean);
const ${countdown} = @callAfter(#outrocallback, ${iterations}.length);
for (let #i = 0; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 0, ${countdown});`
);
}
block.builders.destroy.addBlock(`@destroyEach(${iterations}, detach);`);
}
remount(name: string) {
// TODO consider keyed blocks
return `for (var #i = 0; #i < ${this.vars.iterations}.length; #i += 1) ${this.vars.iterations}[#i].m(${name}._slotted.default, null);`;
}
}

@ -0,0 +1,456 @@
import Attribute from '../../../nodes/Attribute';
import Block from '../../Block';
import fixAttributeCasing from '../../../../utils/fixAttributeCasing';
import ElementWrapper from './index';
import { stringify } from '../../../../utils/stringify';
import deindent from '../../../../utils/deindent';
export default class AttributeWrapper {
node: Attribute;
parent: ElementWrapper;
constructor(parent: ElementWrapper, block: Block, node: Attribute) {
this.node = node;
this.parent = parent;
if (node.dependencies.size > 0) {
parent.cannotUseInnerHTML();
block.addDependencies(node.dependencies);
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select')) select = select.parent;
if (select && select.selectBindingDependencies) {
select.selectBindingDependencies.forEach(prop => {
this.node.dependencies.forEach((dependency: string) => {
this.parent.renderer.component.indirectDependencies.get(prop).add(dependency);
});
});
}
}
}
}
render(block: Block) {
const element = this.parent;
const name = fixAttributeCasing(this.node.name);
let metadata = element.node.namespace ? null : attributeLookup[name];
if (metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf(element.node.name))
metadata = null;
const isIndirectlyBoundValue =
name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' &&
element.node.bindings.find(
(binding: Binding) =>
/checked|group/.test(binding.name)
)));
const propertyName = isIndirectlyBoundValue
? '__value'
: metadata && metadata.propertyName;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = /-/.test(element.node.name)
? '@setCustomElementData'
: name.slice(0, 6) === 'xlink:'
? '@setXlinkAttribute'
: '@setAttribute';
const isLegacyInputType = element.renderer.component.options.legacy && name === 'type' && this.parent.node.name === 'input';
const isDataSet = /^data-/.test(name) && !element.renderer.component.options.legacy && !element.node.namespace;
const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
return m[1].toUpperCase();
}) : name;
if (this.node.isDynamic) {
let value;
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.node.chunks.length === 1) {
// single {tag} — may be a non-string
value = this.node.chunks[0].snippet;
} else {
// '{foo} {bar}' — treat as string concatenation
value =
(this.node.chunks[0].type === 'Text' ? '' : `"" + `) +
this.node.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
return chunk.getPrecedence() <= 13
? `(${chunk.snippet})`
: chunk.snippet;
}
})
.join(' + ');
}
const isSelectValueAttribute =
name === 'value' && element.node.name === 'select';
const shouldCache = this.node.shouldCache || isSelectValueAttribute;
const last = shouldCache && block.getUniqueName(
`${element.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
if (shouldCache) block.addVariable(last);
let updater;
const init = shouldCache ? `${last} = ${value}` : value;
if (isLegacyInputType) {
block.builders.hydrate.addLine(
`@setInputType(${element.var}, ${init});`
);
updater = `@setInputType(${element.var}, ${shouldCache ? last : value});`;
} else if (isSelectValueAttribute) {
// annoying special case
const isMultipleSelect = element.getStaticAttributeValue('multiple');
const i = block.getUniqueName('i');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${last}.indexOf(${option}.__value);`
: deindent`
if (${option}.__value === ${last}) {
${option}.selected = true;
break;
}`;
updater = deindent`
for (var ${i} = 0; ${i} < ${element.var}.options.length; ${i} += 1) {
var ${option} = ${element.var}.options[${i}];
${ifStatement}
}
`;
block.builders.mount.addBlock(deindent`
${last} = ${value};
${updater}
`);
} else if (propertyName) {
block.builders.hydrate.addLine(
`${element.var}.${propertyName} = ${init};`
);
updater = `${element.var}.${propertyName} = ${shouldCache ? last : value};`;
} else if (isDataSet) {
block.builders.hydrate.addLine(
`${element.var}.dataset.${camelCaseName} = ${init};`
);
updater = `${element.var}.dataset.${camelCaseName} = ${shouldCache ? last : value};`;
} else {
block.builders.hydrate.addLine(
`${method}(${element.var}, "${name}", ${init});`
);
updater = `${method}(${element.var}, "${name}", ${shouldCache ? last : value});`;
}
if (this.node.dependencies.size || isSelectValueAttribute) {
const dependencies = Array.from(this.node.dependencies);
const changedCheck = (
(block.hasOutros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
block.builders.update.addConditional(
condition,
updater
);
}
} else {
const value = this.node.getValue();
const statement = (
isLegacyInputType
? `@setInputType(${element.var}, ${value});`
: propertyName
? `${element.var}.${propertyName} = ${value};`
: isDataSet
? `${element.var}.dataset.${camelCaseName} = ${value};`
: `${method}(${element.var}, "${name}", ${value});`
);
block.builders.hydrate.addLine(statement);
// special case autofocus. has to be handled in a bit of a weird way
if (this.node.isTrue && name === 'autofocus') {
block.autofocus = element.var;
}
}
if (isIndirectlyBoundValue) {
const updateValue = `${element.var}.value = ${element.var}.__value;`;
block.builders.hydrate.addLine(updateValue);
if (this.node.isDynamic) block.builders.update.addLine(updateValue);
}
}
stringify() {
const value = this.node.chunks;
if (value === true) return '';
if (value.length === 0) return `=""`;
return `="${value.map(chunk => {
return chunk.type === 'Text'
? chunk.data.replace(/"/g, '\\"')
: `\${${chunk.snippet}}`
})}"`;
}
}
// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
const attributeLookup = {
accept: { appliesTo: ['form', 'input'] },
'accept-charset': { propertyName: 'acceptCharset', appliesTo: ['form'] },
accesskey: { propertyName: 'accessKey' },
action: { appliesTo: ['form'] },
align: {
appliesTo: [
'applet',
'caption',
'col',
'colgroup',
'hr',
'iframe',
'img',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
],
},
allowfullscreen: { propertyName: 'allowFullscreen', appliesTo: ['iframe'] },
alt: { appliesTo: ['applet', 'area', 'img', 'input'] },
async: { appliesTo: ['script'] },
autocomplete: { appliesTo: ['form', 'input'] },
autofocus: { appliesTo: ['button', 'input', 'keygen', 'select', 'textarea'] },
autoplay: { appliesTo: ['audio', 'video'] },
autosave: { appliesTo: ['input'] },
bgcolor: {
propertyName: 'bgColor',
appliesTo: [
'body',
'col',
'colgroup',
'marquee',
'table',
'tbody',
'tfoot',
'td',
'th',
'tr',
],
},
border: { appliesTo: ['img', 'object', 'table'] },
buffered: { appliesTo: ['audio', 'video'] },
challenge: { appliesTo: ['keygen'] },
charset: { appliesTo: ['meta', 'script'] },
checked: { appliesTo: ['command', 'input'] },
cite: { appliesTo: ['blockquote', 'del', 'ins', 'q'] },
class: { propertyName: 'className' },
code: { appliesTo: ['applet'] },
codebase: { propertyName: 'codeBase', appliesTo: ['applet'] },
color: { appliesTo: ['basefont', 'font', 'hr'] },
cols: { appliesTo: ['textarea'] },
colspan: { propertyName: 'colSpan', appliesTo: ['td', 'th'] },
content: { appliesTo: ['meta'] },
contenteditable: { propertyName: 'contentEditable' },
contextmenu: {},
controls: { appliesTo: ['audio', 'video'] },
coords: { appliesTo: ['area'] },
data: { appliesTo: ['object'] },
datetime: { propertyName: 'dateTime', appliesTo: ['del', 'ins', 'time'] },
default: { appliesTo: ['track'] },
defer: { appliesTo: ['script'] },
dir: {},
dirname: { propertyName: 'dirName', appliesTo: ['input', 'textarea'] },
disabled: {
appliesTo: [
'button',
'command',
'fieldset',
'input',
'keygen',
'optgroup',
'option',
'select',
'textarea',
],
},
download: { appliesTo: ['a', 'area'] },
draggable: {},
dropzone: {},
enctype: { appliesTo: ['form'] },
for: { propertyName: 'htmlFor', appliesTo: ['label', 'output'] },
form: {
appliesTo: [
'button',
'fieldset',
'input',
'keygen',
'label',
'meter',
'object',
'output',
'progress',
'select',
'textarea',
],
},
formaction: { appliesTo: ['input', 'button'] },
headers: { appliesTo: ['td', 'th'] },
height: {
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
},
hidden: {},
high: { appliesTo: ['meter'] },
href: { appliesTo: ['a', 'area', 'base', 'link'] },
hreflang: { appliesTo: ['a', 'area', 'link'] },
'http-equiv': { propertyName: 'httpEquiv', appliesTo: ['meta'] },
icon: { appliesTo: ['command'] },
id: {},
indeterminate: { appliesTo: ['input'] },
ismap: { propertyName: 'isMap', appliesTo: ['img'] },
itemprop: {},
keytype: { appliesTo: ['keygen'] },
kind: { appliesTo: ['track'] },
label: { appliesTo: ['track'] },
lang: {},
language: { appliesTo: ['script'] },
loop: { appliesTo: ['audio', 'bgsound', 'marquee', 'video'] },
low: { appliesTo: ['meter'] },
manifest: { appliesTo: ['html'] },
max: { appliesTo: ['input', 'meter', 'progress'] },
maxlength: { propertyName: 'maxLength', appliesTo: ['input', 'textarea'] },
media: { appliesTo: ['a', 'area', 'link', 'source', 'style'] },
method: { appliesTo: ['form'] },
min: { appliesTo: ['input', 'meter'] },
multiple: { appliesTo: ['input', 'select'] },
muted: { appliesTo: ['audio', 'video'] },
name: {
appliesTo: [
'button',
'form',
'fieldset',
'iframe',
'input',
'keygen',
'object',
'output',
'select',
'textarea',
'map',
'meta',
'param',
],
},
novalidate: { propertyName: 'noValidate', appliesTo: ['form'] },
open: { appliesTo: ['details'] },
optimum: { appliesTo: ['meter'] },
pattern: { appliesTo: ['input'] },
ping: { appliesTo: ['a', 'area'] },
placeholder: { appliesTo: ['input', 'textarea'] },
poster: { appliesTo: ['video'] },
preload: { appliesTo: ['audio', 'video'] },
radiogroup: { appliesTo: ['command'] },
readonly: { propertyName: 'readOnly', appliesTo: ['input', 'textarea'] },
rel: { appliesTo: ['a', 'area', 'link'] },
required: { appliesTo: ['input', 'select', 'textarea'] },
reversed: { appliesTo: ['ol'] },
rows: { appliesTo: ['textarea'] },
rowspan: { propertyName: 'rowSpan', appliesTo: ['td', 'th'] },
sandbox: { appliesTo: ['iframe'] },
scope: { appliesTo: ['th'] },
scoped: { appliesTo: ['style'] },
seamless: { appliesTo: ['iframe'] },
selected: { appliesTo: ['option'] },
shape: { appliesTo: ['a', 'area'] },
size: { appliesTo: ['input', 'select'] },
sizes: { appliesTo: ['link', 'img', 'source'] },
span: { appliesTo: ['col', 'colgroup'] },
spellcheck: {},
src: {
appliesTo: [
'audio',
'embed',
'iframe',
'img',
'input',
'script',
'source',
'track',
'video',
],
},
srcdoc: { appliesTo: ['iframe'] },
srclang: { appliesTo: ['track'] },
srcset: { appliesTo: ['img'] },
start: { appliesTo: ['ol'] },
step: { appliesTo: ['input'] },
style: { propertyName: 'style.cssText' },
summary: { appliesTo: ['table'] },
tabindex: { propertyName: 'tabIndex' },
target: { appliesTo: ['a', 'area', 'base', 'form'] },
title: {},
type: {
appliesTo: [
'button',
'command',
'embed',
'object',
'script',
'source',
'style',
'menu',
],
},
usemap: { propertyName: 'useMap', appliesTo: ['img', 'input', 'object'] },
value: {
appliesTo: [
'button',
'option',
'input',
'li',
'meter',
'progress',
'param',
'select',
'textarea',
],
},
volume: { appliesTo: ['audio', 'video'] },
width: {
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
},
wrap: { appliesTo: ['textarea'] },
};
Object.keys(attributeLookup).forEach(name => {
const metadata = attributeLookup[name];
if (!metadata.propertyName) metadata.propertyName = name;
});

@ -0,0 +1,324 @@
import Binding from '../../../nodes/Binding';
import ElementWrapper from '.';
import { dimensions } from '../../../../utils/patterns';
import getObject from '../../../../utils/getObject';
import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
import Renderer from '../../Renderer';
import flattenReference from '../../../../utils/flattenReference';
import getTailSnippet from '../../../../utils/getTailSnippet';
// TODO this should live in a specific binding
const readOnlyMediaAttributes = new Set([
'duration',
'buffered',
'seekable',
'played'
]);
export default class BindingWrapper {
node: Binding;
parent: ElementWrapper;
object: string;
handler: any; // TODO
updateDom: string;
initialUpdate: string;
needsLock: boolean;
updateCondition: string;
constructor(block: Block, node: Binding, parent: ElementWrapper) {
this.node = node;
this.parent = parent;
const { dependencies } = this.node.value;
block.addDependencies(dependencies);
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
if (parent.node.name === 'select') {
parent.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
parent.renderer.component.indirectDependencies.set(prop, new Set());
});
}
}
isReadOnlyMediaAttribute() {
return readOnlyMediaAttributes.has(this.node.name);
}
munge(block: Block) {
const { parent } = this;
const { renderer } = parent;
const needsLock = (
parent.node.name !== 'input' ||
!/radio|checkbox|range|color/.test(parent.node.getStaticAttributeValue('type'))
);
const isReadOnly = (
(parent.node.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) ||
dimensions.test(this.node.name)
);
let updateConditions: string[] = [];
const { name } = getObject(this.node.value.node);
const { snippet } = this.node.value;
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
// and `selected` is an object chosen with a <select>, then when `checked` changes,
// we need to tell the component to update all the values `selected` might be
// pointing to
// TODO should this happen in preprocess?
const dependencies = new Set(this.node.value.dependencies);
this.node.value.dependencies.forEach((prop: string) => {
const indirectDependencies = renderer.component.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
dependencies.add(indirectDependency);
});
}
});
// view to model
const valueFromDom = getValueFromDom(renderer, this.parent, this);
const handler = getEventHandler(this, renderer, block, name, snippet, dependencies, valueFromDom);
// model to view
let updateDom = getDomUpdater(parent, this, snippet);
let initialUpdate = updateDom;
// special cases
if (this.node.name === 'group') {
const bindingGroup = getBindingGroup(renderer, this.node.value.node);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${parent.var});`
);
block.builders.destroy.addLine(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${parent.var}), 1);`
);
}
if (this.node.name === 'currentTime' || this.node.name === 'volume') {
updateConditions.push(`!isNaN(${snippet})`);
if (this.node.name === 'currentTime') initialUpdate = null;
}
if (this.node.name === 'paused') {
// this is necessary to prevent audio restarting by itself
const last = block.getUniqueName(`${parent.var}_is_paused`);
block.addVariable(last, 'true');
updateConditions.push(`${last} !== (${last} = ${snippet})`);
updateDom = `${parent.var}[${last} ? "pause" : "play"]();`;
initialUpdate = null;
}
// bind:offsetWidth and bind:offsetHeight
if (dimensions.test(this.node.name)) {
initialUpdate = null;
updateDom = null;
}
const dependencyArray = [...this.node.value.dependencies]
if (dependencyArray.length === 1) {
updateConditions.push(`changed.${dependencyArray[0]}`)
} else if (dependencyArray.length > 1) {
updateConditions.push(
`(${dependencyArray.map(prop => `changed.${prop}`).join(' || ')})`
)
}
return {
name: this.node.name,
object: name,
handler: handler,
usesContext: handler.usesContext,
updateDom: updateDom,
initialUpdate: initialUpdate,
needsLock: !isReadOnly && needsLock,
updateCondition: updateConditions.length ? updateConditions.join(' && ') : undefined,
isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute()
};
}
}
function getDomUpdater(
element: ElementWrapper,
binding: BindingWrapper,
snippet: string
) {
const { node } = element;
if (binding.isReadOnlyMediaAttribute()) {
return null;
}
if (node.name === 'select') {
return node.getStaticAttributeValue('multiple') === true ?
`@selectOptions(${element.var}, ${snippet})` :
`@selectOption(${element.var}, ${snippet})`;
}
if (binding.node.name === 'group') {
const type = node.getStaticAttributeValue('type');
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${element.var}.__value)`
: `${element.var}.__value === ${snippet}`;
return `${element.var}.checked = ${condition};`
}
return `${element.var}.${binding.node.name} = ${snippet};`;
}
function getBindingGroup(renderer: Renderer, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = renderer.bindingGroups.indexOf(keypath);
if (index === -1) {
index = renderer.bindingGroups.length;
renderer.bindingGroups.push(keypath);
}
return index;
}
function getEventHandler(
binding: BindingWrapper,
renderer: Renderer,
block: Block,
name: string,
snippet: string,
dependencies: Set<string>,
value: string
) {
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
let dependenciesArray = [...dependencies].filter(prop => prop[0] !== '$');
if (binding.node.isContextual) {
const tail = binding.node.value.node.type === 'MemberExpression'
? getTailSnippet(binding.node.value.node)
: '';
const head = block.bindings.get(name);
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${head()}${tail} = ${value};`,
props: dependenciesArray.map(prop => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
if (binding.node.value.node.type === 'MemberExpression') {
// This is a little confusing, and should probably be tidied up
// at some point. It addresses a tricky bug (#893), wherein
// Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b)
// we should probably populate `component.target.readonly` sooner so
// that we don't have to do the `.some()` here
dependenciesArray = dependenciesArray.filter(prop => !renderer.component.computations.some(computation => computation.key === prop));
return {
usesContext: false,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${snippet} = ${value}`,
props: dependenciesArray.map((prop: string) => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
let props;
let storeProps;
if (name[0] === '$') {
props = [];
storeProps = [`${name.slice(1)}: ${value}`];
} else {
props = [`${name}: ${value}`];
storeProps = [];
}
return {
usesContext: false,
usesState: false,
usesStore: false,
mutation: null,
props,
storeProps
};
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}
function getValueFromDom(
renderer: Renderer,
element: ElementWrapper,
binding: BindingWrapper
) {
const { node } = element;
const { name } = binding.node;
// <select bind:value='selected>
if (node.name === 'select') {
return node.getStaticAttributeValue('multiple') === true ?
`@selectMultipleValue(${element.var})` :
`@selectValue(${element.var})`;
}
const type = node.getStaticAttributeValue('type');
// <input type='checkbox' bind:group='foo'>
if (name === 'group') {
const bindingGroup = getBindingGroup(renderer, binding.node.value.node);
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}
return `${element.var}.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@toNumber(${element.var}.${name})`;
}
if ((name === 'buffered' || name === 'seekable' || name === 'played')) {
return `@timeRangesToArray(${element.var}.${name})`
}
// everything else
return `${element.var}.${name}`;
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -0,0 +1,178 @@
import Attribute from '../../../nodes/Attribute';
import Block from '../../Block';
import AttributeWrapper from './Attribute';
import Node from '../../../nodes/shared/Node';
import ElementWrapper from '.';
import { stringify } from '../../../../utils/stringify';
export interface StyleProp {
key: string;
value: Node[];
}
export default class StyleAttributeWrapper extends AttributeWrapper {
node: Attribute;
parent: ElementWrapper;
render(block: Block) {
const styleProps = optimizeStyle(this.node.chunks);
if (!styleProps) return super.render(block);
styleProps.forEach((prop: StyleProp) => {
let value;
if (isDynamic(prop.value)) {
const propDependencies = new Set();
let shouldCache;
value =
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
prop.value
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = chunk;
dependencies.forEach(d => {
propDependencies.add(d);
});
return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
if (propDependencies.size) {
const dependencies = Array.from(propDependencies);
const condition = (
(block.hasOutros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
block.builders.update.addConditional(
condition,
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
);
}
} else {
value = stringify(prop.value[0].data);
}
block.builders.hydrate.addLine(
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
);
});
}
}
function optimizeStyle(value: Node[]) {
const props: { key: string, value: Node[] }[] = [];
let chunks = value.slice();
while (chunks.length) {
const chunk = chunks[0];
if (chunk.type !== 'Text') return null;
const keyMatch = /^\s*([\w-]+):\s*/.exec(chunk.data);
if (!keyMatch) return null;
const key = keyMatch[1];
const offset = keyMatch.index + keyMatch[0].length;
const remainingData = chunk.data.slice(offset);
if (remainingData) {
chunks[0] = {
start: chunk.start + offset,
end: chunk.end,
type: 'Text',
data: remainingData
};
} else {
chunks.shift();
}
const result = getStyleValue(chunks);
if (!result) return null;
props.push({ key, value: result.value });
chunks = result.chunks;
}
return props;
}
function getStyleValue(chunks: Node[]) {
const value: Node[] = [];
let inUrl = false;
let quoteMark = null;
let escaped = false;
while (chunks.length) {
const chunk = chunks.shift();
if (chunk.type === 'Text') {
let c = 0;
while (c < chunk.data.length) {
const char = chunk.data[c];
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quoteMark) {
quoteMark === null;
} else if (char === '"' || char === "'") {
quoteMark = char;
} else if (char === ')' && inUrl) {
inUrl = false;
} else if (char === 'u' && chunk.data.slice(c, c + 4) === 'url(') {
inUrl = true;
} else if (char === ';' && !inUrl && !quoteMark) {
break;
}
c += 1;
}
if (c > 0) {
value.push({
type: 'Text',
start: chunk.start,
end: chunk.start + c,
data: chunk.data.slice(0, c)
});
}
while (/[;\s]/.test(chunk.data[c])) c += 1;
const remainingData = chunk.data.slice(c);
if (remainingData) {
chunks.unshift({
start: chunk.start + c,
end: chunk.end,
type: 'Text',
data: remainingData
});
break;
}
}
else {
value.push(chunk);
}
}
return {
chunks,
value
};
}
function isDynamic(value: Node[]) {
return value.length > 1 || value[0].type !== 'Text';
}

@ -0,0 +1,918 @@
import Renderer from '../../Renderer';
import Element from '../../../nodes/Element';
import Wrapper from '../shared/Wrapper';
import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
import { quotePropIfNecessary, quoteNameIfNecessary } from '../../../../utils/quoteIfNecessary';
import isVoidElementName from '../../../../utils/isVoidElementName';
import FragmentWrapper from '../Fragment';
import { stringify, escapeHTML, escape } from '../../../../utils/stringify';
import TextWrapper from '../Text';
import fixAttributeCasing from '../../../../utils/fixAttributeCasing';
import deindent from '../../../../utils/deindent';
import namespaces from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute';
import { dimensions } from '../../../../utils/patterns';
import Binding from './Binding';
import InlineComponentWrapper from '../InlineComponent';
const events = [
{
eventNames: ['input'],
filter: (node: Element, name: string) =>
node.name === 'textarea' ||
node.name === 'input' && !/radio|checkbox|range/.test(node.getStaticAttributeValue('type'))
},
{
eventNames: ['change'],
filter: (node: Element, name: string) =>
node.name === 'select' ||
node.name === 'input' && /radio|checkbox/.test(node.getStaticAttributeValue('type'))
},
{
eventNames: ['change', 'input'],
filter: (node: Element, name: string) =>
node.name === 'input' && node.getStaticAttributeValue('type') === 'range'
},
{
eventNames: ['resize'],
filter: (node: Element, name: string) =>
dimensions.test(name)
},
// media events
{
eventNames: ['timeupdate'],
filter: (node: Element, name: string) =>
node.isMediaNode() &&
(name === 'currentTime' || name === 'played')
},
{
eventNames: ['durationchange'],
filter: (node: Element, name: string) =>
node.isMediaNode() &&
name === 'duration'
},
{
eventNames: ['play', 'pause'],
filter: (node: Element, name: string) =>
node.isMediaNode() &&
name === 'paused'
},
{
eventNames: ['progress'],
filter: (node: Element, name: string) =>
node.isMediaNode() &&
name === 'buffered'
},
{
eventNames: ['loadedmetadata'],
filter: (node: Element, name: string) =>
node.isMediaNode() &&
(name === 'buffered' || name === 'seekable')
},
{
eventNames: ['volumechange'],
filter: (node: Element, name: string) =>
node.isMediaNode() &&
name === 'volume'
}
];
export default class ElementWrapper extends Wrapper {
node: Element;
fragment: FragmentWrapper;
attributes: AttributeWrapper[];
bindings: Binding[];
classDependencies: string[];
initialUpdate: string;
slotOwner?: InlineComponentWrapper;
selectBindingDependencies?: Set<string>;
var: string;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Element,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.var = node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
this.classDependencies = [];
this.attributes = this.node.attributes.map(attribute => {
if (attribute.name === 'slot') {
// TODO make separate subclass for this?
let owner = this.parent;
while (owner) {
if (owner.node.type === 'InlineComponent') {
break;
}
if (owner.node.type === 'Element' && /-/.test(owner.node.name)) {
break;
}
owner = owner.parent;
}
if (owner && owner.node.type === 'InlineComponent') {
this.slotOwner = <InlineComponentWrapper>owner;
owner._slots.add(attribute.getStaticValue());
}
}
if (attribute.name === 'style') {
return new StyleAttributeWrapper(this, block, attribute);
}
return new AttributeWrapper(this, block, attribute);
});
let has_bindings;
const binding_lookup = {};
this.node.bindings.forEach(binding => {
binding_lookup[binding.name] = binding;
has_bindings = true;
});
const type = this.node.getStaticAttributeValue('type');
// ordinarily, there'll only be one... but we need to handle
// the rare case where an element can have multiple bindings,
// e.g. <audio bind:paused bind:currentTime>
this.bindings = this.node.bindings.map(binding => new Binding(block, binding, this));
// TODO remove this, it's just useful during refactoring
if (has_bindings && !this.bindings.length) {
throw new Error(`no binding was created`);
}
if (node.intro || node.outro) {
if (node.intro) block.addIntro();
if (node.outro) block.addOutro();
}
if (node.animation) {
block.addAnimation();
}
// add directive and handler dependencies
[node.animation, node.outro, ...node.actions, ...node.classes].forEach(directive => {
if (directive && directive.expression) {
block.addDependencies(directive.expression.dependencies);
}
});
node.handlers.forEach(handler => {
block.addDependencies(handler.dependencies);
});
if (this.parent) {
if (node.actions.length > 0) this.parent.cannotUseInnerHTML();
if (node.animation) this.parent.cannotUseInnerHTML();
if (node.bindings.length > 0) this.parent.cannotUseInnerHTML();
if (node.classes.length > 0) this.parent.cannotUseInnerHTML();
if (node.intro || node.outro) this.parent.cannotUseInnerHTML();
if (node.handlers.length > 0) this.parent.cannotUseInnerHTML();
if (node.ref) this.parent.cannotUseInnerHTML();
if (this.node.name === 'option') this.parent.cannotUseInnerHTML();
if (renderer.options.dev) {
this.parent.cannotUseInnerHTML(); // need to use addLoc
}
}
this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling);
}
render(block: Block, parentNode: string, parentNodes: string) {
const { renderer } = this;
if (this.node.name === 'slot') {
const slotName = this.getStaticAttributeValue('name') || 'default';
renderer.slots.add(slotName);
}
if (this.node.name === 'noscript') return;
const node = this.var;
const nodes = parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parentNodes is null
const slot = this.node.attributes.find((attribute: Node) => attribute.name === 'slot');
const prop = slot && quotePropIfNecessary(slot.chunks[0].data);
let initialMountNode;
if (this.slotOwner) {
initialMountNode = `${this.slotOwner.var}._slotted${prop}`;
} else {
initialMountNode = parentNode;
}
block.addVariable(node);
const renderStatement = this.getRenderStatement();
block.builders.create.addLine(
`${node} = ${renderStatement};`
);
if (renderer.options.hydratable) {
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${node} = ${this.getClaimStatement(parentNodes)};
var ${nodes} = @children(${this.node.name === 'template' ? `${node}.content` : node});
`);
} else {
block.builders.claim.addLine(
`${node} = ${renderStatement};`
);
}
}
if (initialMountNode) {
block.builders.mount.addLine(
`@append(${initialMountNode}, ${node});`
);
if (initialMountNode === 'document.head') {
block.builders.destroy.addLine(`@detachNode(${node});`);
}
} else {
block.builders.mount.addLine(`@insert(#target, ${node}, anchor);`);
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.destroy.addConditional('detach', `@detachNode(${node});`);
}
// insert static children with textContent or innerHTML
if (!this.node.namespace && this.canUseInnerHTML && this.fragment.nodes.length > 0) {
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
block.builders.create.addLine(
`${node}.textContent = ${stringify(this.fragment.nodes[0].data)};`
);
} else {
const innerHTML = escape(
this.fragment.nodes
.map(toHTML)
.join('')
);
block.builders.create.addLine(
`${node}.innerHTML = \`${innerHTML}\`;`
);
}
} else {
this.fragment.nodes.forEach((child: Wrapper) => {
child.render(
block,
this.node.name === 'template' ? `${node}.content` : node,
nodes
);
});
}
let hasHoistedEventHandlerOrBinding = (
//(this.hasAncestor('EachBlock') && this.bindings.length > 0) ||
this.node.handlers.some(handler => handler.shouldHoist)
);
const eventHandlerOrBindingUsesComponent = (
this.bindings.length > 0 ||
this.node.handlers.some(handler => handler.usesComponent)
);
const eventHandlerOrBindingUsesContext = (
this.bindings.some(binding => binding.node.usesContext) ||
this.node.handlers.some(handler => handler.usesContext)
);
if (hasHoistedEventHandlerOrBinding) {
const initialProps: string[] = [];
const updates: string[] = [];
if (eventHandlerOrBindingUsesComponent) {
const component = block.alias('component');
initialProps.push(component === 'component' ? 'component' : `component: ${component}`);
}
if (eventHandlerOrBindingUsesContext) {
initialProps.push(`ctx`);
block.builders.update.addLine(`${node}._svelte.ctx = ctx;`);
block.maintainContext = true;
}
if (initialProps.length) {
block.builders.hydrate.addBlock(deindent`
${node}._svelte = { ${initialProps.join(', ')} };
`);
}
} else {
if (eventHandlerOrBindingUsesContext) {
block.maintainContext = true;
}
}
this.addBindings(block);
this.addEventHandlers(block);
if (this.node.ref) this.addRef(block);
this.addAttributes(block);
this.addTransitions(block);
this.addAnimation(block);
this.addActions(block);
this.addClasses(block);
if (this.initialUpdate) {
block.builders.mount.addBlock(this.initialUpdate);
}
if (nodes) {
block.builders.claim.addLine(
`${nodes}.forEach(@detachNode);`
);
}
function toHTML(wrapper: ElementWrapper | TextWrapper) {
if (wrapper.node.type === 'Text') {
const { parent } = wrapper.node;
const raw = parent && (
parent.name === 'script' ||
parent.name === 'style'
);
return raw
? wrapper.node.data
: escapeHTML(wrapper.node.data)
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
}
if (wrapper.node.name === 'noscript') return '';
let open = `<${wrapper.node.name}`;
(<ElementWrapper>wrapper).attributes.forEach((attr: AttributeWrapper) => {
open += ` ${fixAttributeCasing(attr.node.name)}${attr.stringify()}`
});
if (isVoidElementName(wrapper.node.name)) return open + '>';
return `${open}>${wrapper.fragment.nodes.map(toHTML).join('')}</${wrapper.node.name}>`;
}
if (renderer.options.dev) {
const loc = renderer.locate(this.node.start);
block.builders.hydrate.addLine(
`@addLoc(${this.var}, ${renderer.fileVar}, ${loc.line}, ${loc.column}, ${this.node.start});`
);
}
}
getRenderStatement() {
const { name, namespace } = this.node;
if (namespace === 'http://www.w3.org/2000/svg') {
return `@createSvgElement("${name}")`;
}
if (namespace) {
return `document.createElementNS("${namespace}", "${name}")`;
}
return `@createElement("${name}")`;
}
getClaimStatement(nodes: string) {
const attributes = this.node.attributes
.filter((attr: Node) => attr.type === 'Attribute')
.map((attr: Node) => `${quoteNameIfNecessary(attr.name)}: true`)
.join(', ');
const name = this.node.namespace
? this.node.name
: this.node.name.toUpperCase();
return `@claimElement(${nodes}, "${name}", ${attributes
? `{ ${attributes} }`
: `{}`}, ${this.node.namespace === namespaces.svg ? true : false})`;
}
addBindings(block: Block) {
const { renderer } = this;
if (this.bindings.length === 0) return;
if (this.node.name === 'select' || this.isMediaNode()) {
this.renderer.hasComplexBindings = true;
}
const needsLock = this.node.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
// TODO munge in constructor
const mungedBindings = this.bindings.map(binding => binding.munge(block));
const lock = mungedBindings.some(binding => binding.needsLock) ?
block.getUniqueName(`${this.var}_updating`) :
null;
if (lock) block.addVariable(lock, 'false');
const groups = events
.map(event => {
return {
events: event.eventNames,
bindings: mungedBindings.filter(binding => event.filter(this.node, binding.name))
};
})
.filter(group => group.bindings.length);
groups.forEach(group => {
const handler = block.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);
const needsLock = group.bindings.some(binding => binding.needsLock);
group.bindings.forEach(binding => {
if (!binding.updateDom) return;
const updateConditions = needsLock ? [`!${lock}`] : [];
if (binding.updateCondition) updateConditions.push(binding.updateCondition);
block.builders.update.addLine(
updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom
);
});
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
const props = new Set();
const storeProps = new Set();
group.bindings.forEach(binding => {
binding.handler.props.forEach(prop => {
props.add(prop);
});
binding.handler.storeProps.forEach(prop => {
storeProps.add(prop);
});
}); // TODO use stringifyProps here, once indenting is fixed
// media bindings — awkward special case. The native timeupdate events
// fire too infrequently, so we need to take matters into our
// own hands
let animation_frame;
if (group.events[0] === 'timeupdate') {
animation_frame = block.getUniqueName(`${this.var}_animationframe`);
block.addVariable(animation_frame);
}
block.builders.init.addBlock(deindent`
function ${handler}() {
${
animation_frame && deindent`
cancelAnimationFrame(${animation_frame});
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
}
${usesStore && `var $ = #component.store.get();`}
${needsLock && `${lock} = true;`}
${mutations.length > 0 && mutations}
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
${needsLock && `${lock} = false;`}
}
`);
group.events.forEach(name => {
if (name === 'resize') {
// special case
const resize_listener = block.getUniqueName(`${this.var}_resize_listener`);
block.addVariable(resize_listener);
block.builders.mount.addLine(
`${resize_listener} = @addResizeListener(${this.var}, ${handler});`
);
block.builders.destroy.addLine(
`${resize_listener}.cancel();`
);
} else {
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${name}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${name}", ${handler});`
);
}
});
const allInitialStateIsDefined = group.bindings
.map(binding => `'${binding.object}' in ctx`)
.join(' && ');
if (this.node.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
renderer.hasComplexBindings = true;
block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
);
}
if (group.events[0] === 'resize') {
renderer.hasComplexBindings = true;
block.builders.hydrate.addLine(
`#component.root._beforecreate.push(${handler});`
);
}
});
this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
}
addAttributes(block: Block) {
if (this.node.attributes.find(attr => attr.type === 'Spread')) {
this.addSpreadAttributes(block);
return;
}
this.attributes.forEach((attribute: Attribute) => {
if (attribute.node.name === 'class' && attribute.node.isDynamic) {
this.classDependencies.push(...attribute.node.dependencies);
}
attribute.render(block);
});
}
addSpreadAttributes(block: Block) {
const levels = block.getUniqueName(`${this.var}_levels`);
const data = block.getUniqueName(`${this.var}_data`);
const initialProps = [];
const updates = [];
this.node.attributes
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
.forEach(attr => {
const condition = attr.dependencies.size > 0
? `(${[...attr.dependencies].map(d => `changed.${d}`).join(' || ')})`
: null;
if (attr.isSpread) {
const { snippet, dependencies } = attr.expression;
initialProps.push(snippet);
updates.push(condition ? `${condition} && ${snippet}` : snippet);
} else {
const snippet = `{ ${quoteNameIfNecessary(attr.name)}: ${attr.getValue()} }`;
initialProps.push(snippet);
updates.push(condition ? `${condition} && ${snippet}` : snippet);
}
});
block.builders.init.addBlock(deindent`
var ${levels} = [
${initialProps.join(',\n')}
];
var ${data} = {};
for (var #i = 0; #i < ${levels}.length; #i += 1) {
${data} = @assign(${data}, ${levels}[#i]);
}
`);
block.builders.hydrate.addLine(
`@setAttributes(${this.var}, ${data});`
);
block.builders.update.addBlock(deindent`
@setAttributes(${this.var}, @getSpreadUpdate(${levels}, [
${updates.join(',\n')}
]));
`);
}
addEventHandlers(block: Block) {
const { renderer } = this;
const { component } = renderer;
this.node.handlers.forEach(handler => {
const isCustomEvent = component.events.has(handler.name);
if (handler.callee) {
// TODO move handler render method into a wrapper
handler.render(this.renderer.component, block, this.var, handler.shouldHoist);
}
const target = handler.shouldHoist ? 'this' : this.var;
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (handler.shouldHoist ? component : block).getUniqueName(
`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
const component_name = block.alias('component'); // can't use #component, might be hoisted
// create the handler body
const handlerBody = deindent`
${handler.shouldHoist && (
handler.usesComponent || handler.usesContext
? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;`
: null
)}
${handler.snippet ?
handler.snippet :
`${component_name}.fire("${handler.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(${component_name}, ${this.var}, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.destroy();
`);
} else {
const handlerFunction = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (handler.shouldHoist) {
renderer.blocks.push(handlerFunction);
} else {
block.builders.init.addBlock(handlerFunction);
}
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
);
}
});
}
addRef(block: Block) {
const ref = `#component.refs.${this.node.ref.name}`;
block.builders.mount.addLine(
`${ref} = ${this.var};`
);
block.builders.destroy.addLine(
`if (${ref} === ${this.var}) ${ref} = null;`
);
}
addTransitions(
block: Block
) {
const { intro, outro } = this.node;
if (!intro && !outro) return;
if (intro === outro) {
const name = block.getUniqueName(`${this.var}_transition`);
const snippet = intro.expression
? intro.expression.snippet
: '{}';
block.addVariable(name);
const fn = `%transitions-${intro.name}`;
block.builders.intro.addConditional(`#component.root._intro`, deindent`
if (${name}) ${name}.invalidate();
#component.root._aftercreate.push(() => {
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
${name}.run(1);
});
`);
block.builders.outro.addBlock(deindent`
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false);
${name}.run(0, () => {
#outrocallback();
${name} = null;
});
`);
block.builders.destroy.addConditional('detach', `if (${name}) ${name}.abort();`);
} else {
const introName = intro && block.getUniqueName(`${this.var}_intro`);
const outroName = outro && block.getUniqueName(`${this.var}_outro`);
if (intro) {
block.addVariable(introName);
const snippet = intro.expression
? intro.expression.snippet
: '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
if (outro) {
block.builders.intro.addBlock(deindent`
if (${introName}) ${introName}.abort(1);
if (${outroName}) ${outroName}.abort(1);
`);
}
block.builders.intro.addConditional(`#component.root._intro`, deindent`
#component.root._aftercreate.push(() => {
${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
${introName}.run(1);
});
`);
}
if (outro) {
block.addVariable(outroName);
const snippet = outro.expression
? outro.expression.snippet
: '{}';
const fn = `%transitions-${outro.name}`;
block.builders.intro.addBlock(deindent`
if (${outroName}) ${outroName}.abort(1);
`);
// TODO hide elements that have outro'd (unless they belong to a still-outroing
// group) prior to their removal from the DOM
block.builders.outro.addBlock(deindent`
${outroName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false);
${outroName}.run(0, #outrocallback);
`);
block.builders.destroy.addConditional('detach', `if (${outroName}) ${outroName}.abort();`);
}
}
}
addAnimation(block: Block) {
if (!this.node.animation) return;
const rect = block.getUniqueName('rect');
const animation = block.getUniqueName('animation');
block.addVariable(rect);
block.addVariable(animation);
block.builders.measure.addBlock(deindent`
${rect} = ${this.var}.getBoundingClientRect();
`);
block.builders.fix.addBlock(deindent`
@fixPosition(${this.var});
if (${animation}) ${animation}.stop();
`);
const params = this.node.animation.expression ? this.node.animation.expression.snippet : '{}';
block.builders.animate.addBlock(deindent`
if (${animation}) ${animation}.stop();
${animation} = @wrapAnimation(${this.var}, ${rect}, %animations-${this.node.animation.name}, ${params});
`);
}
addActions(block: Block) {
this.node.actions.forEach(action => {
const { expression } = action;
let snippet, dependencies;
if (expression) {
snippet = expression.snippet;
dependencies = expression.dependencies;
}
const name = block.getUniqueName(
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
);
block.addVariable(name);
const fn = `%actions-${action.name}`;
block.builders.mount.addLine(
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
);
if (dependencies && dependencies.size > 0) {
let conditional = `typeof ${name}.update === 'function' && `;
const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.size > 1 ? `(${deps})` : deps;
block.builders.update.addConditional(
conditional,
`${name}.update.call(#component, ${snippet});`
);
}
block.builders.destroy.addLine(
`if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy.call(#component);`
);
});
}
addClasses(block: Block) {
this.node.classes.forEach(classDir => {
const { expression, name } = classDir;
let snippet, dependencies;
if (expression) {
snippet = expression.snippet;
dependencies = expression.dependencies;
} else {
snippet = `ctx${quotePropIfNecessary(name)}`;
dependencies = new Set([name]);
}
const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`;
block.builders.hydrate.addLine(updater);
if ((dependencies && dependencies.size > 0) || this.classDependencies.length) {
const allDeps = this.classDependencies.concat(...dependencies);
const deps = allDeps.map(dependency => `changed${quotePropIfNecessary(dependency)}`).join(' || ');
const condition = allDeps.length > 1 ? `(${deps})` : deps;
block.builders.update.addConditional(
condition,
updater
);
}
});
}
getStaticAttributeValue(name: string) {
const attribute = this.node.attributes.find(
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
);
if (!attribute) return null;
if (attribute.isTrue) return true;
if (attribute.chunks.length === 0) return '';
if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
return attribute.chunks[0].data;
}
return null;
}
isMediaNode() {
return this.node.name === 'audio' || this.node.name === 'video';
}
remount(name: string) {
const slot = this.attributes.find(attribute => attribute.name === 'slot');
if (slot) {
const prop = quotePropIfNecessary(slot.chunks[0].data);
return `@append(${name}._slotted${prop}, ${this.var});`;
}
return `@append(${name}._slotted.default, ${this.var});`;
}
addCssClass(className = this.component.stylesheet.id) {
const classAttribute = this.attributes.find(a => a.name === 'class');
if (classAttribute && !classAttribute.isTrue) {
if (classAttribute.chunks.length === 1 && classAttribute.chunks[0].type === 'Text') {
(<Text>classAttribute.chunks[0]).data += ` ${className}`;
} else {
(<Node[]>classAttribute.chunks).push(
new Text(this.component, this, this.scope, {
type: 'Text',
data: ` ${className}`
})
);
}
} else {
this.attributes.push(
new Attribute(this.component, this, this.scope, {
type: 'Attribute',
name: 'class',
value: [{ type: 'Text', data: className }]
})
);
}
}
}

@ -0,0 +1,140 @@
import Wrapper from './shared/Wrapper';
import AwaitBlock from './AwaitBlock';
import DebugTag from './DebugTag';
import EachBlock from './EachBlock';
import Element from './Element';
import Head from './Head';
import IfBlock from './IfBlock';
import InlineComponent from './InlineComponent';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import Text from './Text';
import Title from './Title';
import Window from './Window';
import Node from '../../nodes/shared/Node';
import { trimStart, trimEnd } from '../../../utils/trim';
import TextWrapper from './Text';
import Renderer from '../Renderer';
import Block from '../Block';
const wrappers = {
AwaitBlock,
Comment: null,
DebugTag,
EachBlock,
Element,
Head,
IfBlock,
InlineComponent,
MustacheTag,
RawMustacheTag,
Slot,
Text,
Title,
Window
};
function link(next: Wrapper, prev: Wrapper) {
prev.next = next;
if (next) next.prev = prev;
}
export default class FragmentWrapper {
nodes: Wrapper[];
constructor(
renderer: Renderer,
block: Block,
nodes: Node[],
parent: Wrapper,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
this.nodes = [];
let lastChild: Wrapper;
let windowWrapper;
let i = nodes.length;
while (i--) {
const child = nodes[i];
if (!(child.type in wrappers)) {
throw new Error(`TODO implement ${child.type}`);
}
// special case — this is an easy way to remove whitespace surrounding
// <svelte:window/>. lil hacky but it works
if (child.type === 'Window') {
windowWrapper = new Window(renderer, block, parent, child);
continue;
}
if (child.type === 'Text') {
let { data } = child;
// We want to remove trailing whitespace inside an element/component/block,
// *unless* there is no whitespace between this node and its next sibling
if (this.nodes.length === 0) {
const shouldTrim = (
nextSibling ? (nextSibling.node.type === 'Text' && /^\s/.test(nextSibling.data)) : !child.hasAncestor('EachBlock')
);
if (shouldTrim) {
data = trimEnd(data);
if (!data) continue;
}
}
// glue text nodes (which could e.g. be separated by comments) together
if (lastChild && lastChild.node.type === 'Text') {
lastChild.data = data + lastChild.data;
continue;
}
const wrapper = new TextWrapper(renderer, block, parent, child, data);
if (wrapper.skip) continue;
this.nodes.unshift(wrapper);
link(lastChild, lastChild = wrapper);
} else {
const Wrapper = wrappers[child.type];
if (!Wrapper) continue;
const wrapper = new Wrapper(renderer, block, parent, child, stripWhitespace, lastChild || nextSibling);
this.nodes.unshift(wrapper);
link(lastChild, lastChild = wrapper);
}
}
if (stripWhitespace) {
const first = <TextWrapper>this.nodes[0];
if (first && first.node.type === 'Text') {
first.data = trimStart(first.data);
if (!first.data) {
first.var = null;
this.nodes.shift();
if (this.nodes[0]) {
this.nodes[0].prev = null;
}
}
}
}
if (windowWrapper) {
this.nodes.unshift(windowWrapper);
link(lastChild, windowWrapper);
}
}
render(block: Block, parentNode: string, parentNodes: string) {
for (let i = 0; i < this.nodes.length; i += 1) {
this.nodes[i].render(block, parentNode, parentNodes);
}
}
}

@ -0,0 +1,33 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import Head from '../../nodes/Head';
import FragmentWrapper from './Fragment';
export default class HeadWrapper extends Wrapper {
fragment: FragmentWrapper;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Head,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.fragment = new FragmentWrapper(
renderer,
block,
node.children,
this,
stripWhitespace,
nextSibling
);
}
render(block: Block, parentNode: string, parentNodes: string) {
this.fragment.render(block, 'document.head', null);
}
}

@ -0,0 +1,473 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import EachBlock from '../../nodes/EachBlock';
import IfBlock from '../../nodes/IfBlock';
import createDebuggingComment from '../../../utils/createDebuggingComment';
import ElseBlock from '../../nodes/ElseBlock';
import FragmentWrapper from './Fragment';
import deindent from '../../../utils/deindent';
function isElseIf(node: ElseBlock) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
class IfBlockBranch extends Wrapper {
block: Block;
fragment: FragmentWrapper;
condition: string;
isDynamic: boolean;
var = null;
constructor(
renderer: Renderer,
block: Block,
parent: IfBlockWrapper,
node: IfBlock | ElseBlock,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.condition = (<IfBlock>node).expression && (<IfBlock>node).expression.snippet;
this.block = block.child({
comment: createDebuggingComment(node, parent.renderer.component),
name: parent.renderer.component.getUniqueName(
(<IfBlock>node).expression ? `create_if_block` : `create_else_block`
)
});
this.fragment = new FragmentWrapper(renderer, this.block, node.children, parent, stripWhitespace, nextSibling);
this.isDynamic = this.block.dependencies.size > 0;
}
}
export default class IfBlockWrapper extends Wrapper {
node: IfBlock;
branches: IfBlockBranch[];
var = 'if_block';
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EachBlock,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
const { component } = renderer;
this.cannotUseInnerHTML();
this.branches = [];
const blocks: Block[] = [];
let isDynamic = false;
let hasIntros = false;
let hasOutros = false;
const createBranches = (node: IfBlock) => {
const branch = new IfBlockBranch(
renderer,
block,
this,
node,
stripWhitespace,
nextSibling
);
this.branches.push(branch);
blocks.push(branch.block);
block.addDependencies(node.expression.dependencies);
if (branch.block.dependencies.size > 0) {
isDynamic = true;
block.addDependencies(branch.block.dependencies);
}
if (branch.block.hasIntros) hasIntros = true;
if (branch.block.hasOutros) hasOutros = true;
if (isElseIf(node.else)) {
createBranches(node.else.children[0]);
} else if (node.else) {
const branch = new IfBlockBranch(
renderer,
block,
this,
node.else,
stripWhitespace,
nextSibling
);
this.branches.push(branch);
blocks.push(branch.block);
if (branch.block.dependencies.size > 0) {
isDynamic = true;
block.addDependencies(branch.block.dependencies);
}
if (branch.block.hasIntros) hasIntros = true;
if (branch.block.hasOutros) hasOutros = true;
}
};
createBranches(this.node);
if (component.options.nestedTransitions) {
if (hasIntros) block.addIntro();
if (hasOutros) block.addOutro();
}
blocks.forEach(block => {
block.hasUpdateMethod = isDynamic;
block.hasIntroMethod = hasIntros;
block.hasOutroMethod = hasOutros;
});
renderer.blocks.push(...blocks);
}
render(
block: Block,
parentNode: string,
parentNodes: string
) {
const name = this.var;
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (this.next && this.next.var) || 'null';
const hasElse = !(this.branches[this.branches.length - 1].condition);
const if_name = hasElse ? '' : `if (${name}) `;
const dynamic = this.branches[0].block.hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
const hasOutros = this.branches[0].block.hasOutroMethod;
const vars = { name, anchor, if_name, hasElse };
if (this.node.else) {
if (hasOutros) {
this.renderCompoundWithOutros(block, parentNode, parentNodes, dynamic, vars);
if (this.renderer.options.nestedTransitions) {
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
`);
}
} else {
this.renderCompound(block, parentNode, parentNodes, dynamic, vars);
}
} else {
this.renderSimple(block, parentNode, parentNodes, dynamic, vars);
if (hasOutros && this.renderer.options.nestedTransitions) {
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
`);
}
}
block.builders.create.addLine(`${if_name}${name}.c();`);
if (parentNodes) {
block.builders.claim.addLine(
`${if_name}${name}.l(${parentNodes});`
);
}
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
this.branches.forEach(branch => {
branch.fragment.render(branch.block, null, 'nodes');
});
}
renderCompound(
block: Block,
parentNode: string,
parentNodes: string,
dynamic,
{ name, anchor, hasElse, if_name }
) {
const select_block_type = this.renderer.component.getUniqueName(`select_block_type`);
const current_block_type = block.getUniqueName(`current_block_type`);
const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
block.builders.init.addBlock(deindent`
function ${select_block_type}(ctx) {
${this.branches
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block.name};`)
.join('\n')}
}
`);
block.builders.init.addBlock(deindent`
var ${current_block_type} = ${select_block_type}(ctx);
var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
`);
const mountOrIntro = this.branches[0].block.hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_name}${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const changeBlock = deindent`
${if_name}${name}.d(1);
${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
${if_name}${name}.c();
${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
`;
if (dynamic) {
block.builders.update.addBlock(deindent`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) {
${name}.p(changed, ctx);
} else {
${changeBlock}
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) {
${changeBlock}
}
`);
}
block.builders.destroy.addLine(`${if_name}${name}.d(${parentNode ? '' : 'detach'});`);
}
// if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?)
renderCompoundWithOutros(
block: Block,
parentNode: string,
parentNodes: string,
dynamic,
{ name, anchor, hasElse }
) {
const select_block_type = this.renderer.component.getUniqueName(`select_block_type`);
const current_block_type_index = block.getUniqueName(`current_block_type_index`);
const previous_block_index = block.getUniqueName(`previous_block_index`);
const if_block_creators = block.getUniqueName(`if_block_creators`);
const if_blocks = block.getUniqueName(`if_blocks`);
const if_current_block_type_index = hasElse
? ''
: `if (~${current_block_type_index}) `;
block.addVariable(current_block_type_index);
block.addVariable(name);
block.builders.init.addBlock(deindent`
var ${if_block_creators} = [
${this.branches.map(branch => branch.block.name).join(',\n')}
];
var ${if_blocks} = [];
function ${select_block_type}(ctx) {
${this.branches
.map(({ condition }, i) => `${condition ? `if (${condition}) ` : ''}return ${i};`)
.join('\n')}
${!hasElse && `return -1;`}
}
`);
if (hasElse) {
block.builders.init.addBlock(deindent`
${current_block_type_index} = ${select_block_type}(ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
`);
} else {
block.builders.init.addBlock(deindent`
if (~(${current_block_type_index} = ${select_block_type}(ctx))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
}
`);
}
const mountOrIntro = this.branches[0].block.hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_current_block_type_index}${if_blocks}[${current_block_type_index}].${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const destroyOldBlock = deindent`
@groupOutros();
${name}.o(function() {
${if_blocks}[${previous_block_index}].d(1);
${if_blocks}[${previous_block_index}] = null;
});
`;
const createNewBlock = deindent`
${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
${name}.c();
}
${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
`;
const changeBlock = hasElse
? deindent`
${destroyOldBlock}
${createNewBlock}
`
: deindent`
if (${name}) {
${destroyOldBlock}
}
if (~${current_block_type_index}) {
${createNewBlock}
} else {
${name} = null;
}
`;
if (dynamic) {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
} else {
${changeBlock}
}
`);
} else {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} !== ${previous_block_index}) {
${changeBlock}
}
`);
}
block.builders.destroy.addLine(deindent`
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].d(${parentNode ? '' : 'detach'});
`);
}
renderSimple(
block: Block,
parentNode: string,
parentNodes: string,
dynamic,
{ name, anchor, if_name }
) {
const branch = this.branches[0];
block.builders.init.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block.name}(#component, ctx);
`);
const mountOrIntro = branch.block.hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`if (${name}) ${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const enter = dynamic
? (branch.block.hasIntroMethod || branch.block.hasOutroMethod)
? deindent`
if (${name}) {
${name}.p(changed, ctx);
} else {
${name} = ${branch.block.name}(#component, ctx);
if (${name}) ${name}.c();
}
${name}.i(${updateMountNode}, ${anchor});
`
: deindent`
if (${name}) {
${name}.p(changed, ctx);
} else {
${name} = ${branch.block.name}(#component, ctx);
${name}.c();
${name}.m(${updateMountNode}, ${anchor});
}
`
: (branch.block.hasIntroMethod || branch.block.hasOutroMethod)
? deindent`
if (!${name}) {
${name} = ${branch.block.name}(#component, ctx);
${name}.c();
}
${name}.i(${updateMountNode}, ${anchor});
`
: deindent`
if (!${name}) {
${name} = ${branch.block.name}(#component, ctx);
${name}.c();
${name}.m(${updateMountNode}, ${anchor});
}
`;
// no `p()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
const exit = branch.block.hasOutroMethod
? deindent`
@groupOutros();
${name}.o(function() {
${name}.d(1);
${name} = null;
});
`
: deindent`
${name}.d(1);
${name} = null;
`;
block.builders.update.addBlock(deindent`
if (${branch.condition}) {
${enter}
} else if (${name}) {
${exit}
}
`);
block.builders.destroy.addLine(`${if_name}${name}.d(${parentNode ? '' : 'detach'});`);
}
}

@ -0,0 +1,478 @@
import Wrapper from '../shared/Wrapper';
import Renderer from '../../Renderer';
import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
import InlineComponent from '../../../nodes/InlineComponent';
import FragmentWrapper from '../Fragment';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../../utils/quoteIfNecessary';
import stringifyProps from '../../../../utils/stringifyProps';
import addToSet from '../../../../utils/addToSet';
import deindent from '../../../../utils/deindent';
import Attribute from '../../../nodes/Attribute';
import CodeBuilder from '../../../../utils/CodeBuilder';
import getObject from '../../../../utils/getObject';
import Binding from '../../../nodes/Binding';
import getTailSnippet from '../../../../utils/getTailSnippet';
export default class InlineComponentWrapper extends Wrapper {
var: string;
_slots: Set<string>; // TODO lose the underscore
node: InlineComponent;
fragment: FragmentWrapper;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: InlineComponent,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
if (this.node.expression) {
block.addDependencies(this.node.expression.dependencies);
}
this.node.attributes.forEach(attr => {
block.addDependencies(attr.dependencies);
});
this.node.bindings.forEach(binding => {
block.addDependencies(binding.value.dependencies);
});
this.node.handlers.forEach(handler => {
block.addDependencies(handler.dependencies);
});
this.var = (
this.node.name === 'svelte:self' ? renderer.component.name :
this.node.name === 'svelte:component' ? 'switch_instance' :
this.node.name
).toLowerCase();
if (this.node.children.length) {
this._slots = new Set(['default']);
this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling);
}
if (renderer.component.options.nestedTransitions) {
block.addOutro();
}
}
render(
block: Block,
parentNode: string,
parentNodes: string
) {
const { renderer } = this;
const { component } = renderer;
const name = this.var;
const componentInitProperties = [
`root: #component.root`,
`store: #component.store`
];
if (this.fragment) {
const slots = Array.from(this._slots).map(name => `${quoteNameIfNecessary(name)}: @createFragment()`);
componentInitProperties.push(`slots: { ${slots.join(', ')} }`);
this.fragment.nodes.forEach((child: Wrapper) => {
child.render(block, `${this.var}._slotted.default`, 'nodes');
});
}
const statements: string[] = [];
const name_initial_data = block.getUniqueName(`${name}_initial_data`);
const name_changes = block.getUniqueName(`${name}_changes`);
let name_updating: string;
let beforecreate: string = null;
const updates: string[] = [];
const usesSpread = !!this.node.attributes.find(a => a.isSpread);
const attributeObject = usesSpread
? '{}'
: stringifyProps(
this.node.attributes.map(attr => `${quoteNameIfNecessary(attr.name)}: ${attr.getValue()}`)
);
if (this.node.attributes.length || this.node.bindings.length) {
componentInitProperties.push(`data: ${name_initial_data}`);
}
if (!usesSpread && (this.node.attributes.filter(a => a.isDynamic).length || this.node.bindings.length)) {
updates.push(`var ${name_changes} = {};`);
}
if (this.node.attributes.length) {
if (usesSpread) {
const levels = block.getUniqueName(`${this.var}_spread_levels`);
const initialProps = [];
const changes = [];
const allDependencies = new Set();
this.node.attributes.forEach(attr => {
addToSet(allDependencies, attr.dependencies);
});
this.node.attributes.forEach(attr => {
const { name, dependencies } = attr;
const condition = dependencies.size > 0 && (dependencies.size !== allDependencies.size)
? `(${[...dependencies].map(d => `changed.${d}`).join(' || ')})`
: null;
if (attr.isSpread) {
const value = attr.expression.snippet;
initialProps.push(value);
changes.push(condition ? `${condition} && ${value}` : value);
} else {
const obj = `{ ${quoteNameIfNecessary(name)}: ${attr.getValue()} }`;
initialProps.push(obj);
changes.push(condition ? `${condition} && ${obj}` : obj);
}
});
block.builders.init.addBlock(deindent`
var ${levels} = [
${initialProps.join(',\n')}
];
`);
statements.push(deindent`
for (var #i = 0; #i < ${levels}.length; #i += 1) {
${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]);
}
`);
const conditions = [...allDependencies].map(dep => `changed.${dep}`).join(' || ');
updates.push(deindent`
var ${name_changes} = ${allDependencies.size === 1 ? `${conditions}` : `(${conditions})`} ? @getSpreadUpdate(${levels}, [
${changes.join(',\n')}
]) : {};
`);
} else {
this.node.attributes
.filter((attribute: Attribute) => attribute.isDynamic)
.forEach((attribute: Attribute) => {
if (attribute.dependencies.size > 0) {
updates.push(deindent`
if (${[...attribute.dependencies]
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name_changes}${quotePropIfNecessary(attribute.name)} = ${attribute.getValue()};
`);
}
});
}
}
if (this.node.bindings.length) {
renderer.hasComplexBindings = true;
name_updating = block.alias(`${name}_updating`);
block.addVariable(name_updating, '{}');
let hasLocalBindings = false;
let hasStoreBindings = false;
const builder = new CodeBuilder();
this.node.bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value.node);
let setFromChild;
if (binding.isContextual) {
const computed = isComputed(binding.value.node);
const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : '';
const head = block.bindings.get(key);
const lhs = binding.value.node.type === 'MemberExpression'
? binding.value.snippet
: `${head()}${tail} = childState${quotePropIfNecessary(binding.name)}`;
setFromChild = deindent`
${lhs} = childState${quotePropIfNecessary(binding.name)};
${[...binding.value.dependencies]
.map((name: string) => {
const isStoreProp = name[0] === '$';
const prop = isStoreProp ? name.slice(1) : name;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
return `${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(name)};`;
})}
`;
}
else {
const isStoreProp = key[0] === '$';
const prop = isStoreProp ? key.slice(1) : key;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
if (binding.value.node.type === 'MemberExpression') {
setFromChild = deindent`
${binding.value.snippet} = childState${quotePropIfNecessary(binding.name)};
${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(key)};
`;
}
else {
setFromChild = `${newState}${quotePropIfNecessary(prop)} = childState${quotePropIfNecessary(binding.name)};`;
}
}
statements.push(deindent`
if (${binding.value.snippet} !== void 0) {
${name_initial_data}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet};
${name_updating}${quotePropIfNecessary(binding.name)} = true;
}`
);
builder.addConditional(
`!${name_updating}${quotePropIfNecessary(binding.name)} && changed${quotePropIfNecessary(binding.name)}`,
setFromChild
);
updates.push(deindent`
if (!${name_updating}${quotePropIfNecessary(binding.name)} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet};
${name_updating}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet} !== void 0;
}
`);
});
block.maintainContext = true; // TODO put this somewhere more logical
const initialisers = [
hasLocalBindings && 'newState = {}',
hasStoreBindings && 'newStoreState = {}',
].filter(Boolean).join(', ');
// TODO use component.on('state', ...) instead of _bind
componentInitProperties.push(deindent`
_bind(changed, childState) {
var ${initialisers};
${builder}
${hasStoreBindings && `#component.store.set(newStoreState);`}
${hasLocalBindings && `#component._set(newState);`}
${name_updating} = {};
}
`);
beforecreate = deindent`
#component.root._beforecreate.push(() => {
${name}._bind({ ${this.node.bindings.map(b => `${quoteNameIfNecessary(b.name)}: 1`).join(', ')} }, ${name}.get());
});
`;
}
this.node.handlers.forEach(handler => {
handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky
handler.render(component, block, this.var, false); // TODO hoist when possible
if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this?
});
if (this.node.name === 'svelte:component') {
const switch_value = block.getUniqueName('switch_value');
const switch_props = block.getUniqueName('switch_props');
const { snippet } = this.node.expression;
block.builders.init.addBlock(deindent`
var ${switch_value} = ${snippet};
function ${switch_props}(ctx) {
${(this.node.attributes.length || this.node.bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
return {
${componentInitProperties.join(',\n')}
};
}
if (${switch_value}) {
var ${name} = new ${switch_value}(${switch_props}(ctx));
${beforecreate}
}
${this.node.handlers.map(handler => deindent`
function ${handler.var}(event) {
${handler.snippet || `#component.fire("${handler.name}", event);`}
}
if (${name}) ${name}.on("${handler.name}", ${handler.var});
`)}
`);
block.builders.create.addLine(
`if (${name}) ${name}._fragment.c();`
);
if (parentNodes) {
block.builders.claim.addLine(
`if (${name}) ${name}._fragment.l(${parentNodes});`
);
}
block.builders.mount.addBlock(deindent`
if (${name}) {
${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
}
`);
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor);
if (updates.length) {
block.builders.update.addBlock(deindent`
${updates}
`);
}
block.builders.update.addBlock(deindent`
if (${switch_value} !== (${switch_value} = ${snippet})) {
if (${name}) {
${component.options.nestedTransitions
? deindent`
@groupOutros();
const old_component = ${name};
old_component._fragment.o(() => {
old_component.destroy();
});`
: `${name}.destroy();`}
}
if (${switch_value}) {
${name} = new ${switch_value}(${switch_props}(ctx));
${this.node.bindings.length > 0 && deindent`
#component.root._beforecreate.push(() => {
const changed = {};
${this.node.bindings.map(binding => deindent`
if (${binding.value.snippet} === void 0) changed.${binding.name} = 1;`)}
${name}._bind(changed, ${name}.get());
});`}
${name}._fragment.c();
${this.fragment && this.fragment.nodes.map(child => child.remount(name))}
${name}._mount(${updateMountNode}, ${anchor});
${this.node.handlers.map(handler => deindent`
${name}.on("${handler.name}", ${handler.var});
`)}
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
} else {
${name} = null;
${this.node.ref && deindent`
if (#component.refs.${this.node.ref.name} === ${name}) {
#component.refs.${this.node.ref.name} = null;
}`}
}
}
`);
if (updates.length) {
block.builders.update.addBlock(deindent`
else if (${switch_value}) {
${name}._set(${name_changes});
${this.node.bindings.length && `${name_updating} = {};`}
}
`);
}
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(${parentNode ? '' : 'detach'});`);
} else {
const expression = this.node.name === 'svelte:self'
? component.name
: `%components-${this.node.name}`;
block.builders.init.addBlock(deindent`
${(this.node.attributes.length || this.node.bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
});
${beforecreate}
${this.node.handlers.map(handler => deindent`
${name}.on("${handler.name}", function(event) {
${handler.snippet || `#component.fire("${handler.name}", event);`}
});
`)}
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
`);
block.builders.create.addLine(`${name}._fragment.c();`);
if (parentNodes) {
block.builders.claim.addLine(
`${name}._fragment.l(${parentNodes});`
);
}
block.builders.mount.addLine(
`${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
);
if (updates.length) {
block.builders.update.addBlock(deindent`
${updates}
${name}._set(${name_changes});
${this.node.bindings.length && `${name_updating} = {};`}
`);
}
block.builders.destroy.addLine(deindent`
${name}.destroy(${parentNode ? '' : 'detach'});
${this.node.ref && `if (#component.refs.${this.node.ref.name} === ${name}) #component.refs.${this.node.ref.name} = null;`}
`);
}
if (component.options.nestedTransitions) {
block.builders.outro.addLine(
`if (${name}) ${name}._fragment.o(#outrocallback);`
);
}
}
remount(name: string) {
return `${this.var}._mount(${name}._slotted.default, null);`;
}
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -0,0 +1,27 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Tag from './shared/Tag';
export default class MustacheTagWrapper extends Tag {
var = 'text';
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
}
render(block: Block, parentNode: string, parentNodes: string) {
const { init } = this.renameThisMethod(
block,
value => `@setData(${this.var}, ${value});`
);
block.addElement(
this.var,
`@createText(${init})`,
parentNodes && `@claimText(${parentNodes}, ${init})`,
parentNode
);
}
}

@ -0,0 +1,103 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Tag from './shared/Tag';
import Wrapper from './shared/wrapper';
import deindent from '../../../utils/deindent';
export default class RawMustacheTagWrapper extends Tag {
var = 'raw';
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Node
) {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
}
render(block: Block, parentNode: string, parentNodes: string) {
const name = this.var;
// TODO use isDomNode instead of type === 'Element'?
const needsAnchorBefore = this.prev ? this.prev.node.type !== 'Element' : !parentNode;
const needsAnchorAfter = this.next ? this.next.node.type !== 'Element' : !parentNode;
const anchorBefore = needsAnchorBefore
? block.getUniqueName(`${name}_before`)
: (this.prev && this.prev.var) || 'null';
const anchorAfter = needsAnchorAfter
? block.getUniqueName(`${name}_after`)
: (this.next && this.next.var) || 'null';
let detach: string;
let insert: (content: string) => string;
let useInnerHTML = false;
if (anchorBefore === 'null' && anchorAfter === 'null') {
useInnerHTML = true;
detach = `${parentNode}.innerHTML = '';`;
insert = content => `${parentNode}.innerHTML = ${content};`;
} else if (anchorBefore === 'null') {
detach = `@detachBefore(${anchorAfter});`;
insert = content => `${anchorAfter}.insertAdjacentHTML("beforebegin", ${content});`;
} else if (anchorAfter === 'null') {
detach = `@detachAfter(${anchorBefore});`;
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
} else {
detach = `@detachBetween(${anchorBefore}, ${anchorAfter});`;
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
}
const { init } = this.renameThisMethod(
block,
content => deindent`
${!useInnerHTML && detach}
${insert(content)}
`
);
// we would have used comments here, but the `insertAdjacentHTML` api only
// exists for `Element`s.
if (needsAnchorBefore) {
block.addElement(
anchorBefore,
`@createElement('noscript')`,
parentNodes && `@createElement('noscript')`,
parentNode,
true
);
}
function addAnchorAfter() {
block.addElement(
anchorAfter,
`@createElement('noscript')`,
parentNodes && `@createElement('noscript')`,
parentNode
);
}
if (needsAnchorAfter && anchorBefore === 'null') {
// anchorAfter needs to be in the DOM before we
// insert the HTML...
addAnchorAfter();
}
block.builders.mount.addLine(insert(init));
if (!parentNode) {
block.builders.destroy.addConditional('detach', needsAnchorBefore
? `${detach}\n@detachNode(${anchorBefore});`
: detach);
}
if (needsAnchorAfter && anchorBefore !== 'null') {
// ...otherwise it should go afterwards
addAnchorAfter();
}
}
}

@ -0,0 +1,144 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import Slot from '../../nodes/Slot';
import { quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
import FragmentWrapper from './Fragment';
import deindent from '../../../utils/deindent';
function sanitize(name) {
return name.replace(/[^a-zA-Z]+/g, '_').replace(/^_/, '').replace(/_$/, '');
}
export default class SlotWrapper extends Wrapper {
node: Slot;
fragment: FragmentWrapper;
var = 'slot';
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Slot,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
this.fragment = new FragmentWrapper(
renderer,
block,
node.children,
parent,
stripWhitespace,
nextSibling
);
}
render(
block: Block,
parentNode: string,
parentNodes: string
) {
const { renderer } = this;
const slotName = this.node.getStaticAttributeValue('name') || 'default';
renderer.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${sanitize(slotName)}`);
const prop = quotePropIfNecessary(slotName);
block.addVariable(content_name, `#component._slotted${prop}`);
// TODO can we use isDomNode instead of type === 'Element'?
const needsAnchorBefore = this.prev ? this.prev.node.type !== 'Element' : !parentNode;
const needsAnchorAfter = this.next ? this.next.node.type !== 'Element' : !parentNode;
const anchorBefore = needsAnchorBefore
? block.getUniqueName(`${content_name}_before`)
: (this.prev && this.prev.var) || 'null';
const anchorAfter = needsAnchorAfter
? block.getUniqueName(`${content_name}_after`)
: (this.next && this.next.var) || 'null';
if (needsAnchorBefore) block.addVariable(anchorBefore);
if (needsAnchorAfter) block.addVariable(anchorAfter);
let mountBefore = block.builders.mount.toString();
let destroyBefore = block.builders.destroy.toString();
block.builders.create.pushCondition(`!${content_name}`);
block.builders.hydrate.pushCondition(`!${content_name}`);
block.builders.mount.pushCondition(`!${content_name}`);
block.builders.update.pushCondition(`!${content_name}`);
block.builders.destroy.pushCondition(`!${content_name}`);
this.fragment.render(block, parentNode, parentNodes);
block.builders.create.popCondition();
block.builders.hydrate.popCondition();
block.builders.mount.popCondition();
block.builders.update.popCondition();
block.builders.destroy.popCondition();
const mountLeadin = block.builders.mount.toString() !== mountBefore
? `else`
: `if (${content_name})`;
if (parentNode) {
block.builders.mount.addBlock(deindent`
${mountLeadin} {
${needsAnchorBefore && `@append(${parentNode}, ${anchorBefore} || (${anchorBefore} = @createComment()));`}
@append(${parentNode}, ${content_name});
${needsAnchorAfter && `@append(${parentNode}, ${anchorAfter} || (${anchorAfter} = @createComment()));`}
}
`);
} else {
block.builders.mount.addBlock(deindent`
${mountLeadin} {
${needsAnchorBefore && `@insert(#target, ${anchorBefore} || (${anchorBefore} = @createComment()), anchor);`}
@insert(#target, ${content_name}, anchor);
${needsAnchorAfter && `@insert(#target, ${anchorAfter} || (${anchorAfter} = @createComment()), anchor);`}
}
`);
}
// if the slot is unmounted, move nodes back into the document fragment,
// so that it can be reinserted later
// TODO so that this can work with public API, component._slotted should
// be all fragments, derived from options.slots. Not === options.slots
const unmountLeadin = block.builders.destroy.toString() !== destroyBefore
? `else`
: `if (${content_name})`;
if (anchorBefore === 'null' && anchorAfter === 'null') {
block.builders.destroy.addBlock(deindent`
${unmountLeadin} {
@reinsertChildren(${parentNode}, ${content_name});
}
`);
} else if (anchorBefore === 'null') {
block.builders.destroy.addBlock(deindent`
${unmountLeadin} {
@reinsertBefore(${anchorAfter}, ${content_name});
}
`);
} else if (anchorAfter === 'null') {
block.builders.destroy.addBlock(deindent`
${unmountLeadin} {
@reinsertAfter(${anchorBefore}, ${content_name});
}
`);
} else {
block.builders.destroy.addBlock(deindent`
${unmountLeadin} {
@reinsertBetween(${anchorBefore}, ${anchorAfter}, ${content_name});
@detachNode(${anchorBefore});
@detachNode(${anchorAfter});
}
`);
}
}
}

@ -0,0 +1,67 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Text from '../../nodes/Text';
import Wrapper from './shared/Wrapper';
import { CompileOptions } from '../../../interfaces';
import { stringify } from '../../../utils/stringify';
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This
// list is almost certainly very incomplete)
const elementsWithoutText = new Set([
'audio',
'datalist',
'dl',
'optgroup',
'select',
'video',
]);
// TODO this should probably be in Fragment
function shouldSkip(node: Text) {
if (/\S/.test(node.data)) return false;
const parentElement = node.findNearest(/(?:Element|InlineComponent|Head)/);
if (!parentElement) return false;
if (parentElement.type === 'Head') return true;
if (parentElement.type === 'InlineComponent') return parentElement.children.length === 1 && node === parentElement.children[0];
return parentElement.namespace || elementsWithoutText.has(parentElement.name);
}
export default class TextWrapper extends Wrapper {
node: Text;
data: string;
skip: boolean;
var: string;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Text,
data: string
) {
super(renderer, block, parent, node);
this.skip = shouldSkip(this.node);
this.data = data;
this.var = this.skip ? null : 'text';
}
render(block: Block, parentNode: string, parentNodes: string) {
if (this.skip) return;
block.addElement(
this.var,
`@createText(${stringify(this.data)})`,
parentNodes && `@claimText(${parentNodes}, ${stringify(this.data)})`,
parentNode
);
}
remount(name: string) {
return `@append(${name}._slotted.default, ${this.var});`;
}
}

@ -0,0 +1,99 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import Title from '../../nodes/Title';
import FragmentWrapper from './Fragment';
import { stringify } from '../../../utils/stringify';
export default class TitleWrapper extends Wrapper {
node: Title;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Title,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
}
render(block: Block, parentNode: string, parentNodes: string) {
const isDynamic = !!this.node.children.find(node => node.type !== 'Text');
if (isDynamic) {
let value;
const allDependencies = new Set();
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.node.children.length === 1) {
// single {tag} — may be a non-string
const { expression } = this.node.children[0];
const { dependencies, snippet } = this.node.children[0].expression;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
});
} else {
// '{foo} {bar}' — treat as string concatenation
value =
(this.node.children[0].type === 'Text' ? '' : `"" + `) +
this.node.children
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = chunk.expression;
dependencies.forEach(d => {
allDependencies.add(d);
});
return chunk.expression.getPrecedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
}
const last = this.node.shouldCache && block.getUniqueName(
`title_value`
);
if (this.node.shouldCache) block.addVariable(last);
let updater;
const init = this.node.shouldCache ? `${last} = ${value}` : value;
block.builders.init.addLine(
`document.title = ${init};`
);
updater = `document.title = ${this.node.shouldCache ? last : value};`;
if (allDependencies.size) {
const dependencies = Array.from(allDependencies);
const changedCheck = (
( block.hasOutros ? `!#current || ` : '' ) +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = this.node.shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
block.builders.update.addConditional(
condition,
updater
);
}
} else {
const value = stringify(this.node.children[0].data);
block.builders.hydrate.addLine(`document.title = ${value};`);
}
}
}

@ -0,0 +1,198 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Wrapper from './shared/Wrapper';
import deindent from '../../../utils/deindent';
const associatedEvents = {
innerWidth: 'resize',
innerHeight: 'resize',
outerWidth: 'resize',
outerHeight: 'resize',
scrollX: 'scroll',
scrollY: 'scroll',
};
const properties = {
scrollX: 'pageXOffset',
scrollY: 'pageYOffset'
};
const readonly = new Set([
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'online',
]);
export default class WindowWrapper extends Wrapper {
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
super(renderer, block, parent, node);
}
render(block: Block, parentNode: string, parentNodes: string) {
const { renderer } = this;
const { component } = renderer;
const events = {};
const bindings: Record<string, string> = {};
this.node.handlers.forEach(handler => {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
component.addSourcemapLocations(handler.expression);
const isCustomEvent = component.events.has(handler.name);
let usesState = handler.dependencies.size > 0;
handler.render(component, block, 'window', false); // TODO hoist?
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent`
${usesState && `var ctx = #component.get();`}
${handler.snippet};
`;
if (isCustomEvent) {
// TODO dry this out
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(#component, window, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.destroy();
`);
} else {
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${handler.name}", ${handlerName});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${handler.name}", ${handlerName});
`);
}
});
this.node.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
renderer.readonly.add(binding.value.node.name);
}
bindings[binding.name] = binding.value.node.name;
// bind:online is a special case, we need to listen for two separate events
if (binding.name === 'online') return;
const associatedEvent = associatedEvents[binding.name];
const property = properties[binding.name] || binding.name;
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push(
`${binding.value.node.name}: this.${property}`
);
// add initial value
renderer.metaBindings.push(
`this._state.${binding.value.node.name} = window.${property};`
);
});
const lock = block.getUniqueName(`window_updating`);
const clear = block.getUniqueName(`clear_window_updating`);
const timeout = block.getUniqueName(`window_updating_timeout`);
Object.keys(events).forEach(event => {
const handlerName = block.getUniqueName(`onwindow${event}`);
const props = events[event].join(',\n');
if (event === 'scroll') {
// TODO other bidirectional bindings...
block.addVariable(lock, 'false');
block.addVariable(clear, `function() { ${lock} = false; }`);
block.addVariable(timeout);
}
const handlerBody = deindent`
${event === 'scroll' && deindent`
if (${lock}) return;
${lock} = true;
`}
${component.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({
${props}
});
${component.options.dev && `component._updatingReadonlyProperty = false;`}
${event === 'scroll' && `${lock} = false;`}
`;
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${event}", ${handlerName});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${event}", ${handlerName});
`);
});
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX || bindings.scrollY) {
block.builders.init.addBlock(deindent`
#component.on("state", ({ changed, current }) => {
if (${
[bindings.scrollX, bindings.scrollY].map(
binding => binding && `changed["${binding}"]`
).filter(Boolean).join(' || ')
} && !${lock}) {
${lock} = true;
clearTimeout(${timeout});
window.scrollTo(${
bindings.scrollX ? `current["${bindings.scrollX}"]` : `window.pageXOffset`
}, ${
bindings.scrollY ? `current["${bindings.scrollY}"]` : `window.pageYOffset`
});
${timeout} = setTimeout(${clear}, 100);
}
});
`);
}
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${component.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({ ${bindings.online}: navigator.onLine });
${component.options.dev && `component._updatingReadonlyProperty = false;`}
}
window.addEventListener("online", ${handlerName});
window.addEventListener("offline", ${handlerName});
`);
// add initial value
renderer.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;`
);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("online", ${handlerName});
window.removeEventListener("offline", ${handlerName});
`);
}
}
}

@ -0,0 +1,62 @@
import Renderer from '../../Renderer';
import Block from '../../Block';
import Wrapper from './Wrapper';
import EventHandler from '../../../nodes/EventHandler';
import validCalleeObjects from '../../../../utils/validCalleeObjects';
export default class EventHandlerWrapper extends Wrapper {
node: EventHandler;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EventHandler,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
}
render(block: Block, parentNode: string, parentNodes: string) {
const { renderer } = this;
const { component } = renderer;
const hoisted = this.node.shouldHoist;
if (this.node.insertionPoint === null) return; // TODO handle shorthand events here?
if (!validCalleeObjects.has(this.node.callee.name)) {
const component_name = hoisted ? `component` : block.alias(`component`);
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (this.node.callee.name[0] === '$' && !component.methods.has(this.node.callee.name)) {
component.code.overwrite(
this.node.insertionPoint,
this.node.insertionPoint + 1,
`${component_name}.store.`
);
} else {
component.code.prependRight(
this.node.insertionPoint,
`${component_name}.`
);
}
}
if (this.node.isCustomEvent) {
this.node.args.forEach(arg => {
arg.overwriteThis(this.parent.var);
});
if (this.node.callee && this.node.callee.name === 'this') {
const node = this.node.callee.nodes[0];
component.code.overwrite(node.start, node.end, this.parent.var, {
storeName: true,
contentOnly: true
});
}
}
}
}

@ -0,0 +1,67 @@
import Wrapper from './Wrapper';
import Renderer from '../../Renderer';
import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
import MustacheTag from '../../../nodes/MustacheTag';
import RawMustacheTag from '../../../nodes/RawMustacheTag';
export default class Tag extends Wrapper {
node: MustacheTag | RawMustacheTag;
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
block.addDependencies(node.expression.dependencies);
}
render(block: Block, parentNode: string, parentNodes: string) {
const { init } = this.renameThisMethod(
block,
value => `@setData(${this.var}, ${value});`
);
block.addElement(
this.var,
`@createText(${init})`,
parentNodes && `@claimText(${parentNodes}, ${init})`,
parentNode
);
}
renameThisMethod(
block: Block,
update: ((value: string) => string)
) {
const { snippet, dependencies } = this.node.expression;
const value = this.node.shouldCache && block.getUniqueName(`${this.var}_value`);
const content = this.node.shouldCache ? value : snippet;
if (this.node.shouldCache) block.addVariable(value, snippet);
if (dependencies.size) {
const changedCheck = (
(block.hasOutros ? `!#current || ` : '') +
[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
const condition = this.node.shouldCache ?
(dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
changedCheck;
block.builders.update.addConditional(
condition,
update(content)
);
}
return { init: content };
}
remount(name: string) {
return `@append(${name}._slotted.default, ${this.var});`;
}
}

@ -0,0 +1,92 @@
import Renderer from '../../Renderer';
import Node from '../../../nodes/shared/Node';
import Block from '../../Block';
export default class Wrapper {
renderer: Renderer;
parent: Wrapper;
node: Node;
prev: Wrapper | null;
next: Wrapper | null;
var: string;
canUseInnerHTML: boolean;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Node
) {
this.node = node;
// make these non-enumerable so that they can be logged sensibly
// (TODO in dev only?)
Object.defineProperties(this, {
renderer: {
value: renderer
},
parent: {
value: parent
}
});
this.canUseInnerHTML = !renderer.options.hydratable;
block.wrappers.push(this);
}
cannotUseInnerHTML() {
this.canUseInnerHTML = false;
if (this.parent) this.parent.cannotUseInnerHTML();
}
// TODO do we still need equivalent method on Node?
findNearest(pattern) {
if (pattern.test(this.node.type)) return this;
return this.parent && this.parent.findNearest(pattern);
}
getOrCreateAnchor(block: Block, parentNode: string, parentNodes: string) {
// TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${this.var}_anchor`)
: (this.next && this.next.var) || 'null';
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
return anchor;
}
getUpdateMountNode(anchor: string) {
return (this.parent && this.parent.isDomNode())
? this.parent.var
: `${anchor}.parentNode`;
}
isDomNode() {
return (
this.node.type === 'Element' ||
this.node.type === 'Text' ||
this.node.type === 'MustacheTag'
);
}
render(block: Block, parentNode: string, parentNodes: string) {
throw new Error(`render method not implemented by subclass ${this.node.type}`);
}
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
}
}

@ -0,0 +1,71 @@
import AwaitBlock from './handlers/AwaitBlock';
import Comment from './handlers/Comment';
import DebugTag from './handlers/DebugTag';
import EachBlock from './handlers/EachBlock';
import Element from './handlers/Element';
import Head from './handlers/Head';
import HtmlTag from './handlers/HtmlTag';
import IfBlock from './handlers/IfBlock';
import InlineComponent from './handlers/InlineComponent';
import Slot from './handlers/Slot';
import Tag from './handlers/Tag';
import Text from './handlers/Text';
import Title from './handlers/Title';
import { CompileOptions } from '../../interfaces';
type Handler = (node: any, renderer: Renderer, options: CompileOptions) => void;
function noop(){}
const handlers: Record<string, Handler> = {
AwaitBlock,
Comment,
DebugTag,
EachBlock,
Element,
Head,
IfBlock,
InlineComponent,
MustacheTag: Tag, // TODO MustacheTag is an anachronism
RawMustacheTag: HtmlTag,
Slot,
Text,
Title,
Window: noop
};
type AppendTarget = any; // TODO
export default class Renderer {
bindings: string[];
code: string;
targets: AppendTarget[];
constructor() {
this.bindings = [];
this.code = '';
this.targets = [];
}
append(code: string) {
if (this.targets.length) {
const target = this.targets[this.targets.length - 1];
const slotName = target.slotStack[target.slotStack.length - 1];
target.slots[slotName] += code;
} else {
this.code += code;
}
}
render(nodes, options) {
nodes.forEach(node => {
const handler = handlers[node.type];
if (!handler) {
throw new Error(`No handler for '${node.type}' nodes`);
}
handler(node, this, options);
});
}
}

@ -0,0 +1,16 @@
import Renderer from '../Renderer';
import { CompileOptions } from '../../../interfaces';
export default function(node, renderer: Renderer, options: CompileOptions) {
const { snippet } = node.expression;
renderer.append('${(function(__value) { if(@isPromise(__value)) return `');
renderer.render(node.pending.children, options);
renderer.append('`; return function(ctx) { return `');
renderer.render(node.then.children, options);
renderer.append(`\`;}(Object.assign({}, ctx, { ${node.value}: __value }));}(${snippet})) }`);
}

@ -0,0 +1,8 @@
import Renderer from '../Renderer';
import { CompileOptions } from '../../../interfaces';
export default function(node, renderer: Renderer, options: CompileOptions) {
if (options.preserveComments) {
renderer.append(`<!--${node.data}-->`);
}
}

@ -0,0 +1,19 @@
import { stringify } from '../../../utils/stringify';
export default function(node, renderer, options) {
if (!options.dev) return;
const filename = options.file || null;
const { line, column } = options.locate(node.start + 1);
const obj = node.expressions.length === 0
? `ctx`
: `{ ${node.expressions
.map(e => e.node.name)
.map(name => `${name}: ctx.${name}`)
.join(', ')} }`;
const str = '${@debug(' + `${filename && stringify(filename)}, ${line}, ${column}, ${obj})}`;
renderer.append(str);
}

@ -0,0 +1,25 @@
export default function(node, renderer, options) {
const { snippet } = node.expression;
const props = node.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
const getContext = node.index
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${node.index}: i })`
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
renderer.append(open);
renderer.render(node.children, options);
const close = `\`)`;
renderer.append(close);
if (node.else) {
renderer.append(` : \``);
renderer.render(node.else.children, options);
renderer.append(`\``);
}
renderer.append('}');
}

@ -0,0 +1,142 @@
import { quotePropIfNecessary, quoteNameIfNecessary } from '../../../utils/quoteIfNecessary';
import isVoidElementName from '../../../utils/isVoidElementName';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const boolean_attributes = new Set([
'async',
'autocomplete',
'autofocus',
'autoplay',
'border',
'challenge',
'checked',
'compact',
'contenteditable',
'controls',
'default',
'defer',
'disabled',
'formnovalidate',
'frameborder',
'hidden',
'indeterminate',
'ismap',
'loop',
'multiple',
'muted',
'nohref',
'noresize',
'noshade',
'novalidate',
'nowrap',
'open',
'readonly',
'required',
'reversed',
'scoped',
'scrolling',
'seamless',
'selected',
'sortable',
'spellcheck',
'translate'
]);
export default function(node, renderer, options) {
let openingTag = `<${node.name}`;
let textareaContents; // awkward special case
const slot = node.getStaticAttributeValue('slot');
if (slot && node.hasAncestor('InlineComponent')) {
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
const slotName = slot.chunks[0].data;
const target = renderer.targets[renderer.targets.length - 1];
target.slotStack.push(slotName);
target.slots[slotName] = '';
}
const classExpr = node.classes.map((classDir: Class) => {
const { expression, name } = classDir;
const snippet = expression ? expression.snippet : `ctx${quotePropIfNecessary(name)}`;
return `${snippet} ? "${name}" : ""`;
}).join(', ');
let addClassAttribute = classExpr ? true : false;
if (node.attributes.find(attr => attr.isSpread)) {
// TODO dry this out
const args = [];
node.attributes.forEach(attribute => {
if (attribute.isSpread) {
args.push(attribute.expression.snippet);
} else {
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = attribute.stringifyForSsr();
} else if (attribute.isTrue) {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: true }`);
} else if (
boolean_attributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`);
} else {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${attribute.stringifyForSsr()}\` }`);
}
}
});
openingTag += "${@spread([" + args.join(', ') + "])}";
} else {
node.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = attribute.stringifyForSsr();
} else if (attribute.isTrue) {
openingTag += ` ${attribute.name}`;
} else if (
boolean_attributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
} else if (attribute.name === 'class' && classExpr) {
addClassAttribute = false;
openingTag += ` class="\${[\`${attribute.stringifyForSsr()}\`, ${classExpr}].join(' ').trim() }"`;
} else {
openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`;
}
});
}
node.bindings.forEach(binding => {
const { name, value: { snippet } } = binding;
if (name === 'group') {
// TODO server-render group bindings
} else {
openingTag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}';
}
});
if (addClassAttribute) {
openingTag += `\${((v) => v ? ' class="' + v + '"' : '')([${classExpr}].join(' ').trim())}`;
}
openingTag += '>';
renderer.append(openingTag);
if (node.name === 'textarea' && textareaContents !== undefined) {
renderer.append(textareaContents);
} else {
renderer.render(node.children, options);
}
if (!isVoidElementName(node.name)) {
renderer.append(`</${node.name}>`);
}
}

@ -0,0 +1,7 @@
export default function(node, renderer, options) {
renderer.append('${(__result.head += `');
renderer.render(node.children, options);
renderer.append('`, "")}');
}

@ -0,0 +1,3 @@
export default function(node, renderer, options) {
renderer.append('${' + node.expression.snippet + '}');
}

@ -0,0 +1,15 @@
export default function(node, renderer, options) {
const { snippet } = node.expression;
renderer.append('${ ' + snippet + ' ? `');
renderer.render(node.children, options);
renderer.append('` : `');
if (node.else) {
renderer.render(node.else.children, options);
}
renderer.append('` }');
}

@ -0,0 +1,130 @@
import { escape, escapeTemplate, stringify } from '../../../utils/stringify';
import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../utils/getTailSnippet';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
import deindent from '../../../utils/deindent';
type AppendTarget = any; // TODO
export default function(node, renderer, options) {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data));
}
return '${@escape( ' + chunk.snippet + ')}';
}
const bindingProps = node.bindings.map(binding => {
const { name } = getObject(binding.value.node);
const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(binding.value.node)
: '';
return `${quoteNameIfNecessary(binding.name)}: ctx${quotePropIfNecessary(name)}${tail}`;
});
function getAttributeValue(attribute) {
if (attribute.isTrue) return `true`;
if (attribute.chunks.length === 0) return `''`;
if (attribute.chunks.length === 1) {
const chunk = attribute.chunks[0];
if (chunk.type === 'Text') {
return stringify(chunk.data);
}
return chunk.snippet;
}
return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
}
const usesSpread = node.attributes.find(attr => attr.isSpread);
const props = usesSpread
? `Object.assign(${
node.attributes
.map(attribute => {
if (attribute.isSpread) {
return attribute.expression.snippet;
} else {
return `{ ${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)} }`;
}
})
.concat(bindingProps.map(p => `{ ${p} }`))
.join(', ')
})`
: `{ ${node.attributes
.map(attribute => `${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)}`)
.concat(bindingProps)
.join(', ')} }`;
const expression = (
node.name === 'svelte:self'
? node.component.name
: node.name === 'svelte:component'
? `((${node.expression.snippet}) || @missingComponent)`
: `%components-${node.name}`
);
node.bindings.forEach(binding => {
const conditions = [];
let parent = node;
while (parent = parent.parent) {
if (parent.type === 'IfBlock') {
// TODO handle contextual bindings...
conditions.push(`(${parent.expression.snippet})`);
}
}
conditions.push(
`!('${binding.name}' in ctx)`,
`${expression}.data`
);
const { name } = getObject(binding.value.node);
renderer.bindings.push(deindent`
if (${conditions.reverse().join('&&')}) {
tmp = ${expression}.data();
if ('${name}' in tmp) {
ctx${quotePropIfNecessary(binding.name)} = tmp.${name};
settled = false;
}
}
`);
});
let open = `\${@validateSsrComponent(${expression}, '${node.name}')._render(__result, ${props}`;
const component_options = [];
component_options.push(`store: options.store`);
if (node.children.length) {
const target: AppendTarget = {
slots: { default: '' },
slotStack: ['default']
};
renderer.targets.push(target);
renderer.render(node.children, options);
const slotted = Object.keys(target.slots)
.map(name => `${quoteNameIfNecessary(name)}: () => \`${target.slots[name]}\``)
.join(', ');
component_options.push(`slotted: { ${slotted} }`);
renderer.targets.pop();
}
if (component_options.length) {
open += `, { ${component_options.join(', ')} }`;
}
renderer.append(open);
renderer.append(')}');
}

@ -0,0 +1,14 @@
import { quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
export default function(node, renderer, options) {
const name = node.attributes.find(attribute => attribute.name === 'name');
const slotName = name && name.chunks[0].data || 'default';
const prop = quotePropIfNecessary(slotName);
renderer.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``);
renderer.render(node.children, options);
renderer.append(`\`}`);
}

@ -0,0 +1,9 @@
export default function(node, renderer, options) {
renderer.append(
node.parent &&
node.parent.type === 'Element' &&
node.parent.name === 'style'
? '${' + node.expression.snippet + '}'
: '${@escape(' + node.expression.snippet + ')}'
);
}

@ -0,0 +1,14 @@
import { escapeHTML, escapeTemplate, escape } from '../../../utils/stringify';
export default function(node, renderer, options) {
let text = node.data;
if (
!node.parent ||
node.parent.type !== 'Element' ||
(node.parent.name !== 'script' && node.parent.name !== 'style')
) {
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
renderer.append(escape(escapeTemplate(text)));
}

@ -0,0 +1,7 @@
export default function(node, renderer, options) {
renderer.append(`<title>`);
renderer.render(node.children, options);
renderer.append(`</title>`);
}

@ -1,61 +1,32 @@
import deindent from '../../utils/deindent';
import Compiler from '../Compiler';
import Stats from '../../Stats';
import Stylesheet from '../../css/Stylesheet';
import { removeNode, removeObjectKey } from '../../utils/removeNode';
import getName from '../../utils/getName';
import Component from '../Component';
import globalWhitelist from '../../utils/globalWhitelist';
import { Ast, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from '../../interfaces';
import { CompileOptions } from '../../interfaces';
import { stringify } from '../../utils/stringify';
export class SsrTarget {
bindings: string[];
renderCode: string;
appendTargets: AppendTarget[];
constructor() {
this.bindings = [];
this.renderCode = '';
this.appendTargets = [];
}
append(code: string) {
if (this.appendTargets.length) {
const appendTarget = this.appendTargets[this.appendTargets.length - 1];
const slotName = appendTarget.slotStack[appendTarget.slotStack.length - 1];
appendTarget.slots[slotName] += code;
} else {
this.renderCode += code;
}
}
}
import CodeBuilder from '../../utils/CodeBuilder';
import Renderer from './Renderer';
export default function ssr(
ast: Ast,
source: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
component: Component,
options: CompileOptions
) {
const format = options.format || 'cjs';
const renderer = new Renderer();
const target = new SsrTarget();
const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, false, target);
const format = options.format || 'cjs';
const { computations, name, templateProperties } = compiler;
const { computations, name, templateProperties } = component;
// create main render() function
trim(compiler.fragment.children).forEach((node: Node) => {
node.ssr();
});
renderer.render(trim(component.fragment.children), Object.assign({
locate: component.locate
}, options));
const css = compiler.customElement ?
const css = component.customElement ?
{ code: null, map: null } :
compiler.stylesheet.render(options.filename, true);
component.stylesheet.render(options.filename, true);
// generate initial state object
const expectedProperties = Array.from(compiler.expectedProperties);
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
@ -77,11 +48,38 @@ export default function ssr(
initialState.push('ctx');
const helpers = new Set();
let js = null;
if (component.javascript) {
const componentDefinition = new CodeBuilder();
// not all properties are relevant to SSR (e.g. lifecycle hooks)
const relevant = new Set([
'data',
'components',
'computed',
'helpers',
'preload',
'store'
]);
component.declarations.forEach(declaration => {
if (relevant.has(declaration.type)) {
componentDefinition.addBlock(declaration.block);
}
});
js = (
component.javascript[0] +
componentDefinition +
component.javascript[1]
);
}
const debugName = `<${component.customElement ? component.tag : name}>`;
// TODO concatenate CSS maps
const result = deindent`
${compiler.javascript}
const result = (deindent`
${js}
var ${name} = {};
@ -117,13 +115,19 @@ export default function ssr(
${templateProperties.store && `options.store = %store();`}
__result.addComponent(${name});
${options.dev && storeProps.length > 0 && deindent`
if (!options.store) {
throw new Error("${debugName} references store properties, but no store was provided");
}
`}
ctx = Object.assign(${initialState.join(', ')});
${computations.map(
({ key }) => `ctx.${key} = %computed-${key}(ctx);`
)}
${target.bindings.length &&
${renderer.bindings.length &&
deindent`
var settled = false;
var tmp;
@ -131,11 +135,11 @@ export default function ssr(
while (!settled) {
settled = true;
${target.bindings.join('\n\n')}
${renderer.bindings.join('\n\n')}
}
`}
return \`${target.renderCode}\`;
return \`${renderer.code}\`;
};
${name}.css = {
@ -146,9 +150,9 @@ export default function ssr(
var warned = false;
${templateProperties.preload && `${name}.preload = %preload;`}
`;
`).trim();
return compiler.generate(result, options, { name, format });
return component.generate(result, options, { name, format });
}
function trim(nodes) {

@ -0,0 +1,16 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function actions(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-actions`,
message: `The 'actions' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
}

@ -0,0 +1,21 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function transitions(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-transitions-property`,
message: `The 'transitions' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach(() => {
// TODO probably some validation that can happen here...
// checking for use of `this` etc?
});
}

@ -1,33 +1,33 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../utils/getName';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import getName from '../../../../utils/getName';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function components(validator: Validator, prop: Node) {
export default function components(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-components-property`,
message: `The 'components' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((component: Node) => {
const name = getName(component.key);
prop.value.properties.forEach((node: Node) => {
const name = getName(node.key);
if (name === 'state') {
// TODO is this still true?
validator.error(component, {
component.error(node, {
code: `invalid-name`,
message: `Component constructors cannot be called 'state' due to technical limitations`
});
}
if (!/^[A-Z]/.test(name)) {
validator.error(component, {
component.error(node, {
code: `component-lowercase`,
message: `Component names must be capitalised`
});

@ -1,50 +1,49 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../utils/getName';
import isValidIdentifier from '../../../utils/isValidIdentifier';
import reservedNames from '../../../utils/reservedNames';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../utils/isThisGetCallExpression';
import validCalleeObjects from '../../../utils/validCalleeObjects';
import getName from '../../../../utils/getName';
import isValidIdentifier from '../../../../utils/isValidIdentifier';
import reservedNames from '../../../../utils/reservedNames';
import { Node } from '../../../../interfaces';
import walkThroughTopFunctionScope from '../../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../../utils/isThisGetCallExpression';
import Component from '../../../Component';
const isFunctionExpression = new Set([
'FunctionExpression',
'ArrowFunctionExpression',
]);
export default function computed(validator: Validator, prop: Node) {
export default function computed(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-computed-property`,
message: `The 'computed' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((computation: Node) => {
const name = getName(computation.key);
if (!isValidIdentifier(name)) {
const suggestion = name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
validator.error(computation.key, {
component.error(computation.key, {
code: `invalid-computed-name`,
message: `Computed property name '${name}' is invalid — must be a valid identifier such as ${suggestion}`
});
}
if (reservedNames.has(name)) {
validator.error(computation.key, {
component.error(computation.key, {
code: `invalid-computed-name`,
message: `Computed property name '${name}' is invalid — cannot be a JavaScript reserved word`
});
}
if (!isFunctionExpression.has(computation.value.type)) {
validator.error(computation.value, {
component.error(computation.value, {
code: `invalid-computed-value`,
message: `Computed properties can be function expressions or arrow function expressions`
});
@ -54,14 +53,14 @@ export default function computed(validator: Validator, prop: Node) {
walkThroughTopFunctionScope(body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) {
validator.error(node, {
component.error(node, {
code: `impure-computed`,
message: `Cannot use this.get(...) — values must be passed into the function as arguments`
});
}
if (node.type === 'ThisExpression') {
validator.error(node, {
component.error(node, {
code: `impure-computed`,
message: `Computed properties should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`
});
@ -69,14 +68,14 @@ export default function computed(validator: Validator, prop: Node) {
});
if (params.length === 0) {
validator.error(computation.value, {
component.error(computation.value, {
code: `impure-computed`,
message: `A computed value must depend on at least one property`
});
}
if (params.length > 1) {
validator.error(computation.value, {
component.error(computation.value, {
code: `invalid-computed-arguments`,
message: `Computed properties must take a single argument`
});

@ -1,13 +1,13 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
export default function data(validator: Validator, prop: Node) {
export default function data(component: Component, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
if (disallowed.has(prop.value.type)) {
validator.error(prop.value, {
component.error(prop.value, {
code: `invalid-data-property`,
message: `'data' must be a function`
});

@ -0,0 +1,16 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function events(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-events-property`,
message: `The 'events' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
}

@ -1,21 +1,20 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { walk } from 'estree-walker';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../utils/isThisGetCallExpression';
import { Node } from '../../../../interfaces';
import walkThroughTopFunctionScope from '../../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../../utils/isThisGetCallExpression';
import Component from '../../../Component';
export default function helpers(validator: Validator, prop: Node) {
export default function helpers(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-helpers-property`,
message: `The 'helpers' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((prop: Node) => {
if (!/FunctionExpression/.test(prop.value.type)) return;
@ -24,14 +23,14 @@ export default function helpers(validator: Validator, prop: Node) {
walkThroughTopFunctionScope(prop.value.body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) {
validator.error(node, {
component.error(node, {
code: `impure-helper`,
message: `Cannot use this.get(...) — values must be passed into the helper function as arguments`
});
}
if (node.type === 'ThisExpression') {
validator.error(node, {
component.error(node, {
code: `impure-helper`,
message: `Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`
});
@ -41,7 +40,7 @@ export default function helpers(validator: Validator, prop: Node) {
});
if (prop.value.params.length === 0 && !usesArguments) {
validator.warn(prop, {
component.warn(prop, {
code: `impure-helper`,
message: `Helpers should be pure functions, with at least one argument`
});

@ -1,9 +1,9 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function immutable(validator: Validator, prop: Node) {
export default function immutable(component: Component, prop: Node) {
if (prop.value.type !== 'Literal' || typeof prop.value.value !== 'boolean') {
validator.error(prop.value, {
component.error(prop.value, {
code: `invalid-immutable-property`,
message: `'immutable' must be a boolean literal`
});

@ -1,5 +1,6 @@
import data from './data';
import actions from './actions';
import animations from './animations';
import computed from './computed';
import oncreate from './oncreate';
import ondestroy from './ondestroy';
@ -23,6 +24,7 @@ import immutable from './immutable';
export default {
data,
actions,
animations,
computed,
oncreate,
ondestroy,

@ -2,29 +2,29 @@ import checkForAccessors from '../utils/checkForAccessors';
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import usesThisOrArguments from '../utils/usesThisOrArguments';
import getName from '../../../utils/getName';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import getName from '../../../../utils/getName';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const builtin = new Set(['set', 'get', 'on', 'fire', 'destroy']);
export default function methods(validator: Validator, prop: Node) {
export default function methods(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-methods-property`,
message: `The 'methods' property must be an object literal`
});
}
checkForAccessors(validator, prop.value.properties, 'Methods');
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForAccessors(component, prop.value.properties, 'Methods');
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((prop: Node) => {
const name = getName(prop.key);
if (builtin.has(name)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-method-name`,
message: `Cannot overwrite built-in method '${name}'`
});
@ -32,7 +32,7 @@ export default function methods(validator: Validator, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-method-value`,
message: `Method '${prop.key.name}' should be a function expression, not an arrow function expression`
});

@ -1,16 +1,16 @@
import * as namespaces from '../../../utils/namespaces';
import nodeToString from '../../../utils/nodeToString'
import * as namespaces from '../../../../utils/namespaces';
import nodeToString from '../../../../utils/nodeToString'
import fuzzymatch from '../../utils/fuzzymatch';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const valid = new Set(namespaces.validNamespaces);
export default function namespace(validator: Validator, prop: Node) {
export default function namespace(component: Component, prop: Node) {
const ns = nodeToString(prop.value);
if (typeof ns !== 'string') {
validator.error(prop, {
component.error(prop, {
code: `invalid-namespace-property`,
message: `The 'namespace' property must be a string literal representing a valid namespace`
});
@ -19,12 +19,12 @@ export default function namespace(validator: Validator, prop: Node) {
if (!valid.has(ns)) {
const match = fuzzymatch(ns, namespaces.validNamespaces);
if (match) {
validator.error(prop, {
component.error(prop, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
validator.error(prop, {
component.error(prop, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}'`
});

@ -1,11 +1,11 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function oncreate(validator: Validator, prop: Node) {
export default function oncreate(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-oncreate-property`,
message: `'oncreate' should be a function expression, not an arrow function expression`
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save