diff --git a/.eslintignore b/.eslintignore index a0ca1e55c847..d1f48d64935c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ **/_actual.js **/expected.js +**/_output/**.js test/*/samples/*/output.js node_modules diff --git a/.gitignore b/.gitignore index 053f905294c8..b0e6896dbeba 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ node_modules /test/sourcemaps/samples/*/output.css.map /yarn-error.log _actual*.* +_output /types /site/cypress/screenshots/ diff --git a/package-lock.json b/package-lock.json index 93e762b3a8a4..97a19aeb6e49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -500,9 +500,9 @@ "dev": true }, "code-red": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.0.26.tgz", - "integrity": "sha512-W4t68vk3xJjmkbuAKfEtaj7E+K82BtV+A4VjBlxHA6gDoSLc+sTB643JdJMSk27vpp5iEqHFuGnHieQGy/GmUQ==", + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.0.27.tgz", + "integrity": "sha512-MSILIi8kkSGag76TW+PRfBP/dN++Ei+uTeiUfqW4Es5teFNrbsAWtyAbPwxKI1vxEsBx64loAfGxS1kVCo1d2g==", "dev": true, "requires": { "acorn": "^7.1.0", diff --git a/package.json b/package.json index fb1d7cbad34d..4d115cb778ea 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "acorn": "^7.1.0", "agadoo": "^1.1.0", "c8": "^5.0.1", - "code-red": "0.0.26", + "code-red": "0.0.27", "codecov": "^3.5.0", "css-tree": "1.0.0-alpha22", "eslint": "^6.3.0", diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index 29389ed3c3d8..0b2dc6bfbcec 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -204,7 +204,6 @@ export default class Renderer { ? x`$$self.$$.dirty` : x`#dirty`) as Identifier | MemberExpression; - let bitmask; const get_bitmask = () => { const bitmask: BitMasks = []; names.forEach((name) => { @@ -228,48 +227,30 @@ export default class Renderer { return bitmask; }; - let operator; - let left; - let right; - return { - get type() { - // we make the type a getter, even though it's always - // a BinaryExpression, because it gives us an opportunity - // to lazily create the node. TODO would be better if - // context was determined before rendering, so that - // this indirection was unnecessary - if (!bitmask) { - bitmask = get_bitmask(); - - if (!bitmask.length) { - ({ operator, left, right } = x`${dirty} & /*${names.join(', ')}*/ 0` as BinaryExpression); - } else if (renderer.context_overflow) { - const expression = bitmask - .map((b, i) => ({ b, i })) - .filter(({ b }) => b) - .map(({ b, i }) => x`${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`) - .reduce((lhs, rhs) => x`${lhs} | ${rhs}`); - - ({ operator, left, right } = expression as BinaryExpression); - } else { - ({ operator, left, right } = x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}` as BinaryExpression); - } + // Using a ParenthesizedExpression allows us to create + // the expression lazily. TODO would be better if + // context was determined before rendering, so that + // this indirection was unnecessary + type: 'ParenthesizedExpression', + get expression() { + const bitmask = get_bitmask(); + + if (!bitmask.length) { + return x`${dirty} & /*${names.join(', ')}*/ 0` as BinaryExpression; } + if (renderer.context_overflow) { + return bitmask + .map((b, i) => ({ b, i })) + .filter(({ b }) => b) + .map(({ b, i }) => x`${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`) + .reduce((lhs, rhs) => x`${lhs} | ${rhs}`); + } - return 'BinaryExpression'; - }, - get operator() { - return operator; - }, - get left() { - return left; - }, - get right() { - return right; + return x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}` as BinaryExpression; } - } as Expression; + } as any; } reference(node: string | Identifier | MemberExpression) { diff --git a/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts b/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts index 24ca813684b8..2adbd3b1d063 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts @@ -2,6 +2,7 @@ import Let from '../../../nodes/Let'; import { x, p } from 'code-red'; import Block from '../../Block'; import TemplateScope from '../../../nodes/shared/TemplateScope'; +import { BinaryExpression } from 'estree'; export function get_slot_definition(block: Block, scope: TemplateScope, lets: Let[]) { if (lets.length === 0) return { block, scope }; @@ -28,21 +29,48 @@ export function get_slot_definition(block: Block, scope: TemplateScope, lets: Le properties: Array.from(names).map(name => p`${block.renderer.context_lookup.get(name).index}: ${name}`) }; - const changes = Array.from(names) - .map(name => { - const { context_lookup } = block.renderer; + const { context_lookup } = block.renderer; - const literal = { - type: 'Literal', - get value() { + // i am well aware that this code is gross + // TODO make it less gross + const changes = { + type: 'ParenthesizedExpression', + get expression() { + if (block.renderer.context_overflow) { + const grouped = []; + + Array.from(names).forEach(name => { const i = context_lookup.get(name).index.value as number; - return 1 << i; + const g = Math.floor(i / 31); + + if (!grouped[g]) grouped[g] = []; + grouped[g].push({ name, n: i % 31 }); + }); + + const elements = []; + + for (let g = 0; g < grouped.length; g += 1) { + elements[g] = grouped[g] + ? grouped[g] + .map(({ name, n }) => x`${name} ? ${1 << n} : 0`) + .reduce((lhs, rhs) => x`${lhs} | ${rhs}`) + : x`0`; } - }; - return x`${name} ? ${literal} : 0`; - }) - .reduce((lhs, rhs) => x`${lhs} | ${rhs}`); + return { + type: 'ArrayExpression', + elements + }; + } + + return Array.from(names) + .map(name => { + const i = context_lookup.get(name).index.value as number; + return x`${name} ? ${1 << i} : 0`; + }) + .reduce((lhs, rhs) => x`${lhs} | ${rhs}`) as BinaryExpression; + } + }; return { block, diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index 7e8769cd885e..b844f1dd4c20 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -77,9 +77,23 @@ export function get_slot_context(definition, ctx, $$scope, fn) { } export function get_slot_changes(definition, $$scope, dirty, fn) { - return definition[2] && fn - ? $$scope.dirty | definition[2](fn(dirty)) - : $$scope.dirty; + if (definition[2] && fn) { + const lets = definition[2](fn(dirty)); + + if (typeof $$scope.dirty === 'object') { + const merged = []; + const len = Math.max($$scope.dirty.length, lets.length); + for (let i = 0; i < len; i += 1) { + merged[i] = $$scope.dirty[i] | lets[i]; + } + + return merged; + } + + return $$scope.dirty | lets; + } + + return $$scope.dirty; } export function exclude_internal_props(props) { diff --git a/test/helpers.js b/test/helpers.js index ff40ac5f79bc..2a03e0f436f4 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -1,6 +1,7 @@ import * as jsdom from 'jsdom'; import * as assert from 'assert'; import * as glob from 'tiny-glob/sync.js'; +import * as path from 'path'; import * as fs from 'fs'; import * as colors from 'kleur'; @@ -237,3 +238,16 @@ export function useFakeTimers() { } }; } + +export function mkdirp(dir) { + const parent = path.dirname(dir); + if (parent === dir) return; + + mkdirp(parent); + + try { + fs.mkdirSync(dir); + } catch (err) { + // do nothing + } +} \ No newline at end of file diff --git a/test/runtime/index.js b/test/runtime/index.js index 60bd70a5d9e2..408fda40f457 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -3,6 +3,7 @@ import * as path from "path"; import * as fs from "fs"; import { rollup } from 'rollup'; import * as virtual from 'rollup-plugin-virtual'; +import * as glob from 'tiny-glob/sync.js'; import { clear_loops, flush, set_now, set_raf } from "../../internal"; import { @@ -10,7 +11,8 @@ import { loadConfig, loadSvelte, env, - setupHtmlEqual + setupHtmlEqual, + mkdirp } from "../helpers.js"; let svelte$; @@ -90,6 +92,33 @@ describe("runtime", () => { const window = env(); + glob('**/*.svelte', { cwd }).forEach(file => { + if (file[0] === '_') return; + + const dir = `${cwd}/_output/${hydrate ? 'hydratable' : 'normal'}`; + const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`; + + if (fs.existsSync(out)) { + fs.unlinkSync(out); + } + + mkdirp(dir); + + try { + const { js } = compile( + fs.readFileSync(`${cwd}/${file}`, 'utf-8'), + { + ...compileOptions, + filename: file + } + ); + + fs.writeFileSync(out, js.code); + } catch (err) { + // do nothing + } + }); + return Promise.resolve() .then(() => { // hack to support transition tests @@ -195,7 +224,7 @@ describe("runtime", () => { } else { throw err; } - }).catch(err => { + }).catch(err => { failed.add(dir); showOutput(cwd, compileOptions, compile); // eslint-disable-line no-console throw err; diff --git a/test/runtime/samples/bitmask-overflow-slot/Echo.svelte b/test/runtime/samples/bitmask-overflow-slot/Echo.svelte new file mode 100644 index 000000000000..28eaa54060a3 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot/Echo.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-slot/_config.js b/test/runtime/samples/bitmask-overflow-slot/_config.js new file mode 100644 index 000000000000..9b24d5541faa --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot/_config.js @@ -0,0 +1,124 @@ +export default { + html: ` +

0

+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

5:36

+

6:37

+

38

+

0

+ `, + + test({ assert, component, target }) { + component.reads = {}; + + component._0 = 'a'; + component._30 = 'b'; + component._31 = 'c'; + component._32 = 'd'; + component._40 = 'e'; + + component._5 = 'f'; + component._6 = 'g'; + component._36 = 'h'; + component._37 = 'i'; + + assert.htmlEqual(target.innerHTML, ` +

a

+

1

+

2

+

3

+

4

+

f

+

g

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

b

+

c

+

d

+

33

+

34

+

35

+

h

+

i

+

38

+

39

+

e

+

f:h

+

g:i

+

38

+

a

+ `); + + assert.deepEqual(component.reads, { + _0: 1, + _5: 3, + _6: 3, + _30: 1, + _31: 1, + _32: 1, + _36: 3, + _37: 3, + _40: 1 + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-slot/main.svelte b/test/runtime/samples/bitmask-overflow-slot/main.svelte new file mode 100644 index 000000000000..89e60ce4b944 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot/main.svelte @@ -0,0 +1,107 @@ + + + +

{read(_0, '_0')}

+

{read(_1, '_1')}

+

{read(_2, '_2')}

+

{read(_3, '_3')}

+

{read(_4, '_4')}

+

{read(_5, '_5')}

+

{read(_6, '_6')}

+

{read(_7, '_7')}

+

{read(_8, '_8')}

+

{read(_9, '_9')}

+

{read(_10, '_10')}

+

{read(_11, '_11')}

+

{read(_12, '_12')}

+

{read(_13, '_13')}

+

{read(_14, '_14')}

+

{read(_15, '_15')}

+

{read(_16, '_16')}

+

{read(_17, '_17')}

+

{read(_18, '_18')}

+

{read(_19, '_19')}

+

{read(_20, '_20')}

+

{read(_21, '_21')}

+

{read(_22, '_22')}

+

{read(_23, '_23')}

+

{read(_24, '_24')}

+

{read(_25, '_25')}

+

{read(_26, '_26')}

+

{read(_27, '_27')}

+

{read(_28, '_28')}

+

{read(_29, '_29')}

+

{read(_30, '_30')}

+

{read(_31, '_31')}

+

{read(_32, '_32')}

+

{read(_33, '_33')}

+

{read(_34, '_34')}

+

{read(_35, '_35')}

+

{read(_36, '_36')}

+

{read(_37, '_37')}

+

{read(_38, '_38')}

+

{read(_39, '_39')}

+

{read(_40, '_40')}

+ +

{read(_5, '_5') + ':' + read(_36, '_36')}

+

{foo}

+

{bar}

+ +

{dummy}

+
\ No newline at end of file