Skip to content

Commit 5df25a8

Browse files
authored
overhaul typescript conversions (#386)
* better whitespace handling * space after colon * preserve docs in JSDoc comments * WIP rewrite typescript conversion logic * fix * satisfies * simplify/fix
1 parent 0bc2837 commit 5df25a8

File tree

3 files changed

+126
-122
lines changed

3 files changed

+126
-122
lines changed

apps/svelte.dev/content/docs/kit/20-core-concepts/30-form-actions.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ In the simplest case, a page declares a `default` action:
1212

1313
```js
1414
/// file: src/routes/login/+page.server.js
15-
/** @type {import('./$types').Actions} */
15+
/** @satisfies {import('./$types').Actions} */
1616
export const actions = {
1717
default: async (event) => {
1818
// TODO log the user in
@@ -56,7 +56,7 @@ Instead of one `default` action, a page can have as many named actions as it nee
5656

5757
```js
5858
/// file: src/routes/login/+page.server.js
59-
/** @type {import('./$types').Actions} */
59+
/** @satisfies {import('./$types').Actions} */
6060
export const actions = {
6161
--- default: async (event) => {---
6262
+++ login: async (event) => {+++
@@ -119,7 +119,7 @@ export async function load({ cookies }) {
119119
return { user };
120120
}
121121

122-
/** @type {import('./$types').Actions} */
122+
/** @satisfies {import('./$types').Actions} */
123123
export const actions = {
124124
login: async ({ cookies, request }) => {
125125
const data = await request.formData();
@@ -168,7 +168,7 @@ declare module '$lib/server/db';
168168
+++import { fail } from '@sveltejs/kit';+++
169169
import * as db from '$lib/server/db';
170170

171-
/** @type {import('./$types').Actions} */
171+
/** @satisfies {import('./$types').Actions} */
172172
export const actions = {
173173
login: async ({ cookies, request }) => {
174174
const data = await request.formData();
@@ -232,7 +232,7 @@ declare module '$lib/server/db';
232232
import { fail, +++redirect+++ } from '@sveltejs/kit';
233233
import * as db from '$lib/server/db';
234234

235-
/** @type {import('./$types').Actions} */
235+
/** @satisfies {import('./$types').Actions} */
236236
export const actions = {
237237
login: async ({ cookies, request, +++url+++ }) => {
238238
const data = await request.formData();
@@ -317,7 +317,7 @@ export function load(event) {
317317
};
318318
}
319319

320-
/** @type {import('./$types').Actions} */
320+
/** @satisfies {import('./$types').Actions} */
321321
export const actions = {
322322
logout: async (event) => {
323323
event.cookies.delete('sessionid', { path: '/' });
@@ -507,7 +507,7 @@ Some forms don't need to `POST` data to the server — search inputs, for exampl
507507
<form action="/search">
508508
<label>
509509
Search
510-
<input name="q">
510+
<input name="q" />
511511
</label>
512512
</form>
513513
```

apps/svelte.dev/content/docs/kit/20-core-concepts/50-state-management.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function load() {
2020
return { user };
2121
}
2222

23-
/** @type {import('./$types').Actions} */
23+
/** @satisfies {import('./$types').Actions} */
2424
export const actions = {
2525
default: async ({ request }) => {
2626
const data = await request.formData();

packages/site-kit/src/lib/markdown/renderer.ts

Lines changed: 118 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -368,91 +368,112 @@ async function convert_to_ts(js_code: string, indent = '', offset = '') {
368368

369369
async function walk(node: ts.Node) {
370370
const jsdoc = get_jsdoc(node);
371+
371372
if (jsdoc) {
372-
for (const comment of jsdoc) {
373-
let modified = false;
374-
375-
let count = 0;
376-
for (const tag of comment.tags ?? []) {
377-
if (ts.isJSDocTypeTag(tag)) {
378-
const [name, generics] = await get_type_info(tag);
379-
380-
if (ts.isFunctionDeclaration(node)) {
381-
const is_export = node.modifiers?.some(
382-
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
383-
)
384-
? 'export '
385-
: '';
386-
const is_async = node.modifiers?.some(
387-
(modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword
388-
);
389-
390-
const type = generics !== undefined ? `${name}<${generics}>` : name;
391-
392-
if (node.name && node.body) {
393-
code.overwrite(
394-
node.getStart(),
395-
node.name.getEnd(),
396-
`${is_export ? 'export ' : ''}const ${node.name.getText()}: ${type} = (${
397-
is_async ? 'async ' : ''
398-
}`
399-
);
400-
401-
code.appendLeft(node.body.getStart(), '=> ');
402-
code.appendLeft(node.body.getEnd(), ');');
403-
404-
modified = true;
405-
}
406-
} else if (
407-
ts.isVariableStatement(node) &&
408-
node.declarationList.declarations.length === 1
409-
) {
410-
const variable_statement = node.declarationList.declarations[0];
411-
412-
if (variable_statement.name.getText() === 'actions') {
413-
let i = variable_statement.getEnd();
414-
while (code.original[i - 1] !== '}') i -= 1;
415-
code.appendLeft(i, ` satisfies ${name}`);
416-
} else {
417-
code.appendLeft(
418-
variable_statement.name.getEnd(),
419-
`: ${name}${generics ? `<${generics}>` : ''}`
420-
);
421-
}
422-
423-
modified = true;
424-
} else {
425-
throw new Error('Unhandled @type JsDoc->TS conversion: ' + js_code);
426-
}
427-
} else if (ts.isJSDocParameterTag(tag) && ts.isFunctionDeclaration(node)) {
428-
const sanitised_param = tag
429-
.getFullText()
430-
.replace(/\s+/g, '')
431-
.replace(/(^\*|\*$)/g, '');
432-
433-
const [, param_type] = /@param{(.+)}(.+)/.exec(sanitised_param) ?? [];
434-
435-
let param_count = 0;
436-
for (const param of node.parameters) {
437-
if (count !== param_count) {
438-
param_count++;
439-
continue;
440-
}
441-
442-
code.appendLeft(param.getEnd(), `:${param_type}`);
443-
444-
param_count++;
445-
}
446-
447-
modified = true;
373+
// this isn't an exhaustive list of tags we could potentially encounter (no `@template` etc)
374+
// but it's good enough to cover what's actually in the docs right now
375+
let type: string | null = null;
376+
let params: string[] = [];
377+
let returns: string | null = null;
378+
let satisfies: string | null = null;
379+
380+
if (jsdoc.length > 1) {
381+
throw new Error('woah nelly');
382+
}
383+
384+
const { comment, tags = [] } = jsdoc[0];
385+
386+
for (const tag of tags) {
387+
if (ts.isJSDocTypeTag(tag)) {
388+
type = get_type_info(tag.typeExpression);
389+
} else if (ts.isJSDocParameterTag(tag)) {
390+
params.push(get_type_info(tag.typeExpression!));
391+
} else if (ts.isJSDocReturnTag(tag)) {
392+
returns = get_type_info(tag.typeExpression!);
393+
} else if (ts.isJSDocSatisfiesTag(tag)) {
394+
satisfies = get_type_info(tag.typeExpression!);
395+
} else {
396+
throw new Error('Unhandled tag');
397+
}
398+
399+
let start = tag.getStart();
400+
let end = tag.getEnd();
401+
402+
while (start > 0 && code.original[start] !== '\n') start -= 1;
403+
while (end > 0 && code.original[end] !== '\n') end -= 1;
404+
code.remove(start, end);
405+
}
406+
407+
if (type && satisfies) {
408+
throw new Error('Cannot combine @type and @satisfies');
409+
}
410+
411+
if (ts.isFunctionDeclaration(node)) {
412+
// convert function to a `const`
413+
if (type || satisfies) {
414+
const is_export = node.modifiers?.some(
415+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
416+
);
417+
418+
const is_async = node.modifiers?.some(
419+
(modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword
420+
);
421+
422+
code.overwrite(
423+
node.getStart(),
424+
node.name!.getStart(),
425+
is_export ? `export const ` : `const `
426+
);
427+
428+
const modifier = is_async ? 'async ' : '';
429+
code.appendLeft(
430+
node.name!.getEnd(),
431+
type ? `: ${type} = ${modifier}` : ` = ${modifier}(`
432+
);
433+
434+
code.prependRight(node.body!.getStart(), '=> ');
435+
436+
code.appendLeft(node.getEnd(), satisfies ? `) satisfies ${satisfies};` : ';');
437+
}
438+
439+
for (let i = 0; i < node.parameters.length; i += 1) {
440+
if (params[i] !== undefined) {
441+
code.appendLeft(node.parameters[i].getEnd(), `: ${params[i]}`);
448442
}
443+
}
449444

450-
count++;
445+
if (returns) {
446+
let start = node.body!.getStart();
447+
while (code.original[start - 1] !== ')') start -= 1;
448+
code.appendLeft(start, `: ${returns}`);
451449
}
450+
} else if (ts.isVariableStatement(node) && node.declarationList.declarations.length === 1) {
451+
if (params.length > 0 || returns) {
452+
throw new Error('TODO handle @params and @returns in variable declarations');
453+
}
454+
455+
const declaration = node.declarationList.declarations[0];
452456

453-
if (modified) {
454-
code.overwrite(comment.getStart(), comment.getEnd(), '');
457+
if (type) {
458+
code.appendLeft(declaration.name.getEnd(), `: ${type}`);
455459
}
460+
461+
if (satisfies) {
462+
let end = declaration.getEnd();
463+
if (code.original[end - 1] === ';') end -= 1;
464+
code.appendLeft(end, ` satisfies ${satisfies}`);
465+
}
466+
} else {
467+
throw new Error('Unhandled @type JsDoc->TS conversion: ' + js_code);
468+
}
469+
470+
if (!comment) {
471+
// remove the whole thing
472+
let start = jsdoc[0].getStart();
473+
let end = jsdoc[0].getEnd();
474+
475+
while (start > 0 && code.original[start] !== '\n') start -= 1;
476+
code.overwrite(start, end, '');
456477
}
457478
}
458479

@@ -487,42 +508,25 @@ async function convert_to_ts(js_code: string, indent = '', offset = '') {
487508

488509
let transformed = code.toString();
489510

490-
return transformed === js_code ? undefined : transformed.replace(/\n\s*\n\s*\n/g, '\n\n');
491-
492-
async function get_type_info(tag: ts.JSDocTypeTag | ts.JSDocParameterTag) {
493-
const type_text = tag.typeExpression?.getText();
494-
let name = type_text?.slice(1, -1); // remove { }
495-
496-
const single_line_name = (
497-
await prettier.format(name ?? '', {
498-
printWidth: 1000,
499-
parser: 'typescript',
500-
semi: false,
501-
singleQuote: true
502-
})
503-
).replace('\n', '');
511+
return transformed === js_code ? undefined : transformed;
512+
513+
function get_type_info(expression: ts.JSDocTypeExpression) {
514+
const type = expression
515+
?.getText()!
516+
.slice(1, -1) // remove surrounding `{` and `}`
517+
.replace(/ \* ?/gm, '')
518+
.replace(/import\('(.+?)'\)\.(\w+)(?:(<.+>))?/gms, (_, source, name, args = '') => {
519+
const existing = imports.get(source);
520+
if (existing) {
521+
existing.add(name);
522+
} else {
523+
imports.set(source, new Set([name]));
524+
}
504525

505-
const import_match = /import\('(.+?)'\)\.(\w+)(?:<(.+)>)?$/s.exec(single_line_name);
526+
return name + args;
527+
});
506528

507-
if (import_match) {
508-
const [, from, _name, generics] = import_match;
509-
name = _name;
510-
const existing = imports.get(from);
511-
if (existing) {
512-
existing.add(name);
513-
} else {
514-
imports.set(from, new Set([name]));
515-
}
516-
if (generics !== undefined) {
517-
return [
518-
name,
519-
generics
520-
.replaceAll('*', '') // get rid of JSDoc asterisks
521-
.replace(' }>', '}>') // unindent closing brace
522-
];
523-
}
524-
}
525-
return [name];
529+
return type;
526530
}
527531
}
528532

@@ -690,7 +694,7 @@ async function syntax_highlight({
690694

691695
// munge shiki output: put whitespace outside `<span>` elements, so that
692696
// highlight delimiters fall outside tokens
693-
html = html.replace(/(<span[^<]+?>)(\s+)/g, '$2$1').replace(/(\s+)(<\/span>)/g, '$2$1');
697+
html = html.replace(/(<span[^>]+?>)(\s+)/g, '$2$1').replace(/(\s+)(<\/span>)/g, '$2$1');
694698

695699
html = html
696700
.replace(/ {13}([^ ][^]+?) {13}/g, (_, content) => {

0 commit comments

Comments
 (0)