Skip to content

Commit cfa861e

Browse files
committed
feat: allow typescript to know used variables in template
1 parent 12c1157 commit cfa861e

File tree

4 files changed

+182
-5
lines changed

4 files changed

+182
-5
lines changed

src/transformers/typescript.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { dirname, isAbsolute, join } from 'path';
22

33
import ts from 'typescript';
4+
import { compile } from 'svelte/compiler';
45

56
import { throwTypescriptError } from '../modules/errors';
67
import type { Transformer, Options } from '../types';
@@ -53,6 +54,45 @@ const importTransformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
5354
return (node) => ts.visitNode(node, visit);
5455
};
5556

57+
function stripTags(markup: string): string {
58+
return markup
59+
.replace(/<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi, '')
60+
.replace(/<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi, '');
61+
}
62+
63+
function appendVarsToCode({
64+
content,
65+
markup,
66+
filename,
67+
}: {
68+
content: string;
69+
markup?: string;
70+
filename?: string;
71+
}): string {
72+
if (!markup) return content;
73+
74+
const { vars } = compile(stripTags(markup), {
75+
generate: false,
76+
varsReport: 'full',
77+
errorMode: 'warn',
78+
filename,
79+
});
80+
81+
return `${content}\nlet $$$$$$$$ = [${vars.map((v) => v.name).join(',')}];`;
82+
}
83+
84+
function stripVarsFromCode({
85+
compiledCode,
86+
markup,
87+
}: {
88+
compiledCode: string;
89+
markup?: string;
90+
}): string {
91+
return markup
92+
? compiledCode.slice(0, compiledCode.indexOf('let $$$$$$$$'))
93+
: compiledCode;
94+
}
95+
5696
export function loadTsconfig(
5797
compilerOptionsJSON: any,
5898
filename: string,
@@ -103,6 +143,7 @@ export function loadTsconfig(
103143
const transformer: Transformer<Options.Typescript> = ({
104144
content,
105145
filename,
146+
markup,
106147
options = {},
107148
}) => {
108149
// default options
@@ -140,16 +181,14 @@ const transformer: Transformer<Options.Typescript> = ({
140181
}
141182

142183
const {
143-
outputText: code,
184+
outputText: compiledCode,
144185
sourceMapText: map,
145186
diagnostics,
146-
} = ts.transpileModule(content, {
187+
} = ts.transpileModule(appendVarsToCode({ content, markup, filename }), {
147188
fileName: filename,
148189
compilerOptions,
149190
reportDiagnostics: options.reportDiagnostics !== false,
150-
transformers: {
151-
before: [importTransformer],
152-
},
191+
transformers: markup ? {} : { before: [importTransformer] },
153192
});
154193

155194
if (diagnostics.length > 0) {
@@ -167,6 +206,8 @@ const transformer: Transformer<Options.Typescript> = ({
167206
}
168207
}
169208

209+
const code = stripVarsFromCode({ compiledCode, markup });
210+
170211
return {
171212
code,
172213
map,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<script lang="ts">
2+
import { fly } from "svelte/transition";
3+
import { flip } from "svelte/animate";
4+
import Nested from "./Nested.svelte";
5+
import { hello } from "./script";
6+
import { AValue, AType } from "./types";
7+
const ui = { MyNested: Nested };
8+
const val: AType = "test1";
9+
const prom: Promise<AType> = Promise.resolve("test2");
10+
const arr: AType[] = ["test1", "test2"];
11+
const isTest1 = (v: AType) => v === "test1";
12+
const obj = {
13+
fn: () => "test",
14+
val: "test1" as const
15+
};
16+
let inputVal: string;
17+
const action = (node: Element, options: { id: string; }) => { node.id = options.id; };
18+
const action2 = (node: Element) => { node.classList.add("test"); };
19+
let nested: Nested;
20+
21+
let scrollY = 500;
22+
let innerWidth = 500;
23+
24+
const duration = 200;
25+
function onKeyDown(e: KeyboardEvent): void {
26+
e.preventDefault();
27+
}
28+
</script>
29+
30+
<style>
31+
.value { color: #ccc; }
32+
</style>
33+
34+
<svelte:window on:keydown={onKeyDown} {scrollY} bind:innerWidth />
35+
<svelte:body on:keydown={onKeyDown} />
36+
37+
<svelte:head>
38+
<title>Title: {val}</title>
39+
</svelte:head>
40+
41+
<div>
42+
<Nested let:var1 let:var2={var3}>
43+
<Nested bind:this={nested} />
44+
<Nested {...obj} />
45+
<Nested {...{ var1, var3 }} />
46+
47+
<svelte:fragment slot="slot1" let:var4={var5}>
48+
<Nested {...{ var5 }} />
49+
</svelte:fragment>
50+
51+
<div slot="slot2" let:var6={var7}>
52+
<Nested {...{ var7 }} />
53+
</div>
54+
55+
<div slot="slot3">
56+
<Nested {...{ val }} />
57+
</div>
58+
</Nested>
59+
60+
<svelte:component this={ui.MyNested} {val} on:keydown={onKeyDown} bind:inputVal />
61+
62+
<p class:value={!!inputVal}>{hello}</p>
63+
<input bind:value={inputVal} use:action={{ id: val }} use:action2 />
64+
65+
{#if AValue && val}
66+
<p class="value" transition:fly={{ duration }}>There is a value: {AValue}</p>
67+
{/if}
68+
69+
{#if val && isTest1(val) && AValue && true && "test"}
70+
<p class="value">There is a value: {AValue}</p>
71+
{:else if obj.val && obj.fn() && isTest1(obj.val)}
72+
<p class="value">There is a value: {AValue}</p>
73+
{:else}
74+
Else
75+
{/if}
76+
77+
{#each arr as item (item)}
78+
<p animate:flip={{ duration }}>{item}</p>
79+
{/each}
80+
81+
{#each arr as item}
82+
<p>{item}</p>
83+
{:else}
84+
<p>No items</p>
85+
{/each}
86+
87+
{#await prom}
88+
Loading...
89+
{:then value}
90+
<input type={val} {value} on:input={e => inputVal = e.currentTarget.value} />
91+
{:catch err}
92+
<p>Error: {err}</p>
93+
{/await}
94+
95+
{#await prom then value}
96+
<p>{value}</p>
97+
{/await}
98+
99+
{#key val}
100+
<p>Keyed {val}</p>
101+
{/key}
102+
103+
<slot name="slot0" {inputVal}>
104+
<p>{inputVal}</p>
105+
</slot>
106+
107+
{@html val}
108+
{@debug val, inputVal}
109+
</div>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts" context="module">
2+
import { AValue, AType } from "./types";
3+
</script>
4+
5+
<script lang="ts">
6+
const val: AType = "test1";
7+
</script>
8+
9+
{val} {AValue}

test/transformers/typescript.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,24 @@ describe('transformer - typescript', () => {
111111
return expect(code).toContain(getFixtureContent('script.js'));
112112
});
113113

114+
it('should strip unused and type imports', async () => {
115+
const tpl = getFixtureContent('TypeScriptImports.svelte');
116+
117+
const opts = sveltePreprocess({ typescript: { tsconfigFile: false } });
118+
const { code } = await preprocess(tpl, opts);
119+
120+
return expect(code).toContain(`import { AValue } from "./types";`);
121+
});
122+
123+
it('should strip unused and type imports in context="module" tags', async () => {
124+
const tpl = getFixtureContent('TypeScriptImportsModule.svelte');
125+
126+
const opts = sveltePreprocess({ typescript: { tsconfigFile: false } });
127+
const { code } = await preprocess(tpl, opts);
128+
129+
return expect(code).toContain(`import { AValue } from "./types";`);
130+
});
131+
114132
it('supports extends field', () => {
115133
const { options } = loadTsconfig({}, getTestAppFilename(), {
116134
tsconfigFile: './test/fixtures/tsconfig.extends1.json',

0 commit comments

Comments
 (0)