├── 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 | A Button
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 |
7 |
8 | Content Here
9 |
10 |
11 | {/if}
12 |
--------------------------------------------------------------------------------
/test/cases/001-jsComponent/Button.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 | {title}
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 | (number += 1)} {disabled}>Cliques: {number}
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 | {readonlyText}
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 | {title} {num}
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 | [](https://www.npmjs.com/package/svelte-dts-gen)
6 | [](https://github.com/si3nloong/svelte-dts-gen/blob/master/LICENSE)
7 | [](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 | [](https://github.com/open-source)
32 | [](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 |
--------------------------------------------------------------------------------