-
-
Notifications
You must be signed in to change notification settings - Fork 188
/
ExportedNames.ts
403 lines (356 loc) · 14.6 KB
/
ExportedNames.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
import MagicString from 'magic-string';
import ts from 'typescript';
import { surroundWithIgnoreComments } from '../../utils/ignore';
import { preprendStr, overwriteStr } from '../../utils/magic-string';
import { findExportKeyword, getLastLeadingDoc, isInterfaceOrTypeDeclaration } from '../utils/tsAst';
export function is$$PropsDeclaration(
node: ts.Node
): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration {
return isInterfaceOrTypeDeclaration(node) && node.name.text === '$$Props';
}
interface ExportedName {
isLet: boolean;
type?: string;
identifierText?: string;
required?: boolean;
doc?: string;
}
export class ExportedNames {
public uses$$Props = false;
private exports = new Map<string, ExportedName>();
private possibleExports = new Map<
string,
ExportedName & {
declaration: ts.VariableDeclarationList;
}
>();
private doneDeclarationTransformation = new Set<ts.VariableDeclarationList>();
private getters = new Set<string>();
constructor(private str: MagicString, private astOffset: number) {}
handleVariableStatement(node: ts.VariableStatement, parent: ts.Node): void {
const exportModifier = findExportKeyword(node);
if (exportModifier) {
const isLet = node.declarationList.flags === ts.NodeFlags.Let;
const isConst = node.declarationList.flags === ts.NodeFlags.Const;
this.handleExportedVariableDeclarationList(node.declarationList, (_, ...args) =>
this.addExport(...args)
);
if (isLet) {
this.propTypeAssertToUserDefined(node.declarationList);
} else if (isConst) {
node.declarationList.forEachChild((n) => {
if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) {
this.addGetter(n.name);
}
});
}
this.removeExport(exportModifier.getStart(), exportModifier.end);
} else if (ts.isSourceFile(parent)) {
this.handleExportedVariableDeclarationList(
node.declarationList,
this.addPossibleExport.bind(this)
);
}
}
handleExportFunctionOrClass(node: ts.ClassDeclaration | ts.FunctionDeclaration): void {
const exportModifier = findExportKeyword(node);
if (!exportModifier) {
return;
}
this.removeExport(exportModifier.getStart(), exportModifier.end);
this.addGetter(node.name);
// Can't export default here
if (node.name) {
this.addExport(node.name, false);
}
}
handleExportDeclaration(node: ts.ExportDeclaration): void {
const { exportClause } = node;
if (ts.isNamedExports(exportClause)) {
for (const ne of exportClause.elements) {
if (ne.propertyName) {
this.addExport(ne.propertyName, false, ne.name);
} else {
this.addExport(ne.name, false);
}
}
//we can remove entire statement
this.removeExport(node.getStart(), node.end);
}
}
private removeExport(start: number, end: number) {
const exportStart = this.str.original.indexOf('export', start + this.astOffset);
const exportEnd = exportStart + (end - start);
this.str.remove(exportStart, exportEnd);
}
/**
* Appends `prop = __sveltets_1_any(prop)` to given declaration in order to
* trick TS into widening the type. Else for example `let foo: string | undefined = undefined`
* is narrowed to `undefined` by TS.
*/
private propTypeAssertToUserDefined(node: ts.VariableDeclarationList) {
if (this.doneDeclarationTransformation.has(node)) {
return;
}
const handleTypeAssertion = (declaration: ts.VariableDeclaration) => {
const identifier = declaration.name;
const tsType = declaration.type;
const jsDocType = ts.getJSDocType(declaration);
const type = tsType || jsDocType;
if (
ts.isIdentifier(identifier) &&
// Ensure initialization for proper control flow and to avoid "possibly undefined" type errors.
// Also ensure prop is typed as any with a type annotation in TS strict mode
(!declaration.initializer ||
// Widen the type, else it's narrowed to the initializer
type ||
// Edge case: TS infers `export let bla = false` to type `false`.
// prevent that by adding the any-wrap in this case, too.
(!type &&
[ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.TrueKeyword].includes(
declaration.initializer.kind
)))
) {
const name = identifier.getText();
const end = declaration.end + this.astOffset;
preprendStr(
this.str,
end,
surroundWithIgnoreComments(`;${name} = __sveltets_1_any(${name});`)
);
}
};
const findComma = (target: ts.Node) =>
target.getChildren().filter((child) => child.kind === ts.SyntaxKind.CommaToken);
const splitDeclaration = () => {
const commas = node
.getChildren()
.filter((child) => child.kind === ts.SyntaxKind.SyntaxList)
.map(findComma)
.reduce((current, previous) => [...current, ...previous], []);
commas.forEach((comma) => {
const start = comma.getStart() + this.astOffset;
const end = comma.getEnd() + this.astOffset;
overwriteStr(this.str, start, end, ';let ');
});
};
for (const declaration of node.declarations) {
handleTypeAssertion(declaration);
}
// need to be append after the type assert treatment
splitDeclaration();
this.doneDeclarationTransformation.add(node);
}
private handleExportedVariableDeclarationList(
list: ts.VariableDeclarationList,
add: ExportedNames['addPossibleExport']
) {
const isLet = list.flags === ts.NodeFlags.Let;
ts.forEachChild(list, (node) => {
if (ts.isVariableDeclaration(node)) {
if (ts.isIdentifier(node.name)) {
add(list, node.name, isLet, node.name, node.type, !node.initializer);
} else if (
ts.isObjectBindingPattern(node.name) ||
ts.isArrayBindingPattern(node.name)
) {
ts.forEachChild(node.name, (element) => {
if (ts.isBindingElement(element)) {
add(list, element.name, isLet);
}
});
}
}
});
}
private addGetter(node: ts.Identifier): void {
if (!node) {
return;
}
this.getters.add(node.text);
}
createClassGetters(): string {
return Array.from(this.getters)
.map((name) => `\n get ${name}() { return this.$$prop_def.${name} }`)
.join('');
}
createClassAccessors(): string {
const accessors: string[] = [];
for (const value of this.exports.values()) {
if (this.getters.has(value.identifierText)) {
continue;
}
accessors.push(value.identifierText);
}
return accessors
.map(
(name) =>
`\n get ${name}() { return this.$$prop_def.${name} }` +
`\n /**accessor*/\n set ${name}(_) {}`
)
.join('');
}
/**
* Marks a top level declaration as a possible export
* which could be exported through `export { .. }` later.
*/
private addPossibleExport(
declaration: ts.VariableDeclarationList,
name: ts.BindingName,
isLet: boolean,
target: ts.BindingName = null,
type: ts.TypeNode = null,
required = false
) {
if (!ts.isIdentifier(name)) {
return;
}
if (target && ts.isIdentifier(target)) {
this.possibleExports.set(name.text, {
declaration,
isLet,
type: type?.getText(),
identifierText: (target as ts.Identifier).text,
required,
doc: this.getDoc(target)
});
} else {
this.possibleExports.set(name.text, {
declaration,
isLet
});
}
}
/**
* Adds export to map
*/
private addExport(
name: ts.BindingName,
isLet: boolean,
target: ts.BindingName = null,
type: ts.TypeNode = null,
required = false
): void {
if (name.kind != ts.SyntaxKind.Identifier) {
throw Error('export source kind not supported ' + name);
}
if (target && target.kind != ts.SyntaxKind.Identifier) {
throw Error('export target kind not supported ' + target);
}
const existingDeclaration = this.possibleExports.get(name.text);
if (target) {
this.exports.set(name.text, {
isLet: isLet || existingDeclaration?.isLet,
type: type?.getText() || existingDeclaration?.type,
identifierText: (target as ts.Identifier).text,
required: required || existingDeclaration?.required,
doc: this.getDoc(target) || existingDeclaration?.doc
});
} else {
this.exports.set(name.text, {
isLet: isLet || existingDeclaration?.isLet,
type: existingDeclaration?.type,
required: existingDeclaration?.required,
doc: existingDeclaration?.doc
});
}
if (existingDeclaration?.isLet) {
this.propTypeAssertToUserDefined(existingDeclaration.declaration);
}
}
private getDoc(target: ts.BindingName) {
let doc = undefined;
// Traverse `a` one up. If the declaration is part of a declaration list,
// the comment is at this point already
const variableDeclaration = target?.parent;
// Traverse `a` up to `export let a`
const exportExpr = target?.parent?.parent?.parent;
if (variableDeclaration) {
doc = getLastLeadingDoc(variableDeclaration);
}
if (exportExpr && !doc) {
doc = getLastLeadingDoc(exportExpr);
}
return doc;
}
/**
* Creates a string from the collected props
*
* @param isTsFile Whether this is a TypeScript file or not.
*/
createPropsStr(isTsFile: boolean): string {
const names = Array.from(this.exports.entries());
if (this.uses$$Props) {
const lets = names.filter(([, { isLet }]) => isLet);
const others = names.filter(([, { isLet }]) => !isLet);
// We need to check both ways:
// - The check if exports are assignable to Parial<$$Props> is necessary to make sure
// no props are missing. Partial<$$Props> is needed because props with a default value
// count as optional, but semantically speaking it is still correctly implementing the interface
// - The check if $$Props is assignable to exports is necessary to make sure no extraneous props
// are defined and that no props are required that should be optional
// __sveltets_1_ensureRightProps needs to be declared in a way that doesn't affect the type result of props
return (
'{...__sveltets_1_ensureRightProps<{' +
this.createReturnElementsType(lets).join(',') +
'}>(__sveltets_1_any("") as $$Props), ' +
'...__sveltets_1_ensureRightProps<Partial<$$Props>>({' +
this.createReturnElements(lets, false).join(',') +
'}), ...{} as unknown as $$Props, ...{' +
// We add other exports of classes and functions here because
// they need to appear in the props object in order to properly
// type bind:xx but they are not needed to be part of $$Props
this.createReturnElements(others, false).join(', ') +
'} as {' +
this.createReturnElementsType(others).join(',') +
'}}'
);
}
if (names.length === 0) {
// Necessary, because {} roughly equals to any
return isTsFile
? '{} as Record<string, never>'
: '/** @type {Record<string, never>} */ ({})';
}
const dontAddTypeDef =
!isTsFile || names.every(([_, value]) => !value.type && value.required);
const returnElements = this.createReturnElements(names, dontAddTypeDef);
if (dontAddTypeDef) {
// Only `typeof` exports -> omit the `as {...}` completely.
// If not TS, omit the types to not have a "cannot use types in jsx" error.
return `{${returnElements.join(' , ')}}`;
}
const returnElementsType = this.createReturnElementsType(names);
return `{${returnElements.join(' , ')}} as {${returnElementsType.join(', ')}}`;
}
private createReturnElements(
names: Array<[string, ExportedName]>,
dontAddTypeDef: boolean
): string[] {
return names.map(([key, value]) => {
// Important to not use shorthand props for rename functionality
return `${dontAddTypeDef && value.doc ? `\n${value.doc}` : ''}${
value.identifierText || key
}: ${key}`;
});
}
private createReturnElementsType(names: Array<[string, ExportedName]>) {
return names.map(([key, value]) => {
const identifier = `${value.doc ? `\n${value.doc}` : ''}${value.identifierText || key}${
value.required ? '' : '?'
}`;
if (!value.type) {
return `${identifier}: typeof ${key}`;
}
return `${identifier}: ${value.type}`;
});
}
createOptionalPropsArray(): string[] {
return Array.from(this.exports.entries())
.filter(([_, entry]) => !entry.required)
.map(([name, entry]) => `'${entry.identifierText || name}'`);
}
getExportsMap() {
return this.exports;
}
}