Skip to content

Commit 19f1f17

Browse files
authored
(fix) destructuring inside $ (#445)
* (fix) destructuring inside $ #444 * closer to a solution * remove ';' insertion * comment * support case where user defines some of the destructured variables * add ast extract identifiers util * support array destructuring * do all transformations in the end
1 parent b50eae1 commit 19f1f17

File tree

5 files changed

+224
-31
lines changed

5 files changed

+224
-31
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import ts from 'typescript';
2+
import MagicString from 'magic-string';
3+
import {
4+
getBinaryAssignmentExpr,
5+
extractIdentifiers,
6+
isParenthesizedObjectOrArrayLiteralExpression,
7+
} from '../utils/tsAst';
8+
9+
export class ImplicitTopLevelNames {
10+
private map = new Set<ts.LabeledStatement>();
11+
12+
add(node: ts.LabeledStatement) {
13+
this.map.add(node);
14+
}
15+
16+
modifyCode(rootVariables: Set<string>, astOffset: number, str: MagicString) {
17+
for (const node of this.map.values()) {
18+
const names = this.getNames(node);
19+
if (names.length === 0) {
20+
continue;
21+
}
22+
23+
const implicitTopLevelNames = names.filter((name) => !rootVariables.has(name));
24+
const pos = node.label.getStart();
25+
26+
if (this.hasOnlyImplicitTopLevelNames(names, implicitTopLevelNames)) {
27+
// remove '$:' label
28+
str.remove(pos + astOffset, pos + astOffset + 2);
29+
str.prependRight(pos + astOffset, `let `);
30+
31+
this.removeBracesFromParenthizedExpression(node, astOffset, str);
32+
} else {
33+
implicitTopLevelNames.forEach((name) => {
34+
str.prependRight(pos + astOffset, `let ${name};\n`);
35+
});
36+
}
37+
}
38+
}
39+
40+
private getNames(node: ts.LabeledStatement) {
41+
const leftHandSide = getBinaryAssignmentExpr(node)?.left;
42+
if (!leftHandSide) {
43+
return [];
44+
}
45+
46+
return (
47+
extractIdentifiers(leftHandSide)
48+
.map((id) => id.text)
49+
// svelte won't let you create a variable with $ prefix (reserved for stores)
50+
.filter((name) => !name.startsWith('$'))
51+
);
52+
}
53+
54+
private hasOnlyImplicitTopLevelNames(names: string[], implicitTopLevelNames: string[]) {
55+
return names.length === implicitTopLevelNames.length;
56+
}
57+
58+
private removeBracesFromParenthizedExpression(
59+
node: ts.LabeledStatement,
60+
astOffset: number,
61+
str: MagicString,
62+
) {
63+
// If expression is of type `$: ({a} = b);`,
64+
// remove the surrounding braces so that the transformation
65+
// to `let {a} = b;` produces valid code.
66+
if (
67+
ts.isExpressionStatement(node.statement) &&
68+
isParenthesizedObjectOrArrayLiteralExpression(node.statement.expression)
69+
) {
70+
const start = node.statement.expression.getStart() + astOffset;
71+
str.overwrite(start, start + 1, '', { contentOnly: true });
72+
const end = node.statement.expression.getEnd() + astOffset - 1;
73+
str.overwrite(end, end + 1, '', { contentOnly: true });
74+
}
75+
}
76+
}

packages/svelte2tsx/src/svelte2tsx.ts

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { convertHtmlxToJsx } from './htmlxtojsx';
77
import { Node } from 'estree-walker';
88
import * as ts from 'typescript';
99
import { createEventHandlerTransformer, eventMapToString } from './nodes/event-handler';
10-
import { findExortKeyword } from './utils/tsAst';
10+
import { findExortKeyword, getBinaryAssignmentExpr } from './utils/tsAst';
1111
import { InstanceScriptProcessResult, CreateRenderFunctionPara } from './interfaces';
1212
import { createRenderFunctionGetterStr, createClassGetters } from './nodes/exportgetters';
1313
import { ExportedNames } from './nodes/ExportedNames';
14+
import { ImplicitTopLevelNames } from './nodes/ImplicitTopLevelNames';
1415

1516
function AttributeValueAsJsExpression(htmlx: string, attr: Node): string {
1617
if (attr.value.length == 0) return "''"; //wut?
@@ -391,7 +392,7 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
391392
const exportedNames = new ExportedNames();
392393
const getters = new Set<string>();
393394

394-
const implicitTopLevelNames: Map<string, number> = new Map();
395+
const implicitTopLevelNames = new ImplicitTopLevelNames();
395396
let uses$$props = false;
396397
let uses$$restProps = false;
397398

@@ -638,7 +639,6 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
638639
});
639640
};
640641

641-
const semiRegex = /^\s*;/;
642642
const wrapExpressionWithInvalidate = (expression: ts.Expression | undefined) => {
643643
if (!expression) {
644644
return;
@@ -655,10 +655,8 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
655655

656656
str.prependLeft(start, '__sveltets_invalidate(() => ');
657657
str.appendRight(end, ')');
658-
659-
if (!semiRegex.test(htmlx.substring(end))) {
660-
str.appendRight(end, ';');
661-
}
658+
// Not adding ';' at the end because right now this function is only invoked
659+
// in situations where there is a line break of ; guaranteed to be present (else the code is invalid)
662660
};
663661

664662
const walk = (node: ts.Node, parent: ts.Node) => {
@@ -755,7 +753,9 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
755753
}
756754

757755
//handle stores etc
758-
if (ts.isIdentifier(node)) handleIdentifier(node, parent);
756+
if (ts.isIdentifier(node)) {
757+
handleIdentifier(node, parent);
758+
}
759759

760760
//track implicit declarations in reactive blocks at the top level
761761
if (
@@ -764,22 +764,10 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
764764
node.label.text == '$' &&
765765
node.statement
766766
) {
767-
if (
768-
ts.isExpressionStatement(node.statement) &&
769-
ts.isBinaryExpression(node.statement.expression) &&
770-
node.statement.expression.operatorToken.kind == ts.SyntaxKind.EqualsToken &&
771-
ts.isIdentifier(node.statement.expression.left)
772-
) {
773-
const name = node.statement.expression.left.text;
774-
775-
// svelte won't let you create a variable with $ prefix anyway
776-
const isPotentialStore = name.startsWith('$');
777-
778-
if (!implicitTopLevelNames.has(name) && !isPotentialStore) {
779-
implicitTopLevelNames.set(name, node.label.getStart());
780-
}
781-
782-
wrapExpressionWithInvalidate(node.statement.expression.right);
767+
const binaryExpression = getBinaryAssignmentExpr(node);
768+
if (binaryExpression) {
769+
implicitTopLevelNames.add(node);
770+
wrapExpressionWithInvalidate(binaryExpression.right);
783771
} else {
784772
const start = node.getStart() + astOffset;
785773
const end = node.getEnd() + astOffset;
@@ -802,13 +790,7 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
802790
pendingStoreResolutions.map(resolveStore);
803791

804792
// declare implicit reactive variables we found in the script
805-
for (const [name, pos] of implicitTopLevelNames.entries()) {
806-
if (!rootScope.declared.has(name)) {
807-
// remove '$:' label
808-
str.remove(pos + astOffset, pos + astOffset + 2);
809-
str.prependRight(pos + astOffset, `let `);
810-
}
811-
}
793+
implicitTopLevelNames.modifyCode(rootScope.declared, astOffset, str);
812794

813795
const firstImport = tsAst.statements
814796
.filter(ts.isImportDeclaration)

packages/svelte2tsx/src/utils/tsAst.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,99 @@ import ts from 'typescript';
33
export function findExortKeyword(node: ts.Node) {
44
return node.modifiers?.find((x) => x.kind == ts.SyntaxKind.ExportKeyword);
55
}
6+
7+
/**
8+
* Node is like `bla = ...` or `{bla} = ...` or `[bla] = ...`
9+
*/
10+
function isAssignmentBinaryExpr(node: ts.Expression): node is ts.BinaryExpression {
11+
return (
12+
ts.isBinaryExpression(node) &&
13+
node.operatorToken.kind == ts.SyntaxKind.EqualsToken &&
14+
(ts.isIdentifier(node.left) ||
15+
ts.isObjectLiteralExpression(node.left) ||
16+
ts.isArrayLiteralExpression(node.left))
17+
);
18+
}
19+
20+
/**
21+
* Returns if node is like `$: bla = ...` or `$: ({bla} = ...)` or `$: [bla] = ...=`
22+
*/
23+
export function getBinaryAssignmentExpr(
24+
node: ts.LabeledStatement,
25+
): ts.BinaryExpression | undefined {
26+
if (ts.isExpressionStatement(node.statement)) {
27+
if (isAssignmentBinaryExpr(node.statement.expression)) {
28+
return node.statement.expression;
29+
}
30+
if (
31+
ts.isParenthesizedExpression(node.statement.expression) &&
32+
isAssignmentBinaryExpr(node.statement.expression.expression)
33+
) {
34+
return node.statement.expression.expression;
35+
}
36+
}
37+
}
38+
39+
/**
40+
* Returns true if node is like `({bla} ..)` or `([bla] ...)`
41+
*/
42+
export function isParenthesizedObjectOrArrayLiteralExpression(
43+
node: ts.Expression,
44+
): node is ts.ParenthesizedExpression {
45+
return (
46+
ts.isParenthesizedExpression(node) &&
47+
ts.isBinaryExpression(node.expression) &&
48+
(ts.isObjectLiteralExpression(node.expression.left) ||
49+
ts.isArrayLiteralExpression(node.expression.left))
50+
);
51+
}
52+
53+
/**
54+
*
55+
* Adapted from https://github.com/Rich-Harris/periscopic/blob/d7a820b04e1f88b452313ab3e54771b352f0defb/src/index.ts#L150
56+
*/
57+
export function extractIdentifiers(
58+
node: ts.Node,
59+
identifiers: ts.Identifier[] = [],
60+
): ts.Identifier[] {
61+
if (ts.isIdentifier(node)) {
62+
identifiers.push(node);
63+
} else if (isMember(node)) {
64+
let object: ts.Node = node;
65+
while (isMember(object)) {
66+
object = object.expression;
67+
}
68+
if (ts.isIdentifier(object)) {
69+
identifiers.push(object);
70+
}
71+
} else if (ts.isArrayBindingPattern(node) || ts.isObjectBindingPattern(node)) {
72+
node.elements.forEach((element) => {
73+
extractIdentifiers(element);
74+
});
75+
} else if (ts.isObjectLiteralExpression(node)) {
76+
node.properties.forEach((child) => {
77+
if (ts.isSpreadAssignment(child)) {
78+
extractIdentifiers(child.expression, identifiers);
79+
} else if (ts.isShorthandPropertyAssignment(child)) {
80+
// in ts Ast { a = 1 } and { a } are both ShorthandPropertyAssignment
81+
extractIdentifiers(child.name, identifiers);
82+
}
83+
});
84+
} else if (ts.isArrayLiteralExpression(node)) {
85+
node.elements.forEach((element) => {
86+
if (ts.isSpreadElement(element)) {
87+
extractIdentifiers(element, identifiers);
88+
} else {
89+
extractIdentifiers(element, identifiers);
90+
}
91+
});
92+
}
93+
94+
return identifiers;
95+
}
96+
97+
export function isMember(
98+
node: ts.Node,
99+
): node is ts.ElementAccessExpression | ts.PropertyAccessExpression {
100+
return ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node);
101+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
///<reference types="svelte" />
2+
<></>;function render() {
3+
4+
let { count } = __sveltets_invalidate(() => __sveltets_store_get(data));
5+
let { count2 } = __sveltets_invalidate(() => __sveltets_store_get(data))
6+
let count3;
7+
$: ({ count3 } = __sveltets_invalidate(() => __sveltets_store_get(data)))
8+
let bla4;
9+
let bla5;
10+
$: ({ bla4, bla5 } = __sveltets_invalidate(() => __sveltets_store_get(data)))
11+
12+
let [ count ] = __sveltets_invalidate(() => __sveltets_store_get(data));
13+
let [ count2 ] = __sveltets_invalidate(() => __sveltets_store_get(data))
14+
let count3;
15+
$: ([ count3 ] = __sveltets_invalidate(() => __sveltets_store_get(data)))
16+
let bla4;
17+
let bla5;
18+
$: ([ bla4, bla5 ] = __sveltets_invalidate(() => __sveltets_store_get(data)))
19+
;
20+
() => (<></>);
21+
return { props: {}, slots: {}, getters: {}, events: {} }}
22+
23+
export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(render)) {
24+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
$: ({ count } = $data);
3+
$: ({ count2 } = $data)
4+
let count3;
5+
$: ({ count3 } = $data)
6+
let bla4;
7+
$: ({ bla4, bla5 } = $data)
8+
9+
$: ([ count ] = $data);
10+
$: ([ count2 ] = $data)
11+
let count3;
12+
$: ([ count3 ] = $data)
13+
let bla4;
14+
$: ([ bla4, bla5 ] = $data)
15+
</script>

0 commit comments

Comments
 (0)