Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(svelte2tsx) Allow documenting components with a tagged HTML comment #285

Merged
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
29 changes: 29 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,35 @@ It's also necessary to add a `type="text/language-name"` or `lang="language-name
</style>
```

## Documenting components

To add documentation on a Svelte component that will show up as a docstring in
LSP-compatible editors, you can use an HTML comment with the `@component` tag:

```html
<!--
@component
Here's some documentation for this component. It will show up on hover for
JavaScript/TypeScript projects using a LSP-compatible editor such as VSCode or
Vim/Neovim with coc.nvim.

- You can use markdown here.
- You can use code blocks here.
- JSDoc/TSDoc will be respected by LSP-compatible editors.
- Indentation will be respected as much as possible.
-->

<!-- @component You can use a single line, too -->

<!-- @component But only the last documentation comment will be used -->

<main>
<h1>
Hello world
</h1>
</main>
```

## Troubleshooting / FAQ

### Using TypeScript? See [this section](./preprocessors/typescript.md#troubleshooting--faq)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
let text = document.getText();

try {
const tsx = svelte2tsx(text, { strictMode: options.strictMode });
const tsx = svelte2tsx(
text,
{
strictMode: options.strictMode,
filename: document.getFilePath() ?? undefined,
}
);
text = tsx.code;
tsxMap = tsx.map;
if (tsxMap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ describe('CompletionProviderImpl', () => {
item!,
);

assert.strictEqual(detail, 'Auto import from ./imported-file.svelte\nclass default');
assert.strictEqual(detail, 'Auto import from ./imported-file.svelte\nclass ImportedFile');

assert.strictEqual(
harmonizeNewLines(additionalTextEdits![0]?.newText),
Expand Down Expand Up @@ -362,7 +362,7 @@ describe('CompletionProviderImpl', () => {
item!,
);

assert.strictEqual(detail, 'Auto import from ./imported-file.svelte\nclass default');
assert.strictEqual(detail, 'Auto import from ./imported-file.svelte\nclass ImportedFile');

assert.strictEqual(
harmonizeNewLines(additionalTextEdits![0]?.newText),
Expand Down
6 changes: 5 additions & 1 deletion packages/svelte2tsx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,9 @@
"LICENSE",
"svelte-jsx.d.ts",
"svelte-shims.d.ts"
]
],
"dependencies": {
"dedent-js": "^1.0.1",
"pascal-case": "^3.1.1"
}
}
87 changes: 82 additions & 5 deletions packages/svelte2tsx/src/svelte2tsx.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import dedent from 'dedent-js';
import { pascalCase } from 'pascal-case';
import MagicString from 'magic-string';
import path from 'path';
import { parseHtmlx } from './htmlxparser';
import { convertHtmlxToJsx } from './htmlxtojsx';
import { Node } from 'estree-walker';
Expand Down Expand Up @@ -36,6 +39,8 @@ type TemplateProcessResult = {
slots: Map<string, Map<string, string>>;
scriptTag: Node;
moduleScriptTag: Node;
/** To be added later as a comment on the default class export */
componentDocumentation: string | null;
};

class Scope {
Expand All @@ -53,12 +58,20 @@ type pendingStoreResolution<T> = {
scope: Scope;
};

/**
* Add this tag to a HTML comment in a Svelte component and its contents will
* be added as a docstring in the resulting JSX for the component class.
*/
const COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG = '@component';

function processSvelteTemplate(str: MagicString): TemplateProcessResult {
const htmlxAst = parseHtmlx(str.original);

let uses$$props = false;
let uses$$restProps = false;

let componentDocumentation = null;

//track if we are in a declaration scope
let isDeclaration = false;

Expand Down Expand Up @@ -167,6 +180,18 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult {
const enterArrowFunctionExpression = () => pushScope();
const leaveArrowFunctionExpression = () => popScope();

const handleComment = (node: Node) => {
if (
'data' in node &&
typeof node.data === 'string' &&
node.data.includes(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG)
) {
componentDocumentation = node.data
.replace(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG, '')
.trim();
}
};

const handleIdentifier = (node: Node, parent: Node, prop: string) => {
if (node.name === '$$props') {
uses$$props = true;
Expand Down Expand Up @@ -259,6 +284,9 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult {
}

switch (node.type) {
case 'Comment':
handleComment(node);
break;
case 'Identifier':
handleIdentifier(node, parent, prop);
break;
Expand Down Expand Up @@ -326,6 +354,7 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult {
slots,
uses$$props,
uses$$restProps,
componentDocumentation,
};
}

Expand Down Expand Up @@ -762,11 +791,28 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
};
}

function formatComponentDocumentation(contents?: string | null) {
if (!contents) return '';
if (!contents.includes('\n')) {
return `/** ${contents} */\n`;
}

const lines = dedent(contents)
.split('\n')
.map(line => ` *${line ? ` ${line}` : ''}`)
.join('\n');

return `/**\n${lines}\n */\n`;
}

function addComponentExport(
str: MagicString,
uses$$propsOr$$restProps: boolean,
strictMode: boolean,
isTsFile: boolean,
/** A named export allows for TSDoc-compatible docstrings */
className?: string,
componentDocumentation?: string | null
) {
const propDef =
// Omit partial-wrapper only if both strict mode and ts file, because
Expand All @@ -777,10 +823,30 @@ function addComponentExport(
? '__sveltets_with_any(render().props)'
: 'render().props'
: `__sveltets_partial${uses$$propsOr$$restProps ? '_with_any' : ''}(render().props)`;
str.append(
// eslint-disable-next-line max-len
`\n\nexport default class {\n $$prop_def = ${propDef}\n $$slot_def = render().slots\n}`,
);

const doc = formatComponentDocumentation(componentDocumentation);

// eslint-disable-next-line max-len
const statement = `\n\n${doc}export default class ${className ? `${className} ` : ''}{\n $$prop_def = ${propDef}\n $$slot_def = render().slots\n}`;

str.append(statement);
}

/**
* Returns a Svelte-compatible component name from a filename. Svelte
* components must use capitalized tags, so we try to transform the filename.
*
* https://svelte.dev/docs#Tags
*/
export function classNameFromFilename(filename: string): string | undefined {
try {
const withoutExtensions = path.parse(filename).name?.split('.')[0];
const inPascalCase = pascalCase(withoutExtensions);
return inPascalCase;
} catch (error) {
console.warn(`Failed to create a name for the component class from filename ${filename}`);
return undefined;
}
}

function isTsFile(scriptTag: Node | undefined, moduleScriptTag: Node | undefined) {
Expand Down Expand Up @@ -898,7 +964,14 @@ function createPropsStr(exportedNames: ExportedNames) {
export function svelte2tsx(svelte: string, options?: { filename?: string; strictMode?: boolean }) {
const str = new MagicString(svelte);
// process the htmlx as a svelte template
let { moduleScriptTag, scriptTag, slots, uses$$props, uses$$restProps } = processSvelteTemplate(
let {
moduleScriptTag,
scriptTag,
slots,
uses$$props,
uses$$restProps,
componentDocumentation,
} = processSvelteTemplate(
str,
);

Expand Down Expand Up @@ -948,11 +1021,15 @@ export function svelte2tsx(svelte: string, options?: { filename?: string; strict
processModuleScriptTag(str, moduleScriptTag);
}

const className = options?.filename && classNameFromFilename(options?.filename);

addComponentExport(
str,
uses$$props || uses$$restProps,
!!options?.strictMode,
isTsFile(scriptTag, moduleScriptTag),
className,
componentDocumentation,
);

return {
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte2tsx/test/svelte2tsx/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('svelte2tsx', () => {
const input = fs.readFileSync(`${__dirname}/samples/${dir}/input.svelte`, 'utf-8').replace(/\s+$/, '').replace(/\r\n/g, "\n");
const expectedOutput = fs.readFileSync(`${__dirname}/samples/${dir}/expected.tsx`, 'utf-8').replace(/\s+$/, '').replace(/\r\n/g, "\n");

const { map, code} = svelte2tsx(input, {strictMode: dir.endsWith('strictMode')});
const { map, code} = svelte2tsx(input, {strictMode: dir.endsWith('strictMode'), filename: 'input.svelte'});
assert.equal(code, expectedOutput);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<></>
return { props: {a: a , b: b , c: c}, slots: {} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ __sveltets_store_get(var);
<></>
return { props: {}, slots: {} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<></>
return { props: {}, slots: {} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function render() {
</>})}}</>
return { props: {}, slots: {} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<><input id="dom-input" type="radio" {...__sveltets_any(__sveltets_store_get(compile_options).generate)} value="dom"/></>
return { props: {}, slots: {} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
</>}}}</>
return { props: {}, slots: {} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</div></>
return { props: {}, slots: {default: {a:b}} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</div></>
return { props: {}, slots: {default: {a:b}, test: {c:d, e:e}} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</div></>
return { props: {}, slots: {default: {a:b, b:b, c:"b", d:"__svelte_ts_string", e:b}} }}

export default class {
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<></>;function render() {
<>

<main>At least I am documented</main></>
return { props: {}, slots: {} }}

/** This component does nothing at all */
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- @component This component does nothing at all -->

<main>At least I am documented</main>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<></>;function render() {
<>

<main>At least I am documented</main></>
return { props: {}, slots: {} }}

/**
* This component has indented multiline documentation:
*
* ```typescript
* type Type = 'type'
* ```
*
* An indented list:
* - One item
* - Two items
*
* The output should be indented properly!
*/
export default class Input {
$$prop_def = __sveltets_partial(render().props)
$$slot_def = render().slots
}
Loading