├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── types ├── cli.d.ts ├── index.d.ts ├── util.d.ts ├── transformer │ ├── typescript.d.ts │ └── svelte.d.ts └── generator.d.ts ├── .gitignore ├── bin └── cli.js ├── .vscode ├── extensions.json └── settings.json ├── src ├── index.js ├── util.test.js ├── util.js ├── cli.js ├── transformer │ ├── typescript.js │ └── svelte.js └── generator.js ├── .npmignore ├── test ├── cases │ ├── 005-tsExportImportVar │ │ ├── index.ts │ │ ├── index.ts.txt │ │ ├── Component.svelte │ │ └── Component.svelte.txt │ ├── 010-restProps │ │ ├── Button.svelte │ │ └── Button.svelte.txt │ ├── 003-jsExportTemplate │ │ ├── Component.svelte │ │ └── Component.svelte.txt │ ├── 004-eventForwarding │ │ ├── Button.svelte │ │ └── Button.svelte.txt │ ├── 011-multiRestProps │ │ ├── Component.svelte │ │ └── Component.svelte.txt │ ├── 006-nameSlot │ │ ├── Slot.svelte │ │ └── Slot.svelte.txt │ ├── 001-jsComponent │ │ ├── Button.svelte │ │ └── Button.svelte.txt │ ├── 007-slotPassAttributes │ │ ├── Slot.svelte │ │ ├── Component.svelte │ │ ├── Component.svelte.txt │ │ └── Slot.svelte.txt │ ├── 009-tsComponent │ │ ├── Button.svelte │ │ └── Button.svelte.txt │ ├── 002-jsExportConst │ │ ├── Button.svelte │ │ └── Button.svelte.txt │ └── 008-jsdocComponent │ │ ├── Button.svelte.txt │ │ └── Button.svelte └── index.test.js ├── tsdef.json ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── rollup.config.js ├── LICENSE ├── package.json └── README.md /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /types/cli.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | test/cases/**/*.d.ts -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | require("../dist/cli/index.js"); 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DtsGenerator from "./generator"; 2 | export default DtsGenerator; 3 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default DtsGenerator; 2 | import DtsGenerator from "./generator"; 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | CODE_OF_CONDUCT.md 2 | CONTRIBUTING.md 3 | .DS_Store 4 | test 5 | rollup.config.js 6 | .github 7 | .vscode -------------------------------------------------------------------------------- /test/cases/005-tsExportImportVar/index.ts: -------------------------------------------------------------------------------- 1 | export type Msg = { 2 | name: string; 3 | id: number; 4 | }; 5 | 6 | export class Animal {} 7 | -------------------------------------------------------------------------------- /test/cases/005-tsExportImportVar/index.ts.txt: -------------------------------------------------------------------------------- 1 | export type Msg = { 2 | name: string; 3 | id: number; 4 | }; 5 | export declare class Animal { 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/010-restProps/Button.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /test/cases/003-jsExportTemplate/Component.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {number} 9 |
{title}
10 | 11 | 13 | -------------------------------------------------------------------------------- /test/cases/004-eventForwarding/Button.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /test/cases/011-multiRestProps/Component.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
Hello World
7 | 8 | 11 | -------------------------------------------------------------------------------- /test/cases/006-nameSlot/Slot.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | {#if open} 6 | 11 | {/if} 12 | -------------------------------------------------------------------------------- /test/cases/001-jsComponent/Button.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /test/cases/007-slotPassAttributes/Slot.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /test/cases/007-slotPassAttributes/Component.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 |
{title}
8 |
9 | 10 | 13 | -------------------------------------------------------------------------------- /test/cases/005-tsExportImportVar/Component.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |

{title}

10 |
{message}
11 | {@debug animal} 12 | {@debug animal2} 13 | 14 | 16 | -------------------------------------------------------------------------------- /types/util.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enhance version of `fs.readdirSync` by walk recursively. 3 | * 4 | * @param {string} dir 5 | * @returns {Generator} 6 | */ 7 | export function walkSync(dir: string): Generator; 8 | /** 9 | * Convert string to PaskalCase. 10 | * 11 | * @param {string} str 12 | * @returns {string} 13 | */ 14 | export function toPaskalCase(str: string): string; 15 | -------------------------------------------------------------------------------- /src/util.test.js: -------------------------------------------------------------------------------- 1 | import { it } from "node:test"; 2 | import assert from "node:assert"; 3 | import { toPaskalCase } from "./util.js"; 4 | 5 | it("toPaskalCase", () => { 6 | assert.deepEqual(toPaskalCase(`ghost`), `Ghost`); 7 | assert.deepEqual(toPaskalCase(`g_12_joker`), `G12Joker`); 8 | assert.deepEqual(toPaskalCase(`hello-world`), `HelloWorld`); 9 | assert.deepEqual(toPaskalCase(`under_score`), `UnderScore`); 10 | }); 11 | -------------------------------------------------------------------------------- /test/cases/009-tsComponent/Button.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/cases/007-slotPassAttributes/Component.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ComponentProps {} 6 | 7 | export interface ComponentEvents {} 8 | 9 | export interface ComponentSlots {} 10 | 11 | declare class Component extends SvelteComponentTyped { 12 | } 13 | export default Component; -------------------------------------------------------------------------------- /test/cases/007-slotPassAttributes/Slot.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface SlotProps { 6 | title?: string; 7 | } 8 | 9 | export interface SlotEvents {} 10 | 11 | export interface SlotSlots { 12 | default: never; 13 | } 14 | 15 | declare class Slot extends SvelteComponentTyped { 16 | } 17 | export default Slot; -------------------------------------------------------------------------------- /test/cases/010-restProps/Button.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ButtonProps extends HTMLButtonElement {} 6 | 7 | export interface ButtonEvents {} 8 | 9 | export interface ButtonSlots { 10 | default: never; 11 | } 12 | 13 | declare class Button extends SvelteComponentTyped { 14 | } 15 | export default Button; -------------------------------------------------------------------------------- /test/cases/001-jsComponent/Button.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ButtonProps { 6 | disabled?: boolean; 7 | name?: string; 8 | title?: string; 9 | } 10 | 11 | export interface ButtonEvents {} 12 | 13 | export interface ButtonSlots {} 14 | 15 | declare class Button extends SvelteComponentTyped { 16 | } 17 | export default Button; -------------------------------------------------------------------------------- /test/cases/006-nameSlot/Slot.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface SlotProps { 6 | open?: boolean; 7 | } 8 | 9 | export interface SlotEvents {} 10 | 11 | export interface SlotSlots { 12 | default: never; 13 | footer: never; 14 | header: never; 15 | } 16 | 17 | declare class Slot extends SvelteComponentTyped { 18 | } 19 | export default Slot; -------------------------------------------------------------------------------- /test/cases/011-multiRestProps/Component.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ComponentProps extends HTMLElement {} 6 | 7 | export interface ComponentEvents {} 8 | 9 | export interface ComponentSlots { 10 | default: never; 11 | } 12 | 13 | declare class Component extends SvelteComponentTyped { 14 | } 15 | export default Component; -------------------------------------------------------------------------------- /test/cases/002-jsExportConst/Button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /test/cases/003-jsExportTemplate/Component.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ComponentProps { 6 | number?: string; 7 | title?: string; 8 | } 9 | 10 | export interface ComponentEvents {} 11 | 12 | export interface ComponentSlots {} 13 | 14 | declare class Component extends SvelteComponentTyped { 15 | } 16 | export default Component; -------------------------------------------------------------------------------- /tsdef.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "allowJs": true, 5 | "outDir": "./types", 6 | "lib": ["esnext", "dom"], 7 | "target": "esnext", 8 | "declaration": true, 9 | "emitDeclarationOnly": true, 10 | "allowSyntheticDefaultImports": true, 11 | "importsNotUsedAsValues": "remove", 12 | "strict": true, 13 | "baseUrl": "./src", 14 | "types": ["node"] 15 | }, 16 | "include": ["src"], 17 | "exclude": ["__sapper__/*", "src/*.test.js"] 18 | } 19 | -------------------------------------------------------------------------------- /test/cases/009-tsComponent/Button.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ButtonProps { 6 | disabled?: boolean; 7 | initialNumber: number; 8 | } 9 | 10 | export interface ButtonEvents { 11 | change?: CustomEvent; 12 | } 13 | 14 | export interface ButtonSlots {} 15 | 16 | declare class Button extends SvelteComponentTyped { 17 | } 18 | export default Button; -------------------------------------------------------------------------------- /test/cases/004-eventForwarding/Button.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ButtonProps {} 6 | 7 | export interface ButtonEvents { 8 | change?: WindowEventMap["change"]; 9 | click?: WindowEventMap["click"]; 10 | mousedown?: WindowEventMap["mousedown"]; 11 | } 12 | 13 | export interface ButtonSlots {} 14 | 15 | declare class Button extends SvelteComponentTyped { 16 | } 17 | export default Button; -------------------------------------------------------------------------------- /types/transformer/typescript.d.ts: -------------------------------------------------------------------------------- 1 | export default TypescriptTransformer; 2 | declare class TypescriptTransformer { 3 | /** 4 | * 5 | * @param {string} fileName 6 | * @param {string} dir 7 | * @param {string} moduleName 8 | * @param {boolean} isDefault 9 | */ 10 | constructor(fileName: string, dir: string, moduleName: string, isDefault: boolean); 11 | /** 12 | * @returns {string} 13 | */ 14 | toString(): string; 15 | /** 16 | * @returns {void} 17 | */ 18 | destructor(): void; 19 | #private; 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | target-branch: "dev" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /test/cases/008-jsdocComponent/Button.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ButtonProps { 6 | disabled?: boolean; 7 | expanded: boolean; 8 | items: []; 9 | num: number; 10 | title: string; 11 | type?: Readonly; 12 | } 13 | 14 | export interface ButtonEvents { 15 | click?: WindowEventMap["click"]; 16 | } 17 | 18 | export interface ButtonSlots {} 19 | 20 | declare class Button extends SvelteComponentTyped { 21 | } 22 | export default Button; -------------------------------------------------------------------------------- /test/cases/005-tsExportImportVar/Component.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | import type { Msg } from "./index"; 5 | import type { Animal } from "./index"; 6 | 7 | export interface ComponentProps { 8 | animal: Animal; 9 | animal2: Animal; 10 | message: Msg; 11 | title?: string; 12 | } 13 | 14 | export interface ComponentEvents {} 15 | 16 | export interface ComponentSlots {} 17 | 18 | declare class Component extends SvelteComponentTyped { 19 | } 20 | export default Component; -------------------------------------------------------------------------------- /test/cases/002-jsExportConst/Button.svelte.txt: -------------------------------------------------------------------------------- 1 | // Code generated by svelte-dts-gen, version 1.0.0. DO NOT EDIT. 2 | 3 | import type { SvelteComponentTyped } from "svelte/internal"; 4 | 5 | export interface ButtonProps { 6 | arr?: string[]; 7 | flag?: boolean; 8 | list?: any[]; 9 | map?: Map; 10 | name?: string; 11 | obj?: any; 12 | readonlyText?: Readonly; 13 | ref?: any; 14 | } 15 | 16 | export interface ButtonEvents { 17 | click?: WindowEventMap["click"]; 18 | } 19 | 20 | export interface ButtonSlots {} 21 | 22 | declare class Button extends SvelteComponentTyped { 23 | } 24 | export default Button; -------------------------------------------------------------------------------- /types/transformer/svelte.d.ts: -------------------------------------------------------------------------------- 1 | export default SvelteTransformer; 2 | declare class SvelteTransformer { 3 | /** 4 | * 5 | * @param {string} content 6 | * @param {import("svelte/types/compiler/interfaces").Ast} ast 7 | * @param {string} dir 8 | * @param {string} fileName 9 | * @param {string} moduleName 10 | * @param {boolean} isDefault 11 | */ 12 | constructor(content: string, ast: any, dir: string, fileName: string, moduleName: string, isDefault: boolean); 13 | /** 14 | * 15 | * @returns {Promise} 16 | */ 17 | toString(): Promise; 18 | destructor(): void; 19 | #private; 20 | } 21 | -------------------------------------------------------------------------------- /types/generator.d.ts: -------------------------------------------------------------------------------- 1 | export default DtsGenerator; 2 | declare class DtsGenerator { 3 | /** 4 | * 5 | * @param {string} input 6 | * @param {defaultOpts} options 7 | */ 8 | constructor(input: string, options: { 9 | outDir: string; 10 | compact: boolean; 11 | force: boolean; 12 | }); 13 | /** 14 | * @param {Optional<{ each: (v: { output: string[] }) => void }>} opts 15 | * @returns {Promise} 16 | */ 17 | run(opts: Optional<{ 18 | each: (v: { 19 | output: string[]; 20 | }) => void; 21 | }>): Promise; 22 | write(): Promise; 23 | #private; 24 | } 25 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from "@rollup/plugin-json"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 4 | 5 | const plugins = [nodeResolve(), commonjs(), json()]; 6 | 7 | export default [ 8 | { 9 | input: "src/index.js", 10 | plugins, 11 | output: [ 12 | { 13 | file: "dist/index.mjs", 14 | format: "esm", 15 | }, 16 | { 17 | file: "dist/index.js", 18 | format: "cjs", 19 | }, 20 | ], 21 | }, 22 | // { 23 | // input: "src/cli.js", 24 | // plugins, 25 | // output: [ 26 | // { 27 | // file: "dist/cli/index.js", 28 | // format: "cjs", 29 | // }, 30 | // ], 31 | // }, 32 | ]; 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.fontSize": 14, 4 | "editor.tabSize": 2, 5 | "editor.insertSpaces": true, 6 | "files.autoSave": "onWindowChange", 7 | "files.trimTrailingWhitespace": true, 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", 9 | "files.associations": { 10 | "**/*.min.css": "ignore", 11 | "template.html": "ignore" 12 | }, 13 | "[markdown]": { 14 | "files.trimTrailingWhitespace": false 15 | }, 16 | "[svelte]": { 17 | "editor.defaultFormatter": "svelte.svelte-vscode" 18 | }, 19 | "[javascript]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[ignore]": { 23 | "editor.formatOnSave": false 24 | }, 25 | "[xml]": { 26 | "editor.defaultFormatter": "redhat.vscode-xml" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/cases/008-jsdocComponent/Button.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | {#each items as item} 35 |

{item.a}

36 | {/each} 37 | 38 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SianLoong. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | 4 | /** 5 | * Enhance version of `fs.readdirSync` by walk recursively. 6 | * 7 | * @param {string} dir 8 | * @returns {Generator} 9 | */ 10 | export function* walkSync(dir) { 11 | const files = fs.readdirSync(dir, { withFileTypes: true }); 12 | for (const file of files) { 13 | if (file.isDirectory()) { 14 | yield* walkSync(path.join(dir, file.name)); 15 | } else { 16 | yield path.join(dir, file.name); 17 | } 18 | } 19 | } 20 | 21 | /** 22 | * Convert string to PaskalCase. 23 | * 24 | * @param {string} str 25 | * @returns {string} 26 | */ 27 | export function toPaskalCase(str) { 28 | // Replace all non-alphanumeric characters with spaces 29 | str = str.replace(/[^A-Za-z0-9]/g, " "); 30 | 31 | // Convert string to lowercase and split into words 32 | var words = str.toLowerCase().split(" "); 33 | 34 | // Convert the first letter of each word to uppercase 35 | for (var i = 0; i < words.length; i++) { 36 | words[i] = words[i].charAt(0).toUpperCase() + words[i].slice(1); 37 | } 38 | 39 | // Concatenate the words and return the result 40 | return words.join(""); 41 | } 42 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | import { Command, Option } from "commander"; 2 | import DtsGenerator from "./generator.js"; 3 | import packageJson from "../package.json" assert { type: "json" }; 4 | 5 | (async function () { 6 | const program = new Command(); 7 | 8 | program 9 | .name(packageJson.name) 10 | .description(packageJson.description) 11 | .version(packageJson.version); 12 | 13 | program 14 | .argument("", "source") 15 | .addOption(new Option("--outDir ", "output directory")) 16 | .addOption( 17 | new Option("--compact", "output the definition file in compact mode") 18 | ) 19 | .addOption( 20 | new Option("-f --force", "force overwrite output file if it exists") 21 | ); 22 | 23 | program.parse(process.argv); 24 | 25 | const options = program.opts(); 26 | 27 | try { 28 | /** @type {{ default: { types: string }}} */ 29 | const { default: pkg } = await import( 30 | path.join(process.cwd(), "package.json"), 31 | { 32 | assert: { type: "json" }, 33 | } 34 | ); 35 | console.log(pkg); 36 | } catch (e) {} 37 | console.log("Arguments ->", program.args); 38 | console.log("Options ->", options); 39 | 40 | const dtsgen = new DtsGenerator(program.args[0], options); 41 | await dtsgen.run(); 42 | })(); 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Matrix Testing 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - dev 11 | 12 | pull_request: 13 | branches: 14 | - main 15 | - dev 16 | 17 | jobs: 18 | deploy: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | node: ["18", "19"] 23 | name: Node ${{ matrix.node }} sample 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ matrix.node }} 31 | cache: "npm" 32 | 33 | # - name: Cache dependencies 34 | # id: cache-modules 35 | # uses: actions/cache@v2 36 | # with: 37 | # path: ~/.npm 38 | # key: npm-${{ hashFiles('package-lock.json') }} 39 | # restore-keys: npm- 40 | 41 | - name: NPM Install 42 | # if: steps.cache-modules.outputs.cache-hit != 'true' 43 | run: | 44 | npm ci 45 | 46 | - name: Run Test 47 | run: | 48 | npm run test:unit 49 | npm run test 50 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | import assert from "node:assert"; 4 | import test from "node:test"; 5 | import DtsGenerator from "../src/generator.js"; 6 | import { walkSync } from "../src/util.js"; 7 | 8 | test("test cases", async () => { 9 | const rootDir = path.join(process.cwd(), "test/cases"); 10 | for (const filePath of walkSync(rootDir)) { 11 | // const pathParser = path.parse(filePath); 12 | // if (![".svelte"].includes(pathParser.ext)) { 13 | // continue; 14 | // } 15 | 16 | const dtsGen = new DtsGenerator(filePath, { 17 | force: true, 18 | }); 19 | await dtsGen.run({ 20 | /** @param {{ input: string; outputs: string[] }} */ 21 | each: async ({ input, outputs }) => { 22 | while (outputs.length > 0) { 23 | console.log(input); 24 | const output = outputs[0]; 25 | const pathParser = path.parse(input); 26 | /** @type {string[]} */ 27 | const result = await Promise.all([ 28 | fs.promises.readFile(output), 29 | fs.promises.readFile( 30 | path.join(pathParser.dir, `${pathParser.base}.txt`) 31 | ), 32 | ]).then((p) => p.map((v) => v.toString("utf-8"))); 33 | assert.deepEqual(result[0], result[1]); 34 | outputs.shift(); 35 | } 36 | }, 37 | }); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-dts-gen", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "module": "./dist/index.mjs", 6 | "description": "Generate the Typescript declaration files for your Svelte library and project.", 7 | "bin": { 8 | "svelte-dts-gen": "./bin/cli.js" 9 | }, 10 | "license": "MIT", 11 | "engines": { 12 | "npm": ">=7.0.0", 13 | "node": ">=18.0.0" 14 | }, 15 | "types": "./types", 16 | "main": "./dist/index.js", 17 | "exports": { 18 | "./package.json": "./package.json" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/si3nloong/svelte-dts-gen/issues" 22 | }, 23 | "scripts": { 24 | "test:unit": "node --test ./src/", 25 | "test": "node --test ./test", 26 | "tsd": "rm -rf ./types && tsc -p tsdef.json", 27 | "build": "rm -rf ./dist && rollup -c && npm run tsd" 28 | }, 29 | "keywords": [ 30 | "svelte-dts", 31 | "svelte", 32 | "typescript" 33 | ], 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/si3nloong/svelte-dts-gen" 37 | }, 38 | "author": "Si3nLoong (https://github.com/si3nloong)", 39 | "dependencies": { 40 | "commander": "^10.0.0", 41 | "svelte": "^3.57.0", 42 | "typescript": "^4.9.5" 43 | }, 44 | "devDependencies": { 45 | "@rollup/plugin-commonjs": "^24.0.1", 46 | "@rollup/plugin-json": "^6.0.0", 47 | "@rollup/plugin-node-resolve": "^15.0.1", 48 | "@types/node": "^18.15.3", 49 | "rollup": "^3.19.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte dts generator 2 | 3 | > Create the Typescript declaration files for your Svelte library and project. 4 | 5 | [![npm version](https://badge.fury.io/js/svelte-dts-gen.svg)](https://www.npmjs.com/package/svelte-dts-gen) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/si3nloong/svelte-dts-gen/blob/master/LICENSE) 7 | [![Node.js CI](https://github.com/si3nloong/svelte-dts-gen/workflows/Matrix%20Testing/badge.svg)](https://github.com/si3nloong/svelte-dts-gen/actions/workflows/test.yml) 8 | 9 | ## Requirement 10 | 11 | - Nodejs ^18.0.0 12 | 13 | ## Installation 14 | 15 | ```console 16 | npm i svelte-dts-gen 17 | ``` 18 | 19 | ## How it works 20 | 21 | The `svelte-dts-gen` interpret the properties, events and slot properties in the svelte code, using typescript and svelte compiler to generate the **TypeScript Declaration** files. `svelte-dts-gen` will generate the declaration based on typescript type, jsdoc, or javascript default type. 22 | 23 | ## License 24 | 25 | [svelte-dts-gen](https://github.com/si3nloong/svelte-dts-gen) is 100% free and open-source, under the [MIT license](https://github.com/si3nloong/svelte-dts-gen/blob/main/LICENSE). 26 | 27 | ## Big Thanks To 28 | 29 | Thanks to these awesome companies for their support of Open Source developers ❤ 30 | 31 | [![GitHub](https://jstools.dev/img/badges/github.svg)](https://github.com/open-source) 32 | [![NPM](https://jstools.dev/img/badges/npm.svg)](https://www.npmjs.com/) 33 | 34 | Inspired by [svelte-dts](https://github.com/si3nloong/svelte-dts-gen). 35 | -------------------------------------------------------------------------------- /src/transformer/typescript.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | // import { promises as fs } from "node:fs"; 3 | import ts from "typescript"; 4 | 5 | class TypescriptTransformer { 6 | /** @type {string} */ 7 | #dir; 8 | 9 | /** @type {string} */ 10 | #fileName; 11 | 12 | /** @type {string} */ 13 | #moduleName; 14 | 15 | /** @type {boolean} */ 16 | #isDefault; 17 | 18 | /** @type {string} */ 19 | #declaration; 20 | 21 | /** 22 | * 23 | * @param {string} fileName 24 | * @param {string} dir 25 | * @param {string} moduleName 26 | * @param {boolean} isDefault 27 | */ 28 | constructor(fileName, dir, moduleName, isDefault) { 29 | this.#fileName = fileName; 30 | this.#dir = dir; 31 | // this.subdir = path.dirname(this.fileName).replace(this.dir, ''); 32 | this.#moduleName = moduleName; 33 | this.#isDefault = isDefault; 34 | this.#declaration = ""; 35 | } 36 | 37 | /** 38 | * @returns {void} 39 | */ 40 | exec() { 41 | /** @type {ts.CompilerOptions} */ 42 | const options = { 43 | declaration: true, 44 | emitDeclarationOnly: true, 45 | allowJs: true, 46 | }; 47 | const host = ts.createCompilerHost(options); 48 | host.writeFile = (_, contents) => { 49 | this.#declaration = contents; 50 | }; 51 | const program = ts.createProgram([this.#fileName], options, host); 52 | program.emit(); 53 | } 54 | 55 | /** 56 | * @returns {string} 57 | */ 58 | toString() { 59 | // const pathParse = path.parse(this.#fileName); 60 | let string = ``; 61 | // if (this.isDefault) { 62 | // string = `declare module '${this.moduleName}' {\n`; 63 | // } 64 | string += this.#declaration; 65 | // .replace(/declare /g, '') 66 | // .split('\n') 67 | // .map((item) => (item !== '' ? `\t${item}` : undefined)) 68 | // .filter((item) => !!item) 69 | // .join('\n'); 70 | // string += `\n}\n\n`; 71 | return string; 72 | } 73 | 74 | // async appendFile(path: string): Promise { 75 | // this.exec(); 76 | // await fs.appendFile(path, this.toString()); 77 | // } 78 | 79 | /** 80 | * @returns {void} 81 | */ 82 | destructor() { 83 | this.#dir = ""; 84 | this.#fileName = ""; 85 | this.#moduleName = ""; 86 | this.#declaration = ""; 87 | } 88 | } 89 | 90 | export default TypescriptTransformer; 91 | -------------------------------------------------------------------------------- /src/generator.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | import ts from "typescript"; 4 | // import preprocess from "svelte-preprocess"; 5 | import { 6 | preprocess as sveltePreprocess, 7 | compile as svelteCompile, 8 | } from "svelte/compiler"; 9 | import { walkSync } from "./util.js"; 10 | import SvelteTransformer from "./transformer/svelte.js"; 11 | import TypescriptTransformer from "./transformer/typescript.js"; 12 | 13 | const defaultOpts = { 14 | outDir: "", 15 | compact: false, 16 | force: false, 17 | }; 18 | 19 | class DtsGenerator { 20 | /** 21 | * Valid file extensions 22 | * @type {string[]} 23 | */ 24 | #extensions; 25 | 26 | /** @type {string} */ 27 | #dir; 28 | 29 | /** @type {string} */ 30 | #input; 31 | 32 | /** @type {string} */ 33 | #output; 34 | 35 | /** @type {boolean} */ 36 | #overwrite; 37 | 38 | /** 39 | * 40 | * @param {string} input 41 | * @param {defaultOpts} options 42 | */ 43 | constructor(input, options) { 44 | this.#init(input, Object.assign(defaultOpts, options)); 45 | } 46 | 47 | /** 48 | * 49 | * @param {string} input 50 | * @param {defaultOpts} options 51 | * @returns {void} 52 | */ 53 | #init(input, options) { 54 | this.#input = path.isAbsolute(input) 55 | ? input 56 | : path.join(process.cwd(), input); 57 | this.#dir = path.dirname(this.#input); 58 | // Output and input folder will be similar 59 | this.#output = options.outDir || this.#dir; 60 | this.#extensions = options.extensions || [".svelte", ".ts", ".js"]; 61 | this.#overwrite = 62 | typeof options.force === "boolean" ? options.force : false; 63 | } 64 | 65 | /** 66 | * @param {Optional<{ each: (v: { output: string[] }) => void }>} opts 67 | * @returns {Promise} 68 | */ 69 | async run(opts) { 70 | opts = Object.assign({ each: () => {} }, opts); 71 | let files = function* () { 72 | yield this.#input; 73 | }.bind(this)(); 74 | if (fs.lstatSync(this.#input).isDirectory()) { 75 | files = walkSync(this.#input); 76 | } 77 | for (const file of files) { 78 | // console.log("File ->", file); 79 | const pathParser = path.parse(file); 80 | 81 | if ([".test.", ".spec."].includes(pathParser.base)) { 82 | continue; 83 | } 84 | 85 | if (!this.#extensions.includes(pathParser.ext)) { 86 | continue; 87 | } 88 | 89 | if (/.*\.d\.ts$/gi.test(pathParser.base)) { 90 | continue; 91 | } 92 | 93 | const outputs = await this.#readFile(file); 94 | await opts.each({ input: file, outputs }); 95 | } 96 | } 97 | 98 | /** 99 | * 100 | * @param {string} filename 101 | * @returns {Promise} 102 | */ 103 | async #readFile(filename) { 104 | const extension = path.extname(filename); 105 | 106 | let result = []; 107 | switch (extension) { 108 | case ".svelte": 109 | result = await this.#readSvelteFile(filename); 110 | break; 111 | case ".ts": 112 | result = await this.#readTypeScriptFile(filename); 113 | break; 114 | // case (".js", ".cjs", ".mjs"): 115 | // break; 116 | } 117 | 118 | return result; 119 | } 120 | 121 | /** 122 | * Read `.svelte` file and transpile it 123 | * 124 | * @param {string} filename 125 | * @returns {Promise} 126 | */ 127 | async #readSvelteFile(filename) { 128 | const fileContent = await fs.promises.readFile(filename, { 129 | encoding: "utf-8", 130 | }); 131 | 132 | let scriptTsContent = ""; 133 | const resultPreprocess = await sveltePreprocess( 134 | fileContent, 135 | // sveltePreprocess(), 136 | [ 137 | { 138 | script: ({ content }) => { 139 | scriptTsContent = content; 140 | 141 | const resultTranspile = ts.transpileModule(content, { 142 | compilerOptions: { 143 | module: ts.ModuleKind.ESNext, 144 | target: ts.ScriptTarget.ESNext, 145 | allowJs: true, 146 | moduleResolution: ts.ModuleResolutionKind.NodeJs, 147 | strict: true, 148 | }, 149 | }); 150 | 151 | return { code: resultTranspile.outputText }; 152 | // return { code: content }; 153 | }, 154 | }, 155 | ], 156 | { filename } 157 | ); 158 | 159 | const compiled = svelteCompile(resultPreprocess.code, { 160 | filename, 161 | }); 162 | const pathParser = path.parse(filename); 163 | const transformer = new SvelteTransformer( 164 | scriptTsContent, 165 | compiled.ast, 166 | this.#dir, 167 | pathParser.base 168 | ); 169 | transformer.exec(); 170 | return await this.#write(filename, await transformer.toString(), true); 171 | } 172 | 173 | /** 174 | * 175 | * @param {string} filename 176 | * @returns {Promise} 177 | */ 178 | async #readTypeScriptFile(filename) { 179 | const transformer = new TypescriptTransformer(filename, this.#dir); 180 | transformer.exec(); 181 | return await this.#write(filename, await transformer.toString()); 182 | } 183 | 184 | /** 185 | * 186 | * @param {string} filename 187 | * @param {string} content 188 | * @returns 189 | */ 190 | async #write(filename, content, hasExtension = false) { 191 | // const pathParser = path.parse(filename); 192 | // Construct the output file name, should be end with `.d.ts` 193 | const outDir = path.join( 194 | this.#output, 195 | path.relative(this.#dir, path.dirname(filename)) 196 | ); 197 | let basename = path.basename(filename, path.extname(filename)); 198 | if (hasExtension) { 199 | basename = path.basename(filename); 200 | } 201 | fs.mkdirSync(outDir, { recursive: true }); 202 | const outputFile = path.join(outDir, `${basename}.d.ts`); 203 | if (!this.#overwrite && fs.existsSync(outputFile)) { 204 | return []; 205 | } 206 | fs.writeFileSync(outputFile, content); 207 | return [outputFile]; 208 | } 209 | } 210 | 211 | export default DtsGenerator; 212 | -------------------------------------------------------------------------------- /src/transformer/svelte.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { createEventDispatcher } from "svelte"; 3 | import ts from "typescript"; 4 | import { walk } from "svelte/compiler"; 5 | import { toPaskalCase } from "../util.js"; 6 | import packageJson from "../../package.json" assert { type: "json" }; 7 | 8 | const htmlElementMap = { 9 | a: "HTMLAnchorElement", 10 | abbr: "HTMLElement", 11 | address: "HTMLElement", 12 | area: "HTMLAreaElement", 13 | article: "HTMLElement", 14 | aside: "HTMLElement", 15 | audio: "HTMLAudioElement", 16 | b: "HTMLElement", 17 | base: "HTMLBaseElement", 18 | bdi: "HTMLElement", 19 | bdo: "HTMLElement", 20 | blockquote: "HTMLQuoteElement", 21 | body: "HTMLBodyElement", 22 | br: "HTMLBRElement", 23 | button: "HTMLButtonElement", 24 | canvas: "HTMLCanvasElement", 25 | caption: "HTMLTableCaptionElement", 26 | cite: "HTMLElement", 27 | code: "HTMLElement", 28 | col: "HTMLTableColElement", 29 | colgroup: "HTMLTableColElement", 30 | data: "HTMLDataElement", 31 | datalist: "HTMLDataListElement", 32 | dd: "HTMLElement", 33 | del: "HTMLModElement", 34 | details: "HTMLDetailsElement", 35 | dfn: "HTMLElement", 36 | dialog: "HTMLDialogElement", 37 | div: "HTMLDivElement", 38 | dl: "HTMLDListElement", 39 | dt: "HTMLElement", 40 | em: "HTMLElement", 41 | embed: "HTMLEmbedElement", 42 | fieldset: "HTMLFieldSetElement", 43 | figcaption: "HTMLElement", 44 | figure: "HTMLElement", 45 | footer: "HTMLElement", 46 | form: "HTMLFormElement", 47 | h1: "HTMLHeadingElement", 48 | h2: "HTMLHeadingElement", 49 | h3: "HTMLHeadingElement", 50 | h4: "HTMLHeadingElement", 51 | h5: "HTMLHeadingElement", 52 | h6: "HTMLHeadingElement", 53 | head: "HTMLHeadElement", 54 | header: "HTMLElement", 55 | hgroup: "HTMLElement", 56 | hr: "HTMLHRElement", 57 | html: "HTMLHtmlElement", 58 | i: "HTMLElement", 59 | iframe: "HTMLIFrameElement", 60 | img: "HTMLImageElement", 61 | input: "HTMLInputElement", 62 | ins: "HTMLModElement", 63 | kbd: "HTMLElement", 64 | label: "HTMLLabelElement", 65 | legend: "HTMLLegendElement", 66 | li: "HTMLLIElement", 67 | link: "HTMLLinkElement", 68 | main: "HTMLElement", 69 | map: "HTMLMapElement", 70 | mark: "HTMLElement", 71 | menu: "HTMLMenuElement", 72 | meta: "HTMLMetaElement", 73 | meter: "HTMLMeterElement", 74 | nav: "HTMLElement", 75 | noscript: "HTMLElement", 76 | object: "HTMLObjectElement", 77 | ol: "HTMLOListElement", 78 | optgroup: "HTMLOptGroupElement", 79 | option: "HTMLOptionElement", 80 | output: "HTMLOutputElement", 81 | p: "HTMLParagraphElement", 82 | picture: "HTMLPictureElement", 83 | pre: "HTMLPreElement", 84 | progress: "HTMLProgressElement", 85 | q: "HTMLQuoteElement", 86 | rp: "HTMLElement", 87 | rt: "HTMLElement", 88 | ruby: "HTMLElement", 89 | s: "HTMLElement", 90 | samp: "HTMLElement", 91 | script: "HTMLScriptElement", 92 | section: "HTMLElement", 93 | select: "HTMLSelectElement", 94 | slot: "HTMLSlotElement", 95 | small: "HTMLElement", 96 | source: "HTMLSourceElement", 97 | span: "HTMLSpanElement", 98 | strong: "HTMLElement", 99 | style: "HTMLStyleElement", 100 | sub: "HTMLElement", 101 | summary: "HTMLElement", 102 | sup: "HTMLElement", 103 | table: "HTMLTableElement", 104 | tbody: "HTMLTableSectionElement", 105 | td: "HTMLTableCellElement", 106 | template: "HTMLTemplateElement", 107 | textarea: "HTMLTextAreaElement", 108 | tfoot: "HTMLTableSectionElement", 109 | th: "HTMLTableCellElement", 110 | thead: "HTMLTableSectionElement", 111 | time: "HTMLTimeElement", 112 | title: "HTMLTitleElement", 113 | tr: "HTMLTableRowElement", 114 | track: "HTMLTrackElement", 115 | u: "HTMLElement", 116 | ul: "HTMLUListElement", 117 | var: "HTMLElement", 118 | video: "HTMLVideoElement", 119 | wbr: "HTMLElement", 120 | }; 121 | 122 | class SvelteTransformer { 123 | /** 124 | * @type {ts.sourceFile} 125 | */ 126 | #sourceFile; 127 | 128 | /** 129 | * @type {ts.TypeChecker} 130 | */ 131 | #typeChecker; 132 | 133 | /** 134 | * Input file directory 135 | * @type {string} 136 | */ 137 | #dir; 138 | 139 | /** 140 | * Svelte file name 141 | * @type {string} 142 | */ 143 | #fileName; 144 | 145 | /** 146 | * Svelte dts component name 147 | * @type {string} 148 | */ 149 | #componentName; 150 | 151 | /** @type {?string} */ 152 | #moduleName; 153 | 154 | /** @type {import("svelte/types/compiler/interfaces").Ast} */ 155 | #ast; 156 | 157 | /** @type {{ name: string; type: string; isOptional: boolean }[]} */ 158 | #props; 159 | 160 | /** @type {Map} */ 161 | #events; 162 | 163 | /** @type {Map} */ 164 | #slots; 165 | 166 | /** @type {ts.ImportDeclaration[]} */ 167 | #typesForSearch; 168 | 169 | /** @type {string[]} */ 170 | #customEventsForSearch; 171 | 172 | /** 173 | * @type {boolean} 174 | */ 175 | #isDefault; 176 | 177 | /** 178 | * @type {string[]} 179 | */ 180 | #restProps; 181 | 182 | /** @type {string[]} */ 183 | #declarationNodes; 184 | 185 | /** @type {string[]} */ 186 | #declarationImports; 187 | 188 | /** 189 | * 190 | * @param {string} content 191 | * @param {import("svelte/types/compiler/interfaces").Ast} ast 192 | * @param {string} dir 193 | * @param {string} fileName 194 | * @param {string} moduleName 195 | * @param {boolean} isDefault 196 | */ 197 | constructor(content, ast, dir, fileName, moduleName, isDefault) { 198 | this.#sourceFile = ts.createSourceFile( 199 | fileName, 200 | content, 201 | ts.ScriptTarget.Latest, 202 | true 203 | ); 204 | this.#typeChecker = ts 205 | .createProgram({ 206 | rootNames: [this.#sourceFile.fileName], 207 | options: { 208 | target: ts.ScriptTarget.ES2020, 209 | module: ts.ModuleKind.CommonJS, 210 | }, 211 | }) 212 | .getTypeChecker(); 213 | this.#fileName = fileName; 214 | this.#componentName = toPaskalCase( 215 | path.basename(this.#fileName, path.extname(fileName)) 216 | ); 217 | this.#ast = ast; 218 | this.#props = []; 219 | this.#events = new Map(); 220 | this.#slots = new Map(); 221 | this.#dir = dir; 222 | this.#customEventsForSearch = []; 223 | this.#typesForSearch = []; 224 | this.#moduleName = moduleName; 225 | this.#isDefault = isDefault; 226 | this.#restProps = []; 227 | this.#declarationNodes = []; 228 | this.#declarationImports = []; 229 | } 230 | 231 | /** 232 | * 233 | * @param {ts.VariableStatement} node 234 | * @returns {boolean} 235 | */ 236 | #isExportModifier = (node) => { 237 | if (node.modifiers) { 238 | return node.modifiers.some( 239 | (node) => node.kind === ts.SyntaxKind.ExportKeyword 240 | ); 241 | } 242 | 243 | return false; 244 | }; 245 | 246 | /** 247 | * 248 | * @param {ts.VariableStatement} node 249 | * @returns {boolean} 250 | */ 251 | #isEventDispatcher = (node) => { 252 | return node.declarationList.declarations.some( 253 | (item) => 254 | ts.isVariableDeclaration(item) && 255 | item.initializer && 256 | ts.isCallExpression(item.initializer) && 257 | item.initializer.expression.getText(this.#sourceFile) === 258 | createEventDispatcher.name 259 | ); 260 | }; 261 | 262 | /** 263 | * 264 | * @param {ts.TypeReferenceNode} newType 265 | */ 266 | #addTypeForSearch(newType) { 267 | const includeType = this.#typesForSearch.some( 268 | (item) => 269 | item.getText(this.#sourceFile) === newType.getText(this.#sourceFile) 270 | ); 271 | 272 | if (!includeType || this.#typesForSearch.length === 0) { 273 | this.#typesForSearch.push(newType); 274 | } 275 | } 276 | 277 | /** 278 | * 279 | * @param {ts.ImportDeclaration} node 280 | * @param {string} name 281 | */ 282 | #verifyImportDeclaration(node, name) { 283 | if ( 284 | node.importClause && 285 | node.importClause.namedBindings && 286 | ts.isNamedImports(node.importClause.namedBindings) 287 | ) { 288 | const elements = node.importClause.namedBindings.elements; 289 | const newElements = elements.filter( 290 | (element) => element.name.getText() === name 291 | ); 292 | 293 | if (newElements.length > 0) { 294 | const importString = newElements 295 | .map((item) => item.name.getText(this.#sourceFile)) 296 | .join(", "); 297 | 298 | this.#declarationImports.push( 299 | `import type { ${importString} } from ${node.moduleSpecifier.getText( 300 | this.#sourceFile 301 | )};` 302 | ); 303 | } 304 | } 305 | } 306 | 307 | /** 308 | * 309 | * @param {ts.VariableStatement} node 310 | * @returns {void} 311 | */ 312 | #compileProperty(node) { 313 | /* 314 | Type should be infer from the priority below: 315 | 1. TypeScript type 316 | 2. JS value type 317 | 3. JSDoc @type 318 | */ 319 | 320 | let type = "any"; 321 | const jsDoc = ts.getJSDocType(node); 322 | if (jsDoc) { 323 | type = getSyntaxKindString(jsDoc.kind); 324 | } 325 | 326 | let readOnly = false; 327 | if (node.declarationList.flags == ts.NodeFlags.Const) { 328 | readOnly = true; 329 | } 330 | 331 | node.declarationList.declarations.forEach((declaration) => { 332 | const name = declaration.name.getText(this.#sourceFile); 333 | 334 | let isOptional = false; 335 | if (declaration.type) { 336 | type = declaration.type.getText(this.#sourceFile); 337 | 338 | if (ts.isTypeReferenceNode(declaration.type)) { 339 | this.#addTypeForSearch(declaration.type); 340 | } 341 | 342 | if (ts.isUnionTypeNode(declaration.type)) { 343 | const nameValidTypes = declaration.type.types.reduce((acc, type) => { 344 | const typeForCheck = ts.isLiteralTypeNode(type) 345 | ? type.literal 346 | : type; 347 | 348 | if ( 349 | typeForCheck.kind === ts.SyntaxKind.NullKeyword || 350 | typeForCheck.kind === ts.SyntaxKind.UndefinedKeyword 351 | ) { 352 | isOptional = true; 353 | return acc; 354 | } 355 | 356 | return [...acc, type.getText(this.#sourceFile)]; 357 | }, []); 358 | 359 | type = nameValidTypes.join(" | "); 360 | } 361 | } else if (declaration.initializer) { 362 | isOptional = true; 363 | const { initializer } = declaration; 364 | 365 | // If it's a template literal, it's always string 366 | if (ts.isTemplateLiteral(initializer)) { 367 | type = "string"; 368 | } else if (ts.isObjectLiteralExpression(initializer)) { 369 | } else if ( 370 | // ts.isIdentifier(initializer) || 371 | ts.isArrayLiteralExpression(initializer) 372 | ) { 373 | const initializerType = 374 | this.#typeChecker.getTypeAtLocation(initializer); 375 | type = this.#typeChecker.typeToString(initializerType); 376 | } else if (ts.isCallOrNewExpression(initializer)) { 377 | const signature = this.#typeChecker.getResolvedSignature(initializer); 378 | if (signature) { 379 | type = this.#typeChecker.typeToString(signature.getReturnType()); 380 | } 381 | } else if (ts.isIdentifier(initializer)) { 382 | } else { 383 | type = getSyntaxKindString(declaration.initializer.kind); 384 | } 385 | } 386 | 387 | this.#props.push({ name, type, isOptional, readOnly }); 388 | }); 389 | } 390 | 391 | /** 392 | * Compile svelte event dispatcher node. 393 | * @param {ts.VariableStatement} node 394 | * @returns {void} 395 | */ 396 | async #compileEventDispatcher(node) { 397 | node.declarationList.declarations.forEach((declaration) => { 398 | if ( 399 | declaration.initializer && 400 | ts.isCallExpression(declaration.initializer) && 401 | declaration.initializer.typeArguments 402 | ) { 403 | declaration.initializer.typeArguments.forEach((arg) => { 404 | if (ts.isTypeLiteralNode(arg)) { 405 | arg.members.forEach((member) => { 406 | if (ts.isPropertySignature(member)) { 407 | const name = member.name.getText(this.#sourceFile); 408 | const type = member.type?.getText(this.#sourceFile) || "any"; 409 | if (member.type && ts.isTypeReferenceNode(member.type)) { 410 | this.#addTypeForSearch(member.type); 411 | } 412 | this.#events.set(name, { type, custom: true }); 413 | } 414 | }); 415 | } 416 | }); 417 | } else { 418 | // If data type is not declared, we will put it to cache and process it later 419 | this.#customEventsForSearch.push( 420 | declaration.name.getText(this.#sourceFile) 421 | ); 422 | } 423 | }); 424 | } 425 | 426 | /** 427 | * Compile typescript expiression node. 428 | * @param {ts.ExpressionStatement} node 429 | * @returns {void} 430 | */ 431 | #compileExpression(node) { 432 | if (ts.isCallExpression(node.expression)) { 433 | if (node.expression.arguments.length <= 0) { 434 | return; 435 | } 436 | 437 | // Event name must be string or identifier or template 438 | const event = node.expression.arguments[0]; 439 | if ( 440 | !( 441 | ts.isStringLiteral(event) || 442 | ts.isIdentifier(event) || 443 | ts.isTemplateExpression(event) 444 | ) 445 | ) { 446 | return; 447 | } 448 | 449 | this.#events.set(event.getText(this.#sourceFile).replaceAll(`"`, ""), { 450 | custom: true, 451 | }); 452 | } 453 | } 454 | 455 | /** 456 | * Compile svelte event node. 457 | * @param {import("svelte/types/compiler/interfaces").Attribute} node 458 | * @returns {void} 459 | */ 460 | #compileEvent(node) { 461 | if (node.expression) return; 462 | // If event forwarding, we should push it 463 | this.#events.set(node.name, {}); 464 | } 465 | 466 | /** 467 | * Compile svelte slot node. 468 | * @param {import("svelte/types/compiler/interfaces").Element} node 469 | * @returns {void} 470 | */ 471 | #compileSlot(node) { 472 | const name = node.attributes.find((v) => v.name === "name"); 473 | if (!name) { 474 | this.#slots.set("default", { name: "" }); 475 | return; 476 | } 477 | 478 | this.#slots.set(`${name.value[0].raw}`, {}); 479 | } 480 | 481 | /** 482 | * @returns {void} 483 | */ 484 | exec() { 485 | ts.forEachChild(this.#sourceFile, (node) => { 486 | if ( 487 | node.importClause && 488 | ts.isImportDeclaration(node) && 489 | node.importClause.isTypeOnly 490 | ) { 491 | this.#declarationNodes.push(node); 492 | } 493 | 494 | if (ts.isVariableStatement(node)) { 495 | if (this.#isExportModifier(node)) { 496 | this.#compileProperty(node); 497 | } else if (this.#isEventDispatcher(node)) { 498 | this.#compileEventDispatcher(node); 499 | } 500 | } else if ( 501 | this.#customEventsForSearch.length > 0 && 502 | ts.isExpressionStatement(node) 503 | ) { 504 | this.#compileExpression(node); 505 | } 506 | }); 507 | 508 | this.#typesForSearch.forEach((search) => { 509 | this.#declarationNodes.forEach((node) => { 510 | this.#verifyImportDeclaration(node, search.getText(this.#sourceFile)); 511 | }); 512 | }); 513 | 514 | walk(this.#ast.html, { 515 | /** 516 | * @param {import("svelte/types/compiler/interfaces").TemplateNode} node 517 | * @param {Optional} parent 518 | */ 519 | enter: (node, parent) => { 520 | if (node.type === "EventHandler") { 521 | this.#compileEvent(node); 522 | } 523 | if (node.type === "Slot") { 524 | this.#compileSlot(node); 525 | } 526 | if (node.type === "Element") { 527 | /** @type {import("svelte/types/compiler/interfaces").Element} */ 528 | let n = node; 529 | if ( 530 | n.attributes.some( 531 | (attr) => 532 | attr.type === "Spread" && attr.expression.name === "$$restProps" 533 | ) 534 | ) { 535 | this.#restProps.push(node.name); 536 | } 537 | } 538 | }, 539 | }); 540 | // } 541 | 542 | // if (node.children) { 543 | // node.children.forEach((item) => this.execSlotProperty(item)); 544 | // } 545 | } 546 | 547 | /** 548 | * 549 | * @returns {Promise} 550 | */ 551 | async toString() { 552 | let template = `// Code generated by ${packageJson.name}, version ${packageJson.version}. DO NOT EDIT.`; 553 | template += `\n\nimport type { SvelteComponentTyped } from "svelte/internal";`; 554 | 555 | if (this.#declarationImports.length > 0) { 556 | this.#declarationImports.forEach((declaration) => { 557 | template += `\n${declaration}`; 558 | }); 559 | } 560 | 561 | // Write properties 562 | template += `\n\nexport interface ${this.#componentName}Props `; 563 | if (this.#restProps.length > 0) { 564 | template += `extends ${ 565 | this.#restProps[0] in htmlElementMap 566 | ? htmlElementMap[this.#restProps[0]] 567 | : "HTMLElement" 568 | } `; 569 | } else if (this.#restProps.length > 1) { 570 | template += `extends HTMLElement `; 571 | } 572 | template += "{"; 573 | if (this.#props.length > 0) { 574 | this.#props 575 | .sort((a, b) => a.name.localeCompare(b.name)) 576 | .forEach((prop) => { 577 | let propName = prop.name; 578 | let propType = prop.type; 579 | if (prop.isOptional) { 580 | propName += "?"; 581 | } 582 | if (prop.readOnly) { 583 | propType = `Readonly<${propType}>`; 584 | } 585 | template += `\n\t${propName}: ${propType};`; 586 | }); 587 | template += "\n"; 588 | } 589 | template += `}`; 590 | 591 | // Write events 592 | template += `\n\nexport interface ${this.#componentName}Events {`; 593 | if (this.#events.size > 0) { 594 | Array.from(this.#events.keys()) 595 | .sort() 596 | .forEach((k) => { 597 | const event = this.#events.get(k); 598 | if (event.custom) { 599 | template += `\n\t${k}?: CustomEvent<${ 600 | event.type ? event.type : "any" 601 | }>;`; 602 | } else { 603 | template += `\n\t${k}?: WindowEventMap["${k}"];`; 604 | } 605 | }); 606 | template += "\n"; 607 | } 608 | template += `}`; 609 | 610 | // Write slots 611 | template += `\n\nexport interface ${this.#componentName}Slots {`; 612 | if (this.#slots.size > 0) { 613 | Array.from(this.#slots.keys()) 614 | .sort() 615 | .forEach((k) => { 616 | template += `\n\t${k}: never;`; 617 | }); 618 | template += "\n"; 619 | } 620 | template += `}`; 621 | 622 | template += `\n\ndeclare class ${ 623 | this.#componentName 624 | } extends SvelteComponentTyped<${this.#componentName}Props, ${ 625 | this.#componentName 626 | }Events, ${this.#componentName}Slots> {`; 627 | template += `\n}`; 628 | template += `\nexport default ${this.#componentName};`; 629 | return template; 630 | } 631 | 632 | destructor() { 633 | this.#sourceFile = null; 634 | this.#ast = null; 635 | this.#dir = ""; 636 | this.#props = []; 637 | this.#events = null; 638 | this.#slots = null; 639 | this.#typesForSearch = []; 640 | this.#customEventsForSearch = []; 641 | this.#declarationImports = []; 642 | } 643 | } 644 | 645 | /** 646 | * 647 | * @param {ts.SyntaxKind} kind 648 | * @param {ts.TypeNode} node 649 | * @returns {string} 650 | */ 651 | function getSyntaxKindString(kind, node) { 652 | switch (kind) { 653 | case ts.SyntaxKind.StringKeyword: 654 | case ts.SyntaxKind.StringLiteral: 655 | return "string"; 656 | case ts.SyntaxKind.BooleanKeyword: 657 | case ts.SyntaxKind.FalseKeyword: 658 | case ts.SyntaxKind.TrueKeyword: 659 | return "boolean"; 660 | case ts.SyntaxKind.NumberKeyword: 661 | return "number"; 662 | case ts.SyntaxKind.ArrayType: 663 | return "[]"; 664 | // Add other cases as needed 665 | default: 666 | return ts.SyntaxKind[kind]; 667 | } 668 | } 669 | 670 | export default SvelteTransformer; 671 | --------------------------------------------------------------------------------