diff --git a/apps/svelte.dev/content/docs/kit/20-core-concepts/30-form-actions.md b/apps/svelte.dev/content/docs/kit/20-core-concepts/30-form-actions.md index ce23f7c43d..5a9d252a1d 100644 --- a/apps/svelte.dev/content/docs/kit/20-core-concepts/30-form-actions.md +++ b/apps/svelte.dev/content/docs/kit/20-core-concepts/30-form-actions.md @@ -12,7 +12,7 @@ In the simplest case, a page declares a `default` action: ```js /// file: src/routes/login/+page.server.js -/** @type {import('./$types').Actions} */ +/** @satisfies {import('./$types').Actions} */ export const actions = { default: async (event) => { // 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 ```js /// file: src/routes/login/+page.server.js -/** @type {import('./$types').Actions} */ +/** @satisfies {import('./$types').Actions} */ export const actions = { --- default: async (event) => {--- +++ login: async (event) => {+++ @@ -119,7 +119,7 @@ export async function load({ cookies }) { return { user }; } -/** @type {import('./$types').Actions} */ +/** @satisfies {import('./$types').Actions} */ export const actions = { login: async ({ cookies, request }) => { const data = await request.formData(); @@ -168,7 +168,7 @@ declare module '$lib/server/db'; +++import { fail } from '@sveltejs/kit';+++ import * as db from '$lib/server/db'; -/** @type {import('./$types').Actions} */ +/** @satisfies {import('./$types').Actions} */ export const actions = { login: async ({ cookies, request }) => { const data = await request.formData(); @@ -232,7 +232,7 @@ declare module '$lib/server/db'; import { fail, +++redirect+++ } from '@sveltejs/kit'; import * as db from '$lib/server/db'; -/** @type {import('./$types').Actions} */ +/** @satisfies {import('./$types').Actions} */ export const actions = { login: async ({ cookies, request, +++url+++ }) => { const data = await request.formData(); @@ -317,7 +317,7 @@ export function load(event) { }; } -/** @type {import('./$types').Actions} */ +/** @satisfies {import('./$types').Actions} */ export const actions = { logout: async (event) => { event.cookies.delete('sessionid', { path: '/' }); @@ -507,7 +507,7 @@ Some forms don't need to `POST` data to the server — search inputs, for exampl
``` diff --git a/apps/svelte.dev/content/docs/kit/20-core-concepts/50-state-management.md b/apps/svelte.dev/content/docs/kit/20-core-concepts/50-state-management.md index 0a955e0869..a3d4ce76eb 100644 --- a/apps/svelte.dev/content/docs/kit/20-core-concepts/50-state-management.md +++ b/apps/svelte.dev/content/docs/kit/20-core-concepts/50-state-management.md @@ -20,7 +20,7 @@ export function load() { return { user }; } -/** @type {import('./$types').Actions} */ +/** @satisfies {import('./$types').Actions} */ export const actions = { default: async ({ request }) => { const data = await request.formData(); diff --git a/packages/site-kit/src/lib/markdown/renderer.ts b/packages/site-kit/src/lib/markdown/renderer.ts index ba41b6fc96..6d2b22c1ba 100644 --- a/packages/site-kit/src/lib/markdown/renderer.ts +++ b/packages/site-kit/src/lib/markdown/renderer.ts @@ -368,91 +368,112 @@ async function convert_to_ts(js_code: string, indent = '', offset = '') { async function walk(node: ts.Node) { const jsdoc = get_jsdoc(node); + if (jsdoc) { - for (const comment of jsdoc) { - let modified = false; - - let count = 0; - for (const tag of comment.tags ?? []) { - if (ts.isJSDocTypeTag(tag)) { - const [name, generics] = await get_type_info(tag); - - if (ts.isFunctionDeclaration(node)) { - const is_export = node.modifiers?.some( - (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword - ) - ? 'export ' - : ''; - const is_async = node.modifiers?.some( - (modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword - ); - - const type = generics !== undefined ? `${name}<${generics}>` : name; - - if (node.name && node.body) { - code.overwrite( - node.getStart(), - node.name.getEnd(), - `${is_export ? 'export ' : ''}const ${node.name.getText()}: ${type} = (${ - is_async ? 'async ' : '' - }` - ); - - code.appendLeft(node.body.getStart(), '=> '); - code.appendLeft(node.body.getEnd(), ');'); - - modified = true; - } - } else if ( - ts.isVariableStatement(node) && - node.declarationList.declarations.length === 1 - ) { - const variable_statement = node.declarationList.declarations[0]; - - if (variable_statement.name.getText() === 'actions') { - let i = variable_statement.getEnd(); - while (code.original[i - 1] !== '}') i -= 1; - code.appendLeft(i, ` satisfies ${name}`); - } else { - code.appendLeft( - variable_statement.name.getEnd(), - `: ${name}${generics ? `<${generics}>` : ''}` - ); - } - - modified = true; - } else { - throw new Error('Unhandled @type JsDoc->TS conversion: ' + js_code); - } - } else if (ts.isJSDocParameterTag(tag) && ts.isFunctionDeclaration(node)) { - const sanitised_param = tag - .getFullText() - .replace(/\s+/g, '') - .replace(/(^\*|\*$)/g, ''); - - const [, param_type] = /@param{(.+)}(.+)/.exec(sanitised_param) ?? []; - - let param_count = 0; - for (const param of node.parameters) { - if (count !== param_count) { - param_count++; - continue; - } - - code.appendLeft(param.getEnd(), `:${param_type}`); - - param_count++; - } - - modified = true; + // this isn't an exhaustive list of tags we could potentially encounter (no `@template` etc) + // but it's good enough to cover what's actually in the docs right now + let type: string | null = null; + let params: string[] = []; + let returns: string | null = null; + let satisfies: string | null = null; + + if (jsdoc.length > 1) { + throw new Error('woah nelly'); + } + + const { comment, tags = [] } = jsdoc[0]; + + for (const tag of tags) { + if (ts.isJSDocTypeTag(tag)) { + type = get_type_info(tag.typeExpression); + } else if (ts.isJSDocParameterTag(tag)) { + params.push(get_type_info(tag.typeExpression!)); + } else if (ts.isJSDocReturnTag(tag)) { + returns = get_type_info(tag.typeExpression!); + } else if (ts.isJSDocSatisfiesTag(tag)) { + satisfies = get_type_info(tag.typeExpression!); + } else { + throw new Error('Unhandled tag'); + } + + let start = tag.getStart(); + let end = tag.getEnd(); + + while (start > 0 && code.original[start] !== '\n') start -= 1; + while (end > 0 && code.original[end] !== '\n') end -= 1; + code.remove(start, end); + } + + if (type && satisfies) { + throw new Error('Cannot combine @type and @satisfies'); + } + + if (ts.isFunctionDeclaration(node)) { + // convert function to a `const` + if (type || satisfies) { + const is_export = node.modifiers?.some( + (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword + ); + + const is_async = node.modifiers?.some( + (modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword + ); + + code.overwrite( + node.getStart(), + node.name!.getStart(), + is_export ? `export const ` : `const ` + ); + + const modifier = is_async ? 'async ' : ''; + code.appendLeft( + node.name!.getEnd(), + type ? `: ${type} = ${modifier}` : ` = ${modifier}(` + ); + + code.prependRight(node.body!.getStart(), '=> '); + + code.appendLeft(node.getEnd(), satisfies ? `) satisfies ${satisfies};` : ';'); + } + + for (let i = 0; i < node.parameters.length; i += 1) { + if (params[i] !== undefined) { + code.appendLeft(node.parameters[i].getEnd(), `: ${params[i]}`); } + } - count++; + if (returns) { + let start = node.body!.getStart(); + while (code.original[start - 1] !== ')') start -= 1; + code.appendLeft(start, `: ${returns}`); } + } else if (ts.isVariableStatement(node) && node.declarationList.declarations.length === 1) { + if (params.length > 0 || returns) { + throw new Error('TODO handle @params and @returns in variable declarations'); + } + + const declaration = node.declarationList.declarations[0]; - if (modified) { - code.overwrite(comment.getStart(), comment.getEnd(), ''); + if (type) { + code.appendLeft(declaration.name.getEnd(), `: ${type}`); } + + if (satisfies) { + let end = declaration.getEnd(); + if (code.original[end - 1] === ';') end -= 1; + code.appendLeft(end, ` satisfies ${satisfies}`); + } + } else { + throw new Error('Unhandled @type JsDoc->TS conversion: ' + js_code); + } + + if (!comment) { + // remove the whole thing + let start = jsdoc[0].getStart(); + let end = jsdoc[0].getEnd(); + + while (start > 0 && code.original[start] !== '\n') start -= 1; + code.overwrite(start, end, ''); } } @@ -487,42 +508,25 @@ async function convert_to_ts(js_code: string, indent = '', offset = '') { let transformed = code.toString(); - return transformed === js_code ? undefined : transformed.replace(/\n\s*\n\s*\n/g, '\n\n'); - - async function get_type_info(tag: ts.JSDocTypeTag | ts.JSDocParameterTag) { - const type_text = tag.typeExpression?.getText(); - let name = type_text?.slice(1, -1); // remove { } - - const single_line_name = ( - await prettier.format(name ?? '', { - printWidth: 1000, - parser: 'typescript', - semi: false, - singleQuote: true - }) - ).replace('\n', ''); + return transformed === js_code ? undefined : transformed; + + function get_type_info(expression: ts.JSDocTypeExpression) { + const type = expression + ?.getText()! + .slice(1, -1) // remove surrounding `{` and `}` + .replace(/ \* ?/gm, '') + .replace(/import\('(.+?)'\)\.(\w+)(?:(<.+>))?/gms, (_, source, name, args = '') => { + const existing = imports.get(source); + if (existing) { + existing.add(name); + } else { + imports.set(source, new Set([name])); + } - const import_match = /import\('(.+?)'\)\.(\w+)(?:<(.+)>)?$/s.exec(single_line_name); + return name + args; + }); - if (import_match) { - const [, from, _name, generics] = import_match; - name = _name; - const existing = imports.get(from); - if (existing) { - existing.add(name); - } else { - imports.set(from, new Set([name])); - } - if (generics !== undefined) { - return [ - name, - generics - .replaceAll('*', '') // get rid of JSDoc asterisks - .replace(' }>', '}>') // unindent closing brace - ]; - } - } - return [name]; + return type; } } @@ -690,7 +694,7 @@ async function syntax_highlight({ // munge shiki output: put whitespace outside `` elements, so that // highlight delimiters fall outside tokens - html = html.replace(/()(\s+)/g, '$2$1').replace(/(\s+)(<\/span>)/g, '$2$1'); + html = html.replace(/(]+?>)(\s+)/g, '$2$1').replace(/(\s+)(<\/span>)/g, '$2$1'); html = html .replace(/ {13}([^ ][^]+?) {13}/g, (_, content) => {