Skip to content

Better Typescript transpilation #392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Sep 4, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: allow typescript to know used variables in template
- inject vars from markup to typescript
- do not use importTransform when markup is available
- inject component script to context=module script
- refactor modules/markup to reuse common functions
- test injection behavior
  • Loading branch information
SomaticIT committed Apr 15, 2021
commit e5a73db6a5af29bc585f6210467370992ef45bb9
47 changes: 32 additions & 15 deletions src/modules/markup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
import type { Transformer, Preprocessor } from '../types';

/** Create a tag matching regexp. */
export function createTagRegex(tagName: string, flags?: string): RegExp {
return new RegExp(
`<!--[^]*?-->|<${tagName}(\\s[^]*?)?(?:>([^]*?)<\\/${tagName}>|\\/>)`,
flags,
);
}

/** Strip script and style tags from markup. */
export function stripTags(markup: string): string {
return markup
.replace(createTagRegex('style', 'gi'), '')
.replace(createTagRegex('script', 'gi'), '');
}

/** Transform an attribute string into a key-value object */
export function parseAttributes(attributesStr: string): Record<string, any> {
return attributesStr
.split(/\s+/)
.filter(Boolean)
.reduce((acc: Record<string, string | boolean>, attr) => {
const [name, value] = attr.split('=');

// istanbul ignore next
acc[name] = value ? value.replace(/['"]/g, '') : true;

return acc;
}, {});
}

export async function transformMarkup(
{ content, filename }: { content: string; filename: string },
transformer: Preprocessor | Transformer<unknown>,
Expand All @@ -9,9 +39,7 @@ export async function transformMarkup(

markupTagName = markupTagName.toLocaleLowerCase();

const markupPattern = new RegExp(
`/<!--[^]*?-->|<${markupTagName}(\\s[^]*?)?(?:>([^]*?)<\\/${markupTagName}>|\\/>)`,
);
const markupPattern = createTagRegex(markupTagName);

const templateMatch = content.match(markupPattern);

Expand All @@ -28,18 +56,7 @@ export async function transformMarkup(

const [fullMatch, attributesStr = '', templateCode] = templateMatch;

/** Transform an attribute string into a key-value object */
const attributes = attributesStr
.split(/\s+/)
.filter(Boolean)
.reduce((acc: Record<string, string | boolean>, attr) => {
const [name, value] = attr.split('=');

// istanbul ignore next
acc[name] = value ? value.replace(/['"]/g, '') : true;

return acc;
}, {});
const attributes = parseAttributes(attributesStr);

/** Transform the found template code */
let { code, map, dependencies } = await transformer({
Expand Down
83 changes: 75 additions & 8 deletions src/transformers/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { dirname, isAbsolute, join } from 'path';

import ts from 'typescript';
import { compile } from 'svelte/compiler';

import { throwTypescriptError } from '../modules/errors';
import { createTagRegex, parseAttributes, stripTags } from '../modules/markup';
import type { Transformer, Options } from '../types';

type CompilerOptions = Options.Typescript['compilerOptions'];
Expand Down Expand Up @@ -53,6 +55,66 @@ const importTransformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
return (node) => ts.visitNode(node, visit);
};

function getComponentScriptContent(markup: string): string {
const regex = createTagRegex('script', 'gi');
let match: RegExpMatchArray;

while ((match = regex.exec(markup)) !== null) {
const { context } = parseAttributes(match[1]);

if (context !== 'module') {
return match[2];
}
}

return '';
}

function injectVarsToCode({
content,
markup,
filename,
attributes,
}: {
content: string;
markup?: string;
filename?: string;
attributes?: Record<string, any>;
}): string {
if (!markup) return content;

const { vars } = compile(stripTags(markup), {
generate: false,
varsReport: 'full',
errorMode: 'warn',
filename,
});

const sep = '\nconst $$$$$$$$ = null;\n';
const varsValues = vars.map((v) => v.name).join(',');
const injectedVars = `const $$vars$$ = [${varsValues}];`;

if (attributes?.context === 'module') {
const componentScript = getComponentScriptContent(markup);

return `${content}${sep}${componentScript}\n${injectedVars}`;
}

return `${content}${sep}${injectedVars}`;
}

function stripInjectedCode({
compiledCode,
markup,
}: {
compiledCode: string;
markup?: string;
}): string {
return markup
? compiledCode.slice(0, compiledCode.indexOf('const $$$$$$$$ = null;'))
: compiledCode;
}

export function loadTsconfig(
compilerOptionsJSON: any,
filename: string,
Expand Down Expand Up @@ -103,7 +165,9 @@ export function loadTsconfig(
const transformer: Transformer<Options.Typescript> = ({
content,
filename,
markup,
options = {},
attributes,
}) => {
// default options
const compilerOptionsJSON = {
Expand Down Expand Up @@ -140,17 +204,18 @@ const transformer: Transformer<Options.Typescript> = ({
}

const {
outputText: code,
outputText: compiledCode,
sourceMapText: map,
diagnostics,
} = ts.transpileModule(content, {
fileName: filename,
compilerOptions,
reportDiagnostics: options.reportDiagnostics !== false,
transformers: {
before: [importTransformer],
} = ts.transpileModule(
injectVarsToCode({ content, markup, filename, attributes }),
{
fileName: filename,
compilerOptions,
reportDiagnostics: options.reportDiagnostics !== false,
transformers: markup ? {} : { before: [importTransformer] },
},
});
);

if (diagnostics.length > 0) {
// could this be handled elsewhere?
Expand All @@ -167,6 +232,8 @@ const transformer: Transformer<Options.Typescript> = ({
}
}

const code = stripInjectedCode({ compiledCode, markup });

return {
code,
map,
Expand Down
109 changes: 109 additions & 0 deletions test/fixtures/TypeScriptImports.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { flip } from "svelte/animate";
import Nested from "./Nested.svelte";
import { hello } from "./script";
import { AValue, AType } from "./types";
const ui = { MyNested: Nested };
const val: AType = "test1";
const prom: Promise<AType> = Promise.resolve("test2");
const arr: AType[] = ["test1", "test2"];
const isTest1 = (v: AType) => v === "test1";
const obj = {
fn: () => "test",
val: "test1" as const
};
let inputVal: string;
const action = (node: Element, options: { id: string; }) => { node.id = options.id; };
const action2 = (node: Element) => { node.classList.add("test"); };
let nested: Nested;

let scrollY = 500;
let innerWidth = 500;

const duration = 200;
function onKeyDown(e: KeyboardEvent): void {
e.preventDefault();
}
</script>

<style>
.value { color: #ccc; }
</style>

<svelte:window on:keydown={onKeyDown} {scrollY} bind:innerWidth />
<svelte:body on:keydown={onKeyDown} />

<svelte:head>
<title>Title: {val}</title>
</svelte:head>

<div>
<Nested let:var1 let:var2={var3}>
<Nested bind:this={nested} />
<Nested {...obj} />
<Nested {...{ var1, var3 }} />

<svelte:fragment slot="slot1" let:var4={var5}>
<Nested {...{ var5 }} />
</svelte:fragment>

<div slot="slot2" let:var6={var7}>
<Nested {...{ var7 }} />
</div>

<div slot="slot3">
<Nested {...{ val }} />
</div>
</Nested>

<svelte:component this={ui.MyNested} {val} on:keydown={onKeyDown} bind:inputVal />

<p class:value={!!inputVal}>{hello}</p>
<input bind:value={inputVal} use:action={{ id: val }} use:action2 />

{#if AValue && val}
<p class="value" transition:fly={{ duration }}>There is a value: {AValue}</p>
{/if}

{#if val && isTest1(val) && AValue && true && "test"}
<p class="value">There is a value: {AValue}</p>
{:else if obj.val && obj.fn() && isTest1(obj.val)}
<p class="value">There is a value: {AValue}</p>
{:else}
Else
{/if}

{#each arr as item (item)}
<p animate:flip={{ duration }}>{item}</p>
{/each}

{#each arr as item}
<p>{item}</p>
{:else}
<p>No items</p>
{/each}

{#await prom}
Loading...
{:then value}
<input type={val} {value} on:input={e => inputVal = e.currentTarget.value} />
{:catch err}
<p>Error: {err}</p>
{/await}

{#await prom then value}
<p>{value}</p>
{/await}

{#key val}
<p>Keyed {val}</p>
{/key}

<slot name="slot0" {inputVal}>
<p>{inputVal}</p>
</slot>

{@html val}
{@debug val, inputVal}
</div>
10 changes: 10 additions & 0 deletions test/fixtures/TypeScriptImportsModule.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts" context="module">
import { AValue, AType } from "./types";
</script>

<script lang="ts">
const val: AType = "test1";
const aValue = AValue;
</script>

{val} {aValue}
18 changes: 18 additions & 0 deletions test/transformers/typescript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ describe('transformer - typescript', () => {
return expect(code).toContain(getFixtureContent('script.js'));
});

it('should strip unused and type imports', async () => {
const tpl = getFixtureContent('TypeScriptImports.svelte');

const opts = sveltePreprocess({ typescript: { tsconfigFile: false } });
const { code } = await preprocess(tpl, opts);

return expect(code).toContain(`import { AValue } from "./types";`);
});

it('should strip unused and type imports in context="module" tags', async () => {
const tpl = getFixtureContent('TypeScriptImportsModule.svelte');

const opts = sveltePreprocess({ typescript: { tsconfigFile: false } });
const { code } = await preprocess(tpl, opts);

return expect(code).toContain(`import { AValue } from "./types";`);
});

it('supports extends field', () => {
const { options } = loadTsconfig({}, getTestAppFilename(), {
tsconfigFile: './test/fixtures/tsconfig.extends1.json',
Expand Down