├── test ├── helpers.js ├── htmlx2jsx │ ├── samples │ │ ├── element-only │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── action-bare │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── class-bare │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── comment │ │ │ ├── expected.jsx │ │ │ └── input.svelte │ │ ├── animation-bare │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── attribute-bare │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── debug-block │ │ │ ├── expected.jsx │ │ │ └── input.svelte │ │ ├── event-handler-bare │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── html-block │ │ │ ├── expected.jsx │ │ │ └── input.svelte │ │ ├── simple-expression │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── action-params │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── binding │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── attribute-quoted │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── attribute-shorthand │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── class │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── event-handler-component-bare │ │ │ ├── expected.jsx │ │ │ └── input.svelte │ │ ├── namespaced-attributes │ │ │ ├── expected.jsx │ │ │ └── input.svelte │ │ ├── attribute-text │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── binding-this │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── event-handler-modifiers │ │ │ ├── expected.jsx │ │ │ └── input.svelte │ │ ├── if-block │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── animation-params │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── attribute-multiple │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── binding-group │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── svg-attributes │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── each-block-basic │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── transition-modifiers │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── auto-closing-tag │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── binding-this-component │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── component-default-slot │ │ │ ├── expected.jsx │ │ │ └── input.svelte │ │ ├── each-block-index │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── binding-bare │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── component-no-slots │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── each-block-key │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── await-block-basic │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── event-handler-component │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── event-handler │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── transition-bare │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── component-default-slot-let │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── if-else-block │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── each-block-key-else │ │ │ ├── expected.jsx │ │ │ └── input.svelte │ │ ├── if-else-if-block │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── await-block-basic-catch │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── await-block-pending │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── void-elements │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── transition-params │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── await-block-pending-catch │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── component-multi-slot │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ ├── directive-quoted │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ │ └── binding-oneway │ │ │ ├── input.svelte │ │ │ └── expected.jsx │ └── index.js ├── svelte2tsx │ ├── samples │ │ ├── single-element │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── ast-offset-none │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── stores-mustache │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── ast-offset-some │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── uses-$$props │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── single-export │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── array-binding-export │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── export-has-type │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── export-interface │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── reactive-declare │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── typed-export-with-default │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── uses-$store │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── script-on-bottom │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── uses-$$props-script │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── binding-group-store │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── object-binding-export │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── self-closing-component │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── import-single-quote │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── export-references-local │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── component-default-slot │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── export-list │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── script-style-like-component │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── uses-$store-in-event-binding │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── renamed-exports │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── export-arrow-function │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── component-slot-crazy-attributes │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── multiple-export │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── imports │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── module-script-and-script-in-line │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── module-script-and-script-in-line2 │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── script-and-module-script │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── component-multiple-slots │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── module-script-and-script │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── module-script-and-script2 │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── await-with-$store │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── uses-svelte-components │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── nested-$-variables-script │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ ├── nested-$-variables-template │ │ │ ├── input.svelte │ │ │ └── expected.tsx │ │ └── circle-drawer-example │ │ │ ├── expected.tsx │ │ │ └── input.svelte │ ├── tsconfig.json │ └── index.js ├── sourcemaps │ ├── simple-element.htmlx.html │ ├── shorthand-prop.htmlx.html │ ├── let.html │ ├── event-binding.html │ ├── index.js │ └── repl.html └── test.js ├── mocha.opts ├── src ├── index.ts ├── svgattributes.ts ├── knownevents.ts ├── htmlxparser.ts ├── htmlxtojsx.ts └── svelte2tsx.ts ├── .gitignore ├── index.d.ts ├── .github └── workflows │ └── test.yml ├── tsconfig.json ├── rollup.config.js ├── .vscode └── launch.json ├── LICENSE ├── rollup.config.test.js ├── README.md ├── package.json └── svelte-shims.d.ts /test/helpers.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | test/test.js -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/element-only/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { svelte2tsx as default } from './svelte2tsx' -------------------------------------------------------------------------------- /test/svelte2tsx/samples/single-element/input.svelte: -------------------------------------------------------------------------------- 1 |

hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/action-bare/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/class-bare/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/comment/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | 3 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/element-only/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/ast-offset-none/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/stores-mustache/input.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/animation-bare/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-bare/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/debug-block/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{ myfile, someOtherFile } -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/debug-block/input.svelte: -------------------------------------------------------------------------------- 1 | {@debug myfile, someOtherFile } -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler-bare/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/html-block/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{ myfile + someOtherFile } -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/html-block/input.svelte: -------------------------------------------------------------------------------- 1 | {@html myfile + someOtherFile } -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/simple-expression/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello {name}

-------------------------------------------------------------------------------- /test/svelte2tsx/samples/ast-offset-some/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/uses-$$props/input.svelte: -------------------------------------------------------------------------------- 1 |

{$$props['name']}

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/action-params/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/simple-expression/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello {name}

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-bare/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-quoted/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-shorthand/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/class/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler-component-bare/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler-component-bare/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/namespaced-attributes/expected.jsx: -------------------------------------------------------------------------------- 1 | <> -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/namespaced-attributes/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-text/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler-bare/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-quoted/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding-this/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler-modifiers/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/if-block/input.svelte: -------------------------------------------------------------------------------- 1 | {#if name == "world"} 2 |

Hello {name}

3 | {/if} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/action-bare/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/animation-params/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-multiple/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-shorthand/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-text/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding-group/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/svg-attributes/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/single-export/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/each-block-basic/input.svelte: -------------------------------------------------------------------------------- 1 | {#each items as item} 2 |
{item}
3 | {/each} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/svg-attributes/expected.jsx: -------------------------------------------------------------------------------- 1 | <> -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/transition-modifiers/input.svelte: -------------------------------------------------------------------------------- 1 |
2 | {item} 3 |
-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/action-params/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/animation-bare/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/auto-closing-tag/input.svelte: -------------------------------------------------------------------------------- 1 |
2 |

test1 3 |

test2 4 |

5 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding-this-component/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/class-bare/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/component-default-slot/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 |

Hello

3 |
-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/component-default-slot/input.svelte: -------------------------------------------------------------------------------- 1 | 2 |

Hello

3 |
4 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/each-block-basic/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{(items).map((item) => <> 2 |
{item}
3 | )} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/each-block-index/input.svelte: -------------------------------------------------------------------------------- 1 | {#each items as item,i} 2 |
{item}{i}
3 | {/each} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/if-block/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{() => {if (name == "world"){<> 2 |

Hello {name}

3 | }}} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/auto-closing-tag/expected.jsx: -------------------------------------------------------------------------------- 1 | <>
2 |

test1 3 |

test2 4 |

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding-bare/input.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/class/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/comment/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/each-block-index/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{(items).map((item,i) => <> 2 |
{item}{i}
3 | )} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler-modifiers/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /test/svelte2tsx/samples/array-binding-export/input.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/attribute-multiple/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding-group/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/component-no-slots/input.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/each-block-key/input.svelte: -------------------------------------------------------------------------------- 1 | {#each items as item,i (item.id)} 2 |
{item}{i}
3 | {/each} -------------------------------------------------------------------------------- /test/svelte2tsx/samples/export-has-type/input.svelte: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/export-interface/input.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/reactive-declare/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/await-block-basic/input.svelte: -------------------------------------------------------------------------------- 1 | {#await somePromise then value} 2 |

Promise Resolved

3 | {/await} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/component-no-slots/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler-component/input.svelte: -------------------------------------------------------------------------------- 1 | click()} on:UpperCaseEvent={() => log('hi')}/> -------------------------------------------------------------------------------- /test/svelte2tsx/samples/typed-export-with-default/input.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/uses-$store/input.svelte: -------------------------------------------------------------------------------- 1 | 2 |

$b=$b.concat(5)}>{$b}

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/each-block-key/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{(items).map((item,i) => (item.id) && <> 2 |
{item}{i}
3 | )} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler/input.svelte: -------------------------------------------------------------------------------- 1 |

console.log("click")} on:UpperCaseEvent={() => log('hi')}>Hello

-------------------------------------------------------------------------------- /test/htmlx2jsx/samples/transition-modifiers/expected.jsx: -------------------------------------------------------------------------------- 1 | <>
2 | {item} 3 |
-------------------------------------------------------------------------------- /test/svelte2tsx/samples/script-on-bottom/input.svelte: -------------------------------------------------------------------------------- 1 |

hello {world}

2 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/uses-$$props-script/input.svelte: -------------------------------------------------------------------------------- 1 |

{name}

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding-bare/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | 3 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding-this/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

console.log("click")} onUpperCaseEvent={() => log('hi')}>Hello

2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/transition-bare/input.svelte: -------------------------------------------------------------------------------- 1 |

Hello

2 |

Hello

3 |

Hello

-------------------------------------------------------------------------------- /test/svelte2tsx/samples/binding-group-store/input.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/object-binding-export/input.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/self-closing-component/input.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/animation-params/expected.jsx: -------------------------------------------------------------------------------- 1 | <>

Hello

2 | 3 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/import-single-quote/input.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /index.js 4 | /index.js.map 5 | /index.mjs 6 | test/typecheck/samples/**/input.svelte.tsx 7 | test/build 8 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/component-default-slot-let/input.svelte: -------------------------------------------------------------------------------- 1 | 2 |

Hello {thing} {n}

3 |
4 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/if-else-block/input.svelte: -------------------------------------------------------------------------------- 1 | {#if name == "world"} 2 |

Hello {name}

3 | {:else} 4 |

hello {name}

5 | {/if} -------------------------------------------------------------------------------- /test/svelte2tsx/samples/export-references-local/input.svelte: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/binding-this-component/expected.jsx: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/component-default-slot/input.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 | Hello 6 |
7 | -------------------------------------------------------------------------------- /test/svelte2tsx/samples/export-list/input.svelte: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /test/sourcemaps/simple-element.htmlx.html: -------------------------------------------------------------------------------- 1 | <>

Hello World

2 | 1=================== 3 | 4 | !Expected 5 |

Hello World

6 | 1=================== 7 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/await-block-basic/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{() => {let _$$p = (somePromise); _$$p.then((value) => {<> 2 |

Promise Resolved

3 | })}} 4 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/each-block-key-else/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{(items).map((item,i) => (item.id) && <> 2 |
{item}{i}
3 | )} 4 |
No Items
5 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/each-block-key-else/input.svelte: -------------------------------------------------------------------------------- 1 | {#each items as item,i (item.id)} 2 |
{item}{i}
3 | {:else} 4 |
No Items
5 | {/each} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/if-else-block/expected.jsx: -------------------------------------------------------------------------------- 1 | <>{() => {if (name == "world"){<> 2 |

Hello {name}

3 | }else{<> 4 |

hello {name}

5 | }}} -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/event-handler-component/expected.jsx: -------------------------------------------------------------------------------- 1 | <> click()))} {...__sveltets_ensureFunction((() => log('hi')))}/> 2 | -------------------------------------------------------------------------------- /test/htmlx2jsx/samples/if-else-if-block/input.svelte: -------------------------------------------------------------------------------- 1 | {#if name1 == "world"} 2 |

Hello {name2}

3 | {:else if name3 == "person"} 4 |

hello {name4}

5 | {/if} -------------------------------------------------------------------------------- /test/sourcemaps/shorthand-prop.htmlx.html: -------------------------------------------------------------------------------- 1 | <> 2 | 2===== 1====== 3 | !Expected 4 | 5 | 2===== 1====== -------------------------------------------------------------------------------- /test/svelte2tsx/samples/script-style-like-component/input.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 101 | 102 |
103 | 104 | 105 |
106 | 107 | 108 | {#each circles as circle} 109 | 117 | {/each} 118 | 119 | 120 | {#if adjusting} 121 |
122 |

adjust diameter of circle at {selected.cx}, {selected.cy}

123 | 124 |
125 | {/if} -------------------------------------------------------------------------------- /src/knownevents.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | "oncopy", 3 | "oncopycapture", 4 | "oncut", 5 | "oncutcapture", 6 | "onpaste", 7 | "onpastecapture", 8 | 9 | // composition events 10 | "oncompositionend", 11 | "oncompositionendcapture", 12 | "oncompositionstart", 13 | "oncompositionstartcapture", 14 | "oncompositionupdate", 15 | "oncompositionupdatecapture", 16 | 17 | // focus events 18 | "onfocus", 19 | "onfocuscapture", 20 | "onblur", 21 | "onblurcapture", 22 | 23 | // form events 24 | "onchange", 25 | "onchangecapture", 26 | "oninput", 27 | "oninputcapture", 28 | "onreset", 29 | "onresetcapture", 30 | "onsubmit", 31 | "onsubmitcapture", 32 | 33 | // image events 34 | "onload", 35 | "onloadcapture", 36 | "onerror", 37 | "onerrorcapture", 38 | 39 | // keyboard events 40 | "onkeydown", 41 | "onkeydowncapture", 42 | "onkeypress", 43 | "onkeypresscapture", 44 | "onkeyup", 45 | "onkeyupcapture", 46 | 47 | // media events 48 | "onabort", 49 | "onabortcapture", 50 | "oncanplay", 51 | "oncanplaycapture", 52 | "oncanplaythrough", 53 | "oncanplaythroughcapture", 54 | "ondurationchange", 55 | "ondurationchangecapture", 56 | "onemptied", 57 | "onemptiedcapture", 58 | "onencrypted", 59 | "onencryptedcapture", 60 | "onended", 61 | "onendedcapture", 62 | "onloadeddata", 63 | "onloadeddatacapture", 64 | "onloadedmetadata", 65 | "onloadedmetadatacapture", 66 | "onloadstart", 67 | "onloadstartcapture", 68 | "onpause", 69 | "onpausecapture", 70 | "onplay", 71 | "onplaycapture", 72 | "onplaying", 73 | "onplayingcapture", 74 | "onprogress", 75 | "onprogresscapture", 76 | "onratechange", 77 | "onratechangecapture", 78 | "onseeked", 79 | "onseekedcapture", 80 | "onseeking", 81 | "onseekingcapture", 82 | "onstalled", 83 | "onstalledcapture", 84 | "onsuspend", 85 | "onsuspendcapture", 86 | "ontimeupdate", 87 | "ontimeupdatecapture", 88 | "onvolumechange", 89 | "onvolumechangecapture", 90 | "onwaiting", 91 | "onwaitingcapture", 92 | 93 | // mouseevents 94 | "onclick", 95 | "onclickcapture", 96 | "oncontextmenu", 97 | "oncontextmenucapture", 98 | "ondoubleclick", 99 | "ondoubleclickcapture", 100 | "ondrag", 101 | "ondragcapture", 102 | "ondragend", 103 | "ondragendcapture", 104 | "ondragenter", 105 | "ondragentercapture", 106 | "ondragexit", 107 | "ondragexitcapture", 108 | "ondragleave", 109 | "ondragleavecapture", 110 | "ondragover", 111 | "ondragovercapture", 112 | "ondragstart", 113 | "ondragstartcapture", 114 | "ondrop", 115 | "ondropcapture", 116 | "onmousedown", 117 | "onmousedowncapture", 118 | "onmouseenter", 119 | "onmouseleave", 120 | "onmousemove", 121 | "onmousemovecapture", 122 | "onmouseout", 123 | "onmouseoutcapture", 124 | "onmouseover", 125 | "onmouseovercapture", 126 | "onmouseup", 127 | "onmouseupcapture", 128 | 129 | // selection events 130 | "onselect", 131 | "onselectcapture", 132 | 133 | // touch events 134 | "ontouchcancel", 135 | "ontouchcancelcapture", 136 | "ontouchend", 137 | "ontouchendcapture", 138 | "ontouchmove", 139 | "ontouchmovecapture", 140 | "ontouchstart", 141 | "ontouchstartcapture", 142 | 143 | // ui events 144 | "onscroll", 145 | "onscrollcapture", 146 | 147 | // wheel events 148 | "onwheel", 149 | "onwheelcapture", 150 | 151 | // animation events 152 | "onanimationstart", 153 | "onanimationstartcapture", 154 | "onanimationend", 155 | "onanimationendcapture", 156 | "onanimationiteration", 157 | "onanimationiterationcapture", 158 | 159 | // transition events 160 | "ontransitionend", 161 | "ontransitionendcapture" 162 | ] -------------------------------------------------------------------------------- /src/htmlxparser.ts: -------------------------------------------------------------------------------- 1 | import parse5, { DefaultTreeDocumentFragment, DefaultTreeElement, DefaultTreeTextNode, DefaultTreeNode } from 'parse5' 2 | import compiler from 'svelte/compiler' 3 | import { Node } from 'svelte/types/compiler/interfaces'; 4 | 5 | 6 | 7 | function walkAst(doc: DefaultTreeElement, action: (c: DefaultTreeElement) => void) { 8 | action(doc); 9 | if (!doc.childNodes) return; 10 | for (let i = 0; i < doc.childNodes.length; i++) { 11 | walkAst(doc.childNodes[i] as DefaultTreeElement, action); 12 | } 13 | } 14 | 15 | export function findVerbatimElements(htmlx: string) { 16 | let elements:Node[] = [] 17 | let tag_names = ['script', 'style']; 18 | 19 | let doc: DefaultTreeDocumentFragment = parse5.parseFragment (htmlx, { sourceCodeLocationInfo: true }) as DefaultTreeDocumentFragment; 20 | 21 | const checkCase = (content: DefaultTreeTextNode, el: parse5.DefaultTreeElement) => { 22 | const orgStart = el.sourceCodeLocation.startOffset || 0; 23 | const orgEnd = el.sourceCodeLocation.endOffset || 0; 24 | const outerHtml = htmlx.substring(orgStart, orgEnd); 25 | const onlyTag = content ? outerHtml.replace(content.value, '') : outerHtml; 26 | 27 | return tag_names.some(tag => onlyTag.match(tag)); 28 | } 29 | 30 | 31 | walkAst(doc as DefaultTreeElement, el => { 32 | if (tag_names.includes(el.nodeName)) { 33 | let content = (el.childNodes && el.childNodes.length > 0) ? el.childNodes[0] as DefaultTreeTextNode : null; 34 | if(!checkCase(content, el)) { 35 | return; 36 | } 37 | elements.push({ 38 | start: el.sourceCodeLocation.startOffset, 39 | end: el.sourceCodeLocation.endOffset, 40 | type: el.nodeName[0].toUpperCase() + el.nodeName.substr(1), 41 | attributes: !el.attrs ? [] : el.attrs.map(a => {return { 42 | type: "Attribute", 43 | name: a.name, 44 | value: [{ 45 | type: "Text", 46 | start: htmlx.indexOf("=", el.sourceCodeLocation.attrs[a.name].startOffset) +1, 47 | end: el.sourceCodeLocation.attrs[a.name].endOffset, 48 | raw: a.value, 49 | }], 50 | start: el.sourceCodeLocation.attrs[a.name].startOffset, 51 | end: el.sourceCodeLocation.attrs[a.name].endOffset 52 | }}), 53 | content: !content ? null : { 54 | type: "Text", 55 | start: content.sourceCodeLocation.startOffset, 56 | end: content.sourceCodeLocation.endOffset, 57 | value: content.value, 58 | raw: content.value 59 | } 60 | }); 61 | } 62 | }); 63 | 64 | return elements; 65 | } 66 | 67 | export function blankVerbatimContent(htmlx: string, verbatimElements: Node[]) { 68 | let output = htmlx; 69 | for (var node of verbatimElements) { 70 | let content = node.content; 71 | if (content) { 72 | output = output.substring(0, content.start) 73 | + output.substring(content.start, content.end).replace(/[^\n]/g, " ") 74 | + output.substring(content.end); 75 | } 76 | } 77 | return output 78 | } 79 | 80 | 81 | export function parseHtmlx(htmlx: string): Node { 82 | //Svelte tries to parse style and script tags which doesn't play well with typescript, so we blank them out. 83 | //HTMLx spec says they should just be retained after processing as is, so this is fine 84 | let verbatimElements = findVerbatimElements(htmlx); 85 | let deconstructed = blankVerbatimContent(htmlx, verbatimElements); 86 | 87 | //extract the html content parsed as htmlx this excludes our script and style tags 88 | let svelteHtmlxAst = compiler.parse(deconstructed).html; 89 | 90 | //restore our script and style tags as nodes to maintain validity with HTMLx 91 | for (var s of verbatimElements) { 92 | svelteHtmlxAst.children.push(s); 93 | svelteHtmlxAst.start = Math.min(svelteHtmlxAst.start, s.start); 94 | svelteHtmlxAst.end = Math.max(svelteHtmlxAst.end, s.end); 95 | } 96 | return svelteHtmlxAst; 97 | } 98 | -------------------------------------------------------------------------------- /test/sourcemaps/index.js: -------------------------------------------------------------------------------- 1 | let svelte2tsx = require('../build/index') 2 | let converter = require('../build/htmlxtojsx') 3 | let fs = require('fs') 4 | let assert = require('assert') 5 | let sm = require('source-map') 6 | 7 | 8 | 9 | describe('sourcemap', () => { 10 | /** 11 | * 12 | * @param {string} input 13 | * 14 | * @returns { {source: string, locations: Map } 15 | */ 16 | function extractLocations(input) { 17 | let lines = input.split("\n") 18 | let line 19 | let source_line = 0; 20 | let source = [] 21 | let locations = new Map() 22 | while (lines.length) { 23 | line = lines.shift() 24 | //are we a range line, we test to see if it starts with whitespace followed by a digit 25 | if (/^\s*[\d=]+[\s\d=]*$/.test(line)) { 26 | //create the ranges 27 | let currentId = null 28 | let offset = 0 29 | let offsets = [] 30 | let start = 0 31 | const endSpan = () => { 32 | if (offsets.length) { 33 | locations.set(currentId, { line: source_line, start: start, offsets:offsets }); 34 | } 35 | offset = 0 36 | offsets = [] 37 | } 38 | 39 | for (let char = 0; char < line.length; char++) { 40 | let c = line[char] 41 | let isDigit = /\d/.test(c), isEquals = /=/.test(c) 42 | if (isDigit) { 43 | endSpan() 44 | currentId = c 45 | start = char + 1; 46 | } 47 | if (isEquals || isDigit) { 48 | offsets.push(offset) 49 | offset++; 50 | } else { 51 | endSpan() 52 | } 53 | } 54 | endSpan(); 55 | } else { 56 | //we are a source line 57 | source.push(line) 58 | source_line++; 59 | } 60 | } 61 | 62 | return { source: source.join("\n") , locations } 63 | } 64 | 65 | 66 | fs.readdirSync(`${__dirname}`).forEach(dir => { 67 | if (dir[0] === '.') return; 68 | 69 | if (!dir.endsWith(".html") && !dir.endsWith(".html.solo")) return; 70 | 71 | // add .solo to a sample directory name to only run that test 72 | const solo = /\.solo$/.test(dir); 73 | 74 | if (solo && process.env.CI) { 75 | throw new Error( 76 | `Forgot to remove '.solo' from test parser/samples/${dir}` 77 | ); 78 | } 79 | 80 | let showWhitespace = (str) => { 81 | return str.replace(/\t/g, "\\t").replace(/\n/g,"\\n\n").replace(/\r/g, "\\r") 82 | } 83 | 84 | (solo ? it.only : it)(dir, () => { 85 | const testContent = fs.readFileSync(`${__dirname}/${dir}`, 'utf-8').replace(/\r\n/g, "\n").replace(/\t/g, " "); 86 | 87 | let [inputBlock, expectedBlock] = testContent.split(/\n!Expected.*?\n/) 88 | let input = extractLocations(inputBlock); 89 | let expected = extractLocations(expectedBlock); 90 | 91 | 92 | // seems backwards but we don't have an "input" source map, so we generate one from our expected output 93 | // but assert that the source it generates matches our input source. 94 | //console.log(expected.source) 95 | const { map, code } = dir.endsWith(".htmlx.html") ? converter.htmlx2jsx(expected.source) : svelte2tsx(expected.source); 96 | assert.equal(showWhitespace(code) , showWhitespace(input.source), "Couldn't generate input source map for test"); 97 | 98 | let decoder = new sm.SourceMapConsumer(map); 99 | 100 | for (let [id, span] of input.locations.entries()) { 101 | let expectedSpan = expected.locations.get(id); 102 | 103 | //walk our generated span checking it lines up 104 | let col = span.start 105 | let input_line = span.line 106 | let expected_line = expectedSpan.line 107 | 108 | 109 | assert.ok(input.source); 110 | let error_source = input.source.split("\n")[span.line-1]; 111 | assert.ok(error_source); 112 | let error_map = new Array(error_source.length).fill(" "); 113 | 114 | let actual_result_line, actual_result_source, actual_result 115 | let errorCount = 0; 116 | 117 | for (var off = 0; off < span.offsets.length; off++) { 118 | let input_col = col + off; 119 | let expected_col = expectedSpan.start + span.offsets[off]; 120 | 121 | //originalPositionFor uses 0 base cols and 1 base lines.... 122 | let { line: actual_line, column: decoded_col } = decoder.originalPositionFor({ line: input_line, column: input_col - 1 }) 123 | let actual_col = decoded_col + 1; 124 | 125 | if (!actual_result) { 126 | actual_result_source = expected.source.split("\n")[actual_line-1] 127 | actual_result = new Array(actual_result_source.length).fill(" "); 128 | actual_result_line = actual_line 129 | } 130 | if (actual_line == actual_result_line) { 131 | if (actual_result_line[actual_col - 1] == " ") { 132 | actual_result[actual_col - 1] = "1" 133 | } else { 134 | //track number of characters mapped to result 135 | actual_result[actual_col - 1] = `${Math.min((actual_result[actual_col - 1] << 0) + 1, 9)}` 136 | } 137 | } else { 138 | actual_result = actual_result + "X" 139 | } 140 | 141 | if (actual_col != expected_col || actual_line != expected_line) { 142 | errorCount++; 143 | error_map[input_col-1] = "X" 144 | } else { 145 | error_map[input_col-1] = "=" 146 | } 147 | } 148 | 149 | if (errorCount != 0) { 150 | assert.fail(` 151 | Errors on span ${id} 152 | 153 | Output 154 | ${actual_result_source} 155 | ${actual_result.join("").replace(/1/g,"=")} 156 | 157 | Errors 158 | ${error_source} 159 | ${error_map.join("")} 160 | `) 161 | } 162 | } 163 | }); 164 | }); 165 | }); -------------------------------------------------------------------------------- /test/sourcemaps/repl.html: -------------------------------------------------------------------------------- 1 | <>; 2 | export async function preload({ params }) { 3 | const res = await this.fetch(`tutorial/${params.slug}.json`); 4 | 5 | if (!res.ok) { 6 | return this.redirect(301, `tutorial/basics`); 7 | } 8 | 9 | return { 10 | slug: params.slug, 11 | chapter: await res.json() 12 | }; 13 | } 14 | ;<>;import Repl from '@sveltejs/svelte-repl'; 15 | import { getContext } from 'svelte'; 16 | import ScreenToggle from '../../../components/ScreenToggle.svelte'; 17 | import TableOfContents from './_TableOfContents.svelte'; 18 | import { 19 | mapbox_setup, // needed for context API tutorial 20 | rollupUrl, 21 | svelteUrl 22 | } from '../../../config'; 23 | function render() { 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | let slug; 34 | let chapter; 35 | 36 | const { sections } = getContext('tutorial'); 37 | 38 | let repl; 39 | let prev; 40 | let scrollable; 41 | const lookup = new Map(); 42 | 43 | let width = process.browser ? window.innerWidth : 1000; 44 | let offset = 0; 45 | 46 | sections.forEach(section => { 47 | section.chapters.forEach(chapter => { 48 | const obj = { 49 | slug: chapter.slug, 50 | section, 51 | chapter, 52 | prev 53 | }; 54 | 55 | lookup.set(chapter.slug, obj); 56 | 57 | if (process.browser) { // pending https://github.com/sveltejs/svelte/issues/2135 58 | if (prev) prev.next = obj; 59 | prev = obj; 60 | } 61 | }); 62 | }); 63 | 64 | // TODO is there a non-hacky way to trigger scroll when chapter changes? 65 | $: if (scrollable) chapter, scrollable.scrollTo(0, 0); 66 | 67 | // TODO: this will need to be changed to the master branch, and probably should be dynamic instead of included 68 | // here statically 69 | const tutorial_repo_link = 'https://github.com/sveltejs/svelte/tree/master/site/content/tutorial'; 70 | 71 | ;let selected; $: selected = lookup.get(slug); 72 | ;let improve_link; $: improve_link = `${tutorial_repo_link}/${selected.chapter.section_dir}/${selected.chapter.chapter_dir}`; 73 | 74 | const clone = file => ({ 75 | name: file.name, 76 | type: file.type, 77 | source: file.source 78 | }); 79 | 80 | $: if (repl) { 81 | completed = false; 82 | repl.set({ 83 | components: chapter.app_a.map(clone) 84 | }); 85 | } 86 | 87 | ;let mobile; $: mobile = width < 768; 88 | 89 | function reset() { 90 | repl.update({ 91 | components: chapter.app_a.map(clone) 92 | }); 93 | } 94 | 95 | function complete() { 96 | repl.update({ 97 | components: chapter.app_b.map(clone) 98 | }); 99 | } 100 | 101 | let completed = false; 102 | 103 | function handle_change(event) { 104 | completed = event.detail.components.every((file, i) => { 105 | const expected = chapter.app_b[i]; 106 | return expected && ( 107 | file.name === expected.name && 108 | file.type === expected.type && 109 | file.source.trim().replace(/\s+$/gm, '') === expected.source.trim().replace(/\s+$/gm, '') 110 | ); 111 | }); 112 | } 113 | ; 114 | <> 115 | 116 | 117 | 118 | 119 | 120 | 121 | {selected.section.title} / {selected.chapter.title} • Svelte Tutorial 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 |
132 |
133 |
134 | 135 |
136 | 137 |
138 | { chapter.html} 139 | 140 |
141 | {() => {if (chapter.app_b){<> 142 | 143 | 146 | }}} 147 | 148 | {() => {if (selected.next){<> 149 | 150 | }}} 151 |
152 | 153 |
154 | Edit this chapter 155 |
156 |
157 |
158 | 159 |
160 | 171 |
172 |
173 | 174 | {() => {if (mobile){<> 175 | 176 | }}} 177 |
178 | return { props: {slug , chapter}, slots: {} }} 179 | 180 | export default class { 181 | $$prop_def = __sveltets_partial(render().props) 182 | $$slot_def = render().slots 183 | } 184 | !Expected 185 | 199 | 200 | 295 | 296 | 445 | 446 | 447 | {selected.section.title} / {selected.chapter.title} • Svelte Tutorial 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 |
457 |
458 |
459 |
460 | 461 |
462 | 463 |
464 | {@html chapter.html} 465 | 466 |
467 | {#if chapter.app_b} 468 | 470 | 473 | {/if} 474 | 475 | {#if selected.next} 476 | 477 | {/if} 478 |
479 | 480 |
481 | Edit this chapter 482 |
483 |
484 |
485 | 486 |
487 | 498 |
499 |
500 | 501 | {#if mobile} 502 | 503 | {/if} 504 |
-------------------------------------------------------------------------------- /src/htmlxtojsx.ts: -------------------------------------------------------------------------------- 1 | import MagicString from 'magic-string'; 2 | import svelte from 'svelte/compiler'; 3 | import { Node } from 'estree-walker'; 4 | import { parseHtmlx } from './htmlxparser'; 5 | import KnownEvents from './knownevents'; 6 | import svgAttributes from './svgattributes'; 7 | 8 | type ElementType = String 9 | const oneWayBindingAttributes:Map = new Map( 10 | ['clientWidth', 'clientHeight', 'offsetWidth', 'offsetHeight'].map(e => [e, 'HTMLDivElement'] as [String, String]).concat( 11 | ['duration','buffered','seekable','seeking', 'played', 'ended'].map(e => [e, 'HTMLMediaElement']) 12 | )); 13 | 14 | 15 | export function convertHtmlxToJsx(str: MagicString, ast: Node, onWalk: (node: Node, parent: Node, prop:string, index: number) => void = null, onLeave: (node: Node, parent: Node, prop: string, index: number) => void = null) { 16 | let htmlx = str.original; 17 | str.prepend("<>"); 18 | str.append(""); 19 | const handleRaw = (rawBlock: Node) => { 20 | let tokenStart = htmlx.indexOf("@html", rawBlock.start); 21 | str.remove(tokenStart, tokenStart + "@html".length); 22 | }; 23 | const handleDebug = (debugBlock: Node) => { 24 | let tokenStart = htmlx.indexOf("@debug", debugBlock.start); 25 | str.remove(tokenStart, tokenStart + "@debug".length); 26 | }; 27 | 28 | const handleEventHandler = (attr: Node, parent: Node) => { 29 | let jsxEventName = attr.name; 30 | 31 | if (parent.type == "Element" /*&& KnownEvents.indexOf('on'+jsxEventName) >= 0*/) { 32 | if (attr.expression) { 33 | let endAttr = htmlx.indexOf("=", attr.start) 34 | str.overwrite(attr.start+'on:'.length-1, endAttr, jsxEventName) 35 | if (htmlx[attr.end - 1] == '"') { 36 | let firstQuote = htmlx.indexOf('"', endAttr); 37 | str.remove(firstQuote, firstQuote + 1); 38 | str.remove(attr.end - 1, attr.end); 39 | } 40 | } else { 41 | str.overwrite(attr.start+'on:'.length-1, attr.end, `${jsxEventName}={null}`) 42 | } 43 | } else { 44 | //We don't know the type of the event handler 45 | if (attr.expression) { 46 | //for handler assignment, we changeIt to call to our __sveltets_ensureFunction 47 | str.overwrite(attr.start, attr.expression.start, "{...__sveltets_ensureFunction(("); 48 | str.overwrite(attr.expression.end, attr.end, "))}"); 49 | } else { 50 | //for passthrough handlers, we just remove 51 | str.remove(attr.start, attr.end) 52 | } 53 | } 54 | } 55 | 56 | const handleClassDirective = (attr: Node) => { 57 | let needCurly = (attr.expression.start == attr.start + "class:".length); 58 | str.overwrite(attr.start, attr.expression.start, `{...__sveltets_ensureType(Boolean, !!(`) 59 | str.appendLeft(attr.expression.end, `))${needCurly ? "}" : ""}`) 60 | if (htmlx[attr.end - 1] == '"') { 61 | str.remove(attr.end - 1, attr.end); 62 | } 63 | } 64 | 65 | const handleActionDirective = (attr: Node) => { 66 | str.overwrite(attr.start, attr.start + "use:".length, "{...__sveltets_ensureAction(") 67 | 68 | if (!attr.expression) { 69 | str.appendLeft(attr.end, ")}"); 70 | return; 71 | } 72 | 73 | str.overwrite(attr.start + `use:${attr.name}`.length, attr.expression.start, ",") 74 | str.appendLeft(attr.expression.end, ")"); 75 | if (htmlx[attr.end - 1] == '"') { 76 | str.remove(attr.end - 1, attr.end); 77 | } 78 | } 79 | 80 | const handleTransitionDirective = (attr: Node) => { 81 | str.overwrite(attr.start, htmlx.indexOf(":", attr.start) + 1, "{...__sveltets_ensureTransition(") 82 | 83 | if (attr.modifiers.length) { 84 | let local = htmlx.indexOf("|", attr.start); 85 | str.remove(local, attr.expression ? attr.expression.start : attr.end); 86 | } 87 | 88 | if (!attr.expression) { 89 | str.appendLeft(attr.end, ", {})}"); 90 | return; 91 | } 92 | 93 | str.overwrite(htmlx.indexOf(":", attr.start) + 1 + `${attr.name}`.length, attr.expression.start, ", ") 94 | str.appendLeft(attr.expression.end, ")"); 95 | if (htmlx[attr.end - 1] == '"') { 96 | str.remove(attr.end - 1, attr.end); 97 | } 98 | } 99 | 100 | const handleAnimateDirective = (attr: Node) => { 101 | str.overwrite(attr.start, htmlx.indexOf(":", attr.start) + 1, "{...__sveltets_ensureAnimation(") 102 | 103 | if (!attr.expression) { 104 | str.appendLeft(attr.end, ", {})}"); 105 | return; 106 | } 107 | str.overwrite(htmlx.indexOf(":", attr.start) + 1 + `${attr.name}`.length, attr.expression.start, ", ") 108 | str.appendLeft(attr.expression.end, ")"); 109 | if (htmlx[attr.end - 1] == '"') { 110 | str.remove(attr.end - 1, attr.end); 111 | } 112 | } 113 | 114 | const handleBinding = (attr: Node, el: Node) => { 115 | //bind group on input 116 | if (attr.name == "group" && el.name == "input") { 117 | str.remove(attr.start, attr.expression.start); 118 | str.appendLeft(attr.expression.start, "{...__sveltets_any(") 119 | str.overwrite(attr.expression.end, attr.end, ")}") 120 | return; 121 | } 122 | 123 | //bind this on element 124 | if (attr.name == "this" && el.type == "Element") { 125 | str.remove(attr.start, attr.expression.start) 126 | str.appendLeft(attr.expression.start, "{...__sveltets_ensureType(HTMLElement, "); 127 | str.overwrite(attr.expression.end, attr.end, ")}"); 128 | return; 129 | } 130 | 131 | //bind this on component 132 | if (attr.name == "this" && el.type == "InlineComponent") { 133 | str.remove(attr.start, attr.expression.start) 134 | str.appendLeft(attr.expression.start, `{...__sveltets_ensureType(${el.name}, `); 135 | str.overwrite(attr.expression.end, attr.end, ")}"); 136 | return; 137 | } 138 | 139 | //one way binding 140 | if (oneWayBindingAttributes.has(attr.name) && el.type == "Element") { 141 | str.remove(attr.start, attr.expression.start) 142 | str.appendLeft(attr.expression.start, `{...__sveltets_any(`); 143 | if (attr.expression.end == attr.end) { 144 | str.appendLeft(attr.end, `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${attr.name})}`) 145 | } else { 146 | str.overwrite(attr.expression.end, attr.end, `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${attr.name})}`) 147 | } 148 | return; 149 | } 150 | 151 | 152 | str.remove(attr.start, attr.start + "bind:".length); 153 | if (attr.expression.start == attr.start + "bind:".length) { 154 | str.appendLeft(attr.end, `={${attr.name}}`); 155 | return 156 | } 157 | 158 | //remove possible quotes 159 | if (htmlx[attr.end - 1] == '"') { 160 | let firstQuote = htmlx.indexOf('"', attr.start); 161 | str.remove(firstQuote, firstQuote + 1); 162 | str.remove(attr.end - 1, attr.end); 163 | } 164 | 165 | } 166 | 167 | const handleSlot = (slotEl: Node, componentName: string, slotName: string) => { 168 | //collect "let" definitions 169 | let hasMoved = false; 170 | for (let attr of slotEl.attributes) { 171 | if (attr.type != "Let") continue; 172 | 173 | if (slotEl.children.length == 0) { 174 | //no children anyway, just wipe out the attribute 175 | str.remove(attr.start, attr.end); 176 | continue; 177 | } 178 | var afterTag = afterTag || htmlx.lastIndexOf(">", slotEl.children[0].start) + 1; 179 | 180 | str.move(attr.start, attr.end, afterTag); 181 | 182 | //remove let: 183 | if (hasMoved) { 184 | str.overwrite(attr.start, attr.start + "let:".length, ", "); 185 | } else { 186 | str.remove(attr.start, attr.start + "let:".length); 187 | } 188 | hasMoved = true; 189 | if (attr.expression) { 190 | //overwrite the = as a : 191 | let equalSign = htmlx.lastIndexOf("=", attr.expression.start); 192 | let curly = htmlx.lastIndexOf("{", attr.expression.start); 193 | str.overwrite(equalSign, curly + 1, ":"); 194 | str.remove(attr.expression.end, attr.end); 195 | } 196 | } 197 | if (!hasMoved) return; 198 | str.appendLeft(afterTag, "{() => { let {"); 199 | str.appendRight(afterTag, "} = __sveltets_instanceOf(" + componentName + ").$$slot_def." + slotName + ";<>") 200 | 201 | let closeTagStart = htmlx.lastIndexOf("<", slotEl.end) 202 | str.appendLeft(closeTagStart, "}}") 203 | } 204 | 205 | 206 | const handleComponent = (el: Node) => { 207 | //we need to remove : if it is a svelte component 208 | if (el.name.startsWith("svelte:")) { 209 | let colon = htmlx.indexOf(":", el.start); 210 | str.remove(colon, colon+1); 211 | 212 | let closeTag = htmlx.lastIndexOf("/"+el.name, el.end) 213 | if (closeTag > el.start) { 214 | let colon = htmlx.indexOf(":",closeTag) 215 | str.remove(colon, colon + 1); 216 | } 217 | } 218 | 219 | //we only need to do something if there is a let or slot 220 | handleSlot(el, el.name, "default"); 221 | 222 | //walk the direct children looking for slots. We do this here because we need the name of our component for handleSlot 223 | //we could lean on leave/enter, but I am lazy 224 | if (!el.children) return; 225 | for (let child of el.children) { 226 | if (!child.attributes) continue; 227 | let slot = child.attributes.find(a => a.name == "slot"); 228 | if (slot) { 229 | if (slot.value && slot.value.length) { 230 | handleSlot(child, el.name, slot.value[0].raw) 231 | } 232 | } 233 | } 234 | } 235 | 236 | const handleAttribute = (attr: Node, parent: Node) => { 237 | 238 | //if we are on an "element" we are case insensitive, lowercase to match our JSX 239 | if (parent.type == "Element") { 240 | //skip Attribute shorthand, that is handled below 241 | if (attr.value !== true && !(attr.value.length && attr.value.length == 1 && attr.value[0].type == "AttributeShorthand")) { 242 | 243 | let name = attr.name; 244 | if (!svgAttributes.find(x => x == name)) { 245 | name = name.toLowerCase(); 246 | } 247 | 248 | //strip ":" from out attribute name and uppercase the next letter to convert to jsx attribute 249 | let colonIndex = name.indexOf(":"); 250 | if (colonIndex >= 0) { 251 | let parts = name.split(":"); 252 | name = parts[0] + parts[1][0].toUpperCase() + parts[1].substring(1); 253 | } 254 | 255 | str.overwrite(attr.start, attr.start + attr.name.length, name) 256 | } 257 | 258 | } 259 | 260 | 261 | 262 | //we are a bare attribute 263 | if (attr.value === true) return; 264 | 265 | if (attr.value.length == 0) return; //wut? 266 | //handle single value 267 | if (attr.value.length == 1) { 268 | let attrVal = attr.value[0]; 269 | 270 | if (attr.name == "slot") { 271 | str.remove(attr.start, attr.end); 272 | return; 273 | } 274 | 275 | if (attrVal.type == "AttributeShorthand") { 276 | let attrName = attrVal.expression.name 277 | if (parent.type == "Element") { 278 | attrName = svgAttributes.find(a => a == attrName) ? attrName :attrName.toLowerCase() 279 | } 280 | 281 | str.appendRight(attr.start, `${attrName}=`); 282 | return; 283 | } 284 | 285 | let equals = htmlx.lastIndexOf("=", attrVal.start); 286 | if (attrVal.type == "Text") { 287 | if (attrVal.end == attr.end) { 288 | //we are not quoted. Add some 289 | str.prependRight(equals + 1, '"'); 290 | str.appendLeft(attr.end, '"'); 291 | } 292 | return; 293 | } 294 | 295 | if (attrVal.type == "MustacheTag") { 296 | //if the end doesn't line up, we are wrapped in quotes 297 | if (attrVal.end != attr.end) { 298 | str.remove(attrVal.start - 1, attrVal.start); 299 | str.remove(attr.end - 1, attr.end); 300 | } 301 | return; 302 | } 303 | return; 304 | } 305 | 306 | // we have multiple attribute values, so we build a string out of them. 307 | // technically the user can do something funky like attr="text "{value} or even attr=text{value} 308 | // so instead of trying to maintain a nice sourcemap with prepends etc, we just overwrite the whole thing 309 | 310 | let equals = htmlx.lastIndexOf("=", attr.value[0].start); 311 | str.overwrite(equals, attr.value[0].start, "={`"); 312 | 313 | for(let n of attr.value) { 314 | if (n.type == "MustacheTag") { 315 | str.appendRight(n.start, "$") 316 | } 317 | } 318 | 319 | if (htmlx[attr.end-1] == '"') { 320 | str.overwrite(attr.end-1, attr.end, "`}") 321 | } else { 322 | str.appendLeft(attr.end, "`}") 323 | } 324 | 325 | } 326 | 327 | const handleElement = (node: Node) => { 328 | //we just have to self close void tags since jsx always wants the /> 329 | let voidTags = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr".split(','); 330 | if (voidTags.find(x => x == node.name)) { 331 | if (htmlx[node.end - 2] != '/') { 332 | str.appendRight(node.end - 1, "/"); 333 | } 334 | } 335 | 336 | //some tags auto close when they encounter certain elements, jsx doesn't support this 337 | if (htmlx[node.end-1] != '>') { 338 | str.appendRight(node.end, ``) 339 | } 340 | } 341 | 342 | const handleIf = (ifBlock: Node) => { 343 | if (ifBlock.elseif) { 344 | //we are an elseif so our work is easier 345 | str.appendLeft(ifBlock.expression.start,"(") 346 | str.appendLeft(ifBlock.expression.end, ")"); 347 | return; 348 | } 349 | // {#if expr} -> 350 | // {() => { if (expr) { <> 351 | str.overwrite(ifBlock.start, ifBlock.expression.start, "{() => {if ("); 352 | let end = htmlx.indexOf("}", ifBlock.expression.end); 353 | str.appendLeft(ifBlock.expression.end, ")"); 354 | str.overwrite(end, end + 1, "{<>"); 355 | 356 | // {/if} -> }}} 357 | let endif = htmlx.lastIndexOf("{", ifBlock.end); 358 | str.overwrite(endif, ifBlock.end, "}}}"); 359 | }; 360 | 361 | // {:else} -> } else {<> 362 | const handleElse = (elseBlock: Node, parent: Node) => { 363 | if (parent.type != "IfBlock") return; 364 | let elseEnd = htmlx.lastIndexOf("}", elseBlock.start); 365 | let elseword = htmlx.lastIndexOf(":else", elseEnd); 366 | let elseStart = htmlx.lastIndexOf("{", elseword); 367 | str.overwrite(elseStart, elseStart + 1, "}"); 368 | str.overwrite(elseEnd, elseEnd + 1, "{<>"); 369 | let colon = htmlx.indexOf(":", elseword); 370 | str.remove(colon, colon + 1); 371 | } 372 | 373 | const handleEach = (eachBlock: Node) => { 374 | // {#each items as item,i (key)} -> 375 | // {(items).map((item,i) => (key) && <> 376 | str.overwrite(eachBlock.start, eachBlock.expression.start, "{("); 377 | str.overwrite(eachBlock.expression.end, eachBlock.context.start, ").map(("); 378 | let contextEnd = eachBlock.context.end; 379 | if (eachBlock.index) { 380 | let idxLoc = htmlx.indexOf(eachBlock.index, contextEnd); 381 | contextEnd = idxLoc + eachBlock.index.length; 382 | } 383 | str.prependLeft(contextEnd, ") =>"); 384 | if (eachBlock.key) { 385 | let endEachStart = htmlx.indexOf("}", eachBlock.key.end); 386 | str.overwrite(endEachStart, endEachStart + 1, " && <>"); 387 | } 388 | else { 389 | let endEachStart = htmlx.indexOf("}", contextEnd); 390 | str.overwrite(endEachStart, endEachStart + 1, " <>"); 391 | } 392 | let endEach = htmlx.lastIndexOf("{", eachBlock.end); 393 | // {/each} -> )} or {:else} -> )} 394 | if (eachBlock.else) { 395 | let elseEnd = htmlx.lastIndexOf("}", eachBlock.else.start); 396 | let elseStart = htmlx.lastIndexOf("{", elseEnd); 397 | str.overwrite(elseStart, elseEnd + 1, ")}"); 398 | str.remove(endEach, eachBlock.end); 399 | } 400 | else { 401 | str.overwrite(endEach, eachBlock.end, ")}"); 402 | } 403 | }; 404 | // {#await somePromise then value} -> 405 | // {() => {let _$$p = (somePromise); 406 | const handleAwait = (awaitBlock: Node) => { 407 | str.overwrite(awaitBlock.start, awaitBlock.expression.start, "{() => {let _$$p = ("); 408 | str.prependLeft(awaitBlock.expression.end, ");"); 409 | // then value } | {:then value} -> 410 | // _$$p.then((value) => {<> 411 | let thenStart: number; 412 | let thenEnd: number; 413 | if (!awaitBlock.pending.skip) { 414 | //thenBlock seems to include the {:then} tag 415 | thenStart = awaitBlock.then.start; 416 | thenEnd = htmlx.indexOf("}", thenStart) + 1; 417 | str.prependLeft(thenStart, "; "); 418 | // add the start tag too 419 | let awaitEnd = htmlx.indexOf("}", awaitBlock.expression.end); 420 | str.remove(awaitEnd, awaitEnd + 1); 421 | str.appendRight(awaitEnd, " <>"); 422 | } 423 | else { 424 | thenEnd = htmlx.lastIndexOf("}", awaitBlock.then.start) + 1; 425 | thenStart = htmlx.indexOf("then", awaitBlock.expression.end); 426 | } 427 | // console.log("overwriting",thenStart, thenEnd); 428 | str.overwrite(thenStart, thenEnd, "_$$p.then((" + awaitBlock.value + ") => {<>"); 429 | //{:catch error} -> 430 | //}).catch((error) => {<> 431 | if (!awaitBlock.catch.skip) { 432 | //catch block includes the {:catch} 433 | let catchStart = awaitBlock.catch.start; 434 | let catchSymbolEnd = htmlx.indexOf(":catch", catchStart) + ":catch".length; 435 | let errorStart = awaitBlock.error ? htmlx.indexOf(awaitBlock.error, catchSymbolEnd) : catchSymbolEnd; 436 | let errorEnd = awaitBlock.error ? errorStart + awaitBlock.error.length : errorStart; 437 | let catchEnd = htmlx.indexOf("}", awaitBlock.catch.start) + 1; 438 | str.overwrite(catchStart, errorStart, "}).catch(("); 439 | str.overwrite(errorEnd, catchEnd, ") => {<>"); 440 | } 441 | // {/await} -> 442 | // <>})} 443 | let awaitEndStart = htmlx.lastIndexOf("{", awaitBlock.end); 444 | str.overwrite(awaitEndStart, awaitBlock.end, "})}}"); 445 | }; 446 | 447 | const handleComment = (node: Node) => { 448 | str.remove(node.start, node.end); 449 | } 450 | 451 | const handleSvelteTag = (node: Node) => { 452 | let colon = htmlx.indexOf(":", node.start); 453 | str.remove(colon, colon + 1); 454 | 455 | let closeTag = htmlx.lastIndexOf("/"+node.name, node.end) 456 | if (closeTag > node.start) { 457 | let colon = htmlx.indexOf(":",closeTag) 458 | str.remove(colon, colon + 1); 459 | } 460 | } 461 | 462 | 463 | (svelte as any).walk(ast, { 464 | enter: (node: Node, parent: Node, prop: string, index: number) => { 465 | try { 466 | switch (node.type) { 467 | case "IfBlock": handleIf(node); break; 468 | case "EachBlock": handleEach(node); break; 469 | case "ElseBlock": handleElse(node, parent); break; 470 | case "AwaitBlock": handleAwait(node); break; 471 | case "RawMustacheTag": handleRaw(node); break; 472 | case "DebugTag": handleDebug(node); break; 473 | case "InlineComponent": handleComponent(node); break; 474 | case "Element": handleElement(node); break; 475 | case "Comment": handleComment(node); break; 476 | case "Binding": handleBinding(node, parent); break; 477 | case "Class": handleClassDirective(node); break; 478 | case "Action": handleActionDirective(node); break; 479 | case "Transition": handleTransitionDirective(node); break; 480 | case "Animation": handleAnimateDirective(node); break; 481 | case "Attribute": handleAttribute(node, parent); break; 482 | case "EventHandler": handleEventHandler(node, parent); break; 483 | case "Options": handleSvelteTag(node); break; 484 | case "Window": handleSvelteTag(node); break; 485 | case "Head": handleSvelteTag(node); break; 486 | case "Body": handleSvelteTag(node); break; 487 | } 488 | if (onWalk) onWalk(node, parent, prop, index); 489 | } catch (e) { 490 | console.error("Error walking node ", node); 491 | throw e; 492 | } 493 | }, 494 | 495 | leave: (node: Node, parent: Node, prop: string, index: number ) => { 496 | try { 497 | if (onLeave) onLeave(node, parent, prop, index); 498 | } catch (e) { 499 | console.error("Error leaving node ", node); 500 | throw e; 501 | } 502 | } 503 | }); 504 | } 505 | 506 | export function htmlx2jsx(htmlx: string) { 507 | let ast = parseHtmlx(htmlx); 508 | let str = new MagicString(htmlx) 509 | 510 | convertHtmlxToJsx(str, ast); 511 | 512 | return { 513 | map: str.generateMap({ hires: true }), 514 | code: str.toString(), 515 | } 516 | } -------------------------------------------------------------------------------- /src/svelte2tsx.ts: -------------------------------------------------------------------------------- 1 | import MagicString from 'magic-string' 2 | import { parseHtmlx } from './htmlxparser'; 3 | import { convertHtmlxToJsx } from './htmlxtojsx'; 4 | import { Node } from 'svelte/compiler' 5 | import * as ts from 'typescript'; 6 | 7 | function AttributeValueAsJsExpression(htmlx: string, attr: Node): string { 8 | if (attr.value.length == 0) return "''"; //wut? 9 | 10 | //handle single value 11 | if (attr.value.length == 1) { 12 | let attrVal = attr.value[0]; 13 | 14 | if (attrVal.type == "AttributeShorthand") { 15 | return attrVal.expression.name; 16 | } 17 | 18 | if (attrVal.type == "Text") { 19 | return '"' + attrVal.raw + '"'; 20 | } 21 | 22 | if (attrVal.type == "MustacheTag") { 23 | return htmlx.substring(attrVal.expression.start, attrVal.expression.end) 24 | } 25 | throw Error("Unknown attribute value type:" + attrVal.type); 26 | } 27 | 28 | // we have multiple attribute values, so we know we are building a string out of them. 29 | // so return a dummy string, it will typecheck the same :) 30 | return '"__svelte_ts_string"'; 31 | } 32 | 33 | 34 | type TemplateProcessResult = { 35 | uses$$props: boolean, 36 | slots: Map>; 37 | scriptTag: Node, 38 | moduleScriptTag: Node 39 | } 40 | 41 | 42 | class Scope { 43 | declared: Set = new Set() 44 | parent: Scope 45 | 46 | constructor(parent?: Scope) { 47 | this.parent = parent 48 | } 49 | } 50 | 51 | type pendingStoreResolution = { 52 | node: T, 53 | parent: T, 54 | scope: Scope 55 | } 56 | 57 | function processSvelteTemplate(str: MagicString): TemplateProcessResult { 58 | let htmlxAst = parseHtmlx(str.original); 59 | 60 | let uses$$props = false; 61 | 62 | //track if we are in a declaration scope 63 | let isDeclaration = false; 64 | 65 | 66 | //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes 67 | //which prevents us just changing all instances of Identity that start with $ 68 | 69 | let pendingStoreResolutions:pendingStoreResolution[] = [] 70 | let scope = new Scope(); 71 | const pushScope = () => scope = new Scope(scope) 72 | const popScope = () => scope = scope.parent 73 | 74 | const handleStore = (node: Node, parent: Node) => { 75 | //handle assign to 76 | if (parent.type == "AssignmentExpression" && parent.left == node && parent.operator == "=") { 77 | let dollar = str.original.indexOf("$", node.start); 78 | str.remove(dollar, dollar+1); 79 | str.overwrite(node.end, str.original.indexOf("=", node.end)+1, ".set("); 80 | str.appendLeft(parent.end, ")"); 81 | return; 82 | } 83 | 84 | //rewrite get 85 | let dollar = str.original.indexOf("$", node.start); 86 | str.overwrite(dollar, dollar+1, "__sveltets_store_get("); 87 | str.prependLeft(node.end, ")") 88 | } 89 | 90 | const resolveStore = (pending: pendingStoreResolution) => { 91 | let { node, parent, scope } = pending; 92 | let name = node.name 93 | while (scope) { 94 | if (scope.declared.has(name)) { 95 | //we were manually declared, this isn't a store access. 96 | return; 97 | } 98 | scope = scope.parent 99 | } 100 | //We haven't been resolved, we must be a store read/write, handle it. 101 | handleStore(node, parent); 102 | } 103 | 104 | const enterBlockStatement = () => pushScope() 105 | const leaveBlockStatement = () => popScope() 106 | 107 | const enterFunctionDeclaration = () => pushScope(); 108 | const leaveFunctionDeclaration = () => popScope(); 109 | 110 | const enterArrowFunctionExpression = () => pushScope(); 111 | const leaveArrowFunctionExpression = () => popScope(); 112 | 113 | const handleIdentifier = (node: Node, parent: Node, prop: string) => { 114 | if (node.name == "$$props") { 115 | uses$$props = true; 116 | return; 117 | } 118 | 119 | //handle potential store 120 | if (node.name[0] == "$") { 121 | if (isDeclaration) { 122 | if (parent.type == "Property" && prop == "key") return; 123 | scope.declared.add(node.name); 124 | } else { 125 | if (parent.type == "MemberExpression" && prop == "property") return; 126 | if (parent.type == "Property" && prop == "key") return; 127 | pendingStoreResolutions.push({ node, parent, scope }) 128 | } 129 | return 130 | } 131 | } 132 | 133 | let scriptTag: Node = null; 134 | let moduleScriptTag: Node = null; 135 | const handleScriptTag = (node: Node) => { 136 | if (node.attributes && node.attributes.find(a => a.name == "context" && a.value.length == 1 && a.value[0].raw == "module")) { 137 | moduleScriptTag = node; 138 | } else { 139 | scriptTag = node; 140 | } 141 | } 142 | 143 | let slots = new Map>(); 144 | const handleSlot = (node: Node) => { 145 | let nameAttr = node.attributes.find(a => a.name == "name"); 146 | let slotName = nameAttr ? nameAttr.value[0].raw : "default"; 147 | //collect attributes 148 | let attributes = new Map(); 149 | for (let attr of node.attributes) { 150 | if (attr.name == "name") continue; 151 | if (!attr.value.length) continue; 152 | attributes.set(attr.name, AttributeValueAsJsExpression(str.original, attr)); 153 | } 154 | slots.set(slotName, attributes) 155 | } 156 | 157 | const handleStyleTag = (node: Node) => { 158 | str.remove(node.start, node.end); 159 | } 160 | 161 | 162 | const onHtmlxWalk = (node:Node, parent:Node, prop: string, index: number) => { 163 | 164 | if (prop == "params" && (parent.type == "FunctionDeclaration" || parent.type == "ArrowFunctionExpression")) { 165 | isDeclaration = true; 166 | } 167 | if (prop == "id" && parent.type == "VariableDeclarator") { 168 | isDeclaration = true; 169 | } 170 | 171 | 172 | switch(node.type) { 173 | case "Identifier": handleIdentifier(node, parent, prop); break; 174 | case "Slot": handleSlot(node); break; 175 | case "Style": handleStyleTag(node); break; 176 | case "Script": handleScriptTag(node); break; 177 | case "BlockStatement": enterBlockStatement(); break; 178 | case "FunctionDeclaration": enterFunctionDeclaration(); break; 179 | case "ArrowFunctionExpression": enterArrowFunctionExpression(); break; 180 | case "VariableDeclarator": isDeclaration = true; break; 181 | } 182 | } 183 | 184 | const onHtmlxLeave = (node:Node, parent:Node, prop: string, index: number) => { 185 | 186 | if (prop == "params" && (parent.type == "FunctionDeclaration" || parent.type == "ArrowFunctionExpression")) { 187 | isDeclaration = false; 188 | } 189 | 190 | if (prop == "id" && parent.type == "VariableDeclarator") { 191 | isDeclaration = false; 192 | } 193 | 194 | switch(node.type) { 195 | case "BlockStatement": leaveBlockStatement(); break; 196 | case "FunctionDeclaration": leaveFunctionDeclaration(); break; 197 | case "ArrowFunctionExpression": leaveArrowFunctionExpression(); break; 198 | } 199 | } 200 | 201 | convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave); 202 | 203 | //resolve stores 204 | pendingStoreResolutions.map(resolveStore) 205 | 206 | return { 207 | moduleScriptTag, 208 | scriptTag, 209 | slots, 210 | uses$$props 211 | } 212 | } 213 | 214 | type InstanceScriptProcessResult = { 215 | exportedNames: Map; 216 | uses$$props: boolean; 217 | } 218 | 219 | function processInstanceScriptContent(str: MagicString, script: Node): InstanceScriptProcessResult { 220 | let htmlx = str.original; 221 | let scriptContent = htmlx.substring(script.content.start, script.content.end) 222 | let tsAst = ts.createSourceFile("component.ts.svelte", scriptContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); 223 | let astOffset = script.content.start; 224 | let exportedNames = new Map(); 225 | 226 | let implicitTopLevelNames: Map = new Map(); 227 | let uses$$props = false; 228 | 229 | 230 | //track if we are in a declaration scope 231 | let isDeclaration = false; 232 | 233 | //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes 234 | //which prevents us just changing all instances of Identity that start with $ 235 | let pendingStoreResolutions:pendingStoreResolution[] = [] 236 | 237 | let scope = new Scope(); 238 | let rootScope = scope; 239 | 240 | const pushScope = () => scope = new Scope(scope) 241 | const popScope = () => scope = scope.parent 242 | 243 | const addExport = (name: ts.BindingName, target: ts.BindingName = null, type: ts.TypeNode = null) => { 244 | if (name.kind != ts.SyntaxKind.Identifier) { 245 | throw Error("export source kind not supported " + name) 246 | } 247 | if (target && target.kind != ts.SyntaxKind.Identifier) { 248 | throw Error("export target kind not supported " + target) 249 | } 250 | if (target) { 251 | exportedNames.set(type ? `${name.text} as ${type.getText()}` : name.text, (target as ts.Identifier).text); 252 | } else { 253 | exportedNames.set(name.text, null); 254 | } 255 | } 256 | 257 | const removeExport = (start: number, end: number) => { 258 | let exportStart = str.original.indexOf("export", start+astOffset); 259 | let exportEnd = exportStart + (end - start); 260 | str.remove(exportStart, exportEnd); 261 | } 262 | 263 | const handleStore = (ident: ts.Node, parent: ts.Node) => { 264 | //handle assign to 265 | if (parent && ts.isBinaryExpression(parent) && parent.operatorToken.kind == ts.SyntaxKind.EqualsToken && parent.left == ident) { 266 | //remove $ 267 | let dollar = str.original.indexOf("$", ident.getStart() + astOffset); 268 | str.remove(dollar, dollar + 1); 269 | // replace = with .set( 270 | str.overwrite(ident.end+astOffset, parent.operatorToken.end + astOffset, ".set("); 271 | // append ) 272 | str.appendLeft(parent.end+astOffset, ")"); 273 | return; 274 | } 275 | 276 | // we must be on the right or not part of assignment 277 | let dollar = str.original.indexOf("$", ident.getStart() + astOffset); 278 | str.overwrite(dollar, dollar+1, "__sveltets_store_get("); 279 | str.appendLeft(ident.end+astOffset, ")"); 280 | } 281 | 282 | const resolveStore = (pending: pendingStoreResolution) => { 283 | let { node, parent, scope } = pending; 284 | let name = (node as ts.Identifier).text; 285 | while (scope) { 286 | if (scope.declared.has(name)) { 287 | //we were manually declared, this isn't a store access. 288 | return; 289 | } 290 | scope = scope.parent 291 | } 292 | //We haven't been resolved, we must be a store read/write, handle it. 293 | handleStore(node, parent); 294 | } 295 | 296 | const handleIdentifier = (ident: ts.Identifier, parent: ts.Node) => { 297 | if (ident.text == "$$props") { 298 | uses$$props = true; 299 | return 300 | } 301 | if (ts.isLabeledStatement(parent) && parent.label == ident) { 302 | return; 303 | } 304 | 305 | if (isDeclaration || ts.isParameter(parent)) { 306 | if (!ts.isBindingElement(ident.parent) || ident.parent.name == ident) { //we are a key, not a name, so don't care 307 | if (ident.text.startsWith('$') || scope == rootScope) { //track all top level declared identifiers and all $ prefixed identifiers 308 | scope.declared.add(ident.text); 309 | } 310 | } 311 | } else { 312 | //track potential store usage to be resolved 313 | if (ident.text.startsWith('$')) { 314 | if ((!ts.isPropertyAccessExpression(parent) || parent.expression == ident ) && 315 | (!ts.isPropertyAssignment(parent) || parent.initializer == ident)) { 316 | pendingStoreResolutions.push({ node: ident, parent, scope }) 317 | } 318 | } 319 | } 320 | } 321 | 322 | const handleExportedVariableDeclarationList = (list: ts.VariableDeclarationList) => { 323 | ts.forEachChild(list, (node) => { 324 | if (ts.isVariableDeclaration(node)) { 325 | if (ts.isIdentifier(node.name)) { 326 | if (node.type) { 327 | addExport(node.name, node.name, node.type); 328 | } else { 329 | addExport(node.name); 330 | } 331 | } else if (ts.isObjectBindingPattern(node.name) || ts.isArrayBindingPattern(node.name)) { 332 | ts.forEachChild(node.name, (element) => { 333 | if (ts.isBindingElement(element)) { 334 | addExport(element.name); 335 | } 336 | }); 337 | } 338 | } 339 | }); 340 | } 341 | 342 | const walk = (node: ts.Node, parent: ts.Node) => { 343 | type onLeaveCallback = () => void; 344 | let onLeaveCallbacks:onLeaveCallback[] = [] 345 | 346 | if (ts.isVariableStatement(node)) { 347 | let exportModifier = node.modifiers ? node.modifiers.find(x => x.kind == ts.SyntaxKind.ExportKeyword): null; 348 | if (exportModifier) { 349 | handleExportedVariableDeclarationList(node.declarationList); 350 | removeExport(exportModifier.getStart(), exportModifier.end); 351 | } 352 | } 353 | 354 | if (ts.isFunctionDeclaration(node)) { 355 | if (node.modifiers) { 356 | let exportModifier = node.modifiers.find(x => x.kind == ts.SyntaxKind.ExportKeyword) 357 | if (exportModifier) { 358 | addExport(node.name) 359 | removeExport(exportModifier.getStart(), exportModifier.end); 360 | } 361 | } 362 | 363 | pushScope(); 364 | onLeaveCallbacks.push(() => popScope()); 365 | } 366 | 367 | if (ts.isBlock(node)) { 368 | pushScope(); 369 | onLeaveCallbacks.push(() => popScope()); 370 | } 371 | 372 | if (ts.isArrowFunction(node)) { 373 | pushScope(); 374 | onLeaveCallbacks.push(() => popScope()); 375 | } 376 | 377 | 378 | if (ts.isExportDeclaration(node)) { 379 | for (let ne of node.exportClause.elements) { 380 | if (ne.propertyName) { 381 | addExport(ne.propertyName, ne.name) 382 | } else { 383 | addExport(ne.name) 384 | } 385 | } 386 | //we can remove entire statement 387 | removeExport(node.getStart(), node.end); 388 | } 389 | 390 | //move imports to top of script so they appear outside our render function 391 | if (ts.isImportDeclaration(node)) { 392 | str.move(node.getStart()+astOffset, node.end+astOffset, script.start+1); 393 | //add in a \n 394 | const originalEndChar = str.original[node.end+astOffset-1]; 395 | str.overwrite(node.end+astOffset-1, node.end+astOffset, originalEndChar+"\n"); 396 | } 397 | 398 | if (ts.isVariableDeclaration(parent) && parent.name == node) { 399 | isDeclaration = true; 400 | onLeaveCallbacks.push(() => isDeclaration = false); 401 | } 402 | 403 | if (ts.isBindingElement(parent) && parent.name == node) { 404 | isDeclaration = true; 405 | onLeaveCallbacks.push(() => isDeclaration = false); 406 | } 407 | 408 | if (ts.isImportClause(node)) { 409 | isDeclaration = true; 410 | onLeaveCallbacks.push(() => isDeclaration = false); 411 | } 412 | 413 | //handle stores etc 414 | if (ts.isIdentifier(node)) handleIdentifier(node, parent); 415 | 416 | 417 | //track implicit declarations in reactive blocks at the top level 418 | if (ts.isLabeledStatement(node) 419 | && parent == tsAst //top level 420 | && node.label.text == "$" 421 | && node.statement 422 | && ts.isExpressionStatement(node.statement) 423 | && ts.isBinaryExpression(node.statement.expression) 424 | && node.statement.expression.operatorToken.kind == ts.SyntaxKind.EqualsToken 425 | && ts.isIdentifier(node.statement.expression.left)) { 426 | 427 | implicitTopLevelNames.set(node.statement.expression.left.text, node.label.getStart() ); 428 | } 429 | 430 | //to save a bunch of condition checks on each node, we recurse into processChild which skips all the checks for top level items 431 | ts.forEachChild(node, n => walk(n, node)) 432 | //fire off the on leave callbacks 433 | onLeaveCallbacks.map(c => c()) 434 | } 435 | 436 | //walk the ast and convert to tsx as we go 437 | tsAst.forEachChild(n => walk(n, tsAst)); 438 | 439 | //resolve stores 440 | pendingStoreResolutions.map(resolveStore) 441 | 442 | // declare implicit reactive variables we found in the script 443 | for ( var [name, pos] of implicitTopLevelNames.entries()) { 444 | if (!rootScope.declared.has(name)) { 445 | //add a declaration 446 | str.prependRight(pos + astOffset, `;let ${name}; `) 447 | } 448 | } 449 | 450 | return { 451 | exportedNames, 452 | uses$$props 453 | } 454 | } 455 | 456 | 457 | function addComponentExport(str: MagicString, uses$$props: boolean) { 458 | str.append(`\n\nexport default class {\n $$prop_def = __sveltets_partial${ uses$$props ? "_with_any" : "" }(render().props)\n $$slot_def = render().slots\n}`); 459 | } 460 | 461 | function processModuleScriptTag(str: MagicString, script: Node) { 462 | let htmlx = str.original; 463 | 464 | let scriptStartTagEnd = htmlx.indexOf(">", script.start)+1; 465 | let scriptEndTagStart = htmlx.lastIndexOf("<", script.end-1); 466 | 467 | str.overwrite(script.start, scriptStartTagEnd, ";"); 468 | str.overwrite(scriptEndTagStart, script.end, ";<>"); 469 | } 470 | 471 | 472 | 473 | function createRenderFunction(str: MagicString, scriptTag: Node, scriptDestination: number, slots: Map>, exportedNames: Map, uses$$props: boolean) { 474 | let htmlx = str.original; 475 | let propsDecl = uses$$props ? " let $$props: SvelteAllProps;" : "" 476 | 477 | if (scriptTag) { 478 | //I couldn't get magicstring to let me put the script before the <> we prepend during conversion of the template to jsx, so we just close it instead 479 | let scriptTagEnd = htmlx.lastIndexOf(">", scriptTag.content.start) + 1; 480 | str.overwrite(scriptTag.start, scriptTag.start+ 1, ";"); 481 | str.overwrite(scriptTag.start+1, scriptTagEnd, `function render() {${propsDecl}\n`); 482 | 483 | let scriptEndTagStart = htmlx.lastIndexOf("<", scriptTag.end-1); 484 | str.overwrite(scriptEndTagStart, scriptTag.end, ";\n<>"); 485 | } else { 486 | str.prependRight(scriptDestination, `;function render() {${propsDecl}\n<>`); 487 | } 488 | 489 | let returnElements = [...exportedNames.entries()].map(([key, value]) => value ? `${value}: ${key}` : key); 490 | let slotsAsDef = "{" + [...slots.entries()].map(([name, attrs]) => { 491 | let attrsAsString = [...attrs.entries()].map(([exportName, expr]) => `${exportName}:${expr}`).join(", "); 492 | return `${name}: {${attrsAsString}}` 493 | }).join(", ") + "}" 494 | 495 | 496 | let returnString = "\nreturn { props: {" + returnElements.join(" , ") + "}, slots: " + slotsAsDef + " }}" 497 | str.append(returnString) 498 | } 499 | 500 | 501 | export function svelte2tsx(svelte: string, filename?: string) { 502 | 503 | let str = new MagicString(svelte); 504 | // process the htmlx as a svelte template 505 | let { moduleScriptTag, scriptTag, slots, uses$$props } = processSvelteTemplate(str); 506 | 507 | /* Rearrange the script tags so that module is first, and instance second followed finally by the template 508 | * This is a bit convoluted due to some trouble I had with magic string. A simple str.move(start,end,0) for each script wasn't enough 509 | * since if the module script was already at 0, it wouldn't move (which is fine) but would mean the order would be swapped when the script tag tried to move to 0 510 | * In this case we instead have to move it to moduleScriptTag.end. We track the location for the script move in the MoveInstanceScriptTarget var 511 | */ 512 | let instanceScriptTarget = 0; 513 | 514 | if (moduleScriptTag) { 515 | if (moduleScriptTag.start != 0) { 516 | //move our module tag to the top 517 | str.move(moduleScriptTag.start, moduleScriptTag.end, 0); 518 | } else { 519 | //since our module script was already at position 0, we need to move our instance script tag to the end of it. 520 | instanceScriptTarget = moduleScriptTag.end; 521 | } 522 | } 523 | 524 | //move the instance script and process the content 525 | let exportedNames = new Map(); 526 | if (scriptTag) { 527 | //ensure it is between the module script and the rest of the template (the variables need to be declared before the jsx template) 528 | if (scriptTag.start != instanceScriptTarget) { 529 | str.move(scriptTag.start, scriptTag.end, instanceScriptTarget); 530 | } 531 | let res = processInstanceScriptContent(str, scriptTag); 532 | exportedNames = res.exportedNames; 533 | uses$$props = uses$$props || res.uses$$props; 534 | } 535 | 536 | //wrap the script tag and template content in a function returning the slot and exports 537 | createRenderFunction(str, scriptTag, instanceScriptTarget, slots, exportedNames, uses$$props); 538 | 539 | // we need to process the module script after the instance script has moved otherwise we get warnings about moving edited items 540 | if (moduleScriptTag) { 541 | processModuleScriptTag(str, moduleScriptTag); 542 | } 543 | 544 | addComponentExport(str, uses$$props); 545 | 546 | return { 547 | code: str.toString(), 548 | map: str.generateMap({ hires: true, source: filename }) 549 | } 550 | } 551 | --------------------------------------------------------------------------------