Merge branch 'master' into gh-648

pull/1157/head
Rich Harris 7 years ago
commit 5dcd568c8c

34
.gitignore vendored

@ -1,20 +1,18 @@
.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
/src/compile/shared.ts
/package-lock.json
/store.umd.js
/yarn-error.log
_actual*.*

@ -1,5 +1,236 @@
# Svelte changelog
## 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))
## 1.56.1
* Fix if-in-each bug ([#1195](https://github.com/sveltejs/svelte/issues/1195))
* Cross-browser `scrollX`/`scrollY` support ([#1175](https://github.com/sveltejs/svelte/issues/1175))
## 1.56.0
* Internal refactor ([#1122](https://github.com/sveltejs/svelte/issues/1122))
* Use correct context for component events ([#1184](https://github.com/sveltejs/svelte/issues/1184))
* Allow observing `$foo` in dev mode ([#1181](https://github.com/sveltejs/svelte/issues/1181))
* Handle dynamic data in default slot ([#1144](https://github.com/sveltejs/svelte/issues/1144))
## 1.55.1
* Fix cancellation of store `onchange` handlers ([#1177](https://github.com/sveltejs/svelte/issues/1177))
* Write `["default"]` instead of `.default` in legacy mode ([#1166](https://github.com/sveltejs/svelte/issues/1166))
* Upgrade Acorn ([#1182](https://github.com/sveltejs/svelte/pull/1182))
* Don't warn about capitalisation if `options.name` begins with non-alphabetical character ([#1179](https://github.com/sveltejs/svelte/pull/1179))
## 1.55.0
* Add `immutable` compiler option for Svelte and runtime option for `Store` ([#1146](https://github.com/sveltejs/svelte/issues/1146))
* Fix component store bindings ([#1100](https://github.com/sveltejs/svelte/issues/1100))
* Fire `oncreate` when custom element is attached ([#1117](https://github.com/sveltejs/svelte/issues/1117))
* Downgrade empty blocks to a warning ([#1156](https://github.com/sveltejs/svelte/pull/1156))
* Error on unclosed comment ([#1156](https://github.com/sveltejs/svelte/pull/1156))
## 1.54.2
* Prevent `await` blocks using stale state ([#1131](https://github.com/sveltejs/svelte/issues/1131))
* Prevent erroneous missing data warnings for custom elements ([#1065](https://github.com/sveltejs/svelte/issues/1065))
* Remove empty selectors in prod mode ([#1138](https://github.com/sveltejs/svelte/issues/1138))
* Escape attribute values in SSR mode ([#1155](https://github.com/sveltejs/svelte/pull/1155))
* Remove `<noscript>` elements in DOM mode ([#1108](https://github.com/sveltejs/svelte/issues/1108))
* Allow hydration of non-root `<script>`/`<style>` tags ([#1163](https://github.com/sveltejs/svelte/pull/1163))
* Allow interpolation in non-root `<style>` tags ([#1163](https://github.com/sveltejs/svelte/pull/1163))
## 1.54.1
* Hoist destructured references ([#1139](https://github.com/sveltejs/svelte/issues/1139))

@ -18,9 +18,9 @@ This is the Svelte compiler, which is primarily intended for authors of tooling
* [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
* [svelte-dev-store](https://github.com/GarethOates/svelte-dev-store) - Use Redux tools to visualise Svelte store
* More to come!
@ -29,7 +29,7 @@ This is the Svelte compiler, which is primarily intended for authors of tooling
```js
import * as svelte from 'svelte';
const { code, map } = svelte.compile( source, {
const { code, map } = svelte.compile(source, {
// the target module format defaults to 'es' (ES2015 modules), can
// also be 'amd', 'cjs', 'umd', 'iife' or 'eval'
format: 'umd',
@ -64,9 +64,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
@ -75,13 +75,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` |
@ -92,6 +90,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) |
@ -130,6 +129,7 @@ The `style` and `script` preprocessors will run *after* the `markup` preprocesso
* [charpeni/svelte-example](https://github.com/charpeni/svelte-example) - Some Svelte examples with configured Rollup, Babel, ESLint, directives, Two-Way binding, and nested components
* [EmilTholin/svelte-test](https://github.com/EmilTholin/svelte-test)
* [lukechinworth/codenames](https://github.com/lukechinworth/codenames/tree/svelte)  example integration with Redux
* [khtdr/svelte-redux-shopping-cart](https://github.com/khtdr/svelte-redux-shopping-cart) Redux Shopping Cart example (with devtools and hot-reloading)
## BrowserStack
<img src="https://cdn.worldvectorlogo.com/logos/browserstack.svg" height="80" width="80" align="left">

@ -1,14 +1,19 @@
{
"name": "svelte",
"version": "1.54.1",
"version": "2.4.4",
"description": "The magical disappearing UI framework",
"main": "compiler/svelte.js",
"bin": {
"svelte": "svelte"
},
"files": [
"cli",
"compiler",
"ssr",
"shared.js",
"store.js",
"store.umd.js",
"svelte",
"README.md"
],
"scripts": {
@ -43,43 +48,46 @@
},
"homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": {
"@types/mocha": "^2.2.41",
"@types/node": "^8.0.17",
"acorn": "^5.1.1",
"acorn-dynamic-import": "^2.0.2",
"chalk": "^2.0.1",
"codecov": "^2.2.0",
"@types/mocha": "^5.2.0",
"@types/node": "^9.6.6",
"acorn": "^5.4.1",
"acorn-dynamic-import": "^3.0.0",
"chalk": "^2.4.0",
"clorox": "^1.0.3",
"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": "^4.19.1",
"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",
"locate-character": "^2.0.5",
"magic-string": "^0.24.0",
"mocha": "3",
"nightmare": "^3.0.1",
"node-resolve": "^1.3.3",
"nyc": "^11.1.0",
"prettier": "^1.7.0",
"reify": "^0.12.3",
"rollup": "^0.48.2",
"rollup-plugin-buble": "^0.15.0",
"rollup-plugin-commonjs": "^8.0.2",
"nyc": "^11.7.1",
"prettier": "^1.12.1",
"reify": "^0.15.1",
"rollup": "^0.58.1",
"rollup-plugin-buble": "^0.19.2",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-json": "^2.1.0",
"rollup-plugin-node-resolve": "^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",
"source-map": "0.6",
"source-map-support": "^0.5.4",
"tiny-glob": "^0.2.0",
"ts-node": "^6.0.0",
"tslib": "^1.8.0",
"typescript": "^2.6.1"
"typescript": "^2.8.3"
},
"nyc": {
"include": [

@ -7,28 +7,11 @@ import typescript from 'rollup-plugin-typescript';
import buble from 'rollup-plugin-buble';
import pkg from './package.json';
const src = path.resolve('src');
export default [
/* compiler/svelte.js */
{
input: 'src/index.ts',
plugins: [
{
resolveId(importee, importer) {
// bit of a hack — TypeScript only really works if it can resolve imports,
// but they misguidedly chose to reject imports with file extensions. This
// means we need to resolve them here
if (
importer &&
importer.startsWith(src) &&
importee[0] === '.' &&
path.extname(importee) === ''
) {
return path.resolve(path.dirname(importer), `${importee}.ts`);
}
}
},
replace({
__VERSION__: pkg.version
}),
@ -51,7 +34,7 @@ export default [
/* ssr/register.js */
{
input: 'src/server-side-rendering/register.js',
input: 'src/ssr/register.js',
plugins: [
resolve(),
commonjs(),
@ -74,6 +57,29 @@ export default [
}
},
/* cli/*.js */
{
input: ['src/cli/index.ts'],
output: {
dir: 'cli',
format: 'cjs'
},
external: ['fs', 'path', 'os', 'svelte'],
paths: {
svelte: '../compiler/svelte.js'
},
plugins: [
json(),
commonjs(),
resolve(),
typescript({
typescript: require('typescript')
})
],
experimentalDynamicImport: true,
experimentalCodeSplitting: true
},
/* shared.js */
{
input: 'src/shared/index.js',

@ -0,0 +1,118 @@
import { Node, Warning } from './interfaces';
import Compiler from './compile/Compiler';
const now = (typeof process !== 'undefined' && process.hrtime)
? () => {
const t = process.hrtime();
return t[0] * 1e3 + t[1] / 1e6;
}
: () => window.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`);
}
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(compiler: Compiler) {
const timings = Object.assign({
total: now() - this.startTime
}, collapseTimings(this.timings));
// TODO would be good to have this info even
// if options.generate is false
const imports = compiler && compiler.imports.map(node => {
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> = compiler && {
oncreate: !!compiler.templateProperties.oncreate,
ondestroy: !!compiler.templateProperties.ondestroy,
onstate: !!compiler.templateProperties.onstate,
onupdate: !!compiler.templateProperties.onupdate
};
return {
timings,
warnings: this.warnings,
imports,
hooks
};
}
warn(warning) {
this.warnings.push(warning);
this.onwarn(warning);
}
}

@ -0,0 +1,139 @@
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,
css: opts.css !== false,
dev: opts.dev,
immutable: opts.immutable,
generate: opts.generate || 'dom',
customElement: opts.customElement,
store: opts.store
};
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 clorox from 'clorox';
function stderr(msg) {
console.error(msg); // eslint-disable-line no-console
}
export default function error(err) {
stderr(`${clorox.red(err.message || err)}`);
if (err.frame) {
stderr(err.frame); // eslint-disable-line no-console
} else if (err.stack) {
stderr(`${clorox.grey(err.stack)}`);
}
process.exit(1);
}

@ -0,0 +1,30 @@
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')
.example('compile App.html > App.js')
.example('compile src -o dest')
.example('compile -f umd MyComponent.html > MyComponent.js')
.action((input, opts) => {
import('./compile.js').then(({ compile }) => {
compile(input, opts);
});
})
.parse(process.argv);

@ -1,22 +1,26 @@
import { parseExpressionAt } from 'acorn';
import MagicString, { Bundle } from 'magic-string';
import isReference from 'is-reference';
import { walk, childKeys } from 'estree-walker';
import { getLocator } from 'locate-character';
import Stats from '../Stats';
import deindent from '../utils/deindent';
import CodeBuilder from '../utils/CodeBuilder';
import getCodeFrame from '../utils/getCodeFrame';
import isReference from '../utils/isReference';
import flattenReference from '../utils/flattenReference';
import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces';
import { removeNode, removeObjectKey } from '../utils/removeNode';
import nodeToString from '../utils/nodeToString';
import wrapModule from './wrapModule';
import annotateWithScopes, { Scope } from '../utils/annotateWithScopes';
import getName from '../utils/getName';
import clone from '../utils/clone';
import Stylesheet from '../css/Stylesheet';
import { test } from '../config';
import nodes from './nodes/index';
import { Node, GenerateOptions, Parsed, CompileOptions, CustomElementOptions } from '../interfaces';
import Fragment from './nodes/Fragment';
import shared from './shared';
import { DomTarget } from './dom/index';
import { SsrTarget } from './ssr/index';
import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
interface Computation {
key: string;
@ -74,12 +78,15 @@ function removeIndentation(
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
childKeys.Attribute = ['value'];
export default class Generator {
ast: Parsed;
parsed: Parsed;
export default class Compiler {
stats: Stats;
ast: Ast;
source: string;
name: string;
options: CompileOptions;
fragment: Fragment;
target: DomTarget | SsrTarget;
customElement: CustomElementOptions;
tag: string;
@ -87,10 +94,13 @@ export default class Generator {
defaultExport: Node[];
imports: Node[];
shorthandImports: ShorthandImport[];
helpers: Set<string>;
components: Set<string>;
events: Set<string>;
methods: Set<string>;
transitions: Set<string>;
actions: Set<string>;
importedComponents: Map<string, string>;
namespace: string;
hasComponents: boolean;
@ -116,24 +126,31 @@ export default class Generator {
usedNames: Set<string>;
constructor(
parsed: Parsed,
ast: Ast,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions,
dom: boolean
stats: Stats,
dom: boolean,
target: DomTarget | SsrTarget
) {
this.ast = clone(parsed);
stats.start('compile');
this.stats = stats;
this.parsed = parsed;
this.ast = ast;
this.source = source;
this.options = options;
this.target = target;
this.imports = [];
this.shorthandImports = [];
this.helpers = new Set();
this.components = new Set();
this.events = new Set();
this.methods = new Set();
this.transitions = new Set();
this.actions = new Set();
this.importedComponents = new Map();
this.slots = new Set();
@ -168,7 +185,7 @@ export default class Generator {
if (options.customElement === true) {
this.customElement = {
tag: this.tag,
props: this.props // TODO autofill this in
props: this.props
}
} else {
this.customElement = options.customElement;
@ -178,7 +195,11 @@ export default class Generator {
throw new Error(`No tag name specified`); // TODO better error
}
this.walkTemplate();
this.fragment = new Fragment(this, ast.html);
// this.walkTemplate();
if (!this.customElement) this.stylesheet.reify();
stylesheet.warnOnUnusedSelectors(options.onwarn);
}
addSourcemapLocations(node: Node) {
@ -198,112 +219,108 @@ export default class Generator {
return this.aliases.get(name);
}
contextualise(
contexts: Map<string, string>,
indexes: Map<string, string>,
expression: Node,
context: string,
isEventHandler: boolean
): {
contexts: Set<string>,
indexes: Set<string>
} {
// this.addSourcemapLocations(expression);
generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
const usedContexts: Set<string> = new Set();
const usedIndexes: Set<string> = new Set();
const helpers = new Set();
const { code, helpers } = this;
// 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);
}
let scope: Scope;
let lexicalDepth = 0;
return this.alias(name);
}
const self = this;
if (sigil === '%') {
return this.templateVars.get(name);
}
walk(expression, {
enter(node: Node, parent: Node, key: string) {
if (/^Function/.test(node.type)) lexicalDepth += 1;
return sigil.slice(1) + name;
});
if (node._scope) {
scope = node._scope;
return;
}
let importedHelpers;
if (node.type === 'ThisExpression') {
if (lexicalDepth === 0 && context)
code.overwrite(node.start, node.end, context, {
storeName: true,
contentOnly: false,
});
} else if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope && scope.has(name)) return;
if (name === 'event' && isEventHandler) {
// noop
} else if (contexts.has(name)) {
const contextName = contexts.get(name);
if (contextName !== name) {
// this is true for 'reserved' names like `state` and `component`,
// also destructured contexts
code.overwrite(
node.start,
node.start + name.length,
contextName,
{ storeName: true, contentOnly: false }
);
const destructuredName = contextName.replace(/\[\d+\]/, '');
if (destructuredName !== contextName) {
// so that hoisting the context works correctly
usedContexts.add(destructuredName);
}
}
if (options.shared) {
if (format !== 'es' && format !== 'cjs') {
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
}
usedContexts.add(name);
} else if (helpers.has(name)) {
let object = node;
while (object.type === 'MemberExpression') object = object.object;
const alias = self.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias);
} else if (indexes.has(name)) {
const context = indexes.get(name);
usedContexts.add(context); // TODO is this right?
usedIndexes.add(name);
} else {
// handle shorthand properties
if (parent && parent.type === 'Property' && parent.shorthand) {
if (key === 'key') {
code.appendLeft(node.start, `${name}: `);
return;
importedHelpers = Array.from(helpers).sort().map(name => {
const alias = this.alias(name);
return { name, alias };
});
} else {
let inlineHelpers = '';
const compiler = this;
importedHelpers = [];
helpers.forEach(name => {
const str = shared[name];
const code = new MagicString(str);
const expression = parseExpressionAt(str, 0);
let { scope } = annotateWithScopes(expression);
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) scope = node._scope;
if (
node.type === 'Identifier' &&
isReference(node, parent) &&
!scope.has(node.name)
) {
if (node.name in shared) {
// this helper function depends on another one
const dependency = node.name;
helpers.add(dependency);
const alias = compiler.alias(dependency);
if (alias !== node.name) {
code.overwrite(node.start, node.end, alias);
}
}
}
},
code.prependRight(node.start, `state.`);
usedContexts.add('state');
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
if (name === 'transitionManager') {
// special case
const global = `_svelteTransitionManager`;
inlineHelpers += `\n\nvar ${this.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`;
} else if (name === 'escaped' || name === 'missingComponent') {
// vars are an awkward special case... would be nice to avoid this
const alias = this.alias(name);
inlineHelpers += `\n\nconst ${alias} = ${code};`
} else {
const alias = this.alias(expression.id.name);
if (alias !== expression.id.name) {
code.overwrite(expression.id.start, expression.id.end, alias);
}
this.skip();
inlineHelpers += `\n\n${code}`;
}
},
leave(node: Node) {
if (/^Function/.test(node.type)) lexicalDepth -= 1;
if (node._scope) scope = scope.parent;
},
});
});
return {
contexts: usedContexts,
indexes: usedIndexes
};
}
result += inlineHelpers;
}
generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
const sharedPath = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const module = wrapModule(result, format, name, options, banner, sharedPath, helpers, this.imports, this.source);
const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source);
const parts = module.split('✂]');
const finalChunk = parts.pop();
@ -343,19 +360,25 @@ export default class Generator {
addString(finalChunk);
const { css, cssMap } = this.customElement ?
{ css: null, cssMap: null } :
const css = this.customElement ?
{ code: null, map: null } :
this.stylesheet.render(options.cssOutputFilename, true);
return {
ast: this.ast,
const js = {
code: compiled.toString(),
map: compiled.generateMap({
includeContent: true,
file: options.outputFilename,
}),
})
};
this.stats.stop('compile');
return {
ast: this.ast,
js,
css,
cssMap
stats: this.stats.render(this)
};
}
@ -373,8 +396,8 @@ export default class Generator {
return alias;
}
getUniqueNameMaker(params: string[]) {
const localUsedNames = new Set(params);
getUniqueNameMaker() {
const localUsedNames = new Set();
function add(name: string) {
localUsedNames.add(name);
@ -402,11 +425,12 @@ export default class Generator {
code,
source,
computations,
methods,
templateProperties,
imports
} = this;
const { js } = this.parsed;
const { js } = this.ast;
const componentDefinition = new CodeBuilder();
@ -451,7 +475,7 @@ export default class Generator {
templateProperties[getName(prop.key)] = prop;
});
['helpers', 'events', 'components', 'transitions'].forEach(key => {
['helpers', 'events', 'components', 'transitions', 'actions'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: Node) => {
this[key].add(getName(prop.key));
@ -497,7 +521,7 @@ export default class Generator {
`);
};
const addDeclaration = (key: string, node: Node, disambiguator?: string, conflicts?: Record<string, boolean>) => {
const addDeclaration = (key: string, node: Node, allowShorthandImport?: boolean, disambiguator?: string, conflicts?: Record<string, boolean>) => {
const qualified = disambiguator ? `${disambiguator}-${key}` : key;
if (node.type === 'Identifier' && node.name === key) {
@ -511,6 +535,11 @@ export default class Generator {
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) {
@ -528,25 +557,36 @@ export default class Generator {
if (templateProperties.components) {
templateProperties.components.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, 'components');
addDeclaration(getName(property.key), property.value, true, 'components');
});
}
if (templateProperties.computed) {
const dependencies = new Map();
const fullStateComputations = [];
templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = getName(prop.key);
const value = prop.value;
const deps = value.params.map(
(param: Node) =>
param.type === 'AssignmentPattern' ? param.left.name : param.name
);
deps.forEach(dep => {
this.expectedProperties.add(dep);
addDeclaration(key, value, false, 'computed', {
state: true,
changed: true
});
dependencies.set(key, deps);
const param = value.params[0];
if (param.type === 'ObjectPattern') {
const deps = param.properties.map(prop => prop.key.name);
deps.forEach(dep => {
this.expectedProperties.add(dep);
});
dependencies.set(key, deps);
} else {
fullStateComputations.push({ key, deps: null })
}
});
const visited = new Set();
@ -563,16 +603,15 @@ export default class Generator {
computations.push({ key, deps });
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
addDeclaration(key, prop.value, 'computed', {
state: true,
changed: true
});
};
templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(getName(prop.key))
);
if (fullStateComputations.length > 0) {
computations.push(...fullStateComputations);
}
}
if (templateProperties.data) {
@ -581,41 +620,51 @@ export default class Generator {
if (templateProperties.events && dom) {
templateProperties.events.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, 'events');
addDeclaration(getName(property.key), property.value, false, 'events');
});
}
if (templateProperties.helpers) {
templateProperties.helpers.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, 'helpers');
addDeclaration(getName(property.key), property.value, false, 'helpers');
});
}
if (templateProperties.methods && dom) {
addDeclaration('methods', templateProperties.methods.value);
templateProperties.methods.value.properties.forEach(prop => {
this.methods.add(prop.key.name);
});
}
if (templateProperties.namespace) {
const ns = templateProperties.namespace.value.value;
const ns = nodeToString(templateProperties.namespace.value);
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.onrender) templateProperties.oncreate = templateProperties.onrender; // remove after v2
if (templateProperties.oncreate && dom) {
addDeclaration('oncreate', templateProperties.oncreate.value);
}
if (templateProperties.onteardown) templateProperties.ondestroy = templateProperties.onteardown; // remove after v2
if (templateProperties.ondestroy && dom) {
addDeclaration('ondestroy', templateProperties.ondestroy.value);
}
if (templateProperties.onstate && dom) {
addDeclaration('onstate', templateProperties.onstate.value);
}
if (templateProperties.onupdate && dom) {
addDeclaration('onupdate', templateProperties.onupdate.value);
}
if (templateProperties.preload) {
addDeclaration('preload', templateProperties.preload.value);
}
if (templateProperties.props) {
this.props = templateProperties.props.value.elements.map((element: Node) => element.value);
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
}
if (templateProperties.setup) {
@ -627,12 +676,18 @@ export default class Generator {
}
if (templateProperties.tag) {
this.tag = templateProperties.tag.value.value;
this.tag = nodeToString(templateProperties.tag.value);
}
if (templateProperties.transitions) {
templateProperties.transitions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, 'transitions');
addDeclaration(getName(property.key), property.value, false, 'transitions');
});
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'actions');
});
}
}
@ -662,205 +717,4 @@ export default class Generator {
}
}
}
walkTemplate() {
const generator = this;
const {
code,
expectedProperties,
helpers
} = this;
const { html } = this.parsed;
const contextualise = (
node: Node, contextDependencies: Map<string, string[]>,
indexes: Set<string>,
isEventHandler: boolean
) => {
this.addSourcemapLocations(node); // TODO this involves an additional walk — can we roll it in somewhere else?
let { scope } = annotateWithScopes(node);
const dependencies: Set<string> = new Set();
walk(node, {
enter(node: Node, parent: Node) {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
if (node._scope) {
scope = node._scope;
return;
}
if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return;
if (contextDependencies.has(name)) {
contextDependencies.get(name).forEach(dependency => {
dependencies.add(dependency);
});
} else if (!indexes.has(name)) {
dependencies.add(name);
}
this.skip();
}
},
leave(node: Node, parent: Node) {
if (node._scope) scope = scope.parent;
}
});
dependencies.forEach(dependency => {
expectedProperties.add(dependency);
});
return {
snippet: `[✂${node.start}-${node.end}✂]`,
dependencies: Array.from(dependencies)
};
}
const contextStack = [];
const indexStack = [];
const dependenciesStack = [];
let contextDependencies = new Map();
const contextDependenciesStack: Map<string, string[]>[] = [contextDependencies];
let indexes = new Set();
const indexesStack: Set<string>[] = [indexes];
function parentIsHead(node) {
if (!node) return false;
if (node.type === 'Component' || node.type === 'Element') return false;
if (node.type === 'Head') return true;
return parentIsHead(node.parent);
}
walk(html, {
enter(node: Node, parent: Node, key: string) {
// TODO this is hacky as hell
if (key === 'parent') return this.skip();
node.parent = parent;
node.generator = generator;
if (node.type === 'Element' && (node.name === ':Component' || node.name === ':Self' || generator.components.has(node.name))) {
node.type = 'Component';
Object.setPrototypeOf(node, nodes.Component.prototype);
} else if (node.name === ':Window') { // TODO do this in parse?
node.type = 'Window';
Object.setPrototypeOf(node, nodes.Window.prototype);
} else if (node.name === ':Head') { // TODO do this in parse?
node.type = 'Head';
Object.setPrototypeOf(node, nodes.Head.prototype);
} else if (node.type === 'Element' && node.name === 'title' && parentIsHead(parent)) { // TODO do this in parse?
node.type = 'Title';
Object.setPrototypeOf(node, nodes.Title.prototype);
} else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) {
node.type = 'Slot';
Object.setPrototypeOf(node, nodes.Slot.prototype);
} else if (node.type in nodes) {
Object.setPrototypeOf(node, nodes[node.type].prototype);
}
if (node.type === 'Element') {
generator.stylesheet.apply(node);
}
if (node.type === 'EachBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
contextDependencies = new Map(contextDependencies);
contextDependencies.set(node.context, node.metadata.dependencies);
if (node.destructuredContexts) {
node.destructuredContexts.forEach((name: string) => {
contextDependencies.set(name, node.metadata.dependencies);
});
}
contextDependenciesStack.push(contextDependencies);
if (node.index) {
indexes = new Set(indexes);
indexes.add(node.index);
indexesStack.push(indexes);
}
}
if (node.type === 'AwaitBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
contextDependencies = new Map(contextDependencies);
contextDependencies.set(node.value, node.metadata.dependencies);
contextDependencies.set(node.error, node.metadata.dependencies);
contextDependenciesStack.push(contextDependencies);
}
if (node.type === 'IfBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
}
if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
this.skip();
}
if (node.type === 'Binding') {
node.metadata = contextualise(node.value, contextDependencies, indexes, false);
this.skip();
}
if (node.type === 'EventHandler' && node.expression) {
node.expression.arguments.forEach((arg: Node) => {
arg.metadata = contextualise(arg, contextDependencies, indexes, true);
});
this.skip();
}
if (node.type === 'Transition' && node.expression) {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
this.skip();
}
if (node.type === 'Component' && node.name === ':Component') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
}
},
leave(node: Node, parent: Node) {
if (node.type === 'EachBlock') {
contextDependenciesStack.pop();
contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1];
if (node.index) {
indexesStack.pop();
indexes = indexesStack[indexesStack.length - 1];
}
}
if (node.type === 'Element' && node.name === 'option') {
// Special case — treat these the same way:
// <option>{{foo}}</option>
// <option value='{{foo}}'>{{foo}}</option>
const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
if (!valueAttribute) {
node.attributes.push(new nodes.Attribute({
generator,
name: 'value',
value: node.children,
parent: node
}));
}
}
}
});
}
}

@ -1,51 +1,29 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { escape } from '../../utils/stringify';
import { DomGenerator } from './index';
import Compiler from '../Compiler';
import { Node } from '../../interfaces';
import shared from './shared';
export interface BlockOptions {
name: string;
generator?: DomGenerator;
expression?: Node;
context?: string;
destructuredContexts?: string[];
compiler?: Compiler;
comment?: string;
key?: string;
contexts?: Map<string, string>;
contextTypes?: Map<string, string>;
indexes?: Map<string, string>;
changeableIndexes?: Map<string, boolean>;
params?: string[];
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;
compiler: Compiler;
name: string;
expression: Node;
context: string;
destructuredContexts?: string[];
comment?: string;
key: string;
first: string;
contexts: Map<string, string>;
contextTypes: Map<string, string>;
indexes: Map<string, string>;
changeableIndexes: Map<string, boolean>;
dependencies: Set<string>;
params: string[];
indexNames: Map<string, string>;
listNames: Map<string, string>;
indexName: string;
listName: string;
bindings: Map<string, string>;
builders: {
init: CodeBuilder;
@ -56,11 +34,10 @@ export default class Block {
intro: CodeBuilder;
update: CodeBuilder;
outro: CodeBuilder;
unmount: CodeBuilder;
detachRaw: CodeBuilder;
destroy: CodeBuilder;
};
maintainContext: boolean;
hasIntroMethod: boolean;
hasOutroMethod: boolean;
outros: number;
@ -73,28 +50,17 @@ export default class Block {
autofocus: string;
constructor(options: BlockOptions) {
this.generator = options.generator;
this.compiler = options.compiler;
this.name = options.name;
this.expression = options.expression;
this.context = options.context;
this.destructuredContexts = options.destructuredContexts;
this.comment = options.comment;
// 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.params = options.params;
this.indexNames = options.indexNames;
this.listNames = options.listNames;
this.listName = options.listName;
this.bindings = options.bindings;
this.builders = {
init: new CodeBuilder(),
@ -105,8 +71,6 @@ export default class Block {
intro: new CodeBuilder(),
update: new CodeBuilder(),
outro: new CodeBuilder(),
unmount: new CodeBuilder(),
detachRaw: new CodeBuilder(),
destroy: new CodeBuilder(),
};
@ -114,14 +78,18 @@ export default class Block {
this.hasOutroMethod = false;
this.outros = 0;
this.aliases = new Map();
this.getUniqueName = this.compiler.getUniqueNameMaker();
this.variables = new Map();
this.getUniqueName = this.generator.getUniqueNameMaker(options.params);
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[]) {
addDependencies(dependencies: Set<string>) {
dependencies.forEach(dependency => {
this.dependencies.add(dependency);
});
@ -131,7 +99,8 @@ 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};`);
@ -139,10 +108,10 @@ export default class Block {
if (parentNode) {
this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`);
if (parentNode === 'document.head') this.builders.unmount.addLine(`@detachNode(${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});`);
if (!noDetach) this.builders.destroy.addConditional('detach', `@detachNode(${name});`);
}
}
@ -165,14 +134,12 @@ export default class Block {
}
child(options: BlockOptions) {
return new Block(Object.assign({}, this, options, { parent: this }));
}
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
}
toString() {
const { dev } = this.compiler.options;
let introing;
const hasIntros = !this.builders.intro.isEmpty();
if (hasIntros) {
@ -191,9 +158,6 @@ export default class Block {
this.builders.mount.addLine(`${this.autofocus}.focus();`);
}
// minor hack we need to ensure that any {{{triples}}} are detached first
this.builders.unmount.addBlockAtStart(this.builders.detachRaw.toString());
const properties = new CodeBuilder();
let localKey;
@ -210,20 +174,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.compiler.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.compiler.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();`}
},
@ -231,9 +201,9 @@ export default class Block {
}
}
if (!this.builders.hydrate.isEmpty()) {
if (this.compiler.options.hydratable && !this.builders.hydrate.isEmpty()) {
properties.addBlock(deindent`
h: function hydrate() {
${dev ? 'h: function hydrate' : 'h'}() {
${this.builders.hydrate}
},
`);
@ -243,18 +213,19 @@ 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()) {
if (this.hasUpdateMethod || this.maintainContext) {
if (this.builders.update.isEmpty() && !this.maintainContext) {
properties.addBlock(`p: @noop,`);
} else {
properties.addBlock(deindent`
p: function update(changed, ${this.params.join(', ')}) {
${dev ? 'p: function update' : 'p'}(changed, ${this.maintainContext ? '_ctx' : 'ctx'}) {
${this.maintainContext && `ctx = _ctx;`}
${this.builders.update}
},
`);
@ -264,7 +235,7 @@ export default class Block {
if (this.hasIntroMethod) {
if (hasIntros) {
properties.addBlock(deindent`
i: function intro(#target, anchor) {
${dev ? 'i: function intro' : 'i'}(#target, anchor) {
if (${introing}) return;
${introing} = true;
${hasOutros && `${outroing} = false;`}
@ -276,7 +247,7 @@ export default class Block {
`);
} else {
properties.addBlock(deindent`
i: function intro(#target, anchor) {
${dev ? 'i: function intro' : 'i'}(#target, anchor) {
this.m(#target, anchor);
},
`);
@ -286,41 +257,28 @@ export default class Block {
if (this.hasOutroMethod) {
if (hasOutros) {
properties.addBlock(deindent`
o: function outro(${this.alias('outrocallback')}) {
${dev ? 'o: function outro' : 'o'}(#outrocallback) {
if (${outroing}) return;
${outroing} = true;
${hasIntros && `${introing} = false;`}
var ${this.alias('outros')} = ${this.outros};
${this.outros > 1 && `var #outros = ${this.outros};`}
${this.builders.outro}
},
`);
} else {
// TODO should this be a helper?
properties.addBlock(deindent`
o: function outro(outrocallback) {
outrocallback();
},
o: @run,
`);
}
}
if (this.builders.unmount.isEmpty()) {
properties.addBlock(`u: @noop,`);
} else {
properties.addBlock(deindent`
u: function unmount() {
${this.builders.unmount}
},
`);
}
if (this.builders.destroy.isEmpty()) {
properties.addBlock(`d: @noop`);
} else {
properties.addBlock(deindent`
d: function destroy() {
${dev ? 'd: function destroy' : 'd'}(detach) {
${this.builders.destroy}
}
`);
@ -328,7 +286,7 @@ export default class Block {
return deindent`
${this.comment && `// ${escape(this.comment)}`}
function ${this.name}(${this.params.join(', ')}, #component${this.key ? `, ${localKey}` : ''}) {
function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) {
${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys())
.map(key => {

@ -0,0 +1,360 @@
import MagicString from 'magic-string';
import isReference from 'is-reference';
import { parseExpressionAt } from 'acorn';
import annotateWithScopes from '../../utils/annotateWithScopes';
import { walk } from 'estree-walker';
import deindent from '../../utils/deindent';
import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist';
import reservedNames from '../../utils/reservedNames';
import Compiler from '../Compiler';
import Stylesheet from '../../css/Stylesheet';
import Stats from '../../Stats';
import Block from './Block';
import { test } from '../../config';
import { Ast, CompileOptions, Node } from '../../interfaces';
export class DomTarget {
blocks: (Block|string)[];
readonly: Set<string>;
metaBindings: string[];
hasIntroTransitions: boolean;
hasOutroTransitions: boolean;
hasComplexBindings: boolean;
constructor() {
this.blocks = [];
this.readonly = new Set();
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
this.metaBindings = [];
}
}
export default function dom(
ast: Ast,
source: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
) {
const format = options.format || 'es';
const target = new DomTarget();
const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, true, target);
const {
computations,
name,
templateProperties,
namespace,
} = compiler;
compiler.fragment.build();
const { block } = compiler.fragment;
// 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 }) => {
if (target.readonly.has(key)) {
// <svelte:window> bindings
throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property`
);
}
target.readonly.add(key);
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}(state)))) changed.${key} = true;`
);
}
});
}
if (compiler.javascript) {
builder.addBlock(compiler.javascript);
}
const css = compiler.stylesheet.render(options.filename, !compiler.customElement);
const styles = compiler.stylesheet.hasStyles && stringify(options.dev ?
`${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` :
css.code, { onlyEscapeAtSymbol: true });
if (styles && compiler.options.css !== false && !compiler.customElement) {
builder.addBlock(deindent`
function @add_css() {
var style = @createElement("style");
style.id = '${compiler.stylesheet.id}-style';
style.textContent = ${styles};
@appendNode(style, document.head);
}
`);
}
target.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', '_mount', '_differs']
.map(n => `${n}: @${n}`)
.join(',\n')}
}`;
const debugName = `<${compiler.customElement ? compiler.tag : name}>`;
// generate initial state object
const expectedProperties = Array.from(compiler.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 && `this._debugName = '${debugName}';`}
${options.dev && !compiler.customElement &&
`if (!options || (!options.target && !options.root)) throw new Error("'target' is a required option");`}
@init(this, options);
${templateProperties.store && `this.store = %store();`}
${compiler.usesRefs && `this.refs = {};`}
this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)};
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
${target.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
Array.from(compiler.expectedProperties).map(prop => {
if (globalWhitelist.has(prop)) return;
if (computations.find(c => c.key === prop)) return;
const message = compiler.components.has(prop) ?
`${debugName} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` :
`${debugName} was created without expected data property '${prop}'`;
const conditions = [`!('${prop}' in this._state)`];
if (compiler.customElement) conditions.push(`!('${prop}' in this.attributes)`);
return `if (${conditions.join(' && ')}) console.warn("${message}");`
})}
${compiler.bindingGroups.length &&
`this._bindingGroups = [${Array(compiler.bindingGroups.length).fill('[]').join(', ')}];`}
${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(', ')
}];`
)}
${compiler.slots.size && `this._slotted = options.slots || {};`}
${compiler.customElement ?
deindent`
this.attachShadow({ mode: 'open' });
${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { onlyEscapeAtSymbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
` :
(compiler.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${compiler.stylesheet.id}-style")) @add_css();`)
}
${(hasInitHooks || compiler.hasComponents || target.hasComplexBindings || target.hasIntroTransitions) && deindent`
if (!options.root) {
this._oncreate = [];
${(compiler.hasComponents || target.hasComplexBindings) && `this._beforecreate = [];`}
${(compiler.hasComponents || target.hasIntroTransitions) && `this._aftercreate = [];`}
}
`}
${compiler.slots.size && `this.slots = {};`}
this._fragment = @create_main_fragment(this, this._state);
${hasInitHooks && deindent`
this.root._oncreate.push(() => {
${templateProperties.onstate && `%onstate.call(this, { changed: @assignTrue({}, this._state), current: this._state });`}
${templateProperties.oncreate && `%oncreate.call(this);`}
this.fire("update", { changed: @assignTrue({}, this._state), current: this._state });
});
`}
${compiler.customElement ? deindent`
this._fragment.c();
this._fragment.${block.hasIntroMethod ? 'i' : 'm'}(this.shadowRoot, null);
if (options.target) this._mount(options.target, options.anchor);
` : deindent`
if (options.target) {
${compiler.options.hydratable
? deindent`
var nodes = @children(options.target);
options.hydrate ? this._fragment.l(nodes) : this._fragment.c();
nodes.forEach(@detachNode);
` :
deindent`
${options.dev && `if (options.hydrate) throw new Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");`}
this._fragment.c();
`}
this._mount(options.target, options.anchor);
${(compiler.hasComponents || target.hasComplexBindings || hasInitHooks || target.hasIntroTransitions) && deindent`
${compiler.hasComponents && `this._lock = true;`}
${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(compiler.hasComponents || hasInitHooks) && `@callAll(this._oncreate);`}
${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${compiler.hasComponents && `this._lock = false;`}
`}
}
`}
`;
if (compiler.customElement) {
const props = compiler.props || Array.from(compiler.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')}
${compiler.slots.size && deindent`
connectedCallback() {
Object.keys(this._slotted).forEach(key => {
this.appendChild(this._slotted[key]);
});
}`}
attributeChangedCallback(attr, oldValue, newValue) {
this.set({ [attr]: newValue });
}
${(compiler.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent`
connectedCallback() {
${compiler.hasComponents && `this._lock = true;`}
${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(compiler.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`}
${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${compiler.hasComponents && `this._lock = false;`}
}
`}
}
@assign(${name}.prototype, ${proto});
${templateProperties.methods && `@assign(${name}.prototype, %methods);`}
@assign(${name}.prototype, {
_mount(target, anchor) {
target.insertBefore(this, anchor);
}
});
customElements.define("${compiler.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(target.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();
const filename = options.filename && (
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename
);
return compiler.generate(result, options, {
banner: `/* ${filename ? `${filename} ` : ``}generated by Svelte v${"__VERSION__"} */`,
sharedPath,
name,
format,
});
}

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

@ -1,45 +1,101 @@
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import { DomGenerator } from '../dom/index';
import addToSet from '../../utils/addToSet';
import Compiler from '../Compiler';
import Node from './shared/Node';
import Element from './Element';
import Text from './Text';
import Block from '../dom/Block';
import Expression from './shared/Expression';
export interface StyleProp {
key: string;
value: Node[];
}
export default class Attribute {
export default class Attribute extends Node {
type: 'Attribute';
start: number;
end: number;
generator: DomGenerator;
compiler: Compiler;
parent: Element;
name: string;
value: true | Node[]
expression: Node;
constructor({
generator,
name,
value,
parent
}: {
generator: DomGenerator,
name: string,
value: Node[],
parent: Element
}) {
this.type = 'Attribute';
this.generator = generator;
this.parent = parent;
this.name = name;
this.value = value;
isSpread: boolean;
isTrue: boolean;
isDynamic: boolean;
isSynthetic: boolean;
shouldCache: boolean;
expression?: Expression;
chunks: (Text | Expression)[];
dependencies: Set<string>;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
if (info.type === 'Spread') {
this.name = null;
this.isSpread = true;
this.isTrue = false;
this.isSynthetic = false;
this.expression = new Expression(compiler, 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(compiler, 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(' + ');
}
render(block: Block) {
@ -47,7 +103,7 @@ export default class Attribute {
const name = fixAttributeCasing(this.name);
if (name === 'style') {
const styleProps = optimizeStyle(this.value);
const styleProps = optimizeStyle(this.chunks);
if (styleProps) {
this.renderStyle(block, styleProps);
return;
@ -62,9 +118,9 @@ export default class Attribute {
name === 'value' &&
(node.name === 'option' || // TODO check it's actually bound
(node.name === 'input' &&
node.attributes.find(
(attribute: Attribute) =>
attribute.type === 'Binding' && /checked|group/.test(attribute.name)
node.bindings.find(
(binding: Binding) =>
/checked|group/.test(binding.name)
)));
const propertyName = isIndirectlyBoundValue
@ -78,77 +134,48 @@ export default class Attribute {
? '@setXlinkAttribute'
: '@setAttribute';
const isDynamic = this.isDynamic();
const isLegacyInputType = this.generator.legacy && name === 'type' && this.parent.name === 'input';
const isLegacyInputType = this.compiler.options.legacy && name === 'type' && this.parent.name === 'input';
const isDataSet = /^data-/.test(name) && !this.generator.legacy && !node.namespace;
const isDataSet = /^data-/.test(name) && !this.compiler.options.legacy && !node.namespace;
const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
return m[1].toUpperCase();
}) : name;
if (isDynamic) {
if (this.isDynamic) {
let value;
const allDependencies = new Set();
let shouldCache;
let hasChangeableIndex;
// 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.value.length === 1) {
// single {{tag}} — may be a non-string
const { expression } = this.value[0];
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = this.value[0].metadata;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
});
hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
shouldCache = (
expression.type !== 'Identifier' ||
block.contexts.has(expression.name) ||
hasChangeableIndex
);
if (this.chunks.length === 1) {
// single {tag} — may be a non-string
value = this.chunks[0].snippet;
} else {
// '{{foo}} {{bar}}' — treat as string concatenation
// '{foo} {bar}' — treat as string concatenation
value =
(this.value[0].type === 'Text' ? '' : `"" + `) +
this.value
(this.chunks[0].type === 'Text' ? '' : `"" + `) +
this.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true;
}
dependencies.forEach(d => {
allDependencies.add(d);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
return chunk.getPrecedence() <= 13
? `(${chunk.snippet})`
: chunk.snippet;
}
})
.join(' + ');
shouldCache = true;
}
const isSelectValueAttribute =
name === 'value' && node.name === 'select';
const last = (shouldCache || isSelectValueAttribute) && block.getUniqueName(
const shouldCache = this.shouldCache || isSelectValueAttribute;
const last = shouldCache && block.getUniqueName(
`${node.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
if (shouldCache || isSelectValueAttribute) block.addVariable(last);
if (shouldCache) block.addVariable(last);
let updater;
const init = shouldCache ? `${last} = ${value}` : value;
@ -185,27 +212,25 @@ export default class Attribute {
${last} = ${value};
${updater}
`);
block.builders.update.addLine(`${last} = ${value};`);
} else if (propertyName) {
block.builders.hydrate.addLine(
`${node.var}.${propertyName} = ${init};`
);
updater = `${node.var}.${propertyName} = ${shouldCache || isSelectValueAttribute ? last : value};`;
updater = `${node.var}.${propertyName} = ${shouldCache ? last : value};`;
} else if (isDataSet) {
block.builders.hydrate.addLine(
`${node.var}.dataset.${camelCaseName} = ${init};`
);
updater = `${node.var}.dataset.${camelCaseName} = ${shouldCache || isSelectValueAttribute ? last : value};`;
updater = `${node.var}.dataset.${camelCaseName} = ${shouldCache ? last : value};`;
} else {
block.builders.hydrate.addLine(
`${method}(${node.var}, "${name}", ${init});`
);
updater = `${method}(${node.var}, "${name}", ${shouldCache || isSelectValueAttribute ? last : value});`;
updater = `${method}(${node.var}, "${name}", ${shouldCache ? last : value});`;
}
if (allDependencies.size || hasChangeableIndex || isSelectValueAttribute) {
const dependencies = Array.from(allDependencies);
if (this.dependencies.size || isSelectValueAttribute) {
const dependencies = Array.from(this.dependencies);
const changedCheck = (
( block.hasOutroMethod ? `#outroing || ` : '' ) +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
@ -223,23 +248,22 @@ export default class Attribute {
);
}
} else {
const value = this.value === true
? 'true'
: this.value.length === 0
? `''`
: stringify(this.value[0].data);
const value = this.getValue();
const statement = (
isLegacyInputType ? `@setInputType(${node.var}, ${value});` :
propertyName ? `${node.var}.${propertyName} = ${value};` :
isDataSet ? `${node.var}.dataset.${camelCaseName} = ${value};` :
`${method}(${node.var}, "${name}", ${value});`
isLegacyInputType
? `@setInputType(${node.var}, ${value});`
: propertyName
? `${node.var}.${propertyName} = ${value};`
: isDataSet
? `${node.var}.dataset.${camelCaseName} = ${value};`
: `${method}(${node.var}, "${name}", ${value});`
);
block.builders.hydrate.addLine(statement);
// special case autofocus. has to be handled in a bit of a weird way
if (this.value === true && name === 'autofocus') {
if (this.isTrue && name === 'autofocus') {
block.autofocus = node.var;
}
}
@ -248,7 +272,7 @@ export default class Attribute {
const updateValue = `${node.var}.value = ${node.var}.__value;`;
block.builders.hydrate.addLine(updateValue);
if (isDynamic) block.builders.update.addLine(updateValue);
if (this.isDynamic) block.builders.update.addLine(updateValue);
}
}
@ -260,9 +284,8 @@ export default class Attribute {
let value;
if (isDynamic(prop.value)) {
const allDependencies = new Set();
const propDependencies = new Set();
let shouldCache;
let hasChangeableIndex;
value =
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
@ -271,26 +294,21 @@ export default class Attribute {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true;
}
const { dependencies, snippet } = chunk;
dependencies.forEach(d => {
allDependencies.add(d);
propDependencies.add(d);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet;
return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
if (allDependencies.size || hasChangeableIndex) {
const dependencies = Array.from(allDependencies);
if (propDependencies.size) {
const dependencies = Array.from(propDependencies);
const condition = (
( block.hasOutroMethod ? `#outroing || ` : '' ) +
(block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
@ -309,10 +327,16 @@ export default class Attribute {
});
}
isDynamic() {
if (this.value === true || this.value.length === 0) return false;
if (this.value.length > 1) return true;
return this.value[0].type !== 'Text';
stringifyForSsr() {
return this.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
return '${@escape(' + chunk.snippet + ')}';
})
.join('');
}
}
@ -516,7 +540,6 @@ const attributeLookup = {
type: {
appliesTo: [
'button',
'input',
'command',
'embed',
'object',

@ -0,0 +1,200 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import Block from '../dom/Block';
import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import { SsrTarget } from '../ssr';
export default class AwaitBlock extends Node {
expression: Expression;
value: string;
error: string;
pending: PendingBlock;
then: ThenBlock;
catch: CatchBlock;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
const deps = this.expression.dependencies;
this.value = info.value;
this.error = info.error;
this.pending = new PendingBlock(compiler, this, scope, info.pending);
this.then = new ThenBlock(compiler, this, scope.add(this.value, deps), info.then);
this.catch = new CatchBlock(compiler, this, scope.add(this.error, deps), info.catch);
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('await_block');
block.addDependencies(this.expression.dependencies);
let isDynamic = false;
let hasIntros = false;
let hasOutros = false;
['pending', 'then', 'catch'].forEach(status => {
const child = this[status];
child.block = block.child({
comment: createDebuggingComment(child, this.compiler),
name: this.compiler.getUniqueName(`create_${status}_block`)
});
child.initChildren(child.block, stripWhitespace, nextSibling);
this.compiler.target.blocks.push(child.block);
if (child.block.dependencies.size > 0) {
isDynamic = true;
block.addDependencies(child.block.dependencies);
}
if (child.block.hasIntroMethod) hasIntros = true;
if (child.block.hasOutroMethod) hasOutros = true;
});
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;
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const name = this.var;
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor);
const { snippet } = this.expression;
const 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.value}'`,
this.catch.block.name && `error: '${this.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';
block.builders.mount.addBlock(deindent`
${info}.block.${this.pending.block.hasIntroMethod ? 'i' : 'm'}(${initialMountNode}, ${info}.anchor = ${anchorNode});
${info}.mount = () => ${updateMountNode};
`);
const conditions = [];
if (this.expression.dependencies.size > 0) {
conditions.push(
`(${[...this.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(' && ')}
`);
}
block.builders.destroy.addBlock(deindent`
${info}.block.d(${parentNode ? '' : 'detach'});
${info} = null;
`);
[this.pending, this.then, this.catch].forEach(status => {
status.children.forEach(child => {
child.build(status.block, null, 'nodes');
});
});
}
ssr() {
const target: SsrTarget = <SsrTarget>this.compiler.target;
const { snippet } = this.expression;
target.append('${(function(__value) { if(@isPromise(__value)) return `');
this.pending.children.forEach((child: Node) => {
child.ssr();
});
target.append('`; return function(ctx) { return `');
this.then.children.forEach((child: Node) => {
child.ssr();
});
target.append(`\`;}(Object.assign({}, ctx, { ${this.value}: __value }));}(${snippet})) }`);
}
}

@ -3,8 +3,10 @@ import Element from './Element';
import getObject from '../../utils/getObject';
import getTailSnippet from '../../utils/getTailSnippet';
import flattenReference from '../../utils/flattenReference';
import { DomGenerator } from '../dom/index';
import Compiler from '../Compiler';
import Block from '../dom/Block';
import Expression from './shared/Expression';
import { dimensions } from '../../utils/patterns';
const readOnlyMediaAttributes = new Set([
'duration',
@ -13,48 +15,80 @@ const readOnlyMediaAttributes = new Set([
'played'
]);
// TODO a lot of this element-specific stuff should live in Element —
// Binding should ideally be agnostic between Element and Component
export default class Binding extends Node {
name: string;
value: Node;
expression: Node;
value: Expression;
isContextual: boolean;
usesContext: boolean;
obj: string;
prop: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.name = info.name;
this.value = new Expression(compiler, 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;
}
munge(
block: Block,
allUsedContexts: Set<string>
block: Block
) {
const node: Element = this.parent;
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type'));
const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name);
const isReadOnly = (
(node.isMediaNode() && readOnlyMediaAttributes.has(this.name)) ||
dimensions.test(this.name)
);
let updateCondition: string;
const { name } = getObject(this.value);
const { contexts } = block.contextualise(this.value);
const { snippet } = this.metadata;
const { name } = getObject(this.value.node);
const { snippet } = this.value;
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
// and `selected` is an object chosen with a <select>, then when `checked` changes,
// we need to tell the component to update all the values `selected` might be
// pointing to
// TODO should this happen in preprocess?
const dependencies = this.metadata.dependencies.slice();
this.metadata.dependencies.forEach((prop: string) => {
const indirectDependencies = this.generator.indirectDependencies.get(prop);
const dependencies = new Set(this.value.dependencies);
this.value.dependencies.forEach((prop: string) => {
const indirectDependencies = this.compiler.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
dependencies.add(indirectDependency);
});
}
});
contexts.forEach(context => {
allUsedContexts.add(context);
});
// view to model
const valueFromDom = getValueFromDom(this.generator, node, this);
const handler = getEventHandler(this.generator, block, name, snippet, this, dependencies, valueFromDom);
const valueFromDom = getValueFromDom(this.compiler, node, this);
const handler = getEventHandler(this, this.compiler, block, name, snippet, dependencies, valueFromDom);
// model to view
let updateDom = getDomUpdater(node, this, snippet);
@ -62,7 +96,7 @@ export default class Binding extends Node {
// special cases
if (this.name === 'group') {
const bindingGroup = getBindingGroup(this.generator, this.value);
const bindingGroup = getBindingGroup(this.compiler, this.value.node);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
@ -76,8 +110,7 @@ export default class Binding extends Node {
if (this.name === 'currentTime' || this.name === 'volume') {
updateCondition = `!isNaN(${snippet})`;
if (this.name === 'currentTime')
initialUpdate = null;
if (this.name === 'currentTime') initialUpdate = null;
}
if (this.name === 'paused') {
@ -90,6 +123,12 @@ export default class Binding extends Node {
initialUpdate = null;
}
// bind:offsetWidth and bind:offsetHeight
if (dimensions.test(this.name)) {
initialUpdate = null;
updateDom = null;
}
return {
name: this.name,
object: name,
@ -135,71 +174,67 @@ function getDomUpdater(
return `${node.var}.${binding.name} = ${snippet};`;
}
function getBindingGroup(generator: DomGenerator, value: Node) {
function getBindingGroup(compiler: Compiler, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = generator.bindingGroups.indexOf(keypath);
let index = compiler.bindingGroups.indexOf(keypath);
if (index === -1) {
index = generator.bindingGroups.length;
generator.bindingGroups.push(keypath);
index = compiler.bindingGroups.length;
compiler.bindingGroups.push(keypath);
}
return index;
}
function getEventHandler(
generator: DomGenerator,
binding: Binding,
compiler: Compiler,
block: Block,
name: string,
snippet: string,
attribute: Node,
dependencies: string[],
value: string,
isContextual: boolean
) {
let storeDependencies = [];
if (generator.options.store) {
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = dependencies.filter(prop => prop[0] !== '$');
}
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = [...dependencies].filter(prop => prop[0] !== '$');
if (block.contexts.has(name)) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
if (binding.isContextual) {
const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(binding.value.node)
: '';
const list = `context.${block.listNames.get(name)}`;
const index = `context.${block.indexNames.get(name)}`;
const head = block.bindings.get(name);
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${list}[${index}]${tail} = ${value};`,
props: dependencies.map(prop => `${prop}: state.${prop}`),
mutation: `${head}${tail} = ${value};`,
props: dependencies.map(prop => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
if (attribute.value.type === 'MemberExpression') {
if (binding.value.node.type === 'MemberExpression') {
// This is a little confusing, and should probably be tidied up
// at some point. It addresses a tricky bug (#893), wherein
// Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b)
// we should probably populate `generator.readonly` sooner so
// we should probably populate `compiler.target.readonly` sooner so
// that we don't have to do the `.some()` here
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
dependencies = dependencies.filter(prop => !compiler.computations.some(computation => computation.key === prop));
return {
usesContext: false,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${snippet} = ${value}`,
props: dependencies.map((prop: string) => `${prop}: state.${prop}`),
props: dependencies.map((prop: string) => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
@ -207,7 +242,7 @@ function getEventHandler(
let props;
let storeProps;
if (generator.options.store && name[0] === '$') {
if (name[0] === '$') {
props = [];
storeProps = [`${name.slice(1)}: ${value}`];
} else {
@ -226,7 +261,7 @@ function getEventHandler(
}
function getValueFromDom(
generator: DomGenerator,
compiler: Compiler,
node: Element,
binding: Node
) {
@ -241,7 +276,7 @@ function getValueFromDom(
// <input type='checkbox' bind:group='foo'>
if (binding.name === 'group') {
const bindingGroup = getBindingGroup(generator, binding.value);
const bindingGroup = getBindingGroup(compiler, binding.value.node);
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}

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

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

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

@ -0,0 +1,483 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
import 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 }>;
children: Node[];
else?: ElseBlock;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.expression = new Expression(compiler, 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 => {
this.scope.add(context.key.name, this.expression.dependencies);
});
this.key = info.key
? new Expression(compiler, 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.children = mapChildren(compiler, this, this.scope, info.children);
this.else = info.else
? new ElseBlock(compiler, this, this.scope, info.else)
: null;
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName(`each`);
this.iterations = block.getUniqueName(`${this.var}_blocks`);
this.get_each_context = this.compiler.getUniqueName(`get_${this.var}_context`);
const { dependencies } = this.expression;
block.addDependencies(dependencies);
this.block = block.child({
comment: createDebuggingComment(this, this.compiler),
name: this.compiler.getUniqueName('create_each_block'),
key: this.key,
bindings: new Map(block.bindings)
});
this.each_block_value = this.compiler.getUniqueName('each_value');
const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`);
this.contexts.forEach(prop => {
this.block.bindings.set(prop.key.name, `ctx.${this.each_block_value}[ctx.${indexName}]${prop.tail}`);
});
if (this.index) {
this.block.getUniqueName(this.index); // this prevents name collisions (#1254)
}
this.contextProps = this.contexts.map(prop => `child_ctx.${prop.key.name} = list[i]${prop.tail};`);
// TODO only add these if necessary
this.contextProps.push(
`child_ctx.${this.each_block_value} = list;`,
`child_ctx.${indexName} = i;`
);
this.compiler.target.blocks.push(this.block);
this.initChildren(this.block, stripWhitespace, nextSibling);
block.addDependencies(this.block.dependencies);
this.block.hasUpdateMethod = this.block.dependencies.size > 0;
if (this.else) {
this.else.block = block.child({
comment: createDebuggingComment(this.else, this.compiler),
name: this.compiler.getUniqueName(`${this.block.name}_else`),
});
this.compiler.target.blocks.push(this.else.block);
this.else.initChildren(
this.else.block,
stripWhitespace,
nextSibling
);
this.else.block.hasUpdateMethod = this.else.block.dependencies.size > 0;
}
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
if (this.children.length === 0) return;
const { compiler } = this;
const each = this.var;
const create_each_block = this.block.name;
const iterations = this.iterations;
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${each}_anchor`)
: (this.next && this.next.var) || 'null';
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = this.start + 2;
while (compiler.source[c] !== 'e') c += 1;
compiler.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
const vars = {
each,
create_each_block,
length,
iterations,
anchor,
mountOrIntro,
};
const { snippet } = this.expression;
block.builders.init.addLine(`var ${this.each_block_value} = ${snippet};`);
this.compiler.target.blocks.push(deindent`
function ${this.get_each_context}(ctx, list, i) {
const child_ctx = Object.create(ctx);
${this.contextProps}
return child_ctx;
}
`);
if (this.key) {
this.buildKeyed(block, parentNode, parentNodes, snippet, vars);
} else {
this.buildUnkeyed(block, parentNode, parentNodes, snippet, vars);
}
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
if (this.else) {
const each_block_else = compiler.getUniqueName(`${each}_else`);
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if (!${this.each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
}
`);
block.builders.mount.addBlock(deindent`
if (${each_block_else}) {
${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null);
}
`);
const initialMountNode = parentNode || `${anchor}.parentNode`;
if (this.else.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (!${this.each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p(changed, ctx);
} else if (!${this.each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
} else if (${each_block_else}) {
${each_block_else}.d(1);
${each_block_else} = null;
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${this.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}, ${anchor});
}
`);
}
block.builders.destroy.addBlock(deindent`
if (${each_block_else}) ${each_block_else}.d(${parentNode ? '' : 'detach'});
`);
}
this.children.forEach((child: Node) => {
child.build(this.block, null, 'nodes');
});
if (this.else) {
this.else.children.forEach((child: Node) => {
child.build(this.else.block, null, 'nodes');
});
}
}
buildKeyed(
block: Block,
parentNode: string,
parentNodes: string,
snippet: string,
{
each,
create_each_block,
length,
anchor,
mountOrIntro,
}
) {
const get_key = block.getUniqueName('get_key');
const blocks = block.getUniqueName(`${each}_blocks`);
const lookup = block.getUniqueName(`${each}_lookup`);
block.addVariable(blocks, '[]');
block.addVariable(lookup, `@blankObject()`);
if (this.children[0].isDomNode()) {
this.block.first = this.children[0].var;
} else {
this.block.first = this.block.getUniqueName('first');
this.block.addElement(
this.block.first,
`@createComment()`,
parentNodes && `@createComment()`,
null
);
}
block.builders.init.addBlock(deindent`
const ${get_key} = ctx => ${this.key.snippet};
for (var #i = 0; #i < ${this.each_block_value}.${length}; #i += 1) {
let child_ctx = ${this.get_each_context}(ctx, ${this.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;
block.builders.update.addBlock(deindent`
var ${this.each_block_value} = ${snippet};
${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context});
`);
block.builders.destroy.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d(${parentNode ? '' : 'detach'});
`);
}
buildUnkeyed(
block: Block,
parentNode: string,
parentNodes: string,
snippet: string,
{
create_each_block,
length,
iterations,
anchor,
mountOrIntro,
}
) {
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for (var #i = 0; #i < ${this.each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(#component, ${this.get_each_context}(ctx, ${this.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.expression;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
// TODO do this for keyed blocks as well
const condition = Array.from(allDependencies)
.map(dependency => `changed.${dependency}`)
.join(' || ');
if (condition !== '') {
const forLoopBody = this.block.hasUpdateMethod
? this.block.hasIntroMethod
? deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, 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`;
const outro = block.getUniqueName('outro');
const destroy = this.block.hasOutroMethod
? deindent`
function ${outro}(i) {
if (${iterations}[i]) {
${iterations}[i].o(function() {
${iterations}[i].d(1);
${iterations}[i] = null;
});
}
}
for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
`
: deindent`
for (; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].d(1);
}
${iterations}.length = ${this.each_block_value}.${length};
`;
block.builders.update.addBlock(deindent`
if (${condition}) {
${this.each_block_value} = ${snippet};
for (var #i = ${start}; #i < ${this.each_block_value}.${length}; #i += 1) {
const child_ctx = ${this.get_each_context}(ctx, ${this.each_block_value}, #i);
${forLoopBody}
}
${destroy}
}
`);
}
block.builders.destroy.addBlock(`@destroyEach(${iterations}, detach);`);
}
remount(name: string) {
// TODO consider keyed blocks
return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`;
}
ssr() {
const { compiler } = this;
const { snippet } = this.expression;
const props = this.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
const getContext = this.index
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${this.index}: i })`
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
const open = `\${ ${this.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
compiler.target.append(open);
this.children.forEach((child: Node) => {
child.ssr();
});
const close = `\`)`;
compiler.target.append(close);
if (this.else) {
compiler.target.append(` : \``);
this.else.children.forEach((child: Node) => {
child.ssr();
});
compiler.target.append(`\``);
}
compiler.target.append('}');
}
}

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,94 @@
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';
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(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.name = info.name;
this.dependencies = new Set();
if (info.expression) {
this.callee = flattenReference(info.expression.callee);
this.insertionPoint = info.expression.start;
this.usesComponent = !validCalleeObjects.has(this.callee.name);
this.usesContext = false;
this.args = info.expression.arguments.map(param => {
const expression = new Expression(compiler, this, scope, param);
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 = compiler.events.has(this.name);
this.shouldHoist = !this.isCustomEvent && parent.hasAncestor('EachBlock');
}
render(compiler, block, hoisted) { // TODO hoist more event handlers
if (this.insertionPoint === null) return; // TODO handle shorthand events here?
if (!validCalleeObjects.has(this.callee.name)) {
const component = hoisted ? `component` : block.alias(`component`);
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (this.callee.name[0] === '$' && !compiler.methods.has(this.callee.name)) {
compiler.code.overwrite(
this.insertionPoint,
this.insertionPoint + 1,
`${component}.store.`
);
} else {
compiler.code.prependRight(
this.insertionPoint,
`${component}.`
);
}
}
if (this.isCustomEvent) {
this.args.forEach(arg => {
arg.overwriteThis(this.parent.var);
});
if (this.callee && this.callee.name === 'this') {
const node = this.callee.nodes[0];
compiler.code.overwrite(node.start, node.end, this.parent.var, {
storeName: true,
contentOnly: true
});
}
}
}
}

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

@ -3,10 +3,18 @@ import { stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import mapChildren from './shared/mapChildren';
export default class Head extends Node {
type: 'Head';
attributes: Attribute[];
children: any[]; // TODO
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children.filter(child => {
return (child.type !== 'Text' || /\S/.test(child.data));
}));
}
init(
block: Block,
@ -21,12 +29,20 @@ export default class Head extends Node {
parentNode: string,
parentNodes: string
) {
const { generator } = this;
this.var = 'document.head';
this.children.forEach((child: Node) => {
child.build(block, 'document.head', null);
});
}
ssr() {
this.compiler.target.append('${(__result.head += `');
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append('`, "")}');
}
}

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

@ -3,12 +3,6 @@ import Tag from './shared/Tag';
import Block from '../dom/Block';
export default class MustacheTag extends Tag {
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('text');
block.addDependencies(this.metadata.dependencies);
}
build(
block: Block,
parentNode: string,
@ -26,4 +20,18 @@ export default class MustacheTag extends Tag {
parentNode
);
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
ssr() {
this.compiler.target.append(
this.parent &&
this.parent.type === 'Element' &&
this.parent.name === 'style'
? '${' + this.expression.snippet + '}'
: '${@escape(' + this.expression.snippet + ')}'
);
}
}

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

@ -4,12 +4,6 @@ import Tag from './shared/Tag';
import Block from '../dom/Block';
export default class RawMustacheTag extends Tag {
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('raw');
block.addDependencies(this.metadata.dependencies);
}
build(
block: Block,
parentNode: string,
@ -62,7 +56,8 @@ export default class RawMustacheTag extends Tag {
anchorBefore,
`@createElement('noscript')`,
parentNodes && `@createElement('noscript')`,
parentNode
parentNode,
true
);
}
@ -82,11 +77,24 @@ 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
addAnchorAfter();
}
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
ssr() {
this.compiler.target.append('${' + this.expression.snippet + '}');
}
}

@ -1,4 +1,6 @@
import deindent from '../../utils/deindent';
import isValidIdentifier from '../../utils/isValidIdentifier';
import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node';
import Element from './Element';
import Attribute from './Attribute';
@ -7,7 +9,7 @@ import Block from '../dom/Block';
export default class Slot extends Element {
type: 'Element';
name: string;
attributes: Attribute[]; // TODO have more specific Attribute type
attributes: Attribute[];
children: Node[];
init(
@ -29,13 +31,14 @@ export default class Slot extends Element {
parentNode: string,
parentNodes: string
) {
const { generator } = this;
const { compiler } = this;
const slotName = this.getStaticAttributeValue('name') || 'default';
generator.slots.add(slotName);
compiler.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${slotName}`);
block.addVariable(content_name, `#component._slotted.${slotName}`);
const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`;
block.addVariable(content_name, `#component._slotted${prop}`);
const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !parentNode;
const needsAnchorAfter = this.next ? this.next.type !== 'Element' : !parentNode;
@ -51,10 +54,13 @@ export default class Slot extends Element {
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.unmount.pushCondition(`!${content_name}`);
block.builders.update.pushCondition(`!${content_name}`);
block.builders.destroy.pushCondition(`!${content_name}`);
this.children.forEach((child: Node) => {
@ -64,13 +70,16 @@ export default class Slot extends Element {
block.builders.create.popCondition();
block.builders.hydrate.popCondition();
block.builders.mount.popCondition();
block.builders.unmount.popCondition();
block.builders.update.popCondition();
block.builders.destroy.popCondition();
// TODO can we use an else here?
const mountLeadin = block.builders.mount.toString() !== mountBefore
? `else`
: `if (${content_name})`;
if (parentNode) {
block.builders.mount.addBlock(deindent`
if (${content_name}) {
${mountLeadin} {
${needsAnchorBefore && `@appendNode(${anchorBefore} || (${anchorBefore} = @createComment()), ${parentNode});`}
@appendNode(${content_name}, ${parentNode});
${needsAnchorAfter && `@appendNode(${anchorAfter} || (${anchorAfter} = @createComment()), ${parentNode});`}
@ -78,7 +87,7 @@ export default class Slot extends Element {
`);
} else {
block.builders.mount.addBlock(deindent`
if (${content_name}) {
${mountLeadin} {
${needsAnchorBefore && `@insertNode(${anchorBefore} || (${anchorBefore} = @createComment()), #target, anchor);`}
@insertNode(${content_name}, #target, anchor);
${needsAnchorAfter && `@insertNode(${anchorAfter} || (${anchorAfter} = @createComment()), #target, anchor);`}
@ -90,28 +99,31 @@ export default class Slot extends Element {
// 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
// TODO can we use an else here?
const unmountLeadin = block.builders.destroy.toString() !== destroyBefore
? `else`
: `if (${content_name})`;
if (anchorBefore === 'null' && anchorAfter === 'null') {
block.builders.unmount.addBlock(deindent`
if (${content_name}) {
block.builders.destroy.addBlock(deindent`
${unmountLeadin} {
@reinsertChildren(${parentNode}, ${content_name});
}
`);
} else if (anchorBefore === 'null') {
block.builders.unmount.addBlock(deindent`
if (${content_name}) {
block.builders.destroy.addBlock(deindent`
${unmountLeadin} {
@reinsertBefore(${anchorAfter}, ${content_name});
}
`);
} else if (anchorAfter === 'null') {
block.builders.unmount.addBlock(deindent`
if (${content_name}) {
block.builders.destroy.addBlock(deindent`
${unmountLeadin} {
@reinsertAfter(${anchorBefore}, ${content_name});
}
`);
} else {
block.builders.unmount.addBlock(deindent`
if (${content_name}) {
block.builders.destroy.addBlock(deindent`
${unmountLeadin} {
@reinsertBetween(${anchorBefore}, ${anchorAfter}, ${content_name});
@detachNode(${anchorBefore});
@detachNode(${anchorAfter});
@ -122,18 +134,31 @@ export default class Slot extends Element {
getStaticAttributeValue(name: string) {
const attribute = this.attributes.find(
(attr: Node) => attr.name.toLowerCase() === name
attr => attr.name.toLowerCase() === name
);
if (!attribute) return null;
if (attribute.value === true) return true;
if (attribute.value.length === 0) return '';
if (attribute.isTrue) return true;
if (attribute.chunks.length === 0) return '';
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') {
return attribute.value[0].data;
if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
return attribute.chunks[0].data;
}
return null;
}
ssr() {
const name = this.attributes.find(attribute => attribute.name === 'name');
const slotName = name && name.chunks[0].data || 'default';
this.compiler.target.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : \``);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`\`}`);
}
}

@ -1,4 +1,4 @@
import { stringify } from '../../utils/stringify';
import { escape, escapeHTML, escapeTemplate, stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
@ -33,6 +33,11 @@ export default class Text extends Node {
data: string;
shouldSkip: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.data = info.data;
}
init(block: Block) {
const parentElement = this.findNearest(/(?:Element|Component)/);
@ -58,4 +63,21 @@ export default class Text extends Node {
parentNode
);
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
ssr() {
let text = this.data;
if (
!this.parent ||
this.parent.type !== 'Element' ||
(this.parent.name !== 'script' && this.parent.name !== 'style')
) {
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
this.compiler.target.append(escape(escapeTemplate(text)));
}
}

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

@ -1,9 +1,25 @@
import { stringify } from '../../utils/stringify';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import Node from './shared/Node';
import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
export default class Title extends Node {
type: 'Title';
children: any[]; // TODO
shouldCache: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
this.shouldCache = info.children.length === 1
? (
info.children[0].type !== 'Identifier' ||
scope.names.has(info.children[0].name)
)
: true;
}
build(
block: Block,
parentNode: string,
@ -15,27 +31,20 @@ export default class Title extends Node {
let value;
const allDependencies = new Set();
let shouldCache;
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.children.length === 1) {
// single {{tag}} — may be a non-string
const { expression } = this.children[0];
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = this.children[0].metadata;
const { dependencies, snippet } = this.children[0].expression;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
});
shouldCache = (
expression.type !== 'Identifier' ||
block.contexts.has(expression.name)
);
} else {
// '{{foo}} {{bar}}' — treat as string concatenation
// '{foo} {bar}' — treat as string concatenation
value =
(this.children[0].type === 'Text' ? '' : `"" + `) +
this.children
@ -43,34 +52,31 @@ export default class Title extends Node {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
const { dependencies, snippet } = chunk.expression;
dependencies.forEach(d => {
allDependencies.add(d);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
return chunk.expression.getPrecedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
shouldCache = true;
}
const last = shouldCache && block.getUniqueName(
const last = this.shouldCache && block.getUniqueName(
`title_value`
);
if (shouldCache) block.addVariable(last);
if (this.shouldCache) block.addVariable(last);
let updater;
const init = shouldCache ? `${last} = ${value}` : value;
const init = this.shouldCache ? `${last} = ${value}` : value;
block.builders.init.addLine(
`document.title = ${init};`
);
updater = `document.title = ${shouldCache ? last : value};`;
updater = `document.title = ${this.shouldCache ? last : value};`;
if (allDependencies.size) {
const dependencies = Array.from(allDependencies);
@ -81,7 +87,7 @@ export default class Title extends Node {
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ?
const condition = this.shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
@ -95,4 +101,14 @@ export default class Title extends Node {
block.builders.hydrate.addLine(`document.title = ${value};`);
}
}
ssr() {
this.compiler.target.append(`<title>`);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`</title>`);
}
}

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

@ -0,0 +1,224 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import flattenReference from '../../utils/flattenReference';
import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node';
import Block from '../dom/Block';
import Binding from './Binding';
import EventHandler from './EventHandler';
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 Window extends Node {
type: 'Window';
handlers: EventHandler[];
bindings: Binding[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.handlers = [];
this.bindings = [];
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(compiler, this, scope, node));
} else if (node.type === 'Binding') {
this.bindings.push(new Binding(compiler, this, scope, node));
}
});
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { compiler } = this;
const events = {};
const bindings: Record<string, string> = {};
this.handlers.forEach(handler => {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
compiler.addSourcemapLocations(handler.expression);
const isCustomEvent = compiler.events.has(handler.name);
let usesState = handler.dependencies.size > 0;
handler.render(compiler, block, false); // TODO hoist?
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent`
${usesState && `var ctx = #component.get();`}
${handler.snippet};
`;
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.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
compiler.target.readonly.add(binding.value.node.name);
}
bindings[binding.name] = binding.value.node.name;
// bind:online is a special case, we need to listen for two separate events
if (binding.name === 'online') return;
const associatedEvent = associatedEvents[binding.name];
const property = properties[binding.name] || binding.name;
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push(
`${binding.value.node.name}: this.${property}`
);
// add initial value
compiler.target.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;
`}
${compiler.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({
${props}
});
${compiler.options.dev && `component._updatingReadonlyProperty = false;`}
${event === 'scroll' && `${lock} = false;`}
`;
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${event}", ${handlerName});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${event}", ${handlerName});
`);
});
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX || bindings.scrollY) {
block.builders.init.addBlock(deindent`
#component.on("state", ({ changed, current }) => {
if (${
[bindings.scrollX, bindings.scrollY].map(
binding => binding && `changed["${binding}"]`
).filter(Boolean).join(' || ')
}) {
${lock} = true;
clearTimeout(${timeout});
window.scrollTo(${
bindings.scrollX ? `current["${bindings.scrollX}"]` : `window.pageXOffset`
}, ${
bindings.scrollY ? `current["${bindings.scrollY}"]` : `window.pageYOffset`
});
${timeout} = setTimeout(${clear}, 100);
}
});
`);
}
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
#component.set({ ${bindings.online}: navigator.onLine });
}
window.addEventListener("online", ${handlerName});
window.addEventListener("offline", ${handlerName});
`);
// add initial value
compiler.target.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;`
);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("online", ${handlerName});
window.removeEventListener("offline", ${handlerName});
`);
}
}
ssr() {
// noop
}
}

@ -0,0 +1,171 @@
import Compiler from '../../Compiler';
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 {
compiler: Compiler;
node: any;
snippet: string;
usesContext: boolean;
references: Set<string>;
dependencies: Set<string>;
thisReferences: Array<{ start: number, end: number }>;
constructor(compiler, parent, scope, info) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
compiler: {
value: compiler
}
});
this.node = info;
this.thisReferences = [];
this.snippet = `[✂${info.start}-${info.end}✂]`;
this.usesContext = false;
const dependencies = new Set();
const { code, helpers } = compiler;
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 (compiler.helpers.has(name)) {
let object = node;
while (object.type === 'MemberExpression') object = object.object;
const alias = compiler.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);
compiler.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.compiler.code.overwrite(ref.start, ref.end, name, {
storeName: true
});
});
}
}

@ -1,37 +1,41 @@
import { DomGenerator } from '../../dom/index';
import Compiler from './../../Compiler';
import Block from '../../dom/Block';
import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node {
type: string;
start: number;
end: number;
[key: string]: any;
readonly start: number;
readonly end: number;
readonly compiler: Compiler;
readonly parent: Node;
readonly type: string;
metadata?: {
dependencies: string[];
snippet: string;
};
parent: Node;
prev?: Node;
next?: Node;
generator: DomGenerator;
canUseInnerHTML: boolean;
var: string;
constructor(data: Record<string, any>) {
Object.assign(this, data);
constructor(compiler: Compiler, 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, {
compiler: {
value: compiler
},
parent: {
value: parent
}
});
}
cannotUseInnerHTML() {
if (this.canUseInnerHTML !== false) {
this.canUseInnerHTML = false;
if (this.parent) {
if (!this.parent.cannotUseInnerHTML) console.log(this.parent.type, this.type);
this.parent.cannotUseInnerHTML();
}
if (this.parent) this.parent.cannotUseInnerHTML();
}
}
@ -58,7 +62,7 @@ export default class Node {
if (child.type === 'Comment') return;
// special case — this is an easy way to remove whitespace surrounding
// <:Window/>. lil hacky but it works
// <svelte:window/>. lil hacky but it works
if (child.type === 'Window') {
windowComponent = child;
return;
@ -82,7 +86,7 @@ export default class Node {
lastChild = null;
cleaned.forEach((child: Node, i: number) => {
child.canUseInnerHTML = !this.generator.hydratable;
child.canUseInnerHTML = !this.compiler.options.hydratable;
child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);
@ -161,4 +165,8 @@ export default class Node {
getUpdateMountNode(anchor: string) {
return this.parent.isDomNode() ? this.parent.var : `${anchor}.parentNode`;
}
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
}
}

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

@ -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,47 @@
import AwaitBlock from '../AwaitBlock';
import Comment from '../Comment';
import Component from '../Component';
import EachBlock from '../EachBlock';
import Element from '../Element';
import Head from '../Head';
import IfBlock from '../IfBlock';
import 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 './Node';
function getConstructor(type): typeof Node {
switch (type) {
case 'AwaitBlock': return AwaitBlock;
case 'Comment': return Comment;
case 'Component': return Component;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
case 'Head': return Head;
case 'IfBlock': return IfBlock;
case 'MustacheTag': return MustacheTag;
case 'RawMustacheTag': return RawMustacheTag;
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(compiler, parent, scope, children: any[]) {
let last = null;
return children.map(child => {
const constructor = getConstructor(child.type);
const node = new constructor(compiler, parent, scope, child);
if (last) last.next = node;
node.prev = last;
last = node;
return node;
});
}

@ -0,0 +1,174 @@
import deindent from '../../utils/deindent';
import Compiler from '../Compiler';
import Stats from '../../Stats';
import Stylesheet from '../../css/Stylesheet';
import { removeNode, removeObjectKey } from '../../utils/removeNode';
import getName from '../../utils/getName';
import globalWhitelist from '../../utils/globalWhitelist';
import { Ast, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from '../../interfaces';
import { stringify } from '../../utils/stringify';
export class SsrTarget {
bindings: string[];
renderCode: string;
appendTargets: AppendTarget[];
constructor() {
this.bindings = [];
this.renderCode = '';
this.appendTargets = [];
}
append(code: string) {
if (this.appendTargets.length) {
const appendTarget = this.appendTargets[this.appendTargets.length - 1];
const slotName = appendTarget.slotStack[appendTarget.slotStack.length - 1];
appendTarget.slots[slotName] += code;
} else {
this.renderCode += code;
}
}
}
export default function ssr(
ast: Ast,
source: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
) {
const format = options.format || 'cjs';
const target = new SsrTarget();
const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, false, target);
const { computations, name, templateProperties } = compiler;
// create main render() function
trim(compiler.fragment.children).forEach((node: Node) => {
node.ssr();
});
const css = compiler.customElement ?
{ code: null, map: null } :
compiler.stylesheet.render(options.filename, true);
// generate initial state object
const expectedProperties = Array.from(compiler.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');
const helpers = new Set();
// TODO concatenate CSS maps
const result = deindent`
${compiler.javascript}
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});
ctx = Object.assign(${initialState.join(', ')});
${computations.map(
({ key }) => `ctx.${key} = %computed-${key}(ctx);`
)}
${target.bindings.length &&
deindent`
var settled = false;
var tmp;
while (!settled) {
settled = true;
${target.bindings.join('\n\n')}
}
`}
return \`${target.renderCode}\`;
};
${name}.css = {
code: ${css.code ? stringify(css.code) : `''`},
map: ${css.map ? stringify(css.map.toString()) : 'null'}
};
var warned = false;
${templateProperties.preload && `${name}.preload = %preload;`}
`;
return compiler.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);
}

@ -1,6 +1,6 @@
import deindent from '../utils/deindent';
import list from '../utils/list';
import { CompileOptions, ModuleFormat, Node } from '../interfaces';
import { CompileOptions, ModuleFormat, Node, ShorthandImport } from '../interfaces';
interface Dependency {
name: string;
@ -19,9 +19,10 @@ export default function wrapModule(
sharedPath: string,
helpers: { name: string, alias: string }[],
imports: Node[],
shorthandImports: ShorthandImport[],
source: string
): string {
if (format === 'es') return es(code, name, options, banner, sharedPath, helpers, imports, source);
if (format === 'es') return es(code, name, options, banner, sharedPath, helpers, imports, shorthandImports, source);
const dependencies = imports.map((declaration, i) => {
const defaultImport = declaration.specifiers.find(
@ -58,7 +59,16 @@ export default function wrapModule(
}
return { name, statements, source: declaration.source.value };
});
})
.concat(
shorthandImports.map(({ name, source }) => ({
name,
statements: [
`${name} = (${name} && ${name}.__esModule) ? ${name}["default"] : ${name};`,
],
source,
}))
);
if (format === 'amd') return amd(code, name, options, banner, dependencies);
if (format === 'cjs') return cjs(code, name, options, banner, sharedPath, helpers, dependencies);
@ -77,9 +87,10 @@ function es(
sharedPath: string,
helpers: { name: string, alias: string }[],
imports: Node[],
shorthandImports: ShorthandImport[],
source: string
) {
const importHelpers = helpers && (
const importHelpers = helpers.length > 0 && (
`import { ${helpers.map(h => h.name === h.alias ? h.name : `${h.name} as ${h.alias}`).join(', ')} } from ${JSON.stringify(sharedPath)};`
);
@ -89,10 +100,15 @@ function es(
.join('\n')
);
const shorthandImportBlock = shorthandImports.length > 0 && (
shorthandImports.map(({ name, source }) => `import ${name} from ${JSON.stringify(source)};`).join('\n')
);
return deindent`
${banner}
${importHelpers}
${importBlock}
${shorthandImportBlock}
${code}
export default ${name};`;
@ -129,12 +145,10 @@ function cjs(
helpers: { name: string, alias: string }[],
dependencies: Dependency[]
) {
const SHARED = '__shared';
const helperBlock = helpers && (
`var ${SHARED} = require(${JSON.stringify(sharedPath)});\n` +
helpers.map(helper => {
return `var ${helper.alias} = ${SHARED}.${helper.name};`;
}).join('\n')
const helperDeclarations = helpers.map(h => `${h.alias === h.name ? h.name : `${h.name}: ${h.alias}`}`).join(', ');
const helperBlock = helpers.length > 0 && (
`var { ${helperDeclarations} } = require(${JSON.stringify(sharedPath)});\n`
);
const requireBlock = dependencies.length > 0 && (
@ -276,6 +290,7 @@ function getGlobals(dependencies: Dependency[], options: CompileOptions) {
onerror(error);
} else {
const warning = {
code: `options-missing-globals`,
message: `No name was supplied for imported module '${d.source}'. Guessing '${d.name}', but you should use options.globals`,
};

@ -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';
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);
@ -31,7 +34,7 @@ export default class Selector {
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;
});
@ -99,7 +102,10 @@ export default class Selector {
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);
validator.error(selector, {
code: `css-invalid-global`,
message: `:global(...) must be the first element in a compound selector`
});
}
}
});
@ -117,7 +123,10 @@ 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);
validator.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`
});
}
}
}
@ -164,11 +173,12 @@ 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)) {
if (node.ref === selector.name) {
node._cssRefAttribute = selector.name;
toEncapsulate.push({ node, block });
return true;
@ -220,20 +230,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

@ -4,9 +4,9 @@ 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 Element from '../compile/nodes/Element';
import { Validator } from '../validate/index';
import { Node, Parsed, Warning } from '../interfaces';
import { Node, Ast, Warning } from '../interfaces';
class Rule {
selectors: Selector[];
@ -14,10 +14,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));
}
@ -25,23 +25,24 @@ class Rule {
this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here?
}
isUsed() {
isUsed(dev: boolean) {
if (this.parent && this.parent.node.type === 'Atrule' && this.parent.node.name === 'keyframes') return true;
if (this.declarations.length === 0) return dev;
return this.selectors.some(s => s.used);
}
minify(code: MagicString, cascade: 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;
@ -65,39 +66,12 @@ class Rule {
code.remove(c, this.node.block.end - 1);
}
transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
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];
let transformed;
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));
}
const attr = `.${id}`;
this.selectors.forEach(selector => selector.transform(code, attr));
this.declarations.forEach(declaration => declaration.transform(code, keyframes));
}
@ -162,7 +136,7 @@ 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);
});
@ -177,11 +151,11 @@ class Atrule {
}
}
isUsed() {
isUsed(dev: boolean) {
return true; // TODO
}
minify(code: MagicString, cascade: 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);
@ -198,6 +172,14 @@ class Atrule {
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
@ -206,9 +188,9 @@ class Atrule {
let c = this.node.block.start + 1;
this.children.forEach(child => {
if (cascade || child.isUsed()) {
if (child.isUsed(dev)) {
code.remove(c, child.node.start);
child.minify(code, cascade);
child.minify(code, dev);
c = child.node.end;
}
});
@ -217,7 +199,7 @@ class Atrule {
}
}
transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (this.node.name === 'keyframes') {
this.node.expression.children.forEach(({ type, name, start, end }: Node) => {
if (type === 'Identifier') {
@ -231,7 +213,7 @@ class Atrule {
}
this.children.forEach(child => {
child.transform(code, id, keyframes, cascade);
child.transform(code, id, keyframes);
})
}
@ -254,9 +236,9 @@ const keys = {};
export default class Stylesheet {
source: string;
parsed: Parsed;
cascade: boolean;
ast: Ast;
filename: string;
dev: boolean;
hasStyles: boolean;
id: string;
@ -264,24 +246,28 @@ export default class Stylesheet {
children: (Rule|Atrule)[];
keyframes: Map<string, string>;
constructor(source: string, parsed: Parsed, filename: string, cascade: boolean) {
nodesWithCssClass: Set<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();
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];
@ -311,7 +297,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) {
@ -341,25 +327,26 @@ 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();
});
}
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);
@ -368,15 +355,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()) {
if (child.isUsed(this.dev)) {
code.remove(c, child.node.start);
child.minify(code, this.cascade);
child.minify(code, this.dev);
c = child.node.end;
}
});
@ -384,8 +371,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
@ -400,26 +387,27 @@ export default class Stylesheet {
}
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}`,
});
};

@ -1,466 +0,0 @@
import MagicString from 'magic-string';
import { parseExpressionAt } from 'acorn';
import annotateWithScopes from '../../utils/annotateWithScopes';
import isReference from '../../utils/isReference';
import { walk } from 'estree-walker';
import deindent from '../../utils/deindent';
import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist';
import reservedNames from '../../utils/reservedNames';
import shared from './shared';
import Generator from '../Generator';
import Stylesheet from '../../css/Stylesheet';
import Block from './Block';
import { test } from '../../config';
import { Parsed, CompileOptions, Node } from '../../interfaces';
export class DomGenerator extends Generator {
blocks: (Block|string)[];
readonly: Set<string>;
metaBindings: string[];
hydratable: boolean;
legacy: boolean;
hasIntroTransitions: boolean;
hasOutroTransitions: boolean;
hasComplexBindings: boolean;
needsEncapsulateHelper: boolean;
constructor(
parsed: Parsed,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
super(parsed, source, name, stylesheet, options, true);
this.blocks = [];
this.readonly = new Set();
this.hydratable = options.hydratable;
this.legacy = options.legacy;
this.needsEncapsulateHelper = false;
// initial values for e.g. window.innerWidth, if there's a <:Window> meta tag
this.metaBindings = [];
}
getUniqueNameMaker(params: string[]) {
const localUsedNames = new Set(params);
function add(name: string) {
localUsedNames.add(name);
}
reservedNames.forEach(add);
this.userVars.forEach(add);
for (const name in shared) {
localUsedNames.add(test ? `${name}$` : name);
}
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;
};
}
}
export default function dom(
parsed: Parsed,
source: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
const format = options.format || 'es';
const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options);
const {
computations,
name,
templateProperties,
namespace,
} = generator;
parsed.html.build();
const { block } = parsed.html;
// prevent fragment being created twice (#1063)
if (options.customElement) block.builders.create.addLine(`this.c = @noop;`);
generator.stylesheet.warnOnUnusedSelectors(options.onwarn);
const builder = new CodeBuilder();
const computationBuilder = new CodeBuilder();
const computationDeps = new Set();
if (computations.length) {
computations.forEach(({ key, deps }) => {
deps.forEach(dep => {
computationDeps.add(dep);
});
if (generator.readonly.has(key)) {
// <:Window> bindings
throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property`
);
}
generator.readonly.add(key);
const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`;
const statement = `if (@differs(state.${key}, (state.${key} = %computed-${key}(${deps
.map(dep => `state.${dep}`)
.join(', ')})))) changed.${key} = true;`;
computationBuilder.addConditional(condition, statement);
});
}
if (generator.javascript) {
builder.addBlock(generator.javascript);
}
if (generator.needsEncapsulateHelper) {
builder.addBlock(deindent`
function @encapsulateStyles(node) {
@setAttribute(node, "${generator.stylesheet.id}", "");
}
`);
}
const { css, cssMap } = generator.stylesheet.render(options.filename, !generator.customElement);
const styles = generator.stylesheet.hasStyles && stringify(options.dev ?
`${css}\n/*# sourceMappingURL=${cssMap.toUrl()} */` :
css, { onlyEscapeAtSymbol: true });
if (styles && generator.options.css !== false && !generator.customElement) {
builder.addBlock(deindent`
function @add_css() {
var style = @createElement("style");
style.id = '${generator.stylesheet.id}-style';
style.textContent = ${styles};
@appendNode(style, document.head);
}
`);
}
generator.blocks.forEach(block => {
builder.addBlock(block.toString());
});
const sharedPath: string = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const prototypeBase =
`${name}.prototype` +
(templateProperties.methods ? `, %methods` : '');
const proto = sharedPath
? `@proto`
: deindent`
{
${['destroy', 'get', 'fire', 'observe', 'on', 'set', 'teardown', '_set', '_mount', '_unmount']
.map(n => `${n}: @${n === 'teardown' ? 'destroy' : n}`)
.join(',\n')}
}`;
const debugName = `<${generator.customElement ? generator.tag : name}>`;
// generate initial state object
const expectedProperties = Array.from(generator.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = options.store || templateProperties.store ? 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 constructorBody = deindent`
${options.dev && `this._debugName = '${debugName}';`}
${options.dev && !generator.customElement &&
`if (!options || (!options.target && !options.root)) throw new Error("'target' is a required option");`}
@init(this, options);
${templateProperties.store && `this.store = %store();`}
${generator.usesRefs && `this.refs = {};`}
this._state = @assign(${initialState.join(', ')});
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
${generator.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
Array.from(generator.expectedProperties).map(prop => {
const message = generator.components.has(prop) ?
`${debugName} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` :
`${debugName} was created without expected data property '${prop}'`;
return `if (!('${prop}' in this._state)) console.warn("${message}");`
})}
${generator.bindingGroups.length &&
`this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`}
${(templateProperties.ondestroy || storeProps.length) && (
`this._handlers.destroy = [${
[templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ')
}];`
)}
${generator.slots.size && `this._slotted = options.slots || {};`}
${generator.customElement ?
deindent`
this.attachShadow({ mode: 'open' });
${css && `this.shadowRoot.innerHTML = \`<style>${escape(css, { onlyEscapeAtSymbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${cssMap.toUrl()} */` : ''}</style>\`;`}
` :
(generator.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${generator.stylesheet.id}-style")) @add_css();`)
}
${templateProperties.oncreate && `var _oncreate = %oncreate.bind(this);`}
${(templateProperties.oncreate || generator.hasComponents || generator.hasComplexBindings || generator.hasIntroTransitions) && deindent`
if (!options.root) {
this._oncreate = [];
${(generator.hasComponents || generator.hasComplexBindings) && `this._beforecreate = [];`}
${(generator.hasComponents || generator.hasIntroTransitions) && `this._aftercreate = [];`}
}
`}
${generator.slots.size && `this.slots = {};`}
this._fragment = @create_main_fragment(this._state, this);
${(templateProperties.oncreate) && deindent`
this.root._oncreate.push(_oncreate);
`}
${generator.customElement ? deindent`
this._fragment.c();
this._fragment.${block.hasIntroMethod ? 'i' : 'm'}(this.shadowRoot, null);
if (options.target) this._mount(options.target, options.anchor || null);
` : deindent`
if (options.target) {
${generator.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._fragment.${block.hasIntroMethod ? 'i' : 'm'}(options.target, options.anchor || null);
${(generator.hasComponents || generator.hasComplexBindings || templateProperties.oncreate || generator.hasIntroTransitions) && deindent`
${generator.hasComponents && `this._lock = true;`}
${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(generator.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`}
${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${generator.hasComponents && `this._lock = false;`}
`}
}
`}
`;
if (generator.customElement) {
const props = generator.props || Array.from(generator.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')}
${generator.slots.size && deindent`
connectedCallback() {
Object.keys(this._slotted).forEach(key => {
this.appendChild(this._slotted[key]);
});
}`}
attributeChangedCallback(attr, oldValue, newValue) {
this.set({ [attr]: newValue });
}
}
customElements.define("${generator.tag}", ${name});
@assign(${prototypeBase}, ${proto}, {
_mount(target, anchor) {
target.insertBefore(this, anchor);
},
_unmount() {
this.parentNode.removeChild(this);
}
});
`);
} else {
builder.addBlock(deindent`
function ${name}(options) {
${constructorBody}
}
@assign(${prototypeBase}, ${proto});
`);
}
builder.addBlock(deindent`
${options.dev && deindent`
${name}.prototype._checkReadOnly = function _checkReadOnly(newState) {
${Array.from(generator.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;`}
`);
const usedHelpers = new Set();
let result = builder
.toString()
.replace(/(%+|@+)(\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`;
usedHelpers.add(name);
}
return generator.alias(name);
}
if (sigil === '%') {
return generator.templateVars.get(name);
}
return sigil.slice(1) + name;
});
let helpers;
if (sharedPath) {
if (format !== 'es' && format !== 'cjs') {
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
}
const used = Array.from(usedHelpers).sort();
helpers = used.map(name => {
const alias = generator.alias(name);
return { name, alias };
});
} else {
let inlineHelpers = '';
usedHelpers.forEach(key => {
const str = shared[key];
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;
usedHelpers.add(dependency);
const alias = generator.alias(dependency);
if (alias !== node.name)
code.overwrite(node.start, node.end, alias);
}
}
},
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
if (key === 'transitionManager') {
// special case
const global = `_svelteTransitionManager`;
inlineHelpers += `\n\nvar ${generator.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`;
} else {
const alias = generator.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 filename = options.filename && (
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename
);
return generator.generate(result, options, {
banner: `/* ${filename ? `${filename} ` : ``}generated by Svelte v${"__VERSION__"} */`,
sharedPath,
helpers,
name,
format,
});
}

@ -1,215 +0,0 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import { DomGenerator } from '../dom/index';
import Block from '../dom/Block';
import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';
import createDebuggingComment from '../../utils/createDebuggingComment';
export default class AwaitBlock extends Node {
value: string;
error: string;
expression: Node;
pending: PendingBlock;
then: ThenBlock;
catch: CatchBlock;
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('await_block');
block.addDependencies(this.metadata.dependencies);
let dynamic = false;
[
['pending', null],
['then', this.value],
['catch', this.error]
].forEach(([status, arg]) => {
const child = this[status];
const context = block.getUniqueName(arg || '_'); // TODO can we remove the extra param from pending blocks?
const contexts = new Map(block.contexts);
contexts.set(arg, context);
const contextTypes = new Map(block.contextTypes);
contextTypes.set(arg, status);
child.block = block.child({
comment: createDebuggingComment(child, this.generator),
name: this.generator.getUniqueName(`create_${status}_block`),
params: block.params.concat(context),
context,
contexts,
contextTypes
});
child.initChildren(child.block, stripWhitespace, nextSibling);
this.generator.blocks.push(child.block);
if (child.block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(child.block.dependencies);
}
});
this.pending.block.hasUpdateMethod = dynamic;
this.then.block.hasUpdateMethod = dynamic;
this.catch.block.hasUpdateMethod = dynamic;
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const name = this.var;
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor);
const params = block.params.join(', ');
block.contextualise(this.expression);
const { snippet } = this.metadata;
const promise = block.getUniqueName(`promise`);
const resolved = block.getUniqueName(`resolved`);
const await_block = block.getUniqueName(`await_block`);
const await_block_type = block.getUniqueName(`await_block_type`);
const token = block.getUniqueName(`token`);
const await_token = block.getUniqueName(`await_token`);
const handle_promise = block.getUniqueName(`handle_promise`);
const replace_await_block = block.getUniqueName(`replace_await_block`);
const old_block = block.getUniqueName(`old_block`);
const value = block.getUniqueName(`value`);
const error = block.getUniqueName(`error`);
const create_pending_block = this.pending.block.name;
const create_then_block = this.then.block.name;
const create_catch_block = this.catch.block.name;
block.addVariable(await_block);
block.addVariable(await_block_type);
block.addVariable(await_token);
block.addVariable(promise);
block.addVariable(resolved);
// the `#component.root.set({})` below is just a cheap way to flush
// any oncreate handlers. We could have a dedicated `flush()` method
// but it's probably not worth it
block.builders.init.addBlock(deindent`
function ${replace_await_block}(${token}, type, ${value}, ${params}) {
if (${token} !== ${await_token}) return;
var ${old_block} = ${await_block};
${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component);
if (${old_block}) {
${old_block}.u();
${old_block}.d();
${await_block}.c();
${await_block}.m(${updateMountNode}, ${anchor});
#component.root.set({});
}
}
function ${handle_promise}(${promise}, ${params}) {
var ${token} = ${await_token} = {};
if (@isPromise(${promise})) {
${promise}.then(function(${value}) {
var state = #component.get();
${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params});
}, function (${error}) {
var state = #component.get();
${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params});
});
// if we previously had a then/catch block, destroy it
if (${await_block_type} !== ${create_pending_block}) {
${replace_await_block}(${token}, ${create_pending_block}, null, ${params});
return true;
}
} else {
${resolved} = ${promise};
if (${await_block_type} !== ${create_then_block}) {
${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params});
return true;
}
}
}
${handle_promise}(${promise} = ${snippet}, ${params});
`);
block.builders.create.addBlock(deindent`
${await_block}.c();
`);
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${await_block}.l(${parentNodes});
`);
}
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addBlock(deindent`
${await_block}.m(${initialMountNode}, ${anchorNode});
`);
const conditions = [];
if (this.metadata.dependencies) {
conditions.push(
`(${this.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
);
}
conditions.push(
`${promise} !== (${promise} = ${snippet})`,
`${handle_promise}(${promise}, ${params})`
);
if (this.pending.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
// nothing
} else {
${await_block}.p(changed, ${params}, ${resolved});
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
${await_block}.c();
${await_block}.m(${anchor}.parentNode, ${anchor});
}
`);
}
block.builders.unmount.addBlock(deindent`
${await_block}.u();
`);
block.builders.destroy.addBlock(deindent`
${await_token} = null;
${await_block}.d();
`);
[this.pending, this.then, this.catch].forEach(status => {
status.children.forEach(child => {
child.build(status.block, null,'nodes');
});
});
}
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class CatchBlock extends Node {
block: Block;
children: Node[];
}

@ -1,5 +0,0 @@
import Node from './shared/Node';
export default class Comment extends Node {
type: 'Comment'
}

@ -1,609 +0,0 @@
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import stringifyProps from '../../utils/stringifyProps';
import CodeBuilder from '../../utils/CodeBuilder';
import getTailSnippet from '../../utils/getTailSnippet';
import getObject from '../../utils/getObject';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
export default class Component extends Node {
type: 'Component';
name: string;
attributes: Attribute[];
children: Node[];
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
attribute.value.forEach((chunk: Node) => {
if (chunk.type !== 'Text') {
const dependencies = chunk.metadata.dependencies;
block.addDependencies(dependencies);
}
});
} else {
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding') {
block.addDependencies(attribute.metadata.dependencies);
}
}
});
this.var = block.getUniqueName(
(
this.name === ':Self' ? this.generator.name :
this.name === ':Component' ? 'switch_instance' :
this.name
).toLowerCase()
);
if (this.children.length) {
this._slots = new Set(['default']);
this.children.forEach(child => {
child.init(block, stripWhitespace, nextSibling);
});
}
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { generator } = this;
generator.hasComponents = true;
const name = this.var;
const componentInitProperties = [`root: #component.root`];
if (this.children.length > 0) {
const slots = Array.from(this._slots).map(name => `${name}: @createFragment()`);
componentInitProperties.push(`slots: { ${slots.join(', ')} }`);
this.children.forEach((child: Node) => {
child.build(block, `${this.var}._slotted.default`, 'nodes');
});
}
const allContexts = new Set();
const statements: string[] = [];
const name_context = block.getUniqueName(`${name}_context`);
let name_updating: string;
let name_initial_data: string;
let beforecreate: string = null;
const attributes = this.attributes
.filter(a => a.type === 'Attribute')
.map(a => mungeAttribute(a, block));
const bindings = this.attributes
.filter(a => a.type === 'Binding')
.map(a => mungeBinding(a, block));
const eventHandlers = this.attributes
.filter((a: Node) => a.type === 'EventHandler')
.map(a => mungeEventHandler(generator, this, a, block, name_context, allContexts));
const ref = this.attributes.find((a: Node) => a.type === 'Ref');
if (ref) generator.usesRefs = true;
const updates: string[] = [];
if (attributes.length || bindings.length) {
const initialProps = attributes
.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`);
const initialPropString = stringifyProps(initialProps);
attributes
.filter((attribute: Attribute) => attribute.dynamic)
.forEach((attribute: Attribute) => {
if (attribute.dependencies.length) {
updates.push(deindent`
if (${attribute.dependencies
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name}_changes.${attribute.name} = ${attribute.value};
`);
}
else {
// TODO this is an odd situation to encounter I *think* it should only happen with
// each block indices, in which case it may be possible to optimise this
updates.push(`${name}_changes.${attribute.name} = ${attribute.value};`);
}
});
if (bindings.length) {
generator.hasComplexBindings = true;
name_updating = block.alias(`${name}_updating`);
name_initial_data = block.getUniqueName(`${name}_initial_data`);
block.addVariable(name_updating, '{}');
statements.push(`var ${name_initial_data} = ${initialPropString};`);
const setParentFromChildOnChange = new CodeBuilder();
const setParentFromChildOnInit = new CodeBuilder();
bindings.forEach((binding: Binding) => {
let setParentFromChild;
binding.contexts.forEach(context => {
allContexts.add(context);
});
const { name: key } = getObject(binding.value);
if (block.contexts.has(key)) {
const prop = binding.dependencies[0];
const computed = isComputed(binding.value);
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
setParentFromChild = deindent`
var list = ${name_context}.${block.listNames.get(key)};
var index = ${name_context}.${block.indexNames.get(key)};
list[index]${tail} = childState.${binding.name};
${binding.dependencies
.map((prop: string) => `newState.${prop} = state.${prop};`)
.join('\n')}
`;
}
else if (binding.value.type === 'MemberExpression') {
setParentFromChild = deindent`
${binding.snippet} = childState.${binding.name};
${binding.dependencies.map((prop: string) => `newState.${prop} = state.${prop};`).join('\n')}
`;
}
else {
setParentFromChild = `newState.${binding.value.name} = childState.${binding.name};`;
}
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}`
);
setParentFromChildOnChange.addConditional(
`!${name_updating}.${binding.name} && changed.${binding.name}`,
setParentFromChild
);
setParentFromChildOnInit.addConditional(
`!${name_updating}.${binding.name}`,
setParentFromChild
);
// TODO could binding.dependencies.length ever be 0?
if (binding.dependencies.length) {
updates.push(deindent`
if (!${name_updating}.${binding.name} && ${binding.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name}_changes.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}
`);
}
});
componentInitProperties.push(`data: ${name_initial_data}`);
componentInitProperties.push(deindent`
_bind: function(changed, childState) {
var state = #component.get(), newState = {};
${setParentFromChildOnChange}
${name_updating} = @assign({}, changed);
#component._set(newState);
${name_updating} = {};
}
`);
beforecreate = deindent`
#component.root._beforecreate.push(function() {
var state = #component.get(), childState = ${name}.get(), newState = {};
if (!childState) return;
${setParentFromChildOnInit}
${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} };
#component._set(newState);
${name_updating} = {};
});
`;
} else if (initialProps.length) {
componentInitProperties.push(`data: ${initialPropString}`);
}
}
const isDynamicComponent = this.name === ':Component';
const switch_vars = isDynamicComponent && {
value: block.getUniqueName('switch_value'),
props: block.getUniqueName('switch_props')
};
const expression = (
this.name === ':Self' ? generator.name :
isDynamicComponent ? switch_vars.value :
`%components-${this.name}`
);
if (isDynamicComponent) {
block.contextualise(this.expression);
const { dependencies, snippet } = this.metadata;
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const params = block.params.join(', ');
block.builders.init.addBlock(deindent`
var ${switch_vars.value} = ${snippet};
function ${switch_vars.props}(${params}) {
${statements.length > 0 && statements.join('\n')}
return {
${componentInitProperties.join(',\n')}
};
}
if (${switch_vars.value}) {
var ${name} = new ${expression}(${switch_vars.props}(${params}));
${beforecreate}
}
${eventHandlers.map(handler => deindent`
function ${handler.var}(event) {
${handler.body}
}
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.addLine(
`if (${name}) ${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
block.builders.update.addBlock(deindent`
if (${switch_vars.value} !== (${switch_vars.value} = ${snippet})) {
if (${name}) ${name}.destroy();
if (${switch_vars.value}) {
${name} = new ${switch_vars.value}(${switch_vars.props}(${params}));
${name}._fragment.c();
${this.children.map(child => remount(generator, child, name))}
${name}._mount(${updateMountNode}, ${anchor});
${eventHandlers.map(handler => deindent`
${name}.on("${handler.name}", ${handler.var});
`)}
${ref && `#component.refs.${ref.name} = ${name};`}
}
${ref && deindent`
else if (#component.refs.${ref.name} === ${name}) {
#component.refs.${ref.name} = null;
}`}
}
`);
if (updates.length) {
block.builders.update.addBlock(deindent`
else {
var ${name}_changes = {};
${updates.join('\n')}
${name}._set(${name}_changes);
${bindings.length && `${name_updating} = {};`}
}
`);
}
if (!parentNode) block.builders.unmount.addLine(`if (${name}) ${name}._unmount();`);
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`);
} else {
block.builders.init.addBlock(deindent`
${statements.join('\n')}
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
});
${beforecreate}
${eventHandlers.map(handler => deindent`
${name}.on("${handler.name}", function(event) {
${handler.body}
});
`)}
${ref && `#component.refs.${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`
var ${name}_changes = {};
${updates.join('\n')}
${name}._set(${name}_changes);
${bindings.length && `${name_updating} = {};`}
`);
}
if (!parentNode) block.builders.unmount.addLine(`${name}._unmount();`);
block.builders.destroy.addLine(deindent`
${name}.destroy(false);
${ref && `if (#component.refs.${ref.name} === ${name}) #component.refs.${ref.name} = null;`}
`);
}
// maintain component context
if (allContexts.size) {
const contexts = Array.from(allContexts);
const initialProps = contexts
.map(contextName => {
if (contextName === 'state') return `state: state`;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
return `${listName}: ${listName},\n${indexName}: ${indexName}`;
})
.join(',\n');
const updates = contexts
.map(contextName => {
if (contextName === 'state') return `${name_context}.state = state;`;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
return `${name_context}.${listName} = ${listName};\n${name_context}.${indexName} = ${indexName};`;
})
.join('\n');
block.builders.init.addBlock(deindent`
var ${name_context} = {
${initialProps}
};
`);
block.builders.update.addBlock(updates);
}
}
}
function mungeAttribute(attribute: Node, block: Block): Attribute {
if (attribute.value === true) {
// attributes without values, e.g. <textarea readonly>
return {
name: attribute.name,
value: true,
dynamic: false
};
}
if (attribute.value.length === 0) {
return {
name: attribute.name,
value: `''`,
dynamic: false
};
}
if (attribute.value.length === 1) {
const value = attribute.value[0];
if (value.type === 'Text') {
// static attributes
return {
name: attribute.name,
value: isNaN(value.data) ? stringify(value.data) : value.data,
dynamic: false
};
}
// simple dynamic attributes
block.contextualise(value.expression); // TODO remove
const { dependencies, snippet } = value.metadata;
// TODO only update attributes that have changed
return {
name: attribute.name,
value: snippet,
dependencies,
dynamic: true
};
}
// otherwise we're dealing with a complex dynamic attribute
const allDependencies = new Set();
const value =
(attribute.value[0].type === 'Text' ? '' : `"" + `) +
attribute.value
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
block.contextualise(chunk.expression); // TODO remove
const { dependencies, snippet } = chunk.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
return {
name: attribute.name,
value,
dependencies: Array.from(allDependencies),
dynamic: true
};
}
function mungeBinding(binding: Node, block: Block): Binding {
const { name } = getObject(binding.value);
const { contexts } = block.contextualise(binding.value);
const { dependencies, snippet } = binding.metadata;
const contextual = block.contexts.has(name);
let obj;
let prop;
if (contextual) {
obj = block.listNames.get(name);
prop = block.indexNames.get(name);
} else if (binding.value.type === 'MemberExpression') {
prop = `[✂${binding.value.property.start}-${binding.value.property.end}✂]`;
if (!binding.value.computed) prop = `'${prop}'`;
obj = `[✂${binding.value.object.start}-${binding.value.object.end}✂]`;
} else {
obj = 'state';
prop = `'${name}'`;
}
return {
name: binding.name,
value: binding.value,
contexts,
snippet,
obj,
prop,
dependencies
};
}
function mungeEventHandler(generator: DomGenerator, node: Node, handler: Node, block: Block, name_context: string, allContexts: Set<string>) {
let body;
if (handler.expression) {
generator.addSourcemapLocations(handler.expression);
generator.code.prependRight(
handler.expression.start,
`${block.alias('component')}.`
);
const usedContexts: string[] = [];
handler.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allContexts.add(context);
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map(name => {
if (name === 'state') return `var state = ${name_context}.state;`;
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
return `var ${listName} = ${name_context}.${listName}, ${indexName} = ${name_context}.${indexName}, ${name} = ${listName}[${indexName}]`;
});
body = deindent`
${declarations}
[${handler.expression.start}-${handler.expression.end}];
`;
} else {
body = deindent`
${block.alias('component')}.fire('${handler.name}', event);
`;
}
return {
name: handler.name,
var: block.getUniqueName(`${node.var}_${handler.name}`),
body
};
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}
function remount(generator: DomGenerator, node: Node, name: string) {
// TODO make this a method of the nodes
if (node.type === 'Component') {
return `${node.var}._mount(${name}._slotted.default, null);`;
}
if (node.type === 'Element') {
const slot = node.attributes.find(attribute => attribute.name === 'slot');
if (slot) {
return `@appendNode(${node.var}, ${name}._slotted.${node.getStaticAttributeValue('slot')});`;
}
return `@appendNode(${node.var}, ${name}._slotted.default);`;
}
if (node.type === 'Text' || node.type === 'MustacheTag' || node.type === 'RawMustacheTag') {
return `@appendNode(${node.var}, ${name}._slotted.default);`;
}
if (node.type === 'EachBlock') {
// TODO consider keyed blocks
return `for (var #i = 0; #i < ${node.iterations}.length; #i += 1) ${node.iterations}[#i].m(${name}._slotted.default, null);`;
}
return `${node.var}.m(${name}._slotted.default, null);`;
}

@ -1,588 +0,0 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import { DomGenerator } from '../dom/index';
import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
export default class EachBlock extends Node {
type: 'EachBlock';
block: Block;
expression: Node;
iterations: string;
index: string;
context: string;
key: string;
destructuredContexts: string[];
children: Node[];
else?: ElseBlock;
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName(`each`);
this.iterations = block.getUniqueName(`${this.var}_blocks`);
const { dependencies } = this.metadata;
block.addDependencies(dependencies);
const indexNames = new Map(block.indexNames);
const indexName =
this.index || block.getUniqueName(`${this.context}_index`);
indexNames.set(this.context, indexName);
const listNames = new Map(block.listNames);
const listName = block.getUniqueName(
(this.expression.type === 'MemberExpression' && !this.expression.computed) ? this.expression.property.name :
this.expression.type === 'Identifier' ? this.expression.name :
`each_value`
);
listNames.set(this.context, listName);
const contextTypes = new Map(block.contextTypes);
contextTypes.set(this.context, 'each');
const context = block.getUniqueName(this.context);
const contexts = new Map(block.contexts);
contexts.set(this.context, context);
const indexes = new Map(block.indexes);
if (this.index) indexes.set(this.index, this.context);
const changeableIndexes = new Map(block.changeableIndexes);
if (this.index) changeableIndexes.set(this.index, this.key);
if (this.destructuredContexts) {
for (let i = 0; i < this.destructuredContexts.length; i += 1) {
contexts.set(this.destructuredContexts[i], `${context}[${i}]`);
}
}
this.block = block.child({
comment: createDebuggingComment(this, this.generator),
name: this.generator.getUniqueName('create_each_block'),
context: this.context,
key: this.key,
contexts,
contextTypes,
indexes,
changeableIndexes,
listName,
indexName,
indexNames,
listNames,
params: block.params.concat(listName, context, indexName),
});
this.generator.blocks.push(this.block);
this.initChildren(this.block, stripWhitespace, nextSibling);
block.addDependencies(this.block.dependencies);
this.block.hasUpdateMethod = this.block.dependencies.size > 0;
if (this.else) {
this.else.block = block.child({
comment: createDebuggingComment(this.else, this.generator),
name: this.generator.getUniqueName(`${this.block.name}_else`),
});
this.generator.blocks.push(this.else.block);
this.else.initChildren(
this.else.block,
stripWhitespace,
nextSibling
);
this.else.block.hasUpdateMethod = this.else.block.dependencies.size > 0;
}
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { generator } = this;
const each = this.var;
const create_each_block = this.block.name;
const each_block_value = this.block.listName;
const iterations = this.iterations;
const params = block.params.join(', ');
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${each}_anchor`)
: (this.next && this.next.var) || 'null';
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = this.start + 3;
while (generator.source[c] !== 'e') c += 1;
generator.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
const vars = {
each,
create_each_block,
each_block_value,
length,
iterations,
params,
anchor,
mountOrIntro,
};
block.contextualise(this.expression);
const { snippet } = this.metadata;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
if (this.key) {
this.buildKeyed(block, parentNode, parentNodes, snippet, vars);
} else {
this.buildUnkeyed(block, parentNode, parentNodes, snippet, vars);
}
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
if (this.else) {
const each_block_else = generator.getUniqueName(`${each}_else`);
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(${params}, #component);
${each_block_else}.c();
}
`);
block.builders.mount.addBlock(deindent`
if (${each_block_else}) {
${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null);
}
`);
const initialMountNode = parentNode || `${anchor}.parentNode`;
if (this.else.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (!${each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p( changed, ${params} );
} else if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(${params}, #component);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
} else if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
${each_block_else} = null;
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${each_block_value}.${length}) {
if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
${each_block_else} = null;
}
} else if (!${each_block_else}) {
${each_block_else} = ${this.else.block.name}(${params}, #component);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
}
`);
}
block.builders.unmount.addLine(
`if (${each_block_else}) ${each_block_else}.u()`
);
block.builders.destroy.addBlock(deindent`
if (${each_block_else}) ${each_block_else}.d();
`);
}
this.children.forEach((child: Node) => {
child.build(this.block, null, 'nodes');
});
if (this.else) {
this.else.children.forEach((child: Node) => {
child.build(this.else.block, null, 'nodes');
});
}
}
buildKeyed(
block: Block,
parentNode: string,
parentNodes: string,
snippet: string,
{
each,
create_each_block,
each_block_value,
length,
params,
anchor,
mountOrIntro,
}
) {
const key = block.getUniqueName('key');
const lookup = block.getUniqueName(`${each}_lookup`);
const iteration = block.getUniqueName(`${each}_iteration`);
const head = block.getUniqueName(`${each}_head`);
const last = block.getUniqueName(`${each}_last`);
const expected = block.getUniqueName(`${each}_expected`);
block.addVariable(lookup, `@blankObject()`);
block.addVariable(head);
block.addVariable(last);
if (this.children[0].isDomNode()) {
this.block.first = this.children[0].var;
} else {
this.block.first = this.block.getUniqueName('first');
this.block.addElement(
this.block.first,
`@createComment()`,
parentNodes && `@createComment()`,
null
);
}
block.builders.init.addBlock(deindent`
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${this.key};
var ${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
if (${last}) ${last}.next = ${iteration};
${iteration}.last = ${last};
${last} = ${iteration};
if (#i === 0) ${head} = ${iteration};
}
`);
const initialMountNode = parentNode || '#target';
const updateMountNode = this.getUpdateMountNode(anchor);
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.c();
${iteration} = ${iteration}.next;
}
`);
if (parentNodes) {
block.builders.claim.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.l(${parentNodes});
${iteration} = ${iteration}.next;
}
`);
}
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.${mountOrIntro}(${initialMountNode}, ${anchorNode});
${iteration} = ${iteration}.next;
}
`);
const dynamic = this.block.hasUpdateMethod;
let destroy;
if (this.block.hasOutroMethod) {
const fn = block.getUniqueName(`${each}_outro`);
block.builders.init.addBlock(deindent`
function ${fn}(iteration) {
iteration.o(function() {
iteration.u();
iteration.d();
${lookup}[iteration.key] = null;
});
}
`);
destroy = deindent`
while (${expected}) {
${fn}(${expected});
${expected} = ${expected}.next;
}
for (#i = 0; #i < discard_pile.length; #i += 1) {
if (discard_pile[#i].discard) {
${fn}(discard_pile[#i]);
}
}
`;
} else {
const fn = block.getUniqueName(`${each}_destroy`);
block.builders.init.addBlock(deindent`
function ${fn}(iteration) {
iteration.u();
iteration.d();
${lookup}[iteration.key] = null;
}
`);
destroy = deindent`
while (${expected}) {
${fn}(${expected});
${expected} = ${expected}.next;
}
for (#i = 0; #i < discard_pile.length; #i += 1) {
var ${iteration} = discard_pile[#i];
if (${iteration}.discard) {
${fn}(${iteration});
}
}
`;
}
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
var ${expected} = ${head};
var ${last} = null;
var discard_pile = [];
for (#i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${this.key};
var ${iteration} = ${lookup}[${key}];
${dynamic &&
`if (${iteration}) ${iteration}.p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);`}
if (${expected}) {
if (${key} === ${expected}.key) {
${expected} = ${expected}.next;
} else {
if (${iteration}) {
// probably a deletion
while (${expected} && ${expected}.key !== ${key}) {
${expected}.discard = true;
discard_pile.push(${expected});
${expected} = ${expected}.next;
};
${expected} = ${expected} && ${expected}.next;
${iteration}.discard = false;
${iteration}.last = ${last};
if (!${expected}) ${iteration}.m(${updateMountNode}, ${anchor});
} else {
// key is being inserted
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
${iteration}.c();
${iteration}.${mountOrIntro}(${updateMountNode}, ${expected}.first);
${expected}.last = ${iteration};
${iteration}.next = ${expected};
}
}
} else {
// we're appending from this point forward
if (${iteration}) {
${iteration}.discard = false;
${iteration}.next = null;
${iteration}.m(${updateMountNode}, ${anchor});
} else {
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
${iteration}.c();
${iteration}.${mountOrIntro}(${updateMountNode}, ${anchor});
}
}
if (${last}) ${last}.next = ${iteration};
${iteration}.last = ${last};
${this.block.hasIntroMethod && `${iteration}.i(${updateMountNode}, ${anchor});`}
${last} = ${iteration};
}
if (${last}) ${last}.next = null;
${destroy}
${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${this.key}];
`);
if (!parentNode) {
block.builders.unmount.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.u();
${iteration} = ${iteration}.next;
}
`);
}
block.builders.destroy.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.d();
${iteration} = ${iteration}.next;
}
`);
}
buildUnkeyed(
block: Block,
parentNode: string,
parentNodes: string,
snippet: string,
{
create_each_block,
each_block_value,
length,
iterations,
params,
anchor,
mountOrIntro,
}
) {
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
}
`);
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.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
// TODO do this for keyed blocks as well
const condition = Array.from(allDependencies)
.map(dependency => `changed.${dependency}`)
.join(' || ');
if (condition !== '') {
const forLoopBody = this.block.hasUpdateMethod
? this.block.hasIntroMethod
? deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
} else {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
}
${iterations}[#i].i(${updateMountNode}, ${anchor});
`
: deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
} else {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
${iterations}[#i].m(${updateMountNode}, ${anchor});
}
`
: deindent`
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor});
`;
const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`;
const outro = block.getUniqueName('outro');
const destroy = this.block.hasOutroMethod
? deindent`
function ${outro}(i) {
if (${iterations}[i]) {
${iterations}[i].o(function() {
${iterations}[i].u();
${iterations}[i].d();
${iterations}[i] = null;
});
}
}
for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
`
: deindent`
for (; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].u();
${iterations}[#i].d();
}
${iterations}.length = ${each_block_value}.${length};
`;
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
if (${condition}) {
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
${forLoopBody}
}
${destroy}
}
`);
}
block.builders.unmount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].u();
}
`);
block.builders.destroy.addBlock(`@destroyEach(${iterations});`);
}
}

@ -1,770 +0,0 @@
import deindent from '../../utils/deindent';
import { stringify, escapeHTML } from '../../utils/stringify';
import flattenReference from '../../utils/flattenReference';
import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Ref from './Ref';
import Transition from './Transition';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
export default class Element extends Node {
type: 'Element';
name: string;
attributes: (Attribute | Binding | EventHandler | Ref | Transition)[]; // TODO split these up sooner
children: Node[];
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
if (this.name === 'slot' || this.name === 'option') {
this.cannotUseInnerHTML();
}
const parentElement = this.parent && this.parent.findNearest(/^Element/);
this.namespace = this.name === 'svg' ?
namespaces.svg :
parentElement ? parentElement.namespace : this.generator.namespace;
this.attributes.forEach(attribute => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
// special case — xmlns
if (attribute.name === 'xmlns') {
// TODO this attribute must be static enforce at compile time
this.namespace = attribute.value[0].data;
}
attribute.value.forEach((chunk: Node) => {
if (chunk.type !== 'Text') {
if (this.parent) this.parent.cannotUseInnerHTML();
const dependencies = chunk.metadata.dependencies;
block.addDependencies(dependencies);
// special case — <option value='{{foo}}'> — see below
if (
this.name === 'option' &&
attribute.name === 'value'
) {
let select = this.parent;
while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
if (select && select.selectBindingDependencies) {
select.selectBindingDependencies.forEach(prop => {
dependencies.forEach((dependency: string) => {
this.generator.indirectDependencies.get(prop).add(dependency);
});
});
}
}
}
});
} else {
if (this.parent) this.parent.cannotUseInnerHTML();
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding') {
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
this.generator.hasIntroTransitions = block.hasIntroMethod = true;
if (attribute.outro) {
this.generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
}
}
});
const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value');
if (this.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if (this.children.length > 0) {
this.attributes.push(new Attribute({
generator: this.generator,
name: 'value',
value: this.children,
parent: this
}));
this.children = [];
}
}
// special case — in a case like this...
//
// <select bind:value='foo'>
// <option value='{{bar}}'>bar</option>
// <option value='{{baz}}'>baz</option>
// </option>
//
// ...we need to know that `foo` depends on `bar` and `baz`,
// so that if `foo.qux` changes, we know that we need to
// mark `bar` and `baz` as dirty too
if (this.name === 'select') {
const binding = this.attributes.find(node => node.type === 'Binding' && node.name === 'value');
if (binding) {
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
const dependencies = binding.metadata.dependencies;
this.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
this.generator.indirectDependencies.set(prop, new Set());
});
} else {
this.selectBindingDependencies = null;
}
}
const slot = this.getStaticAttributeValue('slot');
if (slot && this.hasAncestor('Component')) {
this.cannotUseInnerHTML();
this.slotted = true;
// TODO validate slots — no nesting, no dynamic names...
const component = this.findNearest(/^Component/);
component._slots.add(slot);
}
this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
this.generator.stylesheet.apply(this);
if (this.children.length) {
if (this.name === 'pre' || this.name === 'textarea') stripWhitespace = false;
this.initChildren(block, stripWhitespace, nextSibling);
}
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { generator } = this;
if (this.name === 'slot') {
const slotName = this.getStaticAttributeValue('name') || 'default';
this.generator.slots.add(slotName);
}
const childState = {
parentNode: this.var,
parentNodes: parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parentNodes is null
};
const name = this.var;
const allUsedContexts: Set<string> = new Set();
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
const initialMountNode = this.slotted ?
`${this.findNearest(/^Component/).var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
parentNode;
block.addVariable(name);
const renderStatement = getRenderStatement(this.generator, this.namespace, this.name);
block.builders.create.addLine(
`${name} = ${renderStatement};`
);
if (this.generator.hydratable) {
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(generator, this.namespace, parentNodes, this)};
var ${childState.parentNodes} = @children(${name});
`);
} else {
block.builders.claim.addLine(
`${name} = ${renderStatement};`
);
}
}
if (initialMountNode) {
block.builders.mount.addLine(
`@appendNode(${name}, ${initialMountNode});`
);
if (initialMountNode === 'document.head') {
block.builders.unmount.addLine(`@detachNode(${name});`);
}
} else {
block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`);
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine(`@detachNode(${name});`);
}
// add CSS encapsulation attribute
if (this._needsCssAttribute && !this.generator.customElement) {
this.generator.needsEncapsulateHelper = true;
block.builders.hydrate.addLine(
`@encapsulateStyles(${name});`
);
if (this._cssRefAttribute) {
block.builders.hydrate.addLine(
`@setAttribute(${name}, "svelte-ref-${this._cssRefAttribute}", "");`
)
}
}
// insert static children with textContent or innerHTML
if (!this.namespace && this.canUseInnerHTML && this.children.length > 0) {
if (this.children.length === 1 && this.children[0].type === 'Text') {
block.builders.create.addLine(
`${name}.textContent = ${stringify(this.children[0].data)};`
);
} else {
block.builders.create.addLine(
`${name}.innerHTML = ${stringify(this.children.map(toHTML).join(''))};`
);
}
} else {
this.children.forEach((child: Node) => {
child.build(block, childState.parentNode, childState.parentNodes);
});
}
this.addBindings(block, allUsedContexts);
this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => {
attribute.render(block);
});
// event handlers
let eventHandlerUsesComponent = false;
this.attributes.filter((a: Node) => a.type === 'EventHandler').forEach((attribute: Node) => {
const isCustomEvent = generator.events.has(attribute.name);
const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');
const context = shouldHoist ? null : name;
const usedContexts: string[] = [];
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
}
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allUsedContexts.add(context);
});
});
}
const ctx = context || 'this';
const declarations = usedContexts
.map(name => {
if (name === 'state') {
if (shouldHoist) eventHandlerUsesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
const contextType = block.contextTypes.get(name);
if (contextType === 'each') {
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${ctx}._svelte.${listName}, ${indexName} = ${ctx}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
}
})
.filter(Boolean);
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body
const handlerBody = deindent`
${eventHandlerUsesComponent &&
`var ${block.alias('component')} = ${ctx}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, ${name}, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
} else {
block.builders.init.addBlock(handler);
}
block.builders.hydrate.addLine(
`@addListener(${name}, "${attribute.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${name}, "${attribute.name}", ${handlerName});`
);
}
});
// refs
this.attributes.filter((a: Node) => a.type === 'Ref').forEach((attribute: Node) => {
const ref = `#component.refs.${attribute.name}`;
block.builders.mount.addLine(
`${ref} = ${name};`
);
block.builders.destroy.addLine(
`if (${ref} === ${name}) ${ref} = null;`
);
generator.usesRefs = true; // so component.refs object is created
});
this.addTransitions(block);
if (allUsedContexts.size || eventHandlerUsesComponent) {
const initialProps: string[] = [];
const updates: string[] = [];
if (eventHandlerUsesComponent) {
initialProps.push(`component: #component`);
}
allUsedContexts.forEach((contextName: string) => {
if (contextName === 'state') return;
if (block.contextTypes.get(contextName) !== 'each') return;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
initialProps.push(
`${listName}: ${listName},\n${indexName}: ${indexName}`
);
updates.push(
`${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};`
);
});
if (initialProps.length) {
block.builders.hydrate.addBlock(deindent`
${name}._svelte = {
${initialProps.join(',\n')}
};
`);
}
if (updates.length) {
block.builders.update.addBlock(updates.join('\n'));
}
}
if (this.initialUpdate) {
block.builders.mount.addBlock(this.initialUpdate);
}
if (childState.parentNodes) {
block.builders.claim.addLine(
`${childState.parentNodes}.forEach(@detachNode);`
);
}
function toHTML(node: Element | Text) {
if (node.type === 'Text') return escapeHTML(node.data);
let open = `<${node.name}`;
if (node._needsCssAttribute) {
open += ` ${generator.stylesheet.id}`;
}
if (node._cssRefAttribute) {
open += ` svelte-ref-${node._cssRefAttribute}`;
}
node.attributes.forEach((attr: Node) => {
open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.value)}`
});
if (isVoidElementName(node.name)) return open + '>';
if (node.name === 'script' || node.name === 'style') {
return `${open}>${node.data}</${node.name}>`;
}
return `${open}>${node.children.map(toHTML).join('')}</${node.name}>`;
}
}
addBindings(
block: Block,
allUsedContexts: Set<string>
) {
const bindings: Binding[] = this.attributes.filter((a: Binding) => a.type === 'Binding');
if (bindings.length === 0) return;
if (this.name === 'select' || this.isMediaNode()) this.generator.hasComplexBindings = true;
const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
const mungedBindings = bindings.map(binding => binding.munge(block, allUsedContexts));
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, 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 usesContext = group.bindings.some(binding => binding.handler.usesContext);
const usesState = group.bindings.some(binding => binding.handler.usesState);
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});`
}
${usesContext && `var context = ${this.var}._svelte;`}
${usesState && `var state = #component.get();`}
${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 => {
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 state`)
.join(' && ');
if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
this.generator.hasComplexBindings = true;
block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
);
}
});
this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
}
addTransitions(
block: Block
) {
const intro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.intro);
const outro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.outro);
if (!intro && !outro) return;
if (intro === outro) {
block.contextualise(intro.expression); // TODO remove all these
const name = block.getUniqueName(`${this.var}_transition`);
const snippet = intro.expression
? intro.metadata.snippet
: '{}';
block.addVariable(name);
const fn = `%transitions-${intro.name}`;
block.builders.intro.addBlock(deindent`
#component.root._aftercreate.push(function() {
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true, null);
${name}.run(true, function() {
#component.fire("intro.end", { node: ${this.var} });
});
});
`);
block.builders.outro.addBlock(deindent`
${name}.run(false, function() {
#component.fire("outro.end", { node: ${this.var} });
if (--#outros === 0) #outrocallback();
${name} = null;
});
`);
} else {
const introName = intro && block.getUniqueName(`${this.var}_intro`);
const outroName = outro && block.getUniqueName(`${this.var}_outro`);
if (intro) {
block.contextualise(intro.expression);
block.addVariable(introName);
const snippet = intro.expression
? intro.metadata.snippet
: '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
if (outro) {
block.builders.intro.addBlock(deindent`
if (${introName}) ${introName}.abort();
if (${outroName}) ${outroName}.abort();
`);
}
block.builders.intro.addBlock(deindent`
#component.root._aftercreate.push(function() {
${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true, null);
${introName}.run(true, function() {
#component.fire("intro.end", { node: ${this.var} });
});
});
`);
}
if (outro) {
block.contextualise(outro.expression);
block.addVariable(outroName);
const snippet = outro.expression
? outro.metadata.snippet
: '{}';
const fn = `%transitions-${outro.name}`;
// 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, null);
${outroName}.run(false, function() {
#component.fire("outro.end", { node: ${this.var} });
if (--#outros === 0) #outrocallback();
});
`);
}
}
}
getStaticAttributeValue(name: string) {
const attribute = this.attributes.find(
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
);
if (!attribute) return null;
if (attribute.value === true) return true;
if (attribute.value.length === 0) return '';
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') {
return attribute.value[0].data;
}
return null;
}
isMediaNode() {
return this.name === 'audio' || this.name === 'video';
}
}
function getRenderStatement(
generator: DomGenerator,
namespace: string,
name: string
) {
if (namespace === 'http://www.w3.org/2000/svg') {
return `@createSvgElement("${name}")`;
}
if (namespace) {
return `document.createElementNS("${namespace}", "${name}")`;
}
return `@createElement("${name}")`;
}
function getClaimStatement(
generator: DomGenerator,
namespace: string,
nodes: string,
node: Node
) {
const attributes = node.attributes
.filter((attr: Node) => attr.type === 'Attribute')
.map((attr: Node) => `${quoteProp(attr.name, generator.legacy)}: true`)
.join(', ');
const name = namespace ? node.name : node.name.toUpperCase();
return `@claimElement(${nodes}, "${name}", ${attributes
? `{ ${attributes} }`
: `{}`}, ${namespace === namespaces.svg ? true : false})`;
}
function quoteProp(name: string, legacy: boolean) {
const isLegacyPropName = legacy && reservedNames.has(name);
if (/[^a-zA-Z_$0-9]/.test(name) || isLegacyPropName) return `"${name}"`;
return name;
}
function stringifyAttributeValue(value: Node | true) {
if (value === true) return '';
if (value.length === 0) return `=""`;
const data = value[0].data;
return `=${JSON.stringify(data)}`;
}
const events = [
{
eventNames: ['input'],
filter: (node: Element, name: string) =>
node.name === 'textarea' ||
node.name === 'input' && !/radio|checkbox/.test(node.getStaticAttributeValue('type'))
},
{
eventNames: ['change'],
filter: (node: Element, name: string) =>
node.name === 'select' ||
node.name === 'input' && /radio|checkbox|range/.test(node.getStaticAttributeValue('type'))
},
// 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'
}
];

@ -1,8 +0,0 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class ElseBlock extends Node {
type: 'ElseBlock';
children: Node[];
block: Block;
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
export default class EventHandler extends Node {
name: string;
value: Node[]
expression: Node
}

@ -1,39 +0,0 @@
import Node from './shared/Node';
import { DomGenerator } from '../dom/index';
import Block from '../dom/Block';
export default class Fragment extends Node {
block: Block;
children: Node[];
init() {
this.block = new Block({
generator: this.generator,
name: '@create_main_fragment',
key: null,
contexts: new Map(),
indexes: new Map(),
changeableIndexes: new Map(),
params: ['state'],
indexNames: new Map(),
listNames: new Map(),
dependencies: new Set(),
});
this.generator.blocks.push(this.block);
this.initChildren(this.block, true, null);
this.block.hasUpdateMethod = true;
}
build() {
this.init();
this.children.forEach(child => {
child.build(this.block, null, 'nodes');
});
}
}

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

@ -1,7 +0,0 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class PendingBlock extends Node {
block: Block;
children: Node[];
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
export default class Ref extends Node {
name: string;
value: Node[]
expression: Node
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class ThenBlock extends Node {
block: Block;
children: Node[];
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
export default class Transition extends Node {
name: string;
value: Node[]
expression: Node
}

@ -1,213 +0,0 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import flattenReference from '../../utils/flattenReference';
import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
const associatedEvents = {
innerWidth: 'resize',
innerHeight: 'resize',
outerWidth: 'resize',
outerHeight: 'resize',
scrollX: 'scroll',
scrollY: 'scroll',
};
const readonly = new Set([
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'online',
]);
export default class Window extends Node {
type: 'Window';
attributes: Attribute[];
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const { generator } = this;
const events = {};
const bindings: Record<string, string> = {};
this.attributes.forEach((attribute: Node) => {
if (attribute.type === 'EventHandler') {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations(attribute.expression);
let usesState = false;
attribute.expression.arguments.forEach((arg: Node) => {
block.contextualise(arg, null, true);
const { dependencies } = arg.metadata;
if (dependencies.length) usesState = true;
});
const flattened = flattenReference(attribute.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') {
// allow event.stopPropagation(), this.select() etc
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
}
const handlerName = block.getUniqueName(`onwindow${attribute.name}`);
const handlerBody = deindent`
${usesState && `var state = #component.get();`}
[${attribute.expression.start}-${attribute.expression.end}];
`;
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${attribute.name}", ${handlerName});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${attribute.name}", ${handlerName});
`);
}
if (attribute.type === 'Binding') {
// in dev mode, throw if read-only values are written to
if (readonly.has(attribute.name)) {
generator.readonly.add(attribute.value.name);
}
bindings[attribute.name] = attribute.value.name;
// bind:online is a special case, we need to listen for two separate events
if (attribute.name === 'online') return;
const associatedEvent = associatedEvents[attribute.name];
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push(
`${attribute.value.name}: this.${attribute.name}`
);
// add initial value
generator.metaBindings.push(
`this._state.${attribute.value.name} = window.${attribute.name};`
);
}
});
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;
`}
${generator.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({
${props}
});
${generator.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) {
const observerCallback = block.getUniqueName(`scrollobserver`);
block.builders.init.addBlock(deindent`
function ${observerCallback}() {
${lock} = true;
clearTimeout(${timeout});
var x = ${bindings.scrollX
? `#component.get("${bindings.scrollX}")`
: `window.scrollX`};
var y = ${bindings.scrollY
? `#component.get("${bindings.scrollY}")`
: `window.scrollY`};
window.scrollTo(x, y);
${timeout} = setTimeout(${clear}, 100);
}
`);
if (bindings.scrollX)
block.builders.init.addLine(
`#component.observe("${bindings.scrollX}", ${observerCallback});`
);
if (bindings.scrollY)
block.builders.init.addLine(
`#component.observe("${bindings.scrollY}", ${observerCallback});`
);
} else if (bindings.scrollX || bindings.scrollY) {
const isX = !!bindings.scrollX;
block.builders.init.addBlock(deindent`
#component.observe("${bindings.scrollX || bindings.scrollY}", function(${isX ? 'x' : 'y'}) {
${lock} = true;
clearTimeout(${timeout});
window.scrollTo(${isX ? 'x, window.scrollY' : 'window.scrollX, y'});
${timeout} = setTimeout(${clear}, 100);
});
`);
}
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
#component.set({ ${bindings.online}: navigator.onLine });
}
window.addEventListener("online", ${handlerName});
window.addEventListener("offline", ${handlerName});
`);
// add initial value
generator.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;`
);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("online", ${handlerName});
window.removeEventListener("offline", ${handlerName});
`);
}
}
}

@ -1,52 +0,0 @@
import Node from './shared/Node';
import Attribute from './Attribute';
import AwaitBlock from './AwaitBlock';
import Binding from './Binding';
import CatchBlock from './CatchBlock';
import Comment from './Comment';
import Component from './Component';
import EachBlock from './EachBlock';
import Element from './Element';
import ElseBlock from './ElseBlock';
import EventHandler from './EventHandler';
import Fragment from './Fragment';
import Head from './Head';
import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import PendingBlock from './PendingBlock';
import RawMustacheTag from './RawMustacheTag';
import Ref from './Ref';
import Slot from './Slot';
import Text from './Text';
import ThenBlock from './ThenBlock';
import Title from './Title';
import Transition from './Transition';
import Window from './Window';
const nodes: Record<string, any> = {
Attribute,
AwaitBlock,
Binding,
CatchBlock,
Comment,
Component,
EachBlock,
Element,
ElseBlock,
EventHandler,
Fragment,
Head,
IfBlock,
MustacheTag,
PendingBlock,
RawMustacheTag,
Ref,
Slot,
Text,
ThenBlock,
Title,
Transition,
Window
};
export default nodes;

@ -1,45 +0,0 @@
import Node from './Node';
import Block from '../../dom/Block';
export default class Tag extends Node {
renameThisMethod(
block: Block,
update: ((value: string) => string)
) {
const { indexes } = block.contextualise(this.expression);
const { dependencies, snippet } = this.metadata;
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
const shouldCache = (
this.expression.type !== 'Identifier' ||
block.contexts.has(this.expression.name) ||
hasChangeableIndex
);
const value = shouldCache && block.getUniqueName(`${this.var}_value`);
const content = shouldCache ? value : snippet;
if (shouldCache) block.addVariable(value, snippet);
if (dependencies.length || hasChangeableIndex) {
const changedCheck = (
(block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
const condition = shouldCache ?
(dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
changedCheck;
block.builders.update.addConditional(
condition,
update(content)
);
}
return { init: content };
}
}

@ -1,49 +0,0 @@
import deindent from '../../utils/deindent';
import flattenReference from '../../utils/flattenReference';
import { SsrGenerator } from './index';
import { Node } from '../../interfaces';
import getObject from '../../utils/getObject';
interface BlockOptions {
// TODO
}
export default class Block {
generator: SsrGenerator;
conditions: string[];
contexts: Map<string, string>;
indexes: Map<string, string>;
contextDependencies: Map<string, string[]>;
constructor(options: BlockOptions) {
Object.assign(this, options);
}
addBinding(binding: Node, name: string) {
const conditions = [`!('${binding.name}' in state)`].concat(
// TODO handle contextual bindings...
this.conditions.map(c => `(${c})`)
);
const { name: prop } = getObject(binding.value);
this.generator.bindings.push(deindent`
if (${conditions.join('&&')}) {
tmp = ${name}.data();
if ('${prop}' in tmp) {
state.${binding.name} = tmp.${prop};
settled = false;
}
}
`);
}
child(options: BlockOptions) {
return new Block(Object.assign({}, this, options, { parent: this }));
}
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
}
}

@ -1,268 +0,0 @@
import deindent from '../../utils/deindent';
import Generator from '../Generator';
import Stylesheet from '../../css/Stylesheet';
import Block from './Block';
import visit from './visit';
import { removeNode, removeObjectKey } from '../../utils/removeNode';
import getName from '../../utils/getName';
import globalWhitelist from '../../utils/globalWhitelist';
import { Parsed, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from './interfaces';
import { stringify } from '../../utils/stringify';
export class SsrGenerator extends Generator {
bindings: string[];
renderCode: string;
appendTargets: AppendTarget[];
constructor(
parsed: Parsed,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
super(parsed, source, name, stylesheet, options, false);
this.bindings = [];
this.renderCode = '';
this.appendTargets = [];
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
}
append(code: string) {
if (this.appendTargets.length) {
const appendTarget = this.appendTargets[this.appendTargets.length - 1];
const slotName = appendTarget.slotStack[appendTarget.slotStack.length - 1];
appendTarget.slots[slotName] += code;
} else {
this.renderCode += code;
}
}
}
export default function ssr(
parsed: Parsed,
source: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
const format = options.format || 'cjs';
const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options);
const { computations, name, templateProperties } = generator;
// create main render() function
const mainBlock = new Block({
generator,
contexts: new Map(),
indexes: new Map(),
conditions: [],
});
trim(parsed.html.children).forEach((node: Node) => {
visit(generator, mainBlock, node);
});
const { css, cssMap } = generator.customElement ?
{ css: null, cssMap: null } :
generator.stylesheet.render(options.filename, true);
// generate initial state object
const expectedProperties = Array.from(generator.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = options.store || templateProperties.store ? 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)}"`)}])`
if (options.store || templateProperties.store) {
initialState.push(`options.store.${initialize}`);
}
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}
initialState.push('state');
// TODO concatenate CSS maps
const result = deindent`
${generator.javascript}
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, state, options) {
${templateProperties.store && `options.store = %store();`}
__result.addComponent(${name});
state = Object.assign(${initialState.join(', ')});
${computations.map(
({ key, deps }) =>
`state.${key} = %computed-${key}(${deps.map(dep => `state.${dep}`).join(', ')});`
)}
${generator.bindings.length &&
deindent`
var settled = false;
var tmp;
while (!settled) {
settled = true;
${generator.bindings.join('\n\n')}
}
`}
return \`${generator.renderCode}\`;
};
${name}.css = {
code: ${css ? stringify(css) : `''`},
map: ${cssMap ? stringify(cssMap.toString()) : 'null'}
};
var warned = false;
${name}.renderCss = function() {
if (!warned) {
console.error('Component.renderCss(...) is deprecated and will be removed in v2 — use Component.render(...).css instead');
warned = true;
}
var components = [];
${generator.stylesheet.hasStyles &&
deindent`
components.push({
filename: ${name}.filename,
css: ${name}.css && ${name}.css.code,
map: ${name}.css && ${name}.css.map
});
`}
${templateProperties.components &&
deindent`
var seen = {};
function addComponent(component) {
var result = component.renderCss();
result.components.forEach(x => {
if (seen[x.filename]) return;
seen[x.filename] = true;
components.push(x);
});
}
${templateProperties.components.value.properties.map((prop: Node) => {
return `addComponent(%components-${getName(prop.key)});`;
})}
`}
return {
css: components.map(x => x.css).join('\\n'),
map: null,
components
};
};
${templateProperties.preload && `${name}.preload = %preload;`}
${
// TODO this is a bit hacky
/__escape/.test(generator.renderCode) && deindent`
var escaped = {
'"': '&quot;',
"'": '&##39;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
function __escape(html) {
return String(html).replace(/["'&<>]/g, match => escaped[match]);
}
`
}
${
/__isPromise/.test(generator.renderCode) && deindent`
function __isPromise(value) {
return value && typeof value.then === 'function';
}
`
}
${
/__missingComponent/.test(generator.renderCode) && deindent`
var __missingComponent = {
_render: () => ''
};
`
}
`.replace(/(@+|#+|%+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') return generator.alias(name);
if (sigil === '%') return generator.templateVars.get(name);
return sigil.slice(1) + name;
});
return generator.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);
}

@ -1,14 +0,0 @@
import { SsrGenerator } from './index';
import Block from './Block';
import { Node } from '../../interfaces';
export type Visitor = (
generator: SsrGenerator,
block: Block,
node: Node
) => void;
export interface AppendTarget {
slots: Record<string, string>;
slotStack: string[]
}

@ -1,13 +0,0 @@
import visitors from './visitors/index';
import { SsrGenerator } from './index';
import Block from './Block';
import { Node } from '../../interfaces';
export default function visit(
generator: SsrGenerator,
block: Block,
node: Node
) {
const visitor = visitors[node.type];
visitor(generator, block, node);
}

@ -1,40 +0,0 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitAwaitBlock(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
// TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor
const contexts = new Map(block.contexts);
contexts.set(node.value, '__value');
const contextDependencies = new Map(block.contextDependencies);
contextDependencies.set(node.value, dependencies);
const childBlock = block.child({
contextDependencies,
contexts
});
generator.append('${(function(__value) { if(__isPromise(__value)) return `');
node.pending.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
generator.append('`; return `');
node.then.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
generator.append(`\`;}(${snippet})) }`);
}

@ -1,3 +0,0 @@
export default function visitComment() {
// do nothing
}

@ -1,121 +0,0 @@
import flattenReference from '../../../utils/flattenReference';
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { AppendTarget } from '../interfaces';
import { Node } from '../../../interfaces';
import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../utils/getTailSnippet';
import { stringify } from '../../../utils/stringify';
export default function visitComponent(
generator: SsrGenerator,
block: Block,
node: Node
) {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') return chunk.data;
if (chunk.type === 'MustacheTag') {
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${__escape( ' + snippet + ')}';
}
}
const attributes: Node[] = [];
const bindings: Node[] = [];
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute') {
attributes.push(attribute);
} else if (attribute.type === 'Binding') {
bindings.push(attribute);
}
});
const props = attributes
.map(attribute => {
let value;
if (attribute.value === true) {
value = `true`;
} else if (attribute.value.length === 0) {
value = `''`;
} else if (attribute.value.length === 1) {
const chunk = attribute.value[0];
if (chunk.type === 'Text') {
value = isNaN(chunk.data) ? stringify(chunk.data) : chunk.data;
} else {
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
value = snippet;
}
} else {
value = '`' + attribute.value.map(stringifyAttribute).join('') + '`';
}
return `${attribute.name}: ${value}`;
})
.concat(
bindings.map(binding => {
const { name } = getObject(binding.value);
const tail = binding.value.type === 'MemberExpression'
? getTailSnippet(binding.value)
: '';
const keypath = block.contexts.has(name)
? `${name}${tail}`
: `state.${name}${tail}`;
return `${binding.name}: ${keypath}`;
})
)
.join(', ');
const isDynamicComponent = node.name === ':Component';
if (isDynamicComponent) block.contextualise(node.expression);
const expression = (
node.name === ':Self' ? generator.name :
isDynamicComponent ? `((${node.metadata.snippet}) || __missingComponent)` :
`%components-${node.name}`
);
bindings.forEach(binding => {
block.addBinding(binding, expression);
});
let open = `\${${expression}._render(__result, {${props}}`;
const options = [];
if (generator.options.store) {
options.push(`store: options.store`);
}
if (node.children.length) {
const appendTarget: AppendTarget = {
slots: { default: '' },
slotStack: ['default']
};
generator.appendTargets.push(appendTarget);
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
const slotted = Object.keys(appendTarget.slots)
.map(name => `${name}: () => \`${appendTarget.slots[name]}\``)
.join(', ');
options.push(`slotted: { ${slotted} }`);
generator.appendTargets.pop();
}
if (options.length) {
open += `, { ${options.join(', ')} }`;
}
generator.append(open);
generator.append(')}');
}

@ -1,57 +0,0 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitEachBlock(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : node.context} => \``;
generator.append(open);
// TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor
const contexts = new Map(block.contexts);
contexts.set(node.context, node.context);
const indexes = new Map(block.indexes);
if (node.index) indexes.set(node.index, node.context);
const contextDependencies = new Map(block.contextDependencies);
contextDependencies.set(node.context, dependencies);
if (node.destructuredContexts) {
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
contexts.set(node.destructuredContexts[i], `${node.context}[${i}]`);
contextDependencies.set(node.destructuredContexts[i], dependencies);
}
}
const childBlock = block.child({
contexts,
indexes,
contextDependencies,
});
node.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
const close = `\`).join("")`;
generator.append(close);
if (node.else) {
generator.append(` : \``);
node.else.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append(`\``);
}
generator.append('}');
}

@ -1,82 +0,0 @@
import visitComponent from './Component';
import visitSlot from './Slot';
import isVoidElementName from '../../../utils/isVoidElementName';
import visit from '../visit';
import { SsrGenerator } from '../index';
import Element from '../../nodes/Element';
import Block from '../Block';
import { Node } from '../../../interfaces';
import stringifyAttributeValue from './shared/stringifyAttributeValue';
import { escape } from '../../../utils/stringify';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const booleanAttributes = 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'.split(' '));
export default function visitElement(
generator: SsrGenerator,
block: Block,
node: Element
) {
if (node.name === 'slot') {
visitSlot(generator, block, node);
return;
}
let openingTag = `<${node.name}`;
let textareaContents; // awkward special case
const slot = node.getStaticAttributeValue('slot');
if (slot && node.hasAncestor('Component')) {
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
const slotName = slot.value[0].data;
const appendTarget = generator.appendTargets[generator.appendTargets.length - 1];
appendTarget.slotStack.push(slotName);
appendTarget.slots[slotName] = '';
}
node.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttributeValue(block, attribute.value);
} else if (attribute.value === true) {
openingTag += ` ${attribute.name}`;
} else if (
booleanAttributes.has(attribute.name) &&
attribute.value.length === 1 &&
attribute.value[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
block.contextualise(attribute.value[0].expression);
openingTag += '${' + attribute.value[0].metadata.snippet + ' ? " ' + attribute.name + '" : "" }';
} else {
openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.value)}"`;
}
});
if (node._needsCssAttribute) {
openingTag += ` ${generator.stylesheet.id}`;
if (node._cssRefAttribute) {
openingTag += ` svelte-ref-${node._cssRefAttribute}`;
}
}
openingTag += '>';
generator.append(openingTag);
if (node.name === 'textarea' && textareaContents !== undefined) {
generator.append(textareaContents);
} else if (node.name === 'script' || node.name === 'style') {
generator.append(escape(node.data));
} else {
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
}
if (!isVoidElementName(node.name)) {
generator.append(`</${node.name}>`);
}
}

@ -1,19 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import stringifyAttributeValue from './shared/stringifyAttributeValue';
import visit from '../visit';
export default function visitDocument(
generator: SsrGenerator,
block: Block,
node: Node
) {
generator.append('${(__result.head += `');
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append('`, "")}');
}

@ -1,33 +0,0 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitIfBlock(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${ ' + snippet + ' ? `');
const childBlock = block.child({
conditions: block.conditions.concat(snippet),
});
node.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
generator.append('` : `');
if (node.else) {
node.else.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
}
generator.append('` }');
}

@ -1,14 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitMustacheTag(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${__escape(' + snippet + ')}');
}

@ -1,14 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitRawMustacheTag(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${' + snippet + '}');
}

@ -1,21 +0,0 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitSlot(
generator: SsrGenerator,
block: Block,
node: Node
) {
const name = node.attributes.find((attribute: Node) => attribute.name);
const slotName = name && name.value[0].data || 'default';
generator.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : \``);
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append(`\`}`);
}

@ -1,12 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { escape, escapeHTML } from '../../../utils/stringify';
import { Node } from '../../../interfaces';
export default function visitText(
generator: SsrGenerator,
block: Block,
node: Node
) {
generator.append(escapeHTML(escape(node.data).replace(/(\${|`|\\)/g, '\\$1')));
}

@ -1,19 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { escape } from '../../../utils/stringify';
import visit from '../visit';
import { Node } from '../../../interfaces';
export default function visitTitle(
generator: SsrGenerator,
block: Block,
node: Node
) {
generator.append(`<title>`);
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append(`</title>`);
}

@ -1,3 +0,0 @@
export default function visitWindow() {
// noop
}

@ -1,29 +0,0 @@
import AwaitBlock from './AwaitBlock';
import Comment from './Comment';
import Component from './Component';
import EachBlock from './EachBlock';
import Element from './Element';
import Head from './Head';
import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import Text from './Text';
import Title from './Title';
import Window from './Window';
export default {
AwaitBlock,
Comment,
Component,
EachBlock,
Element,
Head,
IfBlock,
MustacheTag,
RawMustacheTag,
Slot,
Text,
Title,
Window
};

@ -1,17 +0,0 @@
import Block from '../../Block';
import { escape } from '../../../../utils/stringify';
import { Node } from '../../../../interfaces';
export default function stringifyAttributeValue(block: Block, chunks: Node[]) {
return chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return escape(chunk.data).replace(/"/g, '&quot;');
}
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${' + snippet + '}';
})
.join('');
}

@ -1,10 +1,11 @@
import parse from './parse/index';
import validate from './validate/index';
import generate from './generators/dom/index';
import generateSSR from './generators/server-side-rendering/index';
import generate from './compile/dom/index';
import generateSSR from './compile/ssr/index';
import Stats from './Stats';
import { assign } from './shared/index.js';
import Stylesheet from './css/Stylesheet';
import { Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces';
import { Ast, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces';
import { SourceMap } from 'magic-string';
const version = '__VERSION__';
@ -22,9 +23,9 @@ function normalizeOptions(options: CompileOptions): CompileOptions {
}
function defaultOnwarn(warning: Warning) {
if (warning.loc) {
if (warning.start) {
console.warn(
`(${warning.loc.line}:${warning.loc.column}) ${warning.message}`
`(${warning.start.line}:${warning.start.column}) ${warning.message}`
); // eslint-disable-line no-console
} else {
console.warn(warning.message); // eslint-disable-line no-console
@ -105,37 +106,51 @@ export async function preprocess(source: string, options: PreprocessOptions) {
};
}
export function compile(source: string, _options: CompileOptions) {
function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions(_options);
let parsed: Parsed;
let ast: Ast;
const stats = new Stats({
onwarn: options.onwarn
});
try {
parsed = parse(source, options);
stats.start('parse');
ast = parse(source, options);
stats.stop('parse');
} catch (err) {
options.onerror(err);
return;
}
const stylesheet = new Stylesheet(source, parsed, options.filename, options.cascade !== false);
stats.start('stylesheet');
const stylesheet = new Stylesheet(source, ast, options.filename, options.dev);
stats.stop('stylesheet');
validate(parsed, source, stylesheet, options);
stats.start('validate');
validate(ast, source, stylesheet, stats, options);
stats.stop('validate');
if (options.generate === false) {
return { ast, stats: stats.render(null), js: null, css: null };
}
const compiler = options.generate === 'ssr' ? generateSSR : generate;
return compiler(parsed, source, stylesheet, options);
return compiler(ast, source, stylesheet, options, stats);
};
export function create(source: string, _options: CompileOptions = {}) {
function create(source: string, _options: CompileOptions = {}) {
_options.format = 'eval';
const compiled = compile(source, _options);
if (!compiled || !compiled.code) {
if (!compiled || !compiled.js.code) {
return;
}
try {
return (0, eval)(compiled.code);
return (new Function(`return ${compiled.js.code}`))();
} catch (err) {
if (_options.onerror) {
_options.onerror(err);
@ -146,4 +161,4 @@ export function create(source: string, _options: CompileOptions = {}) {
}
}
export { parse, validate, version as VERSION };
export { parse, create, compile, version as VERSION };

@ -20,16 +20,17 @@ export interface Parser {
metaTags: {};
}
export interface Parsed {
hash: number;
export interface Ast {
html: Node;
css: Node;
js: Node;
}
export interface Warning {
loc?: { line: number; column: number; pos?: number };
start?: { line: number; column: number; pos?: number };
end?: { line: number; column: number; };
pos?: number;
code: string;
message: string;
filename?: string;
frame?: string;
@ -42,7 +43,7 @@ export interface CompileOptions {
format?: ModuleFormat;
name?: string;
filename?: string;
generate?: string;
generate?: string | false;
globals?: ((id: string) => string) | object;
amd?: {
id?: string;
@ -52,13 +53,14 @@ export interface CompileOptions {
cssOutputFilename?: string;
dev?: boolean;
immutable?: boolean;
shared?: boolean | string;
cascade?: boolean;
hydratable?: boolean;
legacy?: boolean;
customElement?: CustomElementOptions | true;
css?: boolean;
store?: boolean;
preserveComments?: boolean | false;
onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void;
@ -69,9 +71,13 @@ export interface GenerateOptions {
format: ModuleFormat;
banner?: string;
sharedPath?: string;
helpers?: { name: string, alias: string }[];
}
export interface ShorthandImport {
name: string;
source: string;
};
export interface Visitor {
enter: (node: Node) => void;
leave?: (node: Node) => void;
@ -90,3 +96,8 @@ export interface PreprocessOptions {
}
export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>, filename?: string}) => { code: string, map?: SourceMap | string };
export interface AppendTarget {
slots: Record<string, string>;
slotStack: string[]
}

@ -3,28 +3,15 @@ import { locate, Location } from 'locate-character';
import fragment from './state/fragment';
import { whitespace } from '../utils/patterns';
import { trimStart, trimEnd } from '../utils/trim';
import getCodeFrame from '../utils/getCodeFrame';
import reservedNames from '../utils/reservedNames';
import fullCharCodeAt from '../utils/fullCharCodeAt';
import hash from '../utils/hash';
import { Node, Parsed } from '../interfaces';
import CompileError from '../utils/CompileError';
class ParseError extends CompileError {
constructor(
message: string,
template: string,
index: number,
filename: string
) {
super(message, template, index, filename);
this.name = 'ParseError';
}
}
import { Node, Ast } from '../interfaces';
import error from '../utils/error';
interface ParserOptions {
filename?: string;
bind?: boolean;
customElement?: boolean;
}
type ParserState = (parser: Parser) => (ParserState | void);
@ -32,6 +19,7 @@ type ParserState = (parser: Parser) => (ParserState | void);
export class Parser {
readonly template: string;
readonly filename?: string;
readonly customElement: boolean;
index: number;
stack: Array<Node>;
@ -50,6 +38,7 @@ export class Parser {
this.template = template.replace(/\s+$/, '');
this.filename = options.filename;
this.customElement = options.customElement;
this.allowBindings = options.bind !== false;
@ -79,11 +68,19 @@ export class Parser {
const current = this.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
this.error(`${type} was left open`, current.start);
const slug = current.type === 'Element' ? 'element' : 'block';
this.error({
code: `unclosed-${slug}`,
message: `${type} was left open`
}, current.start);
}
if (state !== fragment) {
this.error('Unexpected end of input');
this.error({
code: `unexpected-eof`,
message: 'Unexpected end of input'
});
}
if (this.html.children.length) {
@ -105,21 +102,33 @@ export class Parser {
}
acornError(err: any) {
this.error(err.message.replace(/ \(\d+:\d+\)$/, ''), err.pos);
this.error({
code: `parse-error`,
message: err.message.replace(/ \(\d+:\d+\)$/, '')
}, err.pos);
}
error(message: string, index = this.index) {
throw new ParseError(message, this.template, index, this.filename);
error({ code, message }: { code: string, message: string }, index = this.index) {
error(message, {
name: 'ParseError',
code,
source: this.template,
start: index,
filename: this.filename
});
}
eat(str: string, required?: boolean) {
eat(str: string, required?: boolean, message?: string) {
if (this.match(str)) {
this.index += str.length;
return true;
}
if (required) {
this.error(`Expected ${str}`);
this.error({
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
message: message || `Expected ${str}`
});
}
return false;
@ -167,7 +176,10 @@ export class Parser {
const identifier = this.template.slice(this.index, this.index = i);
if (reservedNames.has(identifier)) {
this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start);
this.error({
code: `unexpected-reserved-word`,
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
}, start);
}
return identifier;
@ -175,7 +187,10 @@ export class Parser {
readUntil(pattern: RegExp) {
if (this.index >= this.template.length)
this.error('Unexpected end of input');
this.error({
code: `unexpected-eof`,
message: 'Unexpected end of input'
});
const start = this.index;
const match = pattern.exec(this.template.slice(start));
@ -195,7 +210,10 @@ export class Parser {
requireWhitespace() {
if (!whitespace.test(this.template[this.index])) {
this.error(`Expected whitespace`);
this.error({
code: `missing-whitespace`,
message: `Expected whitespace`
});
}
this.allowWhitespace();
@ -205,10 +223,9 @@ export class Parser {
export default function parse(
template: string,
options: ParserOptions = {}
): Parsed {
): Ast {
const parser = new Parser(template, options);
return {
hash: hash(parser.template),
html: parser.html,
css: parser.css,
js: parser.js,

@ -0,0 +1,119 @@
import { Parser } from '../index';
type Identifier = {
start: number;
end: number;
type: 'Identifier';
name: string;
};
type Property = {
start: number;
end: number;
type: 'Property';
key: Identifier;
value: Context;
};
type Context = {
start: number;
end: number;
type: 'Identifier' | 'ArrayPattern' | 'ObjectPattern';
name?: string;
elements?: Context[];
properties?: Property[];
}
function errorOnAssignmentPattern(parser: Parser) {
if (parser.eat('=')) {
parser.error({
code: 'invalid-assignment-pattern',
message: 'Assignment patterns are not supported'
}, parser.index - 1);
}
}
export default function readContext(parser: Parser) {
const context: Context = {
start: parser.index,
end: null,
type: null
};
if (parser.eat('[')) {
context.type = 'ArrayPattern';
context.elements = [];
do {
parser.allowWhitespace();
if (parser.template[parser.index] === ',') {
context.elements.push(null);
} else {
context.elements.push(readContext(parser));
parser.allowWhitespace();
}
} while (parser.eat(','));
errorOnAssignmentPattern(parser);
parser.eat(']', true);
}
else if (parser.eat('{')) {
context.type = 'ObjectPattern';
context.properties = [];
do {
parser.allowWhitespace();
const start = parser.index;
const name = parser.readIdentifier();
const key: Identifier = {
start,
end: parser.index,
type: 'Identifier',
name
};
parser.allowWhitespace();
const value = parser.eat(':')
? (parser.allowWhitespace(), readContext(parser))
: key;
const property: Property = {
start,
end: value.end,
type: 'Property',
key,
value
};
context.properties.push(property);
parser.allowWhitespace();
} while (parser.eat(','));
errorOnAssignmentPattern(parser);
parser.eat('}', true);
}
else {
const name = parser.readIdentifier();
if (name) {
context.type = 'Identifier';
context.end = parser.index;
context.name = name;
}
else {
parser.error({
code: 'invalid-context',
message: 'Expected a name, array pattern or object pattern'
});
}
errorOnAssignmentPattern(parser);
}
return context;
}

@ -2,6 +2,86 @@ import { parseExpressionAt } from 'acorn';
import repeat from '../../utils/repeat';
import { Parser } from '../index';
const DIRECTIVES: Record<string, {
names: string[];
attribute: (
start: number,
end: number,
type: string,
name: string,
expression?: any,
directiveName?: string
) => { start: number, end: number, type: string, name: string, value?: any, expression?: any };
allowedExpressionTypes: string[];
error: string;
}> = {
Ref: {
names: ['ref'],
attribute(start, end, type, name) {
return { start, end, type, name };
},
allowedExpressionTypes: [],
error: 'ref directives cannot have a value'
},
EventHandler: {
names: ['on'],
attribute(start, end, type, name, expression) {
return { start, end, type, name, expression };
},
allowedExpressionTypes: ['CallExpression'],
error: 'Expected a method call'
},
Binding: {
names: ['bind'],
attribute(start, end, type, name, expression) {
return {
start, end, type, name,
value: expression || {
type: 'Identifier',
start: start + 5,
end,
name,
}
};
},
allowedExpressionTypes: ['Identifier', 'MemberExpression'],
error: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
},
Transition: {
names: ['in', 'out', 'transition'],
attribute(start, end, type, name, expression, directiveName) {
return {
start, end, type, name, expression,
intro: directiveName === 'in' || directiveName === 'transition',
outro: directiveName === 'out' || directiveName === 'transition',
};
},
allowedExpressionTypes: ['ObjectExpression'],
error: 'Transition argument must be an object literal, e.g. `{ duration: 400 }`'
},
Action: {
names: [ 'use' ],
attribute(start, end, type, name, expression) {
return { start, end, type, name, expression };
},
allowedExpressionTypes: [ 'Identifier', 'MemberExpression', 'ObjectExpression', 'Literal', 'CallExpression' ],
error: 'Data passed to actions must be an identifier (e.g. `foo`), a member expression ' +
'(e.g. `foo.bar` or `foo[baz]`), a method call (e.g. `foo()`), or a literal (e.g. `true` or `\'a string\'`'
},
};
const lookupByName = {};
Object.keys(DIRECTIVES).forEach(name => {
const directive = DIRECTIVES[name];
directive.names.forEach(type => lookupByName[type] = name);
});
function readExpression(parser: Parser, start: number, quoteMark: string|null) {
let str = '';
let escaped = false;
@ -24,14 +104,16 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) {
} else {
str += char;
}
} else if (/\s/.test(char)) {
} else if (/[\s\/>]/.test(char)) {
break;
} else {
str += char;
}
}
const expression = parseExpressionAt(repeat(' ', start) + str, start);
const expression = parseExpressionAt(repeat(' ', start) + str, start, {
ecmaVersion: 9,
});
parser.index = expression.end;
parser.allowWhitespace();
@ -40,104 +122,29 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) {
return expression;
}
export function readEventHandlerDirective(
export function readDirective(
parser: Parser,
start: number,
name: string,
hasValue: boolean
attrName: string
) {
let expression;
if (hasValue) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const expressionStart = parser.index;
expression = readExpression(parser, expressionStart, quoteMark);
if (expression.type !== 'CallExpression') {
parser.error(`Expected call expression`, expressionStart);
}
}
return {
start,
end: parser.index,
type: 'EventHandler',
name,
expression,
};
}
export function readBindingDirective(
parser: Parser,
start: number,
name: string
) {
let value;
if (parser.eat('=')) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const a = parser.index;
if (parser.eat('{{')) {
let message = 'bound values should not be wrapped';
const b = parser.template.indexOf('}}', a);
if (b !== -1) {
const value = parser.template.slice(parser.index, b);
message += ` — use '${value}', not '{{${value}}}'`;
}
parser.error(message, a);
}
// this is a bit of a hack so that we can give Acorn something parseable
let b;
if (quoteMark) {
b = parser.index = parser.template.indexOf(quoteMark, parser.index);
} else {
parser.readUntil(/[\s\r\n\/>]/);
b = parser.index;
}
const source = repeat(' ', a) + parser.template.slice(a, b);
value = parseExpressionAt(source, a);
if (value.type !== 'Identifier' && value.type !== 'MemberExpression') {
parser.error(`Cannot bind to rvalue`, value.start);
}
parser.allowWhitespace();
if (quoteMark) {
parser.eat(quoteMark, true);
}
} else {
// shorthand bind:foo equivalent to bind:foo='foo'
value = {
type: 'Identifier',
start: start + 5,
end: parser.index,
const [directiveName, name] = attrName.split(':');
if (name === undefined) return; // No colon in the name
if (directiveName === '') {
// not a directive — :foo is short for foo={{foo}}
return {
start: start,
end: start + name.length + 1,
type: 'Attribute',
name,
value: getShorthandValue(start + 1, name)
};
}
return {
start,
end: parser.index,
type: 'Binding',
name,
value,
};
}
const type = lookupByName[directiveName];
if (!type) return; // not a registered directive
export function readTransitionDirective(
parser: Parser,
start: number,
name: string,
type: string
) {
const directive = DIRECTIVES[type];
let expression = null;
if (parser.eat('=')) {
@ -145,20 +152,52 @@ export function readTransitionDirective(
const expressionStart = parser.index;
expression = readExpression(parser, expressionStart, quoteMark);
try {
expression = readExpression(parser, expressionStart, quoteMark);
if (directive.allowedExpressionTypes.indexOf(expression.type) === -1) {
parser.error({
code: `invalid-directive-value`,
message: directive.error
}, expressionStart);
}
} catch (err) {
if (parser.template[expressionStart] === '{') {
// assume the mistake was wrapping the directive arguments.
// this could yield false positives! but hopefully not too many
let message = 'directive values should not be wrapped';
const expressionEnd = parser.template.indexOf('}', expressionStart);
if (expressionEnd !== -1) {
const value = parser.template.slice(expressionStart + 1, expressionEnd);
message += ` — use '${value}', not '{${value}}'`;
}
parser.error({
code: `invalid-directive-value`,
message
}, expressionStart);
}
if (expression.type !== 'ObjectExpression') {
parser.error(`Expected object expression`, expressionStart);
throw err;
}
}
return {
start,
end: parser.index,
type: 'Transition',
name,
intro: type === 'in' || type === 'transition',
outro: type === 'out' || type === 'transition',
expression,
};
return directive.attribute(start, parser.index, type, name, expression, directiveName);
}
function getShorthandValue(start: number, name: string) {
const end = start + name.length;
return [
{
type: 'AttributeShorthand',
start,
end,
expression: {
type: 'Identifier',
start,
end,
name,
},
},
];
}

@ -6,7 +6,7 @@ const literals = new Map([['true', true], ['false', false], ['null', null]]);
export default function readExpression(parser: Parser) {
const start = parser.index;
const name = parser.readUntil(/\s*}}/);
const name = parser.readUntil(/\s*}/);
if (name && /^[a-z]+$/.test(name)) {
const end = start + name.length;
@ -32,6 +32,7 @@ export default function readExpression(parser: Parser) {
try {
const node = parseExpressionAt(parser.template, parser.index, {
ecmaVersion: 9,
preserveParens: true,
});
parser.index = node.end;

@ -12,7 +12,10 @@ export default function readScript(parser: Parser, start: number, attributes: No
const scriptStart = parser.index;
const scriptEnd = parser.template.indexOf(scriptClosingTag, scriptStart);
if (scriptEnd === -1) parser.error(`<script> must have a closing tag`);
if (scriptEnd === -1) parser.error({
code: `unclosed-script`,
message: `<script> must have a closing tag`
});
const source =
repeat(' ', scriptStart) + parser.template.slice(scriptStart, scriptEnd);
@ -22,7 +25,7 @@ export default function readScript(parser: Parser, start: number, attributes: No
try {
ast = acorn.parse(source, {
ecmaVersion: 8,
ecmaVersion: 9,
sourceType: 'module',
plugins: {
dynamicImport: true

@ -17,7 +17,10 @@ export default function readStyle(parser: Parser, start: number, attributes: Nod
});
} catch (err) {
if (err.name === 'CssSyntaxError') {
parser.error(err.message, err.offset);
parser.error({
code: `css-syntax-error`,
message: err.message
}, err.offset);
} else {
throw err;
}

@ -8,7 +8,7 @@ export default function fragment(parser: Parser) {
return tag;
}
if (parser.match('{{')) {
if (parser.match('{')) {
return mustache;
}

@ -1,3 +1,4 @@
import readContext from '../read/context';
import readExpression from '../read/expression';
import { whitespace } from '../../utils/patterns';
import { trimStart, trimEnd } from '../../utils/trim';
@ -6,7 +7,7 @@ import { Parser } from '../index';
import { Node } from '../../interfaces';
function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) {
if (!block.children) return; // AwaitBlock
if (!block.children || block.children.length === 0) return; // AwaitBlock
const firstChild = block.children[0];
const lastChild = block.children[block.children.length - 1];
@ -32,7 +33,7 @@ function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) {
export default function mustache(parser: Parser) {
const start = parser.index;
parser.index += 2;
parser.index += 1;
parser.allowWhitespace();
@ -56,12 +57,15 @@ export default function mustache(parser: Parser) {
} else if (block.type === 'AwaitBlock') {
expected = 'await';
} else {
parser.error(`Unexpected block closing tag`);
parser.error({
code: `unexpected-block-close`,
message: `Unexpected block closing tag`
});
}
parser.eat(expected, true);
parser.allowWhitespace();
parser.eat('}}', true);
parser.eat('}', true);
while (block.elseif) {
block.end = parser.index;
@ -74,8 +78,6 @@ export default function mustache(parser: Parser) {
}
// strip leading/trailing whitespace as necessary
if (block.children && !block.children.length) parser.error(`Empty block`, block.start);
const charBefore = parser.template[block.start - 1];
const charAfter = parser.template[parser.index];
const trimBefore = !charBefore || whitespace.test(charBefore);
@ -85,19 +87,20 @@ export default function mustache(parser: Parser) {
block.end = parser.index;
parser.stack.pop();
} else if (parser.eat('elseif')) {
} else if (parser.eat(':elseif')) {
const block = parser.current();
if (block.type !== 'IfBlock')
parser.error(
'Cannot have an {{elseif ...}} block outside an {{#if ...}} block'
);
parser.error({
code: `invalid-elseif-placement`,
message: 'Cannot have an {{elseif ...}} block outside an {{#if ...}} block'
});
parser.requireWhitespace();
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat('}}', true);
parser.eat('}', true);
block.else = {
start: parser.index,
@ -116,16 +119,17 @@ export default function mustache(parser: Parser) {
};
parser.stack.push(block.else.children[0]);
} else if (parser.eat('else')) {
} else if (parser.eat(':else')) {
const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error(
'Cannot have an {{else}} block outside an {{#if ...}} or {{#each ...}} block'
);
parser.error({
code: `invalid-else-placement`,
message: 'Cannot have an {{else}} block outside an {{#if ...}} or {{#each ...}} block'
});
}
parser.allowWhitespace();
parser.eat('}}', true);
parser.eat('}', true);
block.else = {
start: parser.index,
@ -135,7 +139,7 @@ export default function mustache(parser: Parser) {
};
parser.stack.push(block.else);
} else if (parser.eat('then')) {
} else if (parser.eat(':then')) {
// TODO DRY out this and the next section
const pendingBlock = parser.current();
if (pendingBlock.type === 'PendingBlock') {
@ -147,7 +151,7 @@ export default function mustache(parser: Parser) {
awaitBlock.value = parser.readIdentifier();
parser.allowWhitespace();
parser.eat('}}', true);
parser.eat('}', true);
const thenBlock: Node = {
start,
@ -159,7 +163,7 @@ export default function mustache(parser: Parser) {
awaitBlock.then = thenBlock;
parser.stack.push(thenBlock);
}
} else if (parser.eat('catch')) {
} else if (parser.eat(':catch')) {
const thenBlock = parser.current();
if (thenBlock.type === 'ThenBlock') {
thenBlock.end = start;
@ -170,7 +174,7 @@ export default function mustache(parser: Parser) {
awaitBlock.error = parser.readIdentifier();
parser.allowWhitespace();
parser.eat('}}', true);
parser.eat('}', true);
const catchBlock: Node = {
start,
@ -193,7 +197,10 @@ export default function mustache(parser: Parser) {
} else if (parser.eat('await')) {
type = 'AwaitBlock';
} else {
parser.error(`Expected if, each or await`);
parser.error({
code: `expected-block-type`,
message: `Expected if, each or await`
});
}
parser.requireWhitespace();
@ -242,43 +249,34 @@ export default function mustache(parser: Parser) {
parser.eat('as', true);
parser.requireWhitespace();
if (parser.eat('[')) {
parser.allowWhitespace();
block.destructuredContexts = [];
do {
parser.allowWhitespace();
const destructuredContext = parser.readIdentifier();
if (!destructuredContext) parser.error(`Expected name`);
block.destructuredContexts.push(destructuredContext);
parser.allowWhitespace();
} while (parser.eat(','));
if (!block.destructuredContexts.length) parser.error(`Expected name`);
block.context = block.destructuredContexts.join('_');
parser.allowWhitespace();
parser.eat(']', true);
} else {
block.context = parser.readIdentifier();
if (!block.context) parser.error(`Expected name`);
}
block.context = readContext(parser);
parser.allowWhitespace();
if (parser.eat(',')) {
parser.allowWhitespace();
block.index = parser.readIdentifier();
if (!block.index) parser.error(`Expected name`);
if (!block.index) parser.error({
code: `expected-name`,
message: `Expected name`
});
parser.allowWhitespace();
}
if (parser.eat('@')) {
if (parser.eat('(')) {
parser.allowWhitespace();
block.key = readExpression(parser);
parser.allowWhitespace();
parser.eat(')', true);
parser.allowWhitespace();
} else if (parser.eat('@')) {
block.key = parser.readIdentifier();
if (!block.key) parser.error(`Expected name`);
if (!block.key) parser.error({
code: `expected-name`,
message: `Expected name`
});
parser.allowWhitespace();
}
}
@ -290,7 +288,7 @@ export default function mustache(parser: Parser) {
parser.allowWhitespace();
}
parser.eat('}}', true);
parser.eat('}', true);
parser.current().children.push(block);
parser.stack.push(block);
@ -300,26 +298,12 @@ export default function mustache(parser: Parser) {
childBlock.start = parser.index;
parser.stack.push(childBlock);
}
} else if (parser.eat('yield')) {
// {{yield}}
// TODO deprecate
parser.allowWhitespace();
parser.eat('}}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'Element',
name: 'slot',
attributes: [],
children: []
});
} else if (parser.eat('{')) {
} else if (parser.eat('@html')) {
// {{{raw}}} mustache
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat('}}}', true);
parser.eat('}', true);
parser.current().children.push({
start,
@ -331,7 +315,7 @@ export default function mustache(parser: Parser) {
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat('}}', true);
parser.eat('}', true);
parser.current().children.push({
start,

@ -1,11 +1,7 @@
import readExpression from '../read/expression';
import readScript from '../read/script';
import readStyle from '../read/style';
import {
readEventHandlerDirective,
readBindingDirective,
readTransitionDirective,
} from '../read/directives';
import { readDirective } from '../read/directives';
import { trimStart, trimEnd } from '../../utils/trim';
import { decodeCharacterReferences } from '../utils/html';
import isVoidElementName from '../../utils/isVoidElementName';
@ -14,12 +10,9 @@ import { Node } from '../../interfaces';
const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const SELF = ':Self';
const COMPONENT = ':Component';
const metaTags = new Set([
':Window',
':Head'
const metaTags = new Map([
['svelte:window', 'Window'],
['svelte:head', 'Head']
]);
const specials = new Map([
@ -39,6 +32,9 @@ const specials = new Map([
],
]);
const SELF = 'svelte:self';
const COMPONENT = 'svelte:component';
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
const disallowedContents = new Map([
['li', new Set(['li'])],
@ -64,6 +60,16 @@ const disallowedContents = new Map([
['th', new Set(['td', 'th', 'tr'])],
]);
function parentIsHead(stack) {
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'Head') return true;
if (type === 'Element' || type === 'Component') return false;
}
return false;
}
export default function tag(parser: Parser) {
const start = parser.index++;
@ -71,7 +77,7 @@ export default function tag(parser: Parser) {
if (parser.eat('!--')) {
const data = parser.readUntil(/-->/);
parser.eat('-->');
parser.eat('-->', true, 'comment was left open, expected -->');
parser.current().children.push({
start,
@ -88,31 +94,43 @@ export default function tag(parser: Parser) {
const name = readTagName(parser);
if (metaTags.has(name)) {
const slug = metaTags.get(name).toLowerCase();
if (isClosingTag) {
if (name === ':Window' && parser.current().children.length) {
parser.error(
`<:Window> cannot have children`,
parser.current().children[0].start
);
if (name === 'svelte:window' && parser.current().children.length) {
parser.error({
code: `invalid-window-content`,
message: `<${name}> cannot have children`
}, parser.current().children[0].start);
}
} else {
if (name in parser.metaTags) {
parser.error(`A component can only have one <${name}> tag`, start);
parser.error({
code: `duplicate-${slug}`,
message: `A component can only have one <${name}> tag`
}, start);
}
if (parser.stack.length > 1) {
console.log(parser.stack);
parser.error(`<${name}> tags cannot be inside elements or blocks`, start);
parser.error({
code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`
}, start);
}
parser.metaTags[name] = true;
}
}
const type = metaTags.has(name)
? metaTags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'Component'
: name === 'title' && parentIsHead(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
const element: Node = {
start,
end: null, // filled in later
type: 'Element',
type,
name,
attributes: [],
children: [],
@ -122,21 +140,21 @@ export default function tag(parser: Parser) {
if (isClosingTag) {
if (isVoidElementName(name)) {
parser.error(
`<${name}> is a void element and cannot have children, or a closing tag`,
start
);
parser.error({
code: `invalid-void-content`,
message: `<${name}> is a void element and cannot have children, or a closing tag`
}, start);
}
if (!parser.eat('>')) parser.error(`Expected '>'`);
parser.eat('>', true);
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (parent.name !== name) {
if (parent.type !== 'Element')
parser.error(
`</${name}> attempted to close an element that was not open`,
start
);
parser.error({
code: `invalid-closing-tag`,
message: `</${name}> attempted to close an element that was not open`
}, start);
parent.end = start;
parser.stack.pop();
@ -162,43 +180,60 @@ export default function tag(parser: Parser) {
while (i--) {
const item = parser.stack[i];
if (item.type === 'EachBlock') {
parser.error(
`<slot> cannot be a child of an each-block`,
start
);
parser.error({
code: `invalid-slot-placement`,
message: `<slot> cannot be a child of an each-block`
}, start);
}
}
}
if (name === COMPONENT) {
parser.eat('{', true);
element.expression = readExpression(parser);
parser.allowWhitespace();
parser.eat('}', true);
parser.allowWhitespace();
}
const uniqueNames = new Set();
let attribute;
while ((attribute = readAttribute(parser, uniqueNames))) {
if (attribute.type === 'Binding' && !parser.allowBindings) {
parser.error(`Two-way binding is disabled`, attribute.start);
parser.error({
code: `binding-disabled`,
message: `Two-way binding is disabled`
}, attribute.start);
}
element.attributes.push(attribute);
parser.allowWhitespace();
}
if (name === 'svelte:component') {
// TODO post v2, treat this just as any other attribute
const index = element.attributes.findIndex(attr => attr.name === 'this');
if (!~index) {
parser.error({
code: `missing-component-definition`,
message: `<svelte:component> must have a 'this' attribute`
}, start);
}
const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
parser.error({
code: `invalid-component-definition`,
message: `invalid component definition`
}, definition.start);
}
element.expression = definition.value[0].expression;
}
// special cases top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name);
if (parser[special.property]) {
parser.index = start;
parser.error(
`You can only have one top-level <${name}> tag per component`
);
parser.error({
code: `duplicate-${name}`,
message: `You can only have one top-level <${name}> tag per component`
});
}
parser.eat('>', true);
@ -224,11 +259,21 @@ export default function tag(parser: Parser) {
);
parser.read(/<\/textarea>/);
element.end = parser.index;
} else if (name === 'script' || name === 'style') {
} else if (name === 'script') {
// special case
element.data = parser.readUntil(new RegExp(`</${name}>`));
parser.eat(`</${name}>`, true);
const start = parser.index;
const data = parser.readUntil(/<\/script>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</script>', true);
element.end = parser.index;
} else if (name === 'style') {
// special case
const start = parser.index;
const data = parser.readUntil(/<\/style>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</style>', true);
} else {
parser.stack.push(element);
}
@ -252,10 +297,10 @@ function readTagName(parser: Parser) {
}
if (!legal) {
parser.error(
`<${SELF}> components can only exist inside if-blocks or each-blocks`,
start
);
parser.error({
code: `invalid-self-placement`,
message: `<${SELF}> components can only exist inside if-blocks or each-blocks`
}, start);
}
return SELF;
@ -268,7 +313,10 @@ function readTagName(parser: Parser) {
if (metaTags.has(name)) return name;
if (!validTagName.test(name)) {
parser.error(`Expected valid tag name`, start);
parser.error({
code: `invalid-tag-name`,
message: `Expected valid tag name`
}, start);
}
return name;
@ -277,52 +325,65 @@ function readTagName(parser: Parser) {
function readAttribute(parser: Parser, uniqueNames: Set<string>) {
const start = parser.index;
let name = parser.readUntil(/(\s|=|\/|>)/);
if (!name) return null;
if (uniqueNames.has(name)) {
parser.error('Attributes need to be unique', start);
}
if (parser.eat('{')) {
parser.allowWhitespace();
uniqueNames.add(name);
if (parser.eat('...')) {
const expression = readExpression(parser);
parser.allowWhitespace();
parser.allowWhitespace();
parser.eat('}', true);
if (/^on:/.test(name)) {
return readEventHandlerDirective(parser, start, name.slice(3), parser.eat('='));
}
return {
start,
end: parser.index,
type: 'Spread',
expression
};
} else {
const valueStart = parser.index;
if (/^bind:/.test(name)) {
return readBindingDirective(parser, start, name.slice(5));
}
const name = parser.readIdentifier();
parser.allowWhitespace();
parser.eat('}', true);
if (/^ref:/.test(name)) {
return {
start,
end: parser.index,
type: 'Ref',
name: name.slice(4),
};
return {
start,
end: parser.index,
type: 'Attribute',
name,
value: [{
start: valueStart,
end: valueStart + name.length,
type: 'AttributeShorthand',
expression: {
start: valueStart,
end: valueStart + name.length,
type: 'Identifier',
name
}
}]
};
}
}
const match = /^(in|out|transition):/.exec(name);
if (match) {
return readTransitionDirective(
parser,
start,
name.slice(match[0].length),
match[1]
);
let name = parser.readUntil(/(\s|=|\/|>)/);
if (!name) return null;
if (uniqueNames.has(name)) {
parser.error({
code: `duplicate-attribute`,
message: 'Attributes need to be unique'
}, start);
}
let value;
uniqueNames.add(name);
// :foo is shorthand for foo='{{foo}}'
if (/^:\w+$/.test(name)) {
name = name.slice(1);
value = getShorthandValue(start + 1, name);
} else {
value = parser.eat('=') ? readAttributeValue(parser) : true;
}
parser.allowWhitespace();
const directive = readDirective(parser, start, name);
if (directive) return directive;
let value = parser.eat('=') ? readAttributeValue(parser) : true;
return {
start,
@ -348,24 +409,6 @@ function readAttributeValue(parser: Parser) {
return value;
}
function getShorthandValue(start: number, name: string) {
const end = start + name.length;
return [
{
type: 'AttributeShorthand',
start,
end,
expression: {
type: 'Identifier',
start,
end,
name,
},
},
];
}
function readSequence(parser: Parser, done: () => boolean) {
let currentChunk: Node = {
start: parser.index,
@ -390,7 +433,7 @@ function readSequence(parser: Parser, done: () => boolean) {
});
return chunks;
} else if (parser.eat('{{')) {
} else if (parser.eat('{')) {
if (currentChunk.data) {
currentChunk.end = index;
chunks.push(currentChunk);
@ -398,9 +441,7 @@ function readSequence(parser: Parser, done: () => boolean) {
const expression = readExpression(parser);
parser.allowWhitespace();
if (!parser.eat('}}')) {
parser.error(`Expected }}`);
}
parser.eat('}', true);
chunks.push({
start: index,
@ -420,5 +461,8 @@ function readSequence(parser: Parser, done: () => boolean) {
}
}
parser.error(`Unexpected end of input`);
parser.error({
code: `unexpected-eof`,
message: `Unexpected end of input`
});
}

@ -9,7 +9,7 @@ export default function text(parser: Parser) {
while (
parser.index < parser.template.length &&
!parser.match('<') &&
!parser.match('{{')
!parser.match('{')
) {
data += parser.template[parser.index++];
}

@ -29,7 +29,7 @@
"plugin:import/warnings"
],
"parserOptions": {
"ecmaVersion": 6,
"ecmaVersion": 9,
"sourceType": "module"
},
"settings": {

@ -5,22 +5,17 @@ const acorn = require('acorn');
const declarations = {};
fs.readdirSync(__dirname).forEach(file => {
if (!/^[a-z]+\.js$/.test(file)) return;
if (!/^[a-z\-]+\.js$/.test(file)) return;
const source = fs.readFileSync(path.join(__dirname, file), 'utf-8');
const ast = acorn.parse(source, {
ecmaVersion: 6,
ecmaVersion: 9,
sourceType: 'module'
});
ast.body.forEach(node => {
if (node.type !== 'ExportNamedDeclaration') return;
// check no ES6+ slipped in
acorn.parse(source.slice(node.declaration.start, node.end), {
ecmaVersion: 5
});
const declaration = node.declaration;
if (!declaration) return;
@ -37,7 +32,7 @@ fs.readdirSync(__dirname).forEach(file => {
});
fs.writeFileSync(
'src/generators/dom/shared.ts',
'src/compile/shared.ts',
`// this file is auto-generated, do not edit it
const shared: Record<string, string> = ${JSON.stringify(declarations, null, '\t')};

@ -0,0 +1,56 @@
import { assign, isPromise } from './utils.js';
export function handlePromise(promise, info) {
var token = info.token = {};
function update(type, index, key, value) {
if (info.token !== token) return;
info.resolved = key && { [key]: value };
const child_ctx = assign(assign({}, info.ctx), info.resolved);
const block = type && (info.current = type)(info.component, child_ctx);
if (info.block) {
if (info.blocks) {
info.blocks.forEach((block, i) => {
if (i !== index && block) block.o(() => {
block.d(1);
info.blocks[i] = null;
});
});
} else {
info.block.d(1);
}
block.c();
block[block.i ? 'i' : 'm'](info.mount(), info.anchor);
info.component.root.set({}); // flush any handlers that were created
}
info.block = block;
if (info.blocks) info.blocks[index] = block;
}
if (isPromise(promise)) {
promise.then(value => {
update(info.then, 1, info.value, value);
}, error => {
update(info.catch, 2, info.error, error);
});
// if we previously had a then/catch block, destroy it
if (info.current !== info.pending) {
update(info.pending, 0);
return true;
}
} else {
if (info.current !== info.then) {
update(info.then, 1, info.value, promise);
return true;
}
info.resolved = { [info.value]: promise };
}
}

@ -47,9 +47,9 @@ export function reinsertBefore(after, target) {
while (parent.firstChild !== after) target.appendChild(parent.firstChild);
}
export function destroyEach(iterations) {
export function destroyEach(iterations, detach) {
for (var i = 0; i < iterations.length; i += 1) {
if (iterations[i]) iterations[i].d();
if (iterations[i]) iterations[i].d(detach);
}
}
@ -85,6 +85,21 @@ export function setAttribute(node, attribute, value) {
node.setAttribute(attribute, value);
}
export function setAttributes(node, attributes) {
for (var key in attributes) {
if (key in node) {
node[key] = attributes[key];
} else {
if (attributes[key] === undefined) removeAttribute(node, key);
else setAttribute(node, key, attributes[key]);
}
}
}
export function removeAttribute(node, attribute) {
node.removeAttribute(attribute);
}
export function setXlinkAttribute(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
@ -178,3 +193,35 @@ export function selectMultipleValue(select) {
return option.__value;
});
}
export function addResizeListener(element, fn) {
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
const object = document.createElement('object');
object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
object.type = 'text/html';
let win;
object.onload = () => {
win = object.contentDocument.defaultView;
win.addEventListener('resize', fn);
};
if (/Trident/.test(navigator.userAgent)) {
element.appendChild(object);
object.data = 'about:blank';
} else {
object.data = 'about:blank';
element.appendChild(object);
}
return {
cancel: () => {
win.removeEventListener('resize', fn);
element.removeChild(object);
}
};
}

@ -1,6 +1,10 @@
import { assign } from './utils.js';
import { noop } from './utils.js';
export * from './await-block.js';
export * from './dom.js';
export * from './keyed-each.js';
export * from './spread.js';
export * from './ssr.js';
export * from './transitions.js';
export * from './utils.js';
@ -11,11 +15,11 @@ export function blankObject() {
export function destroy(detach) {
this.destroy = noop;
this.fire('destroy');
this.set = this.get = noop;
this.set = noop;
if (detach !== false) this._fragment.u();
this._fragment.d();
this._fragment = this._state = null;
this._fragment.d(detach !== false);
this._fragment = null;
this._state = {};
}
export function destroyDev(detach) {
@ -25,29 +29,12 @@ export function destroyDev(detach) {
};
}
export function differs(a, b) {
return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
export function _differs(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
export function dispatchObservers(component, group, changed, newState, oldState) {
for (var key in group) {
if (!changed[key]) continue;
var newValue = newState[key];
var oldValue = oldState[key];
var callbacks = group[key];
if (!callbacks) continue;
for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i];
if (callback.__calling) continue;
callback.__calling = true;
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
}
export function _differsImmutable(a, b) {
return a != a ? b == b : a !== b;
}
export function fire(eventName, data) {
@ -56,16 +43,21 @@ export function fire(eventName, data) {
if (!handlers) return;
for (var i = 0; i < handlers.length; i += 1) {
handlers[i].call(this, data);
var handler = handlers[i];
if (!handler.__calling) {
handler.__calling = true;
handler.call(this, data);
handler.__calling = false;
}
}
}
export function get(key) {
return key ? this._state[key] : this._state;
export function get() {
return this._state;
}
export function init(component, options) {
component._observers = { pre: blankObject(), post: blankObject() };
component._handlers = blankObject();
component._bind = options._bind;
@ -74,44 +66,7 @@ export function init(component, options) {
component.store = component.root.store || options.store;
}
export function observe(key, callback, options) {
var group = options && options.defer
? this._observers.post
: this._observers.pre;
(group[key] || (group[key] = [])).push(callback);
if (!options || options.init !== false) {
callback.__calling = true;
callback.call(this, this._state[key]);
callback.__calling = false;
}
return {
cancel: function() {
var index = group[key].indexOf(callback);
if (~index) group[key].splice(index, 1);
}
};
}
export function observeDev(key, callback, options) {
var c = (key = '' + key).search(/[^\w]/);
if (c > -1) {
var message =
'The first argument to component.observe(...) must be the name of a top-level property';
if (c > 0)
message += ", i.e. '" + key.slice(0, c) + "' rather than '" + key + "'";
throw new Error(message);
}
return observe.call(this, key, callback, options);
}
export function on(eventName, handler) {
if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
@ -123,15 +78,8 @@ export function on(eventName, handler) {
};
}
export function onDev(eventName, handler) {
if (eventName === 'teardown') {
console.warn(
"Use component.on('destroy', ...) instead of component.on('teardown', ...) which has been deprecated and will be unsupported in Svelte 2"
);
return this.on('destroy', handler);
}
return on.call(this, eventName, handler);
export function run(fn) {
fn();
}
export function set(newState) {
@ -150,18 +98,18 @@ export function _set(newState) {
dirty = false;
for (var key in newState) {
if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
if (this._differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign({}, oldState, newState);
this._state = assign(assign({}, oldState), newState);
this._recompute(changed, this._state);
if (this._bind) this._bind(changed, this._state);
if (this._fragment) {
dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
this.fire("state", { changed: changed, current: this._state, previous: oldState });
this._fragment.p(changed, this._state);
dispatchObservers(this, this._observers.post, changed, this._state, oldState);
this.fire("update", { changed: changed, current: this._state, previous: oldState });
}
}
@ -181,15 +129,7 @@ export function callAll(fns) {
}
export function _mount(target, anchor) {
this._fragment.m(target, anchor);
}
export function _unmount() {
if (this._fragment) this._fragment.u();
}
export function isPromise(value) {
return value && typeof value.then === 'function';
this._fragment[this._fragment.i ? 'i' : 'm'](target, anchor || null);
}
export var PENDING = {};
@ -201,29 +141,25 @@ export function removeFromStore() {
}
export var proto = {
destroy: destroy,
get: get,
fire: fire,
observe: observe,
on: on,
set: set,
teardown: destroy,
destroy,
get,
fire,
on,
set,
_recompute: noop,
_set: _set,
_mount: _mount,
_unmount: _unmount
_set,
_mount,
_differs
};
export var protoDev = {
destroy: destroyDev,
get: get,
fire: fire,
observe: observeDev,
on: onDev,
get,
fire,
on,
set: setDev,
teardown: destroyDev,
_recompute: noop,
_set: _set,
_mount: _mount,
_unmount: _unmount
_set,
_mount,
_differs
};

@ -0,0 +1,98 @@
export function destroyBlock(block, lookup) {
block.d(1);
lookup[block.key] = null;
}
export function outroAndDestroyBlock(block, lookup) {
block.o(function() {
destroyBlock(block, lookup);
});
}
export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic, ctx, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) {
var o = old_blocks.length;
var n = list.length;
var i = o;
var old_indexes = {};
while (i--) old_indexes[old_blocks[i].key] = i;
var new_blocks = [];
var new_lookup = {};
var deltas = {};
var i = n;
while (i--) {
var child_ctx = get_context(ctx, list, i);
var key = get_key(child_ctx);
var block = lookup[key];
if (!block) {
block = create_each_block(component, key, child_ctx);
block.c();
} else if (dynamic) {
block.p(changed, child_ctx);
}
new_blocks[i] = new_lookup[key] = block;
if (key in old_indexes) deltas[key] = Math.abs(i - old_indexes[key]);
}
var will_move = {};
var did_move = {};
var destroy = has_outro ? outroAndDestroyBlock : destroyBlock;
function insert(block) {
block[intro_method](node, next);
lookup[block.key] = block;
next = block.first;
n--;
}
while (o && n) {
var new_block = new_blocks[n - 1];
var old_block = old_blocks[o - 1];
var new_key = new_block.key;
var old_key = old_block.key;
if (new_block === old_block) {
// do nothing
next = new_block.first;
o--;
n--;
}
else if (!new_lookup[old_key]) {
// remove old block
destroy(old_block, lookup);
o--;
}
else if (!lookup[new_key] || will_move[new_key]) {
insert(new_block);
}
else if (did_move[old_key]) {
o--;
} else if (deltas[new_key] > deltas[old_key]) {
did_move[new_key] = true;
insert(new_block);
} else {
will_move[old_key] = true;
o--;
}
}
while (o--) {
var old_block = old_blocks[o];
if (!new_lookup[old_block.key]) destroy(old_block, lookup);
}
while (n) insert(new_blocks[n - 1]);
return new_blocks;
}

@ -0,0 +1,37 @@
export function getSpreadUpdate(levels, updates) {
var update = {};
var to_null_out = {};
var accounted_for = {};
var i = levels.length;
while (i--) {
var o = levels[i];
var n = updates[i];
if (n) {
for (var key in o) {
if (!(key in n)) to_null_out[key] = 1;
}
for (var key in n) {
if (!accounted_for[key]) {
update[key] = n[key];
accounted_for[key] = 1;
}
}
levels[i] = n;
} else {
for (var key in o) {
accounted_for[key] = 1;
}
}
}
for (var key in to_null_out) {
if (!(key in update)) update[key] = undefined;
}
return update;
}

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

Loading…
Cancel
Save