Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 9 additions & 4 deletions packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { throwError } from '../utils/error';

export class Generics {
private definitions: string[] = [];
private typeReferences: string[] = [];
private references: string[] = [];

constructor(private str: MagicString, private astOffset: number) {}
Expand All @@ -15,11 +16,11 @@ export class Generics {
throw new Error('Invalid $$Generic declaration: Only one type argument allowed');
}
if (node.type.typeArguments?.length === 1) {
this.definitions.push(
`${node.name.text} extends ${node.type.typeArguments[0].getText()}`
);
const typeReference = node.type.typeArguments[0].getText();
this.typeReferences.push(typeReference);
this.definitions.push(`${node.name.text} extends ${typeReference}`);
} else {
this.definitions.push(`${node.name.text}`);
this.definitions.push(node.name.text);
}
this.references.push(node.name.text);
this.str.remove(this.astOffset + node.getStart(), this.astOffset + node.getEnd());
Expand All @@ -45,6 +46,10 @@ export class Generics {
);
}

getTypeReferences() {
return this.typeReferences;
}

toDefinitionString(addIgnore = false) {
const surround = addIgnore ? surroundWithIgnoreComments : (str: string) => str;
return this.definitions.length ? surround(`<${this.definitions.join(',')}>`) : '';
Expand Down
58 changes: 58 additions & 0 deletions packages/svelte2tsx/src/svelte2tsx/nodes/InterfacesAndTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import ts from 'typescript';
import { flatten } from '../../utils/object';

type TypeOrInterface = ts.InterfaceDeclaration | ts.TypeAliasDeclaration;

export class InterfacesAndTypes {
node: TypeOrInterface | null = null;
private all: TypeOrInterface[] = [];
private references: Map<TypeOrInterface, ts.TypeReferenceNode[]> = new Map();

add(node: TypeOrInterface) {
this.all.push(node);
}

getNodesWithNames(names: string[]) {
return this.all.filter((node) => names.includes(node.name.text));
}

// The following could be used to create a informative error message in case
// someone has an interface that both references a generic and is used by one:

addReference(reference: ts.TypeReferenceNode) {
if (!this.node) {
return;
}

const references = this.references.get(this.node) || [];
references.push(reference);
this.references.set(this.node, references);
}

getNodesThatReferenceType(name: string) {
const nodes: TypeOrInterface[] = [];
for (const [node, references] of this.references) {
if (references.some((r) => r.typeName.getText() === name)) {
nodes.push(node);
}
}
return nodes;
}

getNodesThatRecursivelyReferenceType(name: string) {
let types: string[] = [name];
const nodes: Set<TypeOrInterface> = new Set();
while (types.length !== 0) {
const newTypes = flatten(
types.map((type) => this.getNodesThatReferenceType(type))
).filter((t) => !nodes.has(t));
newTypes.forEach((t) => nodes.add(t));
types = newTypes.map((t) => t.name.text);
}
return [...nodes.values()];
}

getNodesThatRecursivelyReferenceTypes(names: string[]) {
return flatten(names.map((name) => this.getNodesThatRecursivelyReferenceType(name)));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import MagicString from 'magic-string';
import ts from 'typescript';
import { moveNode } from '../utils/tsAst';

/**
* move imports to top of script so they appear outside our render function
Expand All @@ -11,62 +12,7 @@ export function handleImportDeclaration(
scriptStart: number,
sourceFile: ts.SourceFile
) {
const scanner = ts.createScanner(
sourceFile.languageVersion,
/*skipTrivia*/ false,
sourceFile.languageVariant
);

const comments = ts.getLeadingCommentRanges(node.getFullText(), 0) ?? [];
if (
!comments.some((comment) => comment.hasTrailingNewLine) &&
isNewGroup(sourceFile, node, scanner)
) {
str.appendRight(node.getStart() + astOffset, '\n');
}

for (const comment of comments) {
const commentEnd = node.pos + comment.end + astOffset;
str.move(node.pos + comment.pos + astOffset, commentEnd, scriptStart + 1);

if (comment.hasTrailingNewLine) {
str.overwrite(commentEnd - 1, commentEnd, str.original[commentEnd - 1] + '\n');
}
}

str.move(node.getStart() + astOffset, node.end + astOffset, scriptStart + 1);
//add in a \n
const originalEndChar = str.original[node.end + astOffset - 1];

str.overwrite(node.end + astOffset - 1, node.end + astOffset, originalEndChar + '\n');
}

/**
* adopted from https://github.com/microsoft/TypeScript/blob/6e0447fdf165b1cec9fc80802abcc15bd23a268f/src/services/organizeImports.ts#L111
*/
function isNewGroup(
sourceFile: ts.SourceFile,
topLevelImportDecl: ts.ImportDeclaration,
scanner: ts.Scanner
) {
const startPos = topLevelImportDecl.getFullStart();
const endPos = topLevelImportDecl.getStart();
scanner.setText(sourceFile.text, startPos, endPos - startPos);

let numberOfNewLines = 0;
while (scanner.getTokenPos() < endPos) {
const tokenKind = scanner.scan();

if (tokenKind === ts.SyntaxKind.NewLineTrivia) {
numberOfNewLines++;

if (numberOfNewLines >= 2) {
return true;
}
}
}

return false;
return moveNode(node, str, astOffset, scriptStart, sourceFile);
}

/**
Expand Down
83 changes: 53 additions & 30 deletions packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import MagicString from 'magic-string';
import { Node } from 'estree-walker';
import * as ts from 'typescript';
import { getBinaryAssignmentExpr, isNotPropertyNameOfImport } from './utils/tsAst';
import { getBinaryAssignmentExpr, isNotPropertyNameOfImport, moveNode } from './utils/tsAst';
import { ExportedNames, is$$PropsDeclaration } from './nodes/ExportedNames';
import { ImplicitTopLevelNames } from './nodes/ImplicitTopLevelNames';
import { ComponentEvents, is$$EventsDeclaration } from './nodes/ComponentEvents';
Expand All @@ -15,6 +15,7 @@ import {
handleFirstInstanceImport,
handleImportDeclaration
} from './nodes/handleImportDeclaration';
import { InterfacesAndTypes } from './nodes/InterfacesAndTypes';

export interface InstanceScriptProcessResult {
exportedNames: ExportedNames;
Expand Down Expand Up @@ -52,6 +53,7 @@ export function processInstanceScriptContent(
const astOffset = script.content.start;
const exportedNames = new ExportedNames(str, astOffset);
const generics = new Generics(str, astOffset);
const interfacesAndTypes = new InterfacesAndTypes();

const implicitTopLevelNames = new ImplicitTopLevelNames(str, astOffset);
let uses$$props = false;
Expand Down Expand Up @@ -219,6 +221,12 @@ export function processInstanceScriptContent(
implicitStoreValues.addImportStatement(node);
}

if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
interfacesAndTypes.node = node;
interfacesAndTypes.add(node);
onLeaveCallbacks.push(() => (interfacesAndTypes.node = null));
}

//handle stores etc
if (ts.isIdentifier(node)) {
handleIdentifier(node, parent);
Expand Down Expand Up @@ -264,12 +272,19 @@ export function processInstanceScriptContent(

handleFirstInstanceImport(tsAst, astOffset, hasModuleScript, str);

// move interfaces and types out of the render function if they are referenced
// by a $$Generic, otherwise it will be used before being defined after the transformation
const nodesToMove = interfacesAndTypes.getNodesWithNames(generics.getTypeReferences());
for (const node of nodesToMove) {
moveNode(node, str, astOffset, script.start, tsAst);
}

if (mode === 'dts') {
// Transform interface declarations to type declarations because indirectly
// using interfaces inside the return type of a function is forbidden.
// This is not a problem for intellisense/type inference but it will
// break dts generation (file will not be generated).
transformInterfacesToTypes(tsAst, str, astOffset);
transformInterfacesToTypes(tsAst, str, astOffset, nodesToMove);
}

return {
Expand All @@ -283,32 +298,40 @@ export function processInstanceScriptContent(
};
}

function transformInterfacesToTypes(tsAst: ts.SourceFile, str: MagicString, astOffset: any) {
tsAst.statements.filter(ts.isInterfaceDeclaration).forEach((node) => {
str.overwrite(
node.getStart() + astOffset,
node.getStart() + astOffset + 'interface'.length,
'type'
);

if (node.heritageClauses?.length) {
const extendsStart = node.heritageClauses[0].getStart() + astOffset;
str.overwrite(extendsStart, extendsStart + 'extends'.length, '=');

const extendsList = node.heritageClauses[0].types;
let prev = extendsList[0];
extendsList.slice(1).forEach((heritageClause) => {
str.overwrite(
prev.getEnd() + astOffset,
heritageClause.getStart() + astOffset,
' & '
);
prev = heritageClause;
});

str.appendLeft(node.heritageClauses[0].getEnd() + astOffset, ' & ');
} else {
str.prependLeft(str.original.indexOf('{', node.getStart() + astOffset), '=');
}
});
function transformInterfacesToTypes(
tsAst: ts.SourceFile,
str: MagicString,
astOffset: any,
movedNodes: ts.Node[]
) {
tsAst.statements
.filter(ts.isInterfaceDeclaration)
.filter((i) => !movedNodes.includes(i))
.forEach((node) => {
str.overwrite(
node.getStart() + astOffset,
node.getStart() + astOffset + 'interface'.length,
'type'
);

if (node.heritageClauses?.length) {
const extendsStart = node.heritageClauses[0].getStart() + astOffset;
str.overwrite(extendsStart, extendsStart + 'extends'.length, '=');

const extendsList = node.heritageClauses[0].types;
let prev = extendsList[0];
extendsList.slice(1).forEach((heritageClause) => {
str.overwrite(
prev.getEnd() + astOffset,
heritageClause.getStart() + astOffset,
' & '
);
prev = heritageClause;
});

str.appendLeft(node.heritageClauses[0].getEnd() + astOffset, ' & ');
} else {
str.prependLeft(str.original.indexOf('{', node.getStart() + astOffset), '=');
}
});
}
65 changes: 65 additions & 0 deletions packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import MagicString from 'magic-string';
import ts from 'typescript';

export function isInterfaceOrTypeDeclaration(
Expand Down Expand Up @@ -206,3 +207,67 @@ export function isSafeToPrefixWithSemicolon(node: ts.Identifier): boolean {
)
);
}

/**
* move node to top of script so they appear outside our render function
*/
export function moveNode(
node: ts.Node,
str: MagicString,
astOffset: number,
scriptStart: number,
sourceFile: ts.SourceFile
) {
const scanner = ts.createScanner(
sourceFile.languageVersion,
/*skipTrivia*/ false,
sourceFile.languageVariant
);

const comments = ts.getLeadingCommentRanges(node.getFullText(), 0) ?? [];
if (
!comments.some((comment) => comment.hasTrailingNewLine) &&
isNewGroup(sourceFile, node, scanner)
) {
str.appendRight(node.getStart() + astOffset, '\n');
}

for (const comment of comments) {
const commentEnd = node.pos + comment.end + astOffset;
str.move(node.pos + comment.pos + astOffset, commentEnd, scriptStart + 1);

if (comment.hasTrailingNewLine) {
str.overwrite(commentEnd - 1, commentEnd, str.original[commentEnd - 1] + '\n');
}
}

str.move(node.getStart() + astOffset, node.end + astOffset, scriptStart + 1);
//add in a \n
const originalEndChar = str.original[node.end + astOffset - 1];

str.overwrite(node.end + astOffset - 1, node.end + astOffset, originalEndChar + '\n');
}

/**
* adopted from https://github.com/microsoft/TypeScript/blob/6e0447fdf165b1cec9fc80802abcc15bd23a268f/src/services/organizeImports.ts#L111
*/
function isNewGroup(sourceFile: ts.SourceFile, topLevelImportDecl: ts.Node, scanner: ts.Scanner) {
const startPos = topLevelImportDecl.getFullStart();
const endPos = topLevelImportDecl.getStart();
scanner.setText(sourceFile.text, startPos, endPos - startPos);

let numberOfNewLines = 0;
while (scanner.getTokenPos() < endPos) {
const tokenKind = scanner.scan();

if (tokenKind === ts.SyntaxKind.NewLineTrivia) {
numberOfNewLines++;

if (numberOfNewLines >= 2) {
return true;
}
}
}

return false;
}
3 changes: 3 additions & 0 deletions packages/svelte2tsx/src/utils/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function flatten<T>(arr: T[][]): T[] {
return arr.reduce((acc, val) => acc.concat(val), []);
}
Loading