From acc82fa9d9f0e13573dcedf00596285966851937 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 11 Jun 2021 11:53:10 +0200 Subject: [PATCH 1/2] (feat) add dts output target to svelte2tsx When setting the new mode option to 'dts', all tsx/jsx code and the template code will be thrown out, all shims will be inlined and the export is rewritten differently. Only the `code` property will be set on the returned element. Use this as an intermediate step to generate type definitions from a component. It is expected to pass the result to TypeScript which should handle emitting the d.ts files. --- packages/svelte2tsx/.gitignore | 1 + packages/svelte2tsx/create-files.js | 11 + packages/svelte2tsx/index.d.ts | 8 + packages/svelte2tsx/package.json | 7 +- packages/svelte2tsx/src/svelte2tsx/index.ts | 82 +++++-- packages/svelte2tsx/test/helpers.ts | 6 +- .../samples/creates-dts/expected.tsx | 226 ++++++++++++++++++ .../samples/creates-dts/input.svelte | 17 ++ .../samples/ts-creates-dts/expected.tsx | 225 +++++++++++++++++ .../samples/ts-creates-dts/input.svelte | 16 ++ 10 files changed, 571 insertions(+), 28 deletions(-) create mode 100644 packages/svelte2tsx/create-files.js create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/input.svelte diff --git a/packages/svelte2tsx/.gitignore b/packages/svelte2tsx/.gitignore index 0603455f4..8f619c078 100644 --- a/packages/svelte2tsx/.gitignore +++ b/packages/svelte2tsx/.gitignore @@ -7,3 +7,4 @@ test/typecheck/samples/**/input.svelte.tsx test/sourcemaps/samples/*/output.tsx test/sourcemaps/samples/*/test.edit.jsx repl/output +src/svelte2tsx/svelteShims.ts diff --git a/packages/svelte2tsx/create-files.js b/packages/svelte2tsx/create-files.js new file mode 100644 index 000000000..866be2da0 --- /dev/null +++ b/packages/svelte2tsx/create-files.js @@ -0,0 +1,11 @@ +const fs = require('fs'); + +let svelteShims = fs.readFileSync('./svelte-shims.d.ts', { encoding: 'utf-8' }); +svelteShims = svelteShims.substr(svelteShims.indexOf('declare class Sv')).replace(/`/g, '\\`'); +fs.writeFileSync( + './src/svelte2tsx/svelteShims.ts', + `/* eslint-disable */ +// prettier-ignore +export const svelteShims = \`${svelteShims}\`; +` +); diff --git a/packages/svelte2tsx/index.d.ts b/packages/svelte2tsx/index.d.ts index eb01dbe5e..5c895dca9 100644 --- a/packages/svelte2tsx/index.d.ts +++ b/packages/svelte2tsx/index.d.ts @@ -36,5 +36,13 @@ export default function svelte2tsx( * The namespace option from svelte config */ namespace?: string; + /** + * When setting this to 'dts', all tsx/jsx code and the template code will be thrown out, + * all shims will be inlined and the component export is written differently. + * Only the `code` property will be set on the returned element. + * Use this as an intermediate step to generate type definitions from a component. + * It is expected to pass the result to TypeScript which should handle emitting the d.ts files. + */ + mode?: 'tsx' | 'dts' } ): SvelteCompiledToTsx diff --git a/packages/svelte2tsx/package.json b/packages/svelte2tsx/package.json index 7c98edb3e..fac08011e 100644 --- a/packages/svelte2tsx/package.json +++ b/packages/svelte2tsx/package.json @@ -44,10 +44,11 @@ "typescript": "^4.1.2" }, "scripts": { - "build": "rollup -c", + "build": "npm run create-files && rollup -c", "prepublishOnly": "npm run build", - "dev": "rollup -c -w", - "test": "mocha test/test.ts" + "dev": "npm run create-files && rollup -c -w", + "test": "npm run create-files && mocha test/test.ts", + "create-files": "node ./create-files.js" }, "files": [ "index.mjs", diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index a25ff3a43..6fe8956ed 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -25,6 +25,7 @@ import { } from './processInstanceScriptContent'; import { processModuleScriptTag } from './processModuleScriptTag'; import { ScopeStack } from './utils/Scope'; +import { svelteShims } from './svelteShims'; interface CreateRenderFunctionPara extends InstanceScriptProcessResult { str: MagicString; @@ -49,6 +50,7 @@ interface AddComponentExportPara { exportedNames: ExportedNames; fileName?: string; componentDocumentation: ComponentDocumentation; + mode: 'dts' | 'tsx'; } type TemplateProcessResult = { @@ -320,7 +322,8 @@ function addComponentExport({ usesAccessors, exportedNames, fileName, - componentDocumentation + componentDocumentation, + mode }: AddComponentExportPara) { const eventsDef = strictEvents ? 'render()' : '__sveltets_with_any_event(render())'; let propDef = ''; @@ -336,15 +339,30 @@ function addComponentExport({ } const doc = componentDocumentation.getFormatted(); - const className = fileName && classNameFromFilename(fileName); - - const statement = - `\n\n${doc}export default class${ - className ? ` ${className}` : '' - } extends createSvelte2TsxComponent(${propDef}) {` + - createClassGetters(getters) + - (usesAccessors ? createClassAccessors(getters, exportedNames) : '') + - '\n}'; + const className = fileName && classNameFromFilename(fileName, mode !== 'dts'); + + let statement: string; + if (mode === 'dts') { + statement = + `\nconst __propDef = ${propDef};\n` + + `export type ${className}Props = typeof __propDef.props;\n` + + `export type ${className}Events = typeof __propDef.events;\n` + + `export type ${className}Slots = typeof __propDef.slots;\n` + + `\n${doc}export default class${ + className ? ` ${className}` : '' + } extends SvelteComponentTyped<${className}Props, ${className}Events, ${className}Slots> {` + + createClassGetters(getters) + + (usesAccessors ? createClassAccessors(getters, exportedNames) : '') + + '\n}'; + } else { + statement = + `\n\n${doc}export default class${ + className ? ` ${className}` : '' + } extends createSvelte2TsxComponent(${propDef}) {` + + createClassGetters(getters) + + (usesAccessors ? createClassAccessors(getters, exportedNames) : '') + + '\n}'; + } str.append(statement); } @@ -355,7 +373,7 @@ function addComponentExport({ * * https://svelte.dev/docs#Tags */ -function classNameFromFilename(filename: string): string | undefined { +function classNameFromFilename(filename: string, appendSuffix: boolean): string | undefined { try { const withoutExtensions = path.parse(filename).name?.split('.')[0]; const withoutInvalidCharacters = withoutExtensions @@ -371,7 +389,7 @@ function classNameFromFilename(filename: string): string | undefined { const withoutLeadingInvalidCharacters = withoutInvalidCharacters.substr(firstValidCharIdx); const inPascalCase = pascalCase(withoutLeadingInvalidCharacters); const finalName = firstValidCharIdx === -1 ? `A${inPascalCase}` : inPascalCase; - return `${finalName}${COMPONENT_SUFFIX}`; + return `${finalName}${appendSuffix ? COMPONENT_SUFFIX : ''}`; } catch (error) { console.warn(`Failed to create a name for the component class from filename ${filename}`); return undefined; @@ -453,12 +471,13 @@ function createRenderFunction({ export function svelte2tsx( svelte: string, - options?: { + options: { filename?: string; isTsFile?: boolean; emitOnTemplateError?: boolean; namespace?: string; - } + mode?: 'tsx' | 'dts'; + } = {} ) { const str = new MagicString(svelte); // process the htmlx as a svelte template @@ -545,15 +564,32 @@ export function svelte2tsx( exportedNames, usesAccessors, fileName: options?.filename, - componentDocumentation + componentDocumentation, + mode: options.mode }); - str.prepend('///\n'); - - return { - code: str.toString(), - map: str.generateMap({ hires: true, source: options?.filename }), - exportedNames, - events: events.createAPI() - }; + if (options.mode === 'dts') { + // Prepend the import and all shims so the file is self-contained. + // TypeScript's dts generation will remove the unused parts later. + str.prepend('import { SvelteComponentTyped } from "svelte"\n' + svelteShims + '\n'); + let code = str.toString(); + // Remove all tsx occurences and the template part from the output + code = + code + .substr(0, code.indexOf('\n() => (<>')) + // prepended before each script block + .replace('<>;', '') + .replace('<>;', '') + code.substr(code.lastIndexOf(');') + ');'.length); + return { + code + }; + } else { + str.prepend('///\n'); + return { + code: str.toString(), + map: str.generateMap({ hires: true, source: options?.filename }), + exportedNames, + events: events.createAPI() + }; + } } diff --git a/packages/svelte2tsx/test/helpers.ts b/packages/svelte2tsx/test/helpers.ts index 7f7b083c8..d80cbaba1 100644 --- a/packages/svelte2tsx/test/helpers.ts +++ b/packages/svelte2tsx/test/helpers.ts @@ -226,7 +226,8 @@ export function test_samples(dir: string, transform: TransformSampleFn, jsx: 'js filename: svelteFile, sampleName: sample.name, emitOnTemplateError: false, - preserveAttributeCase: sample.name.endsWith('-foreign-ns') + preserveAttributeCase: sample.name.endsWith('-foreign-ns'), + mode: sample.name.endsWith('-dts') ? 'dts' : undefined }; if (process.env.CI) { @@ -307,7 +308,8 @@ export function get_svelte2tsx_config(base: BaseConfig, sampleName: string): Sve filename: base.filename, emitOnTemplateError: base.emitOnTemplateError, isTsFile: sampleName.startsWith('ts-'), - namespace: sampleName.endsWith('-foreign-ns') ? 'foreign' : null + namespace: sampleName.endsWith('-foreign-ns') ? 'foreign' : null, + mode: sampleName.endsWith('-dts') ? 'dts' : undefined }; } diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/expected.tsx new file mode 100644 index 000000000..2816db6d9 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/expected.tsx @@ -0,0 +1,226 @@ +import { SvelteComponentTyped } from "svelte" +declare class Svelte2TsxComponent< + Props extends {} = {}, + Events extends {} = {}, + Slots extends {} = {} +> { + // svelte2tsx-specific + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$prop_def: Props; + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$events_def: Events; + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$slot_def: Slots; + // https://svelte.dev/docs#Client-side_component_API + constructor(options: Svelte2TsxComponentConstructorParameters); + /** + * Causes the callback function to be called whenever the component dispatches an event. + * A function is returned that will remove the event listener when called. + */ + $on(event: K, handler: (e: Events[K]) => any): () => void; + /** + * Removes a component from the DOM and triggers any `onDestroy` handlers. + */ + $destroy(): void; + /** + * Programmatically sets props on an instance. + * `component.$set({ x: 1 })` is equivalent to `x = 1` inside the component's ` + + + + + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/expected.tsx new file mode 100644 index 000000000..169fa2156 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/expected.tsx @@ -0,0 +1,225 @@ +import { SvelteComponentTyped } from "svelte" +declare class Svelte2TsxComponent< + Props extends {} = {}, + Events extends {} = {}, + Slots extends {} = {} +> { + // svelte2tsx-specific + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$prop_def: Props; + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$events_def: Events; + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$slot_def: Slots; + // https://svelte.dev/docs#Client-side_component_API + constructor(options: Svelte2TsxComponentConstructorParameters); + /** + * Causes the callback function to be called whenever the component dispatches an event. + * A function is returned that will remove the event listener when called. + */ + $on(event: K, handler: (e: Events[K]) => any): () => void; + /** + * Removes a component from the DOM and triggers any `onDestroy` handlers. + */ + $destroy(): void; + /** + * Programmatically sets props on an instance. + * `component.$set({ x: 1 })` is equivalent to `x = 1` inside the component's ` + + + + + \ No newline at end of file From d443caf38a419dc2c8c7237227df273b4ba72b47 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 11 Jun 2021 12:00:36 +0200 Subject: [PATCH 2/2] lint --- packages/svelte2tsx/.gitignore | 1 - packages/svelte2tsx/create-files.js | 1 + packages/svelte2tsx/src/svelte2tsx/index.ts | 2 +- .../svelte2tsx/src/svelte2tsx/svelteShims.ts | 204 ++++++++++++++++++ 4 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 packages/svelte2tsx/src/svelte2tsx/svelteShims.ts diff --git a/packages/svelte2tsx/.gitignore b/packages/svelte2tsx/.gitignore index 8f619c078..0603455f4 100644 --- a/packages/svelte2tsx/.gitignore +++ b/packages/svelte2tsx/.gitignore @@ -7,4 +7,3 @@ test/typecheck/samples/**/input.svelte.tsx test/sourcemaps/samples/*/output.tsx test/sourcemaps/samples/*/test.edit.jsx repl/output -src/svelte2tsx/svelteShims.ts diff --git a/packages/svelte2tsx/create-files.js b/packages/svelte2tsx/create-files.js index 866be2da0..5c41bc221 100644 --- a/packages/svelte2tsx/create-files.js +++ b/packages/svelte2tsx/create-files.js @@ -5,6 +5,7 @@ svelteShims = svelteShims.substr(svelteShims.indexOf('declare class Sv')).replac fs.writeFileSync( './src/svelte2tsx/svelteShims.ts', `/* eslint-disable */ +// Auto-generated, do not change // prettier-ignore export const svelteShims = \`${svelteShims}\`; ` diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 6fe8956ed..2430e54bd 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -350,7 +350,7 @@ function addComponentExport({ `export type ${className}Slots = typeof __propDef.slots;\n` + `\n${doc}export default class${ className ? ` ${className}` : '' - } extends SvelteComponentTyped<${className}Props, ${className}Events, ${className}Slots> {` + + } extends SvelteComponentTyped<${className}Props, ${className}Events, ${className}Slots> {` + // eslint-disable-line max-len createClassGetters(getters) + (usesAccessors ? createClassAccessors(getters, exportedNames) : '') + '\n}'; diff --git a/packages/svelte2tsx/src/svelte2tsx/svelteShims.ts b/packages/svelte2tsx/src/svelte2tsx/svelteShims.ts new file mode 100644 index 000000000..9c67eec34 --- /dev/null +++ b/packages/svelte2tsx/src/svelte2tsx/svelteShims.ts @@ -0,0 +1,204 @@ +/* eslint-disable */ +// Auto-generated, do not change +// prettier-ignore +export const svelteShims = `declare class Svelte2TsxComponent< + Props extends {} = {}, + Events extends {} = {}, + Slots extends {} = {} +> { + // svelte2tsx-specific + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$prop_def: Props; + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$events_def: Events; + /** + * @internal This is for type checking capabilities only + * and does not exist at runtime. Don't use this property. + */ + $$slot_def: Slots; + // https://svelte.dev/docs#Client-side_component_API + constructor(options: Svelte2TsxComponentConstructorParameters); + /** + * Causes the callback function to be called whenever the component dispatches an event. + * A function is returned that will remove the event listener when called. + */ + $on(event: K, handler: (e: Events[K]) => any): () => void; + /** + * Removes a component from the DOM and triggers any \`onDestroy\` handlers. + */ + $destroy(): void; + /** + * Programmatically sets props on an instance. + * \`component.$set({ x: 1 })\` is equivalent to \`x = 1\` inside the component's \`