merge master -> gh-1207

pull/1208/head
Rich Harris 7 years ago
commit bda2310bcf

@ -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

35
.gitignore vendored

@ -1,20 +1,19 @@
.DS_Store
node_modules
compiler
ssr
shared.js
scratch
!test/compiler
!test/ssr
.nyc_output
coverage
coverage.lcov
test/sourcemaps/samples/*/output.js
test/sourcemaps/samples/*/output.js.map
_actual.*
_actual-bundle.*
src/generators/dom/shared.ts
package-lock.json
.idea/
*.iml
store.umd.js
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
/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,424 @@
# 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))
* Fix `stats` when `options.generate` is `false` ([#1368](https://github.com/sveltejs/svelte/issues/1368))
* Assign custom methods to custom elements ([#1369](https://github.com/sveltejs/svelte/issues/1369))
* Fix `this` value in custom event handlers ([#1297](https://github.com/sveltejs/svelte/issues/1297))
* Re-evaluate `each` values lazily ([#1286](https://github.com/sveltejs/svelte/issues/1286))
* Preserve outer context in `await` blocks ([#1251](https://github.com/sveltejs/svelte/issues/1251))
## 2.2.0
* Internal refactoring ([#1367](https://github.com/sveltejs/svelte/pull/1367))
## 2.1.1
* Report initial `changed` based on state, not expected props ([#1356](https://github.com/sveltejs/svelte/issues/1356))
* Set state to empty object, not null, on destroy ([#1354](https://github.com/sveltejs/svelte/issues/1354))
* Prevent stale state in component event handlers ([#1353](https://github.com/sveltejs/svelte/issues/1353))
## 2.1.0
* Allow shorthand imports ([#1038](https://github.com/sveltejs/svelte/issues/1038))
* Update spread props inside each blocks ([#1337](https://github.com/sveltejs/svelte/issues/1337))
## 2.0.0
*See [the blog post](https://svelte.technology/blog/version-2) for information on how to upgrade your apps*
* New template syntax ([#1318](https://github.com/sveltejs/svelte/issues/1318))
* Emit ES2015 code, not ES5 ([#1348](https://github.com/sveltejs/svelte/pull/1348))
* Add `onstate` and `onupdate` hooks, remove `component.observe` method ([#1197](https://github.com/sveltejs/svelte/issues/1197))
* Use destructuring syntax for computed properties ([#1069](https://github.com/sveltejs/svelte/issues/1069)
* Change signature of `svelte.compile` ([#1298](https://github.com/sveltejs/svelte/pull/1298))
* Remove `validate` and `Stylesheet` from public API ([#1348](https://github.com/sveltejs/svelte/pull/1348))
* Don't typecast numeric attributes ([#657](https://github.com/sveltejs/svelte/issues/657))
* Always compile with `Store` support, and cascading disabled ([#1348](https://github.com/sveltejs/svelte/pull/1348))
* Remove unused `hash` property from AST ([#1348](https://github.com/sveltejs/svelte/pull/1348))
* Rename `loc` property to `start` in warnings and errors ([#1348](https://github.com/sveltejs/svelte/pull/1348))
## 1.64.1
* Fix computed properties in SSR renderer ([#1349](https://github.com/sveltejs/svelte/issues/1349))
## 1.64.0
* Deprecate passing a string argument to `component.get` ([#1347](https://github.com/sveltejs/svelte/pull/1347))
## 1.63.1
* Allow `observe` method to be overwritten
## 1.63.0
* Add `onstate` and `onupdate` lifecycle hooks and deprecate `component.observe` ([#1197](https://github.com/sveltejs/svelte/issues/1197))
* Add `on` and `fire` to `Store`, deprecate `onchange` and `observe` ([#1344](https://github.com/sveltejs/svelte/pull/1344))
* Require computed properties to have destructured argument in v2 mode ([#1069](https://github.com/sveltejs/svelte/issues/1069))
## 1.62.0
* Add a `code` field to errors and warnings ([#474](https://github.com/sveltejs/svelte/issues/474))
* When using v2 syntax, do not use interpolation in non-root `<style>` tags ([#1339](https://github.com/sveltejs/svelte/issues/1339))
## 1.61.0
* Support v2 syntax with `parser: 'v2'` option ([#1318](https://github.com/sveltejs/svelte/issues/1318))
## 1.60.3
* Fix validation of `multiple` attributes on bound `<select>` elements ([#1331](https://github.com/sveltejs/svelte/issues/1331))
## 1.60.2
* Fix order of insertions for keyed each blocks with siblings ([#1306](https://github.com/sveltejs/svelte/issues/1306))
* Bail out of CSS DCE if element has spread attribute ([#1300](https://github.com/sveltejs/svelte/issues/1300))
* Allow `console` etc in component events ([#1278](https://github.com/sveltejs/svelte/issues/1278))
* Deconflict against inherited contexts ([#1275](https://github.com/sveltejs/svelte/issues/1275))
* Make CSS DCE case insensitive ([#1269](https://github.com/sveltejs/svelte/issues/1269))
* Error on dynamic `multiple` attribute for bound select ([#1270](https://github.com/sveltejs/svelte/issues/1270))
* Allow custom events on `<:Window>` ([#1268](https://github.com/sveltejs/svelte/issues/1268))
## 1.60.1
* Fix spread updates on dynamic components ([#1307](https://github.com/sveltejs/svelte/issues/1307))
## 1.60.0
* Spread properties ([#195](https://github.com/sveltejs/svelte/issues/195))
* `svelte.compile` returns an object with `{ js, css, ast }` properties, where `js` and `css` are `{ code, map }` objects ([#1298](https://github.com/sveltejs/svelte/pull/1298))
* Fixed broken compile errors when using Rollup ([#1296](https://github.com/sveltejs/svelte/pull/1296))
## 1.59.0
* Deprecate `teardown` in custom event handlers ([#531](https://github.com/sveltejs/svelte/issues/531))
* Allow static content in keyed `each` block ([#1291](https://github.com/sveltejs/svelte/issues/1291))
* Allow empty content in keyed `each` block ([#1295](https://github.com/sveltejs/svelte/issues/1295))
* Only delete applicable transitions ([#1290](https://github.com/sveltejs/svelte/issues/1290))
## 1.58.5
* Allow backtick string literals for `svg`, `tag`, and `props` properties ([#1284](https://github.com/sveltejs/svelte/issues/1284))
* Fix removal of transition styles under Firefox ([#1288](https://github.com/sveltejs/svelte/pull/1288))
## 1.58.4
* Fix initial state regression ([#1283](https://github.com/sveltejs/svelte/pull/1283))
## 1.58.3
* Actions run in the context of the component ([#1279](https://github.com/sveltejs/svelte/pull/1279))
* Set refs when mounting dynamic components ([#1280](https://github.com/sveltejs/svelte/pull/1280))
## 1.58.2
* (1.58.1 failed to publish)
## 1.58.1
* Actions ([#1247](https://github.com/sveltejs/svelte/pull/1247))
* Support `preserveComments` option in SSR mode ([#1265](https://github.com/sveltejs/svelte/issues/1265))
* Fix performance regression ([#1274](https://github.com/sveltejs/svelte/pull/1274))
## 1.58.0
* Fast row swapping ([#588](https://github.com/sveltejs/svelte/issues/588))
* Better error messages for invalid directives ([#1242](https://github.com/sveltejs/svelte/pull/1242))
* Fix local context variable bugs ([#1240](https://github.com/sveltejs/svelte/pull/1243), [#1254](https://github.com/sveltejs/svelte/pull/1254))
* Skip missing property warnings for computed/global properties in dev mode ([#1246](https://github.com/sveltejs/svelte/pull/1246))
* Add end position to warnings ([#1250](https://github.com/sveltejs/svelte/pull/1250))
## 1.57.4
* Deconflict context names ([#1229](https://github.com/sveltejs/svelte/issues/1229))
* Use `setAttribute` to set input types ([#1209](https://github.com/sveltejs/svelte/issues/1209))
* Scale transition duration correctly ([#1221](https://github.com/sveltejs/svelte/issues/1221))
## 1.57.3
* Fix scoped CSS on static child elements ([#1223](https://github.com/sveltejs/svelte/issues/1223))
## 1.57.2
* Fix scoped CSS on SVG elements ([#1224](https://github.com/sveltejs/svelte/issues/1224))
## 1.57.1
* Add each_value to contextProps ([#1206](https://github.com/sveltejs/svelte/issues/1206))
## 1.57.0
* Use classes (not attributes) for style encapsulation, and base36-encode hashes ([#1118](https://github.com/sveltejs/svelte/issues/1118))
## 1.56.4
* Allow `component` and `state` to be context names ([#1213](https://github.com/sveltejs/svelte/issues/1213))
* Don't remove `@supports` rules when `cascade: false` ([#1215](https://github.com/sveltejs/svelte/issues/1215))
## 1.56.3
* Top-level transitions work inside nested components ([#1188](https://github.com/sveltejs/svelte/issues/1188))
* Always use internal `_mount` method ([#1201](https://github.com/sveltejs/svelte/issues/1201))
## 1.56.2
* Null out `key` for children of keyed each blocks ([#1202](https://github.com/sveltejs/svelte/issues/1202))

@ -5,32 +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
* [svelte-hot-loader](https://github.com/ekhaled/svelte-hot-loader) Webpack loader addon to support HMR
* [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',
@ -65,9 +77,9 @@ const { code, map } = svelte.compile( source, {
The Svelte compiler exposes the following API:
* `compile( source [, options ] ) => { code, map, ast, css }` - Compile the component with the given options (see below). Returns an object containing the compiled JavaScript, a sourcemap, an AST and transformed CSS.
* `create( source [, options ] ) => function` - Compile the component and return the component itself.
* `preprocess( source, options ) => Promise` — Preprocess a source file, e.g. to use PostCSS or CoffeeScript
* `compile(source [, options]) => { js, css, ast }` - Compile the component with the given options (see below). Returns an object containing the compiled JavaScript, a sourcemap, an AST and transformed CSS.
* `create(source [, options]) => function` - Compile the component and return the component itself.
* `preprocess(source, options) => Promise` — Preprocess a source file, e.g. to use PostCSS or CoffeeScript
* `VERSION` - The version of this copy of the Svelte compiler as a string, `'x.x.x'`.
### Compiler options
@ -76,13 +88,11 @@ The Svelte compiler optionally takes a second argument, an object of configurati
| | **Values** | **Description** | **Default** |
|---|---|---|---|
| `generate` | `'dom'`, `'ssr'` | Whether to generate JavaScript code intended for use on the client (`'dom'`), or for use in server-side rendering (`'ssr'`). | `'dom'` |
| `generate` | `'dom'`, `'ssr'`, `false` | Whether to generate JavaScript code intended for use on the client (`'dom'`), or for use in server-side rendering (`'ssr'`). If `false`, component will be parsed and validated but no code will be emitted | `'dom'` |
| `dev` | `true`, `false` | Whether to enable run-time checks in the compiled component. These are helpful during development, but slow your component down. | `false` |
| `css` | `true`, `false` | Whether to include code to inject your component's styles into the DOM. | `true` |
| `store` | `true`, `false` | Whether to support store integration on the compiled component. | `false` |
| `hydratable` | `true`, `false` | Whether to support hydration on the compiled component. | `false` |
| `customElement` | `true`, `false`, `{ tag, props }` | Whether to compile this component to a custom element. If `tag`/`props` are passed, compiles to a custom element and overrides the values exported by the component. | `false` |
| `cascade` | `true`, `false` | Whether to cascade all of the component's styles to child components. If `false`, only selectors wrapped in `:global(...)` and keyframe IDs beginning with `-global-` are cascaded. | `true` |
| `bind` | `boolean` | If `false`, disallows `bind:` directives | `true` |
| | | |
| `shared` | `true`, `false`, `string` | Whether to import various helpers from a shared external library. When you have a project with multiple components, this reduces the overall size of your JavaScript bundle, at the expense of having immediately-usable component. You can pass a string of the module path to use, or `true` will import from `'svelte/shared.js'`. | `false` |
@ -93,6 +103,7 @@ The Svelte compiler optionally takes a second argument, an object of configurati
| `filename` | `string` | The filename to use in sourcemaps and compiler error and warning messages. | `'SvelteComponent.html'` |
| `amd`.`id` | `string` | The AMD module ID to use for the `'amd'` and `'umd'` output formats. | `undefined` |
| `globals` | `object`, `function` | When outputting to the `'umd'`, `'iife'` or `'eval'` formats, an object or function mapping the names of imported dependencies to the names of global variables. | `{}` |
| `preserveComments` | `boolean` | Include comments in rendering. Currently, only applies to SSR rendering | `false` |
| | | |
| `onerror` | `function` | Specify a callback for when Svelte encounters an error while compiling the component. Passed two arguments: the error object, and another function that is Svelte's default onerror handling. | (exception is thrown) |
| `onwarn` | `function` | Specify a callback for when Svelte encounters a non-fatal warning while compiling the component. Passed two arguments: the warning object, and another function that is Svelte's default onwarn handling. | (warning is logged to console) |
@ -128,13 +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)
## 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"
```
## 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>
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,15 +1,20 @@
{
"name": "svelte",
"version": "1.56.2",
"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": {
@ -24,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,43 +50,47 @@
},
"homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": {
"@types/mocha": "^2.2.41",
"@types/node": "^8.0.17",
"@types/mocha": "^5.2.0",
"@types/node": "^10.5.5",
"acorn": "^5.4.1",
"acorn-dynamic-import": "^2.0.2",
"chalk": "^2.0.1",
"codecov": "^2.2.0",
"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.3.0",
"eslint-plugin-html": "^3.0.0",
"eslint-plugin-import": "^2.2.0",
"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.6.1",
"locate-character": "^2.0.0",
"magic-string": "^0.22.3",
"mocha": "^3.2.0",
"nightmare": "^2.10.0",
"jsdom": "^11.8.0",
"kleur": "^2.0.1",
"locate-character": "^2.0.5",
"magic-string": "^0.25.0",
"mocha": "^5.2.0",
"nightmare": "^3.0.1",
"node-resolve": "^1.3.3",
"nyc": "^11.1.0",
"prettier": "^1.7.0",
"reify": "^0.12.3",
"rollup": "^0.56.4",
"rollup-plugin-buble": "^0.15.0",
"rollup-plugin-commonjs": "^8.0.2",
"rollup-plugin-json": "^2.1.0",
"rollup-plugin-node-resolve": "^3.0.0",
"nyc": "^12.0.2",
"prettier": "^1.12.1",
"rollup": "^0.63.5",
"rollup-plugin-buble": "^0.19.2",
"rollup-plugin-commonjs": "^9.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",
"source-map": "^0.5.6",
"source-map-support": "^0.4.8",
"ts-node": "^3.3.0",
"sade": "^1.4.0",
"sander": "^0.6.0",
"shelljs": "^0.8.2",
"source-map": "0.6",
"source-map-support": "^0.5.4",
"tiny-glob": "^0.2.1",
"ts-node": "^7.0.0",
"tslib": "^1.8.0",
"typescript": "^2.6.1"
"typescript": "^3.0.1"
},
"nyc": {
"include": [

@ -34,7 +34,7 @@ export default [
/* ssr/register.js */
{
input: 'src/server-side-rendering/register.js',
input: 'src/ssr/register.js',
plugins: [
resolve(),
commonjs(),
@ -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',

@ -0,0 +1,118 @@
import { Node, Warning } from './interfaces';
import Component from './compile/Component';
const now = (typeof process !== 'undefined' && process.hrtime)
? () => {
const t = process.hrtime();
return t[0] * 1e3 + t[1] / 1e6;
}
: () => self.performance.now();
type Timing = {
label: string;
start: number;
end: number;
children: Timing[];
}
function collapseTimings(timings) {
const result = {};
timings.forEach(timing => {
result[timing.label] = Object.assign({
total: timing.end - timing.start
}, timing.children && collapseTimings(timing.children));
});
return result;
}
export default class Stats {
onwarn: (warning: Warning) => void;
startTime: number;
currentTiming: Timing;
currentChildren: Timing[];
timings: Timing[];
stack: Timing[];
warnings: Warning[];
constructor({ onwarn }: {
onwarn: (warning: Warning) => void
}) {
this.startTime = now();
this.stack = [];
this.currentChildren = this.timings = [];
this.onwarn = onwarn;
this.warnings = [];
}
start(label) {
const timing = {
label,
start: now(),
end: null,
children: []
};
this.currentChildren.push(timing);
this.stack.push(timing);
this.currentTiming = timing;
this.currentChildren = timing.children;
}
stop(label) {
if (label !== this.currentTiming.label) {
throw new Error(`Mismatched timing labels (expected ${this.currentTiming.label}, got ${label})`);
}
this.currentTiming.end = now();
this.stack.pop();
this.currentTiming = this.stack[this.stack.length - 1];
this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings;
}
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 = component && component.imports.map(node => {
return {
source: node.source.value,
specifiers: node.specifiers.map(specifier => {
return {
name: (
specifier.type === 'ImportDefaultSpecifier' ? 'default' :
specifier.type === 'ImportNamespaceSpecifier' ? '*' :
specifier.imported.name
),
as: specifier.local.name
};
})
}
});
const hooks: Record<string, boolean> = component && {
oncreate: !!component.templateProperties.oncreate,
ondestroy: !!component.templateProperties.ondestroy,
onstate: !!component.templateProperties.onstate,
onupdate: !!component.templateProperties.onupdate
};
return {
timings,
warnings: this.warnings,
imports,
hooks
};
}
warn(warning) {
this.warnings.push(warning);
this.onwarn(warning);
}
}

@ -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);

@ -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,16 +1,19 @@
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;
stylesheet: Stylesheet;
blocks: Block[];
localBlocks: Block[];
used: boolean;
constructor(node: Node) {
constructor(node: Node, stylesheet: Stylesheet) {
this.node = node;
this.stylesheet = stylesheet;
this.blocks = groupSelectors(node);
@ -27,11 +30,12 @@ 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 }) => {
node._needsCssAttribute = true;
this.stylesheet.nodesWithCssClass.add(node);
block.shouldEncapsulate = true;
});
@ -73,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
});
@ -93,13 +97,16 @@ 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(`:global(...) must be the first element in a compound selector`, selector.start);
component.error(selector, {
code: `css-invalid-global`,
message: `:global(...) must be the first element in a compound selector`
});
}
}
});
@ -117,17 +124,16 @@ export default class Selector {
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
validator.error(`:global(...) can be at the start or end of a selector sequence, but not in the middle`, this.blocks[i].selectors[0].start);
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`
});
}
}
}
}
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;
@ -136,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];
@ -152,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') {
@ -164,12 +169,13 @@ function applySelector(blocks: Block[], node: Node, stack: Node[], toEncapsulate
}
else if (selector.type === 'TypeSelector') {
if (node.name !== selector.name && selector.name !== '*') return false;
// remove toLowerCase() in v2, when uppercase elements will be forbidden
if (node.name.toLowerCase() !== selector.name.toLowerCase() && selector.name !== '*') return false;
}
else if (selector.type === 'RefSelector') {
if (node.attributes.some((attr: Node) => attr.type === 'Ref' && attr.name === 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;
}
@ -186,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;
}
@ -220,20 +232,23 @@ const operators = {
};
function attributeMatches(node: Node, name: string, expectedValue: string, operator: string, caseInsensitive: boolean) {
const spread = node.attributes.find(attr => attr.type === 'Spread');
if (spread) return true;
const attr = node.attributes.find((attr: Node) => attr.name === name);
if (!attr) return false;
if (attr.value === true) return operator === null;
if (attr.value.length > 1) return true;
if (attr.isTrue) return operator === null;
if (attr.chunks.length > 1) return true;
if (!expectedValue) return true;
const pattern = operators[operator](expectedValue, caseInsensitive ? 'i' : '');
const value = attr.value[0];
const value = attr.chunks[0];
if (!value) return false;
if (value.type === 'Text') return pattern.test(value.data);
const possibleValues = new Set();
gatherPossibleValues(value.expression, possibleValues);
gatherPossibleValues(value.node, possibleValues);
if (possibleValues.has(UNKNOWN)) return true;
for (const x of Array.from(possibleValues)) { // TypeScript for-of is slightly unlike JS
@ -243,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 '../generators/nodes/Element';
import { Validator } from '../validate/index';
import { Node, Parsed, 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[];
@ -14,10 +17,10 @@ class Rule {
node: Node;
parent: Atrule;
constructor(node: Node, parent?: Atrule) {
constructor(node: Node, stylesheet, parent?: Atrule) {
this.node = node;
this.parent = parent;
this.selectors = node.selector.children.map((node: Node) => new Selector(node));
this.selectors = node.selector.children.map((node: Node) => new Selector(node, stylesheet));
this.declarations = node.block.children.map((node: Node) => new Declaration(node));
}
@ -26,23 +29,23 @@ 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);
}
minify(code: MagicString, cascade: boolean, dev: boolean) {
minify(code: MagicString, dev: boolean) {
let c = this.node.start;
let started = false;
this.selectors.forEach((selector, i) => {
if (cascade || selector.used) {
if (selector.used) {
const separator = started ? ',' : '';
if ((selector.node.start - c) > separator.length) {
code.overwrite(c, selector.node.start, separator);
}
if (!cascade) selector.minify(code);
selector.minify(code);
c = selector.node.end;
started = true;
@ -66,45 +69,18 @@ class Rule {
code.remove(c, this.node.block.end - 1);
}
transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
if (this.parent && this.parent.node.type === 'Atrule' && this.parent.node.name === 'keyframes') return true;
const attr = `[${id}]`;
if (cascade) {
this.selectors.forEach(selector => {
// TODO disable cascading (without :global(...)) in v2
const { start, end, children } = selector.node;
const css = code.original;
const selectorString = css.slice(start, end);
const firstToken = children[0];
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (this.parent && this.parent.node.type === 'Atrule' && isKeyframesNode(this.parent.node)) return true;
let transformed;
const attr = `.${id}`;
if (firstToken.type === 'TypeSelector') {
const insert = firstToken.end;
const head = firstToken.name === '*' ? '' : css.slice(start, insert);
const tail = css.slice(insert, end);
transformed = `${head}${attr}${tail},${attr} ${selectorString}`;
} else {
transformed = `${attr}${selectorString},${attr} ${selectorString}`;
}
code.overwrite(start, end, transformed);
});
} else {
this.selectors.forEach(selector => selector.transform(code, attr));
}
this.declarations.forEach(declaration => declaration.transform(code, keyframes));
}
validate(validator: Validator) {
validate(component: Component) {
this.selectors.forEach(selector => {
selector.validate(validator);
selector.validate(component);
});
}
@ -123,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') {
@ -163,13 +139,13 @@ class Atrule {
}
apply(node: Element, stack: Element[]) {
if (this.node.name === 'media') {
if (this.node.name === 'media' || this.node.name === 'supports') {
this.children.forEach(child => {
child.apply(node, stack);
});
}
else if (this.node.name === 'keyframes') {
else if (isKeyframesNode(this.node)) {
this.children.forEach((rule: Rule) => {
rule.selectors.forEach(selector => {
selector.used = true;
@ -182,7 +158,7 @@ class Atrule {
return true; // TODO
}
minify(code: MagicString, cascade: boolean, dev: boolean) {
minify(code: MagicString, dev: boolean) {
if (this.node.name === 'media') {
const expressionChar = code.original[this.node.expression.start];
let c = this.node.start + (expressionChar === '(' ? 6 : 7);
@ -194,11 +170,19 @@ 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);
} else if (this.node.name === 'supports') {
let c = this.node.start + 9;
if (this.node.expression.start - c > 1) code.overwrite(c, this.node.expression.start, ' ');
this.node.expression.children.forEach((query: Node) => {
// TODO minify queries
c = query.end;
});
code.remove(c, this.node.block.start);
}
// TODO other atrules
@ -207,9 +191,9 @@ class Atrule {
let c = this.node.block.start + 1;
this.children.forEach(child => {
if (cascade || child.isUsed(dev)) {
if (child.isUsed(dev)) {
code.remove(c, child.node.start);
child.minify(code, cascade, dev);
child.minify(code, dev);
c = child.node.end;
}
});
@ -218,8 +202,8 @@ class Atrule {
}
}
transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
if (this.node.name === 'keyframes') {
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (isKeyframesNode(this.node)) {
this.node.expression.children.forEach(({ type, name, start, end }: Node) => {
if (type === 'Identifier') {
if (name.startsWith('-global-')) {
@ -232,13 +216,13 @@ class Atrule {
}
this.children.forEach(child => {
child.transform(code, id, keyframes, cascade);
child.transform(code, id, keyframes);
})
}
validate(validator: Validator) {
validate(component: Component) {
this.children.forEach(child => {
child.validate(validator);
child.validate(component);
});
}
@ -255,8 +239,7 @@ const keys = {};
export default class Stylesheet {
source: string;
parsed: Parsed;
cascade: boolean;
ast: Ast;
filename: string;
dev: boolean;
@ -266,25 +249,30 @@ export default class Stylesheet {
children: (Rule|Atrule)[];
keyframes: Map<string, string>;
constructor(source: string, parsed: Parsed, filename: string, cascade: boolean, dev: boolean) {
nodesWithCssClass: Set<Node>;
nodesWithRefCssClass: Map<String, Node>;
constructor(source: string, ast: Ast, filename: string, dev: boolean) {
this.source = source;
this.parsed = parsed;
this.cascade = cascade;
this.ast = ast;
this.filename = filename;
this.dev = dev;
this.children = [];
this.keyframes = new Map();
if (parsed.css && parsed.css.children.length) {
this.id = `svelte-${hash(parsed.css.content.styles)}`;
this.nodesWithCssClass = new Set();
this.nodesWithRefCssClass = new Map();
if (ast.css && ast.css.children.length) {
this.id = `svelte-${hash(ast.css.content.styles)}`;
this.hasStyles = true;
const stack: (Rule | Atrule)[] = [];
let currentAtrule: Atrule = null;
walk(this.parsed.css, {
walk(this.ast.css, {
enter: (node: Node) => {
if (node.type === 'Atrule') {
const last = stack[stack.length - 1];
@ -302,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}`);
@ -314,7 +302,7 @@ export default class Stylesheet {
}
if (node.type === 'Rule') {
const rule = new Rule(node, currentAtrule);
const rule = new Rule(node, this, currentAtrule);
stack.push(rule);
if (currentAtrule) {
@ -344,25 +332,29 @@ export default class Stylesheet {
if (parent.type === 'Element') stack.unshift(<Element>parent);
}
if (this.cascade) {
if (stack.length === 0) node._needsCssAttribute = true;
return;
}
for (let i = 0; i < this.children.length; i += 1) {
const child = this.children[i];
child.apply(node, stack);
}
}
reify() {
this.nodesWithCssClass.forEach((node: Node) => {
node.addCssClass();
});
this.nodesWithRefCssClass.forEach((node: Node, name: String) => {
node.addCssClass(`svelte-ref-${name}`);
})
}
render(cssOutputFilename: string, shouldTransformSelectors: boolean) {
if (!this.hasStyles) {
return { css: null, cssMap: null };
return { code: null, map: null };
}
const code = new MagicString(this.source);
walk(this.parsed.css, {
walk(this.ast.css, {
enter: (node: Node) => {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
@ -371,15 +363,15 @@ export default class Stylesheet {
if (shouldTransformSelectors) {
this.children.forEach((child: (Atrule|Rule)) => {
child.transform(code, this.id, this.keyframes, this.cascade);
child.transform(code, this.id, this.keyframes);
});
}
let c = 0;
this.children.forEach(child => {
if (this.cascade || child.isUsed(this.dev)) {
if (child.isUsed(this.dev)) {
code.remove(c, child.node.start);
child.minify(code, this.cascade, this.dev);
child.minify(code, this.dev);
c = child.node.end;
}
});
@ -387,8 +379,8 @@ export default class Stylesheet {
code.remove(c, this.source.length);
return {
css: code.toString(),
cssMap: code.generateMap({
code: code.toString(),
map: code.generateMap({
includeContent: true,
source: this.filename,
file: cssOutputFilename
@ -396,33 +388,34 @@ export default class Stylesheet {
};
}
validate(validator: Validator) {
validate(component: Component) {
this.children.forEach(child => {
child.validate(validator);
child.validate(component);
});
}
warnOnUnusedSelectors(onwarn: (warning: Warning) => void) {
if (this.cascade) return;
let locator;
const handler = (selector: Selector) => {
const pos = selector.node.start;
if (!locator) locator = getLocator(this.source);
const { line, column } = locator(pos);
if (!locator) locator = getLocator(this.source, { offsetLine: 1 });
const start = locator(pos);
const end = locator(selector.node.end);
const frame = getCodeFrame(this.source, line, column);
const frame = getCodeFrame(this.source, start.line - 1, start.column);
const message = `Unused CSS selector`;
onwarn({
code: `css-unused-selector`,
message,
frame,
loc: { line: line + 1, column },
start,
end,
pos,
filename: this.filename,
toString: () => `${message} (${line + 1}:${column})\n${frame}`,
toString: () => `${message} (${start.line}:${start.column})\n${frame}`,
});
};

@ -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);
}
}

@ -0,0 +1,27 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
export default class Action extends Node {
type: 'Action';
name: string;
expression: Expression;
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(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;
}
}

@ -0,0 +1,114 @@
import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import addToSet from '../../utils/addToSet';
import Component from '../Component';
import Node from './shared/Node';
import Element from './Element';
import Text from './Text';
import Expression from './shared/Expression';
export default class Attribute extends Node {
type: 'Attribute';
start: number;
end: number;
component: Component;
parent: Element;
name: string;
isSpread: boolean;
isTrue: boolean;
isDynamic: boolean;
isSynthetic: boolean;
shouldCache: boolean;
expression?: Expression;
chunks: (Text | Expression)[];
dependencies: Set<string>;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (info.type === 'Spread') {
this.name = null;
this.isSpread = true;
this.isTrue = false;
this.isSynthetic = false;
this.expression = new Expression(component, this, scope, info.expression);
this.dependencies = this.expression.dependencies;
this.chunks = null;
this.isDynamic = true; // TODO not necessarily
this.shouldCache = false; // TODO does this mean anything here?
}
else {
this.name = info.name;
this.isTrue = info.value === true;
this.isSynthetic = info.synthetic;
this.dependencies = new Set();
this.chunks = this.isTrue
? []
: info.value.map(node => {
if (node.type === 'Text') return node;
const expression = new Expression(component, this, scope, node.expression);
addToSet(this.dependencies, expression.dependencies);
return expression;
});
this.isDynamic = this.dependencies.size > 0;
this.shouldCache = this.isDynamic
? this.chunks.length === 1
? this.chunks[0].node.type !== 'Identifier' || scope.names.has(this.chunks[0].node.name)
: true
: false;
}
}
getValue() {
if (this.isTrue) return true;
if (this.chunks.length === 0) return `""`;
if (this.chunks.length === 1) {
return this.chunks[0].type === 'Text'
? stringify(this.chunks[0].data)
: this.chunks[0].snippet;
}
return (this.chunks[0].type === 'Text' ? '' : `"" + `) +
this.chunks
.map(chunk => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
return chunk.getPrecedence() <= 13 ? `(${chunk.snippet})` : chunk.snippet;
}
})
.join(' + ');
}
getStaticValue() {
if (this.isSpread || this.isDynamic) return null;
return this.isTrue
? true
: this.chunks[0]
? this.chunks[0].data
: '';
}
stringifyForSsr() {
return this.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
return '${@escape(' + chunk.snippet + ')}';
})
.join('');
}
}

@ -0,0 +1,29 @@
import Node from './shared/Node';
import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';
import Expression from './shared/Expression';
export default class AwaitBlock extends Node {
expression: Expression;
value: string;
error: string;
pending: PendingBlock;
then: ThenBlock;
catch: CatchBlock;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
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(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);
}
}

@ -0,0 +1,41 @@
import Node from './shared/Node';
import getObject from '../../utils/getObject';
import Expression from './shared/Expression';
export default class Binding extends Node {
name: string;
value: Expression;
isContextual: boolean;
usesContext: boolean;
obj: string;
prop: string;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.value = new Expression(component, this, scope, info.value);
let obj;
let prop;
const { name } = getObject(this.value.node);
this.isContextual = scope.names.has(name);
if (this.value.node.type === 'MemberExpression') {
prop = `[✂${this.value.node.property.start}-${this.value.node.property.end}✂]`;
if (!this.value.node.computed) prop = `'${prop}'`;
obj = `[✂${this.value.node.object.start}-${this.value.node.object.end}✂]`;
this.usesContext = true;
} else {
obj = 'ctx';
prop = `'${name}'`;
this.usesContext = scope.names.has(name);
}
this.obj = obj;
this.prop = prop;
}
}

@ -0,0 +1,15 @@
import Node from './shared/Node';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class CatchBlock extends Node {
block: Block;
children: Node[];
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;
}
}

@ -0,0 +1,11 @@
import Node from './shared/Node';
export default class Comment extends Node {
type: 'Comment';
data: string;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.data = info.data;
}
}

@ -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);
});
}
}

@ -0,0 +1,79 @@
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
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';
block: Block;
expression: Expression;
iterations: string;
index: string;
context: string;
key: Expression;
scope: TemplateScope;
contexts: Array<{ name: string, tail: string }>;
hasAnimation: boolean;
children: Node[];
else?: ElseBlock;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
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.scope = scope.child();
this.contexts = [];
unpackDestructuring(this.contexts, info.context, '');
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`
});
}
this.scope.add(context.key.name, this.expression.dependencies);
});
this.key = info.key
? new Expression(component, this, this.scope, info.key)
: null;
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.hasAnimation = false;
this.children = mapChildren(component, this, this.scope, info.children);
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`
});
}
}
this.warnIfEmptyBlock(); // TODO would be better if EachBlock, IfBlock etc extended an abstract Block class
this.else = info.else
? new ElseBlock(component, this, this.scope, info.else)
: null;
}
}

@ -0,0 +1,636 @@
import isVoidElementName from '../../utils/isVoidElementName';
import { quotePropIfNecessary } from '../../utils/quoteIfNecessary';
import Node from './shared/Node';
import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Transition from './Transition';
import Animation from './Animation';
import Action from './Action';
import Class from './Class';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren';
import { dimensions } from '../../utils/patterns';
import fuzzymatch from '../validate/utils/fuzzymatch';
import Ref from './Ref';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
const ariaAttributes = 'activedescendant atomic autocomplete busy checked controls current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const ariaAttributeSet = new Set(ariaAttributes);
const ariaRoles = 'alert alertdialog application article banner button cell checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document feed figure form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search searchbox section sectionhead select separator slider spinbutton status structure switch tab table tablist tabpanel term textbox timer toolbar tooltip tree treegrid treeitem widget window'.split(' ');
const ariaRoleSet = new Set(ariaRoles);
const a11yRequiredAttributes = {
a: ['href'],
area: ['alt', 'aria-label', 'aria-labelledby'],
// html-has-lang
html: ['lang'],
// iframe-has-title
iframe: ['title'],
img: ['alt'],
object: ['title', 'aria-label', 'aria-labelledby']
};
const a11yDistractingElements = new Set([
'blink',
'marquee'
]);
const a11yRequiredContent = new Set([
// anchor-has-content
'a',
// heading-has-content
'h1',
'h2',
'h3',
'h4',
'h5',
'h6'
])
const invisibleElements = new Set(['meta', 'html', 'script', 'style']);
export default class Element extends Node {
type: 'Element';
name: string;
scope: any; // TODO
attributes: Attribute[];
actions: Action[];
bindings: Binding[];
classes: Class[];
handlers: EventHandler[];
intro?: Transition;
outro?: Transition;
animation?: Animation;
children: Node[];
ref: Ref;
namespace: string;
constructor(component, parent, scope, info: any) {
super(component, parent, scope, info);
this.name = info.name;
this.scope = scope;
const parentElement = parent.findNearest(/^Element/);
this.namespace = this.name === 'svg' ?
namespaces.svg :
parentElement ? parentElement.namespace : this.component.namespace;
if (!this.namespace && svg.test(this.name)) {
this.component.warn(this, {
code: `missing-namespace`,
message: `<${this.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`
});
}
this.attributes = [];
this.actions = [];
this.bindings = [];
this.classes = [];
this.handlers = [];
this.intro = null;
this.outro = null;
this.animation = null;
if (this.name === 'textarea') {
if (info.children.length > 0) {
const valueAttribute = info.attributes.find(node => node.name === 'value');
if (valueAttribute) {
component.error(valueAttribute, {
code: `textarea-duplicate-value`,
message: `A <textarea> can have either a value attribute or (equivalently) child content, but not both`
});
}
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children
});
info.children = [];
}
}
if (this.name === 'option') {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const valueAttribute = info.attributes.find((attribute: Node) => attribute.name === 'value');
if (!valueAttribute) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children,
synthetic: true
});
}
}
info.attributes.forEach(node => {
switch (node.type) {
case 'Action':
this.actions.push(new Action(component, this, scope, node));
break;
case 'Attribute':
case 'Spread':
// special case
if (node.name === 'xmlns') this.namespace = node.value[0].data;
this.attributes.push(new Attribute(component, this, scope, node));
break;
case 'Binding':
this.bindings.push(new Binding(component, this, scope, node));
break;
case 'Class':
this.classes.push(new Class(component, this, scope, node));
break;
case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Transition':
const transition = new Transition(component, this, scope, node);
if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition;
break;
case 'Animation':
this.animation = new Animation(component, this, scope, node);
break;
case 'Ref':
this.ref = new Ref(component, this, scope, node);
break;
default:
throw new Error(`Not implemented: ${node.type}`);
}
});
this.children = mapChildren(component, this, scope, info.children);
this.validate();
component.stylesheet.apply(this);
}
validate() {
if (a11yDistractingElements.has(this.name)) {
// no-distracting-elements
this.component.warn(this, {
code: `a11y-distracting-elements`,
message: `A11y: Avoid <${this.name}> elements`
});
}
if (this.name === 'figcaption') {
if (this.parent.name !== 'figure') {
this.component.warn(this, {
code: `a11y-structure`,
message: `A11y: <figcaption> must be an immediate child of <figure>`
});
}
}
if (this.name === 'figure') {
const children = this.children.filter(node => {
if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data);
return true;
});
const index = children.findIndex(child => child.name === 'figcaption');
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
this.component.warn(children[index], {
code: `a11y-structure`,
message: `A11y: <figcaption> must be first or last child of <figure>`
});
}
}
this.validateAttributes();
this.validateBindings();
this.validateContent();
}
validateAttributes() {
const { component } = this;
const attributeMap = new Map();
this.attributes.forEach(attribute => {
if (attribute.isSpread) return;
const name = attribute.name.toLowerCase();
// aria-props
if (name.startsWith('aria-')) {
if (invisibleElements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: `a11y-aria-attributes`,
message: `A11y: <${this.name}> should not have aria-* attributes`
});
}
const type = name.slice(5);
if (!ariaAttributeSet.has(type)) {
const match = fuzzymatch(type, ariaAttributes);
let message = `A11y: Unknown aria attribute 'aria-${type}'`;
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: `a11y-unknown-aria-attribute`,
message
});
}
if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) {
component.warn(attribute, {
code: `a11y-hidden`,
message: `A11y: <${this.name}> element should not be hidden`
});
}
}
// aria-role
if (name === 'role') {
if (invisibleElements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: `a11y-misplaced-role`,
message: `A11y: <${this.name}> should not have role attribute`
});
}
const value = attribute.getStaticValue();
if (value && !ariaRoleSet.has(value)) {
const match = fuzzymatch(value, ariaRoles);
let message = `A11y: Unknown role '${value}'`;
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: `a11y-unknown-role`,
message
});
}
}
// no-access-key
if (name === 'accesskey') {
component.warn(attribute, {
code: `a11y-accesskey`,
message: `A11y: Avoid using accesskey`
});
}
// no-autofocus
if (name === 'autofocus') {
component.warn(attribute, {
code: `a11y-autofocus`,
message: `A11y: Avoid using autofocus`
});
}
// scope
if (name === 'scope' && this.name !== 'th') {
component.warn(attribute, {
code: `a11y-misplaced-scope`,
message: `A11y: The scope attribute should only be used with <th> elements`
});
}
// tabindex-no-positive
if (name === 'tabindex') {
const value = attribute.getStaticValue();
if (!isNaN(value) && +value > 0) {
component.warn(attribute, {
code: `a11y-positive-tabindex`,
message: `A11y: avoid tabindex values above zero`
});
}
}
if (name === 'slot') {
if (attribute.isDynamic) {
component.error(attribute, {
code: `invalid-slot-attribute`,
message: `slot attribute cannot have a dynamic value`
});
}
let ancestor = this.parent;
do {
if (ancestor.type === 'InlineComponent') break;
if (ancestor.type === 'Element' && /-/.test(ancestor.name)) break;
if (ancestor.type === 'IfBlock' || ancestor.type === 'EachBlock') {
const type = ancestor.type === 'IfBlock' ? 'if' : 'each';
const message = `Cannot place slotted elements inside an ${type}-block`;
component.error(attribute, {
code: `invalid-slotted-content`,
message
});
}
} while (ancestor = ancestor.parent);
if (!ancestor) {
component.error(attribute, {
code: `invalid-slotted-content`,
message: `Element with a slot='...' attribute must be a descendant of a component or custom element`
});
}
}
attributeMap.set(attribute.name, attribute);
});
// handle special cases
if (this.name === 'a') {
const attribute = attributeMap.get('href') || attributeMap.get('xlink:href');
if (attribute) {
const value = attribute.getStaticValue();
if (value === '' || value === '#') {
component.warn(attribute, {
code: `a11y-invalid-attribute`,
message: `A11y: '${value}' is not a valid ${attribute.name} attribute`
});
}
} else {
component.warn(this, {
code: `a11y-missing-attribute`,
message: `A11y: <a> element should have an href attribute`
});
}
}
else {
const requiredAttributes = a11yRequiredAttributes[this.name];
if (requiredAttributes) {
const hasAttribute = requiredAttributes.some(name => attributeMap.has(name));
if (!hasAttribute) {
shouldHaveAttribute(this, requiredAttributes);
}
}
if (this.name === 'input') {
const type = attributeMap.get('type');
if (type && type.getStaticValue() === 'image') {
shouldHaveAttribute(
this,
['alt', 'aria-label', 'aria-labelledby'],
'input type="image"'
);
}
}
}
}
validateBindings() {
const { component } = this;
const checkTypeAttribute = () => {
const attribute = this.attributes.find(
(attribute: Attribute) => attribute.name === 'type'
);
if (!attribute) return null;
if (attribute.isDynamic) {
component.error(attribute, {
code: `invalid-type`,
message: `'type' attribute cannot be dynamic if input uses two-way binding`
});
}
const value = attribute.getStaticValue();
if (value === true) {
component.error(attribute, {
code: `missing-type`,
message: `'type' attribute must be specified`
});
}
return value;
};
this.bindings.forEach(binding => {
const { name } = binding;
if (name === 'value') {
if (
this.name !== 'input' &&
this.name !== 'textarea' &&
this.name !== 'select'
) {
component.error(binding, {
code: `invalid-binding`,
message: `'value' is not a valid binding on <${this.name}> elements`
});
}
if (this.name === 'select') {
const attribute = this.attributes.find(
(attribute: Attribute) => attribute.name === 'multiple'
);
if (attribute && attribute.isDynamic) {
component.error(attribute, {
code: `dynamic-multiple-attribute`,
message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
});
}
} else {
checkTypeAttribute();
}
} else if (name === 'checked' || name === 'indeterminate') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' is not a valid binding on <${this.name}> elements`
});
}
if (checkTypeAttribute() !== 'checkbox') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <input type="checkbox">`
});
}
} else if (name === 'group') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'group' is not a valid binding on <${this.name}> elements`
});
}
const type = checkTypeAttribute();
if (type !== 'checkbox' && type !== 'radio') {
component.error(binding, {
code: `invalid-binding`,
message: `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`
});
}
} else if (name == 'files') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'files' binding acn only be used with <input type="file">`
});
}
const type = checkTypeAttribute();
if (type !== 'file') {
component.error(binding, {
code: `invalid-binding`,
message: `'files' binding can only be used with <input type="file">`
});
}
} else if (
name === 'currentTime' ||
name === 'duration' ||
name === 'paused' ||
name === 'buffered' ||
name === 'seekable' ||
name === 'played' ||
name === 'volume'
) {
if (this.name !== 'audio' && this.name !== 'video') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <audio> or <video>`
});
}
} else if (dimensions.test(name)) {
if (this.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
} else if (svg.test(this.name)) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on SVG elements`
});
} else if (isVoidElementName(this.name)) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on void elements like <${this.name}>. Use a wrapper element instead`
});
}
} else {
component.error(binding, {
code: `invalid-binding`,
message: `'${binding.name}' is not a valid binding`
});
}
});
}
validateContent() {
if (!a11yRequiredContent.has(this.name)) return;
if (this.children.length === 0) {
this.component.warn(this, {
code: `a11y-missing-content`,
message: `A11y: <${this.name}> element should have child content`
});
}
}
getStaticAttributeValue(name: string) {
const attribute = this.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.name === 'audio' || this.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 }]
})
);
}
}
}
function shouldHaveAttribute(
node,
attributes: string[],
name = node.name
) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
node.component.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
}

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

@ -0,0 +1,157 @@
import Node from './shared/Node';
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;
dependencies: Set<string>;
expression: Node;
callee: any; // TODO
usesComponent: boolean;
usesContext: boolean;
isCustomEvent: boolean;
shouldHoist: boolean;
insertionPoint: number;
args: Expression[];
snippet: string;
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(component, this, scope, param);
addToSet(this.dependencies, expression.dependencies);
if (expression.usesContext) this.usesContext = true;
return expression;
});
this.snippet = `[✂${info.expression.start}-${info.expression.end}✂];`;
} else {
this.callee = null;
this.insertionPoint = null;
this.args = null;
this.usesComponent = true;
this.usesContext = false;
this.snippet = null; // TODO handle shorthand events here?
}
this.isCustomEvent = component.events.has(this.name);
this.shouldHoist = !this.isCustomEvent && parent.hasAncestor('EachBlock');
}
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_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] === '$' && !component.methods.has(this.callee.name)) {
component.code.overwrite(
this.insertionPoint,
this.insertionPoint + 1,
`${component_name}.store.`
);
} else {
component.code.prependRight(
this.insertionPoint,
`${component_name}.`
);
}
}
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 (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
});
}
}

@ -0,0 +1,19 @@
import Node from './shared/Node';
import Component from '../Component';
import mapChildren from './shared/mapChildren';
import Block from '../render-dom/Block';
import TemplateScope from './shared/TemplateScope';
export default class Fragment extends Node {
block: Block;
children: Node[];
scope: TemplateScope;
constructor(component: Component, info: any) {
const scope = new TemplateScope();
super(component, null, scope, info);
this.scope = scope;
this.children = mapChildren(component, this, scope, info.children);
}
}

@ -0,0 +1,23 @@
import Node from './shared/Node';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class Head extends Node {
type: 'Head';
children: any[]; // TODO
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (info.attributes.length) {
component.error(info.attributes[0], {
code: `invalid-attribute`,
message: `<svelte:head> should not have any attributes or directives`
});
}
this.children = mapChildren(component, parent, scope, info.children.filter(child => {
return (child.type !== 'Text' || /\S/.test(child.data));
}));
}
}

@ -0,0 +1,27 @@
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Block from '../render-dom/Block';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
export default class IfBlock extends Node {
type: 'IfBlock';
expression: Expression;
children: any[];
else: ElseBlock;
block: Block;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);
this.children = mapChildren(component, this, scope, info.children);
this.else = info.else
? new ElseBlock(component, this, scope, info.else)
: null;
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);
}
}

@ -0,0 +1,3 @@
import Tag from './shared/Tag';
export default class MustacheTag extends Tag {}

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

@ -0,0 +1,3 @@
import Tag from './shared/Tag';
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;
}
}

@ -0,0 +1,78 @@
import Node from './shared/Node';
import Element from './Element';
import Attribute from './Attribute';
export default class Slot extends Element {
type: 'Element';
name: string;
attributes: Attribute[];
children: Node[];
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);
});
// if (node.attributes.length === 0) && validator.slots.has('default')) {
// validator.error(node, {
// code: `duplicate-slot`,
// message: `duplicate default <slot> element`
// });
// }
}
getStaticAttributeValue(name: string) {
const attribute = this.attributes.find(
attr => 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;
}
}

@ -0,0 +1,12 @@
import Node from './shared/Node';
export default class Text extends Node {
type: 'Text';
data: string;
shouldSkip: boolean;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.data = info.data;
}
}

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

@ -0,0 +1,36 @@
import Node from './shared/Node';
import mapChildren from './shared/mapChildren';
export default class Title extends Node {
type: 'Title';
children: any[]; // TODO
shouldCache: boolean;
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
? (
info.children[0].type !== 'Identifier' ||
scope.names.has(info.children[0].name)
)
: true;
}
}

@ -0,0 +1,48 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
export default class Transition extends Node {
type: 'Transition';
name: string;
directive: string;
expression: Expression;
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(component, this, scope, info.expression)
: null;
}
}
function describe(transition: Transition) {
return transition.directive === 'transition'
? `a 'transition'`
: `an '${transition.directive}'`;
}

@ -0,0 +1,74 @@
import Node from './shared/Node';
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 validBindings = [
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'scrollX',
'scrollY',
'online'
];
export default class Window extends Node {
type: 'Window';
handlers: EventHandler[];
bindings: Binding[];
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(component, this, scope, node));
}
else if (node.type === 'Binding') {
if (node.value.type !== 'Identifier') {
const { parts } = flattenReference(node.value);
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('.')}'`
});
}
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>`;
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)}`
});
}
}
this.bindings.push(new Binding(component, this, scope, node));
}
else {
// TODO there shouldn't be anything else here...
}
});
}
}

@ -0,0 +1,173 @@
import Component from '../../Component';
import { walk } from 'estree-walker';
import isReference from 'is-reference';
import flattenReference from '../../../utils/flattenReference';
import { createScopes } from '../../../utils/annotateWithScopes';
import { Node } from '../../../interfaces';
const binaryOperators: Record<string, number> = {
'**': 15,
'*': 14,
'/': 14,
'%': 14,
'+': 13,
'-': 13,
'<<': 12,
'>>': 12,
'>>>': 12,
'<': 11,
'<=': 11,
'>': 11,
'>=': 11,
'in': 11,
'instanceof': 11,
'==': 10,
'!=': 10,
'===': 10,
'!==': 10,
'&': 9,
'^': 8,
'|': 7
};
const logicalOperators: Record<string, number> = {
'&&': 6,
'||': 5
};
const precedence: Record<string, (node?: Node) => number> = {
Literal: () => 21,
Identifier: () => 21,
ParenthesizedExpression: () => 20,
MemberExpression: () => 19,
NewExpression: () => 19, // can be 18 (if no args) but makes no practical difference
CallExpression: () => 19,
UpdateExpression: () => 17,
UnaryExpression: () => 16,
BinaryExpression: (node: Node) => binaryOperators[node.operator],
LogicalExpression: (node: Node) => logicalOperators[node.operator],
ConditionalExpression: () => 4,
AssignmentExpression: () => 3,
YieldExpression: () => 2,
SpreadElement: () => 1,
SequenceExpression: () => 0
};
export default class Expression {
component: Component;
node: any;
snippet: string;
usesContext: boolean;
references: Set<string>;
dependencies: Set<string>;
thisReferences: Array<{ start: number, end: number }>;
constructor(component, parent, scope, info) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
component: {
value: component
}
});
this.node = info;
this.thisReferences = [];
this.snippet = `[✂${info.start}-${info.end}✂]`;
this.usesContext = false;
const dependencies = new Set();
const { code, helpers } = component;
let { map, scope: currentScope } = createScopes(info);
const isEventHandler = parent.type === 'EventHandler';
const expression = this;
const isSynthetic = parent.isSynthetic;
walk(info, {
enter(node: any, parent: any, key: string) {
// don't manipulate shorthand props twice
if (key === 'value' && parent.shorthand) return;
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
if (map.has(node)) {
currentScope = map.get(node);
return;
}
if (node.type === 'ThisExpression') {
expression.thisReferences.push(node);
}
if (isReference(node, parent)) {
const { name, nodes } = flattenReference(node);
if (currentScope.has(name) || (name === 'event' && isEventHandler)) return;
if (component.helpers.has(name)) {
let object = node;
while (object.type === 'MemberExpression') object = object.object;
component.used.helpers.add(name);
const alias = component.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias);
return;
}
expression.usesContext = true;
if (!isSynthetic) {
// <option> value attribute could be synthetic — avoid double editing
code.prependRight(node.start, key === 'key' && parent.shorthand
? `${name}: ctx.`
: 'ctx.');
}
if (scope.names.has(name)) {
scope.dependenciesForName.get(name).forEach(dependency => {
dependencies.add(dependency);
});
} else {
dependencies.add(name);
component.expectedProperties.add(name);
}
if (node.type === 'MemberExpression') {
nodes.forEach(node => {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
});
}
this.skip();
}
},
leave(node: Node, parent: Node) {
if (map.has(node)) currentScope = currentScope.parent;
}
});
this.dependencies = dependencies;
}
getPrecedence() {
return this.node.type in precedence ? precedence[this.node.type](this.node) : 0;
}
overwriteThis(name) {
this.thisReferences.forEach(ref => {
this.component.code.overwrite(ref.start, ref.end, name, {
storeName: true
});
});
}
}

@ -0,0 +1,71 @@
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 component: Component;
readonly parent: Node;
readonly type: string;
prev?: Node;
next?: Node;
canUseInnerHTML: boolean;
var: string;
constructor(component: Component, parent, scope, info: any) {
this.start = info.start;
this.end = info.end;
this.type = info.type;
// this makes properties non-enumerable, which makes logging
// bearable. might have a performance cost. TODO remove in prod?
Object.defineProperties(this, {
component: {
value: component
},
parent: {
value: parent
}
});
}
cannotUseInnerHTML() {
if (this.canUseInnerHTML !== false) {
this.canUseInnerHTML = false;
if (this.parent) this.parent.cannotUseInnerHTML();
}
}
hasAncestor(type: string) {
return this.parent ?
this.parent.type === type || this.parent.hasAncestor(type) :
false;
}
findNearest(selector: RegExp) {
if (selector.test(this.type)) return this;
if (this.parent) return this.parent.findNearest(selector);
}
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
}
warnIfEmptyBlock() {
if (!this.component.options.dev) return;
if (!/Block$/.test(this.type) || !this.children) return;
if (this.children.length > 1) return;
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'
});
}
}
}

@ -0,0 +1,17 @@
import Node from './Node';
import Expression from './Expression';
export default class Tag extends Node {
expression: Expression;
shouldCache: boolean;
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))
);
}
}

@ -0,0 +1,19 @@
export default class TemplateScope {
names: Set<string>;
dependenciesForName: Map<string, string>;
constructor(parent?: TemplateScope) {
this.names = new Set(parent ? parent.names : []);
this.dependenciesForName = new Map(parent ? parent.dependenciesForName : []);
}
add(name, dependencies) {
this.names.add(name);
this.dependenciesForName.set(name, dependencies);
return this;
}
child() {
return new TemplateScope(this);
}
}

@ -0,0 +1,49 @@
import AwaitBlock from '../AwaitBlock';
import Comment from '../Comment';
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';
import Window from '../Window';
import Node from './Node';
function getConstructor(type): typeof Node {
switch (type) {
case 'AwaitBlock': return AwaitBlock;
case 'Comment': return Comment;
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;
case 'Window': return Window;
default: throw new Error(`Not implemented: ${type}`);
}
}
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(component, parent, scope, child);
if (last) last.next = node;
node.prev = last;
last = node;
return node;
});
}

@ -1,49 +1,33 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { escape } from '../../utils/stringify';
import { DomGenerator } from './index';
import { Node } from '../../interfaces';
import shared from './shared';
import Renderer from './Renderer';
import Wrapper from './wrappers/shared/Wrapper';
export interface BlockOptions {
parent?: Block;
name: string;
generator?: DomGenerator;
expression?: Node;
context?: string;
destructuredContexts?: string[];
renderer?: Renderer;
comment?: string;
key?: string;
contexts?: Map<string, string>;
contextTypes?: Map<string, string>;
indexes?: Map<string, string>;
changeableIndexes?: Map<string, boolean>;
indexNames?: Map<string, string>;
listNames?: Map<string, string>;
indexName?: string;
listName?: string;
bindings?: Map<string, () => string>;
dependencies?: Set<string>;
}
export default class Block {
generator: DomGenerator;
parent?: Block;
renderer: Renderer;
name: string;
expression: Node;
context: string;
destructuredContexts?: string[];
comment?: string;
wrappers: Wrapper[];
key: string;
first: string;
contexts: Map<string, string>;
contextTypes: Map<string, string>;
indexes: Map<string, string>;
changeableIndexes: Map<string, boolean>;
dependencies: Set<string>;
indexNames: Map<string, string>;
listNames: Map<string, string>;
indexName: string;
listName: string;
bindings: Map<string, () => string>;
builders: {
init: CodeBuilder;
@ -51,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;
@ -71,28 +60,20 @@ export default class Block {
autofocus: string;
constructor(options: BlockOptions) {
this.generator = options.generator;
this.parent = options.parent;
this.renderer = options.renderer;
this.name = options.name;
this.expression = options.expression;
this.context = options.context;
this.destructuredContexts = options.destructuredContexts;
this.comment = options.comment;
this.wrappers = [];
// for keyed each blocks
this.key = options.key;
this.first = null;
this.contexts = options.contexts;
this.contextTypes = options.contextTypes;
this.indexes = options.indexes;
this.changeableIndexes = options.changeableIndexes;
this.dependencies = new Set();
this.indexNames = options.indexNames;
this.listNames = options.listNames;
this.indexName = options.indexName;
this.listName = options.listName;
this.bindings = options.bindings;
this.builders = {
init: new CodeBuilder(),
@ -100,26 +81,69 @@ 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.aliases = new Map();
this.getUniqueName = this.renderer.component.getUniqueNameMaker();
this.variables = new Map();
this.getUniqueName = this.generator.getUniqueNameMaker();
this.aliases = new Map()
.set('component', this.getUniqueName('component'))
.set('ctx', this.getUniqueName('ctx'));
if (this.key) this.aliases.set('key', this.getUniqueName('key'));
this.hasUpdateMethod = false; // determined later
}
addDependencies(dependencies: string[]) {
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);
});
@ -129,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`
@ -166,55 +208,25 @@ export default class Block {
return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
}
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
}
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');
if (!this.builders.mount.isEmpty()) {
this.builders.mount.addLine(`#current = true;`);
}
let outroing;
const hasOutros = !this.builders.outro.isEmpty();
if (hasOutros) {
outroing = this.alias('outroing');
this.addVariable(outroing);
if (!this.builders.outro.isEmpty()) {
this.builders.outro.addLine(`#current = false;`);
}
}
if (this.autofocus) {
this.builders.mount.addLine(`${this.autofocus}.focus();`);
}
// TODO `this.contexts` is possibly redundant post-#1122
const initializers = [];
const updaters = [];
this.contexts.forEach((alias, name) => {
// TODO only the ones that are actually used in this block...
const assignment = `${alias} = state.${name}`;
initializers.push(assignment);
updaters.push(`${assignment};`);
this.hasUpdateMethod = true;
});
this.indexNames.forEach((alias, name) => {
// TODO only the ones that are actually used in this block...
const assignment = `${alias} = state.${alias}`; // TODO this is wrong!!!
initializers.push(assignment);
updaters.push(`${assignment};`);
this.hasUpdateMethod = true;
});
// 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;
@ -231,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.generator.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();`}
},
@ -252,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}
},
`);
@ -264,85 +282,74 @@ 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}
},
`);
}
if (this.hasUpdateMethod) {
if (this.builders.update.isEmpty() && updaters.length === 0) {
if (this.hasUpdateMethod || this.maintainContext) {
if (this.builders.update.isEmpty() && !this.maintainContext) {
properties.addBlock(`p: @noop,`);
} else {
properties.addBlock(deindent`
p: function update(changed, state) {
${updaters}
${dev ? 'p: function update' : 'p'}(changed, ${this.maintainContext ? '_ctx' : 'ctx'}) {
${this.maintainContext && `ctx = _ctx;`}
${this.builders.update}
},
`);
}
}
if (this.hasIntroMethod) {
if (hasIntros) {
if (this.hasAnimation) {
properties.addBlock(deindent`
i: function intro(#target, anchor) {
if (${introing}) return;
${introing} = true;
${hasOutros && `${outroing} = false;`}
${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(${this.alias('outrocallback')}) {
if (${outroing}) return;
${outroing} = true;
${hasIntros && `${introing} = false;`}
${dev ? 'o: function outro' : 'o'}(#outrocallback) {
if (!#current) return;
var ${this.alias('outros')} = ${this.outros};
${this.outros > 1 && `#outrocallback = @callAfter(#outrocallback, ${this.outros});`}
${this.builders.outro}
},
`);
} else {
// TODO should this be a helper?
properties.addBlock(deindent`
o: function outro(outrocallback) {
outrocallback();
},
`);
}
}
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}
}
`);
@ -350,9 +357,7 @@ export default class Block {
return deindent`
${this.comment && `// ${escape(this.comment)}`}
function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, state) {
${initializers.length > 0 &&
`var ${initializers.join(', ')};`}
function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) {
${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys())
.map(key => {

@ -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');
}
}

@ -0,0 +1,339 @@
import deindent from '../../utils/deindent';
import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist';
import Component from '../Component';
import Renderer from './Renderer';
import { CompileOptions } from '../../interfaces';
export default function dom(
component: Component,
options: CompileOptions
) {
const format = options.format || 'es';
const {
computations,
name,
templateProperties
} = component;
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;`);
const builder = new CodeBuilder();
const computationBuilder = new CodeBuilder();
const computationDeps = new Set();
if (computations.length) {
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`
);
}
renderer.readonly.add(key);
if (deps) {
deps.forEach(dep => {
computationDeps.add(dep);
});
const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`;
const statement = `if (this._differs(state.${key}, (state.${key} = %computed-${key}(state)))) changed.${key} = true;`;
computationBuilder.addConditional(condition, statement);
} else {
// computed property depends on entire state object —
// these must go at the end
computationBuilder.addLine(
`if (this._differs(state.${key}, (state.${key} = %computed-${key}(@exclude(state, "${key}"))))) changed.${key} = true;`
);
}
});
}
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 = 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 && component.options.css !== false && !component.customElement) {
builder.addBlock(deindent`
function @add_css() {
var style = @createElement("style");
style.id = '${component.stylesheet.id}-style';
style.textContent = ${styles};
@append(document.head, style);
}
`);
}
// 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());
});
const sharedPath: string = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const proto = sharedPath
? `@proto`
: deindent`
{
${['destroy', 'get', 'fire', 'on', 'set', '_set', '_stage', '_mount', '_differs']
.map(n => `${n}: @${n}`)
.join(',\n')}
}`;
const debugName = `<${component.customElement ? component.tag : name}>`;
// generate initial state object
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (storeProps.length > 0) {
initialState.push(`this.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}
initialState.push(`options.data`);
const hasInitHooks = !!(templateProperties.oncreate || templateProperties.onstate || templateProperties.onupdate);
const constructorBody = deindent`
${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();`}
${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)}"`)}]);`}
${renderer.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
Array.from(component.expectedProperties).map(prop => {
if (globalWhitelist.has(prop)) return;
if (computations.find(c => c.key === prop)) return;
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 (component.customElement) conditions.push(`!('${prop}' in this.attributes)`);
return `if (${conditions.join(' && ')}) console.warn("${message}");`
})}
${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];`}
${(templateProperties.ondestroy || storeProps.length) && (
`this._handlers.destroy = [${
[templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ')
}];`
)}
${renderer.slots.size && `this._slotted = options.slots || {};`}
${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>\`;`}
` :
(component.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`)
}
${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.oncreate && `%oncreate.call(this);`}
this.fire("update", { changed: @assignTrue({}, this._state), current: this._state });
});
`}
${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) {
${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);
${(component.hasComponents || renderer.hasComplexBindings || hasInitHooks || renderer.hasIntroTransitions) &&
`@flush(this);`}
}
`}
${component.options.skipIntroByDefault && `this._intro = true;`}
`;
if (component.customElement) {
const props = component.props || Array.from(component.expectedProperties);
builder.addBlock(deindent`
class ${name} extends HTMLElement {
constructor(options = {}) {
super();
${constructorBody}
}
static get observedAttributes() {
return ${JSON.stringify(props)};
}
${props.map(prop => deindent`
get ${prop}() {
return this.get().${prop};
}
set ${prop}(value) {
this.set({ ${prop}: value });
}
`).join('\n\n')}
${renderer.slots.size && deindent`
connectedCallback() {
Object.keys(this._slotted).forEach(key => {
this.appendChild(this._slotted[key]);
});
}`}
attributeChangedCallback(attr, oldValue, newValue) {
this.set({ [attr]: newValue });
}
${(component.hasComponents || renderer.hasComplexBindings || templateProperties.oncreate || renderer.hasIntroTransitions) && deindent`
connectedCallback() {
@flush(this);
}
`}
}
@assign(${name}.prototype, ${proto});
${templateProperties.methods && `@assign(${name}.prototype, %methods);`}
@assign(${name}.prototype, {
_mount(target, anchor) {
target.insertBefore(this, anchor);
}
});
customElements.define("${component.tag}", ${name});
`);
} else {
builder.addBlock(deindent`
function ${name}(options) {
${constructorBody}
}
@assign(${name}.prototype, ${proto});
${templateProperties.methods && `@assign(${name}.prototype, %methods);`}
`);
}
const immutable = templateProperties.immutable ? templateProperties.immutable.value.value : options.immutable;
builder.addBlock(deindent`
${options.dev && deindent`
${name}.prototype._checkReadOnly = function _checkReadOnly(newState) {
${Array.from(renderer.readonly).map(
prop =>
`if ('${prop}' in newState && !this._updatingReadonlyProperty) throw new Error("${debugName}: Cannot set read-only property '${prop}'");`
)}
};
`}
${computations.length ? deindent`
${name}.prototype._recompute = function _recompute(changed, state) {
${computationBuilder}
}
` : (!sharedPath && `${name}.prototype._recompute = @noop;`)}
${templateProperties.setup && `%setup(${name});`}
${templateProperties.preload && `${name}.preload = %preload;`}
${immutable && `${name}.prototype._differs = @_differsImmutable;`}
`);
let result = builder.toString();
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
);
}
}

@ -1,24 +1,29 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import Renderer from '../Renderer';
import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Tag from './shared/Tag';
import Block from '../dom/Block';
import Wrapper from './shared/wrapper';
import deindent from '../../../utils/deindent';
export default class RawMustacheTag extends Tag {
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('raw');
block.addDependencies(this.metadata.dependencies);
}
export default class RawMustacheTagWrapper extends Tag {
var = 'raw';
build(
constructor(
renderer: Renderer,
block: Block,
parentNode: string,
parentNodes: string
parent: Wrapper,
node: Node
) {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
}
render(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;
// 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`)
@ -62,7 +67,8 @@ export default class RawMustacheTag extends Tag {
anchorBefore,
`@createElement('noscript')`,
parentNodes && `@createElement('noscript')`,
parentNode
parentNode,
true
);
}
@ -82,7 +88,12 @@ export default class RawMustacheTag extends Tag {
}
block.builders.mount.addLine(insert(init));
block.builders.detachRaw.addBlock(detach);
if (!parentNode) {
block.builders.destroy.addConditional('detach', needsAnchorBefore
? `${detach}\n@detachNode(${anchorBefore});`
: detach);
}
if (needsAnchorAfter && anchorBefore !== 'null') {
// ...otherwise it should go afterwards

@ -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,132 @@
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()}"`;
}
});
}
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>`);
}

@ -0,0 +1,178 @@
import deindent from '../../utils/deindent';
import Component from '../Component';
import globalWhitelist from '../../utils/globalWhitelist';
import { CompileOptions } from '../../interfaces';
import { stringify } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import Renderer from './Renderer';
export default function ssr(
component: Component,
options: CompileOptions
) {
const renderer = new Renderer();
const format = options.format || 'cjs';
const { computations, name, templateProperties } = component;
// create main render() function
renderer.render(trim(component.fragment.children), Object.assign({
locate: component.locate
}, options));
const css = component.customElement ?
{ code: null, map: null } :
component.stylesheet.render(options.filename, true);
// generate initial state object
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (storeProps.length > 0) {
const initialize = `_init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`
initialState.push(`options.store.${initialize}`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}
initialState.push('ctx');
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`
${js}
var ${name} = {};
${options.filename && `${name}.filename = ${stringify(options.filename)}`};
${name}.data = function() {
return ${templateProperties.data ? `%data()` : `{}`};
};
${name}.render = function(state, options = {}) {
var components = new Set();
function addComponent(component) {
components.add(component);
}
var result = { head: '', addComponent };
var html = ${name}._render(result, state, options);
var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\\n');
return {
html,
head: result.head,
css: { code: cssCode, map: null },
toString() {
return html;
}
};
}
${name}._render = function(__result, ctx, options) {
${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);`
)}
${renderer.bindings.length &&
deindent`
var settled = false;
var tmp;
while (!settled) {
settled = true;
${renderer.bindings.join('\n\n')}
}
`}
return \`${renderer.code}\`;
};
${name}.css = {
code: ${css.code ? stringify(css.code) : `''`},
map: ${css.map ? stringify(css.map.toString()) : 'null'}
};
var warned = false;
${templateProperties.preload && `${name}.preload = %preload;`}
`).trim();
return component.generate(result, options, { name, format });
}
function trim(nodes) {
let start = 0;
for (; start < nodes.length; start += 1) {
const node = nodes[start];
if (node.type !== 'Text') break;
node.data = node.data.replace(/^\s+/, '');
if (node.data) break;
}
let end = nodes.length;
for (; end > start; end -= 1) {
const node = nodes[end - 1];
if (node.type !== 'Text') break;
node.data = node.data.replace(/\s+$/, '');
if (node.data) break;
}
return nodes.slice(start, end);
}

@ -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?
});
}

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

@ -0,0 +1,84 @@
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 { 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(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-computed-property`,
message: `The 'computed' property must be an object literal`
});
}
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/, '_$&');
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)) {
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)) {
component.error(computation.value, {
code: `invalid-computed-value`,
message: `Computed properties can be function expressions or arrow function expressions`
});
}
const { body, params } = computation.value;
walkThroughTopFunctionScope(body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) {
component.error(node, {
code: `impure-computed`,
message: `Cannot use this.get(...) — values must be passed into the function as arguments`
});
}
if (node.type === 'ThisExpression') {
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'?`
});
}
});
if (params.length === 0) {
component.error(computation.value, {
code: `impure-computed`,
message: `A computed value must depend on at least one property`
});
}
if (params.length > 1) {
component.error(computation.value, {
code: `invalid-computed-arguments`,
message: `Computed properties must take a single argument`
});
}
});
}

@ -0,0 +1,15 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
export default function data(component: Component, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
if (disallowed.has(prop.value.type)) {
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);
}

@ -0,0 +1,49 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import walkThroughTopFunctionScope from '../../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../../utils/isThisGetCallExpression';
import Component from '../../../Component';
export default function helpers(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-helpers-property`,
message: `The 'helpers' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((prop: Node) => {
if (!/FunctionExpression/.test(prop.value.type)) return;
let usesArguments = false;
walkThroughTopFunctionScope(prop.value.body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) {
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') {
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'?`
});
} else if (node.type === 'Identifier' && node.name === 'arguments') {
usesArguments = true;
}
});
if (prop.value.params.length === 0 && !usesArguments) {
component.warn(prop, {
code: `impure-helper`,
message: `Helpers should be pure functions, with at least one argument`
});
}
});
}

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

@ -1,7 +1,11 @@
import data from './data';
import actions from './actions';
import animations from './animations';
import computed from './computed';
import oncreate from './oncreate';
import ondestroy from './ondestroy';
import onstate from './onstate';
import onupdate from './onupdate';
import onrender from './onrender';
import onteardown from './onteardown';
import helpers from './helpers';
@ -19,9 +23,13 @@ import immutable from './immutable';
export default {
data,
actions,
animations,
computed,
oncreate,
ondestroy,
onstate,
onupdate,
onrender,
onteardown,
helpers,

@ -0,0 +1,42 @@
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 { Node } from '../../../../interfaces';
import Component from '../../../Component';
const builtin = new Set(['set', 'get', 'on', 'fire', 'destroy']);
export default function methods(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-methods-property`,
message: `The 'methods' property must be an object literal`
});
}
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)) {
component.error(prop, {
code: `invalid-method-name`,
message: `Cannot overwrite built-in method '${name}'`
});
}
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
component.error(prop, {
code: `invalid-method-value`,
message: `Method '${prop.key.name}' should be a function expression, not an arrow function expression`
});
}
}
});
}

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

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

@ -0,0 +1,14 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function ondestroy(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
component.error(prop, {
code: `invalid-ondestroy-property`,
message: `'ondestroy' 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