From 8c4223a89073a499c2600a17e605268d2191e8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Demanou?= Date: Sat, 23 Jul 2022 20:15:19 +0200 Subject: [PATCH] Partial TypeScript refactoring --- .eslintignore | 2 +- .eslintrc.yaml | 32 +- .gitignore | 2 + .gitlab-ci.yml | 2 +- .npmignore | 4 +- index.d.ts | 61 - jest.config.js | 21 - lib/Loader.d.ts | 46 - lib/Loader.js | 52 - lib/entity/AbstractCategorizeEntry.js | 12 - lib/entity/AbstractCategorizeTypeEntry.js | 10 - lib/entity/AbstractDecorativeEntry.js | 10 - lib/entity/AbstractEntry.js | 5 - lib/entity/AbstractLiteralEntry.js | 9 - lib/entity/ComputedEntry.js | 11 - lib/entity/DataEntry.js | 10 - lib/entity/DescriptionEntry.js | 7 - lib/entity/EventEntry.js | 30 - lib/entity/InheritAttrsEntry.js | 7 - lib/entity/Keyword.js | 6 - lib/entity/KeywordsEntry.js | 7 - lib/entity/ModelEntry.js | 11 - lib/entity/NameEntry.js | 7 - lib/entity/PropEntry.js | 14 - lib/parser/ClassComponentDataParser.js | 45 - lib/parser/Parser.d.ts | 159 -- lib/parser/Parser.js | 109 - package.json | 72 +- index.js => src/index.ts | 148 +- lib/Enum.js => src/lib/Enum.ts | 51 +- lib/JSDoc.js => src/lib/JSDoc.ts | 31 +- src/lib/Loader.ts | 59 + src/lib/entity/AbstractCategorizeEntry.ts | 15 + src/lib/entity/AbstractCategorizeTypeEntry.ts | 13 + src/lib/entity/AbstractDecorativeEntry.ts | 14 + src/lib/entity/AbstractEntry.ts | 9 + src/lib/entity/AbstractLiteralEntry.ts | 12 + src/lib/entity/ComputedEntry.ts | 21 + src/lib/entity/DataEntry.ts | 20 + src/lib/entity/DescriptionEntry.ts | 8 + src/lib/entity/EventEntry.ts | 39 + src/lib/entity/InheritAttrsEntry.ts | 8 + src/lib/entity/Keyword.ts | 11 + src/lib/entity/KeywordsEntry.ts | 8 + .../lib/entity/MethodEntry.ts | 27 +- src/lib/entity/ModelEntry.ts | 26 + src/lib/entity/NameEntry.ts | 8 + src/lib/entity/PropEntry.ts | 29 + .../lib/entity/RestValue.ts | 5 +- .../lib/entity/SlotEntry.ts | 18 +- .../Value.js => src/lib/entity/Value.ts | 28 +- .../lib/parser/AbstractExpressionParser.ts | 46 +- .../lib/parser/AbstractLiteralParser.ts | 7 +- .../lib/parser/AbstractParser.ts | 440 +-- src/lib/parser/AbstractSourceParser.ts | 11 + .../parser/ClassComponentComputedParser.ts | 7 +- src/lib/parser/ClassComponentDataParser.ts | 49 + .../lib/parser/ClassComponentParser.ts | 239 +- .../lib/parser/CommentParser.ts | 19 +- .../lib/parser/CompositionParser.ts | 102 +- .../lib/parser/ComputedParser.ts | 13 +- .../lib/parser/DataParser.ts | 27 +- .../lib/parser/EventParser.ts | 107 +- .../lib/parser/InheritAttrsParser.ts | 6 +- .../lib/parser/InlineTemplateParser.ts | 8 +- .../lib/parser/JSXParser.ts | 11 +- .../lib/parser/MarkupTemplateParser.ts | 55 +- .../lib/parser/MethodParser.ts | 21 +- .../lib/parser/ModelParser.ts | 5 +- .../lib/parser/NameParser.ts | 6 +- src/lib/parser/Parser.ts | 148 + .../lib/parser/PropParser.ts | 67 +- .../lib/parser/ScriptParser.ts | 172 +- .../lib/parser/SetupParser.ts | 26 +- .../lib/parser/SlotParser.ts | 7 +- .../lib/utils/KeywordsUtils.ts | 13 +- loaders/html.js => src/loaders/html.ts | 3 +- .../loaders/javascript.ts | 3 +- loaders/pug.js => src/loaders/pug.ts | 3 +- .../loaders/typescript.ts | 0 loaders/vue.js => src/loaders/vue.ts | 5 +- schema/options.js => src/schema/options.ts | 0 test/fixtures/checkbox.ts | 2 +- test/lib/TestUtils.js | 6 +- test/lib/VueDocExample.js | 4 +- test/spec/ClassComponent.spec.js | 2 +- test/spec/CommentParser.spec.js | 4 +- test/spec/Compatibility.spec.js | 2 +- test/spec/ComputedParser.spec.js | 2 +- test/spec/DataParser.spec.js | 2 +- test/spec/ECMAScript.spec.js | 32 +- test/spec/EventParser.spec.js | 2 +- test/spec/InheritAttrs.spec.js | 4 +- test/spec/InlineTemplate.spec.js | 4 +- test/spec/JSDoc.spec.js | 6 +- test/spec/JSXParser.spec.js | 2 +- test/spec/MarkupTemplateParser.spec.js | 2 +- test/spec/MethodParser.spec.js | 2 +- test/spec/Model.spec.js | 4 +- test/spec/Parser.spec.js | 642 +++-- test/spec/PropParser.spec.js | 2 +- test/spec/PropTypes.spec.js | 2 +- test/spec/ScriptParser.spec.js | 2 +- test/spec/TypeDoc.spec.js | 2 +- test/spec/Vue3.spec.js | 1035 +++---- test/spec/VuePropertyDecorator.spec.js | 2 +- test/spec/integration.spec.js | 195 +- test/spec/issues.spec.js | 4 +- tsconfig.json | 39 + types/index.d.ts | 424 +++ vitest.config.ts | 11 + yarn.lock | 2521 ++++++----------- 112 files changed, 4023 insertions(+), 3937 deletions(-) delete mode 100644 index.d.ts delete mode 100644 jest.config.js delete mode 100644 lib/Loader.d.ts delete mode 100644 lib/Loader.js delete mode 100644 lib/entity/AbstractCategorizeEntry.js delete mode 100644 lib/entity/AbstractCategorizeTypeEntry.js delete mode 100644 lib/entity/AbstractDecorativeEntry.js delete mode 100644 lib/entity/AbstractEntry.js delete mode 100644 lib/entity/AbstractLiteralEntry.js delete mode 100644 lib/entity/ComputedEntry.js delete mode 100644 lib/entity/DataEntry.js delete mode 100644 lib/entity/DescriptionEntry.js delete mode 100644 lib/entity/EventEntry.js delete mode 100644 lib/entity/InheritAttrsEntry.js delete mode 100644 lib/entity/Keyword.js delete mode 100644 lib/entity/KeywordsEntry.js delete mode 100644 lib/entity/ModelEntry.js delete mode 100644 lib/entity/NameEntry.js delete mode 100644 lib/entity/PropEntry.js delete mode 100644 lib/parser/ClassComponentDataParser.js delete mode 100644 lib/parser/Parser.d.ts delete mode 100644 lib/parser/Parser.js rename index.js => src/index.ts (52%) rename lib/Enum.js => src/lib/Enum.ts (89%) rename lib/JSDoc.js => src/lib/JSDoc.ts (81%) create mode 100644 src/lib/Loader.ts create mode 100644 src/lib/entity/AbstractCategorizeEntry.ts create mode 100644 src/lib/entity/AbstractCategorizeTypeEntry.ts create mode 100644 src/lib/entity/AbstractDecorativeEntry.ts create mode 100644 src/lib/entity/AbstractEntry.ts create mode 100644 src/lib/entity/AbstractLiteralEntry.ts create mode 100644 src/lib/entity/ComputedEntry.ts create mode 100644 src/lib/entity/DataEntry.ts create mode 100644 src/lib/entity/DescriptionEntry.ts create mode 100644 src/lib/entity/EventEntry.ts create mode 100644 src/lib/entity/InheritAttrsEntry.ts create mode 100644 src/lib/entity/Keyword.ts create mode 100644 src/lib/entity/KeywordsEntry.ts rename lib/entity/MethodEntry.js => src/lib/entity/MethodEntry.ts (65%) create mode 100644 src/lib/entity/ModelEntry.ts create mode 100644 src/lib/entity/NameEntry.ts create mode 100644 src/lib/entity/PropEntry.ts rename lib/entity/RestValue.js => src/lib/entity/RestValue.ts (53%) rename lib/entity/SlotEntry.js => src/lib/entity/SlotEntry.ts (52%) rename lib/entity/Value.js => src/lib/entity/Value.ts (62%) rename lib/parser/AbstractExpressionParser.js => src/lib/parser/AbstractExpressionParser.ts (77%) rename lib/parser/AbstractLiteralParser.js => src/lib/parser/AbstractLiteralParser.ts (51%) rename lib/parser/AbstractParser.js => src/lib/parser/AbstractParser.ts (56%) create mode 100644 src/lib/parser/AbstractSourceParser.ts rename lib/parser/ClassComponentComputedParser.js => src/lib/parser/ClassComponentComputedParser.ts (60%) create mode 100644 src/lib/parser/ClassComponentDataParser.ts rename lib/parser/ClassComponentParser.js => src/lib/parser/ClassComponentParser.ts (51%) rename lib/parser/CommentParser.js => src/lib/parser/CommentParser.ts (84%) rename lib/parser/CompositionParser.js => src/lib/parser/CompositionParser.ts (71%) rename lib/parser/ComputedParser.js => src/lib/parser/ComputedParser.ts (92%) rename lib/parser/DataParser.js => src/lib/parser/DataParser.ts (76%) rename lib/parser/EventParser.js => src/lib/parser/EventParser.ts (68%) rename lib/parser/InheritAttrsParser.js => src/lib/parser/InheritAttrsParser.ts (55%) rename lib/parser/InlineTemplateParser.js => src/lib/parser/InlineTemplateParser.ts (50%) rename lib/parser/JSXParser.js => src/lib/parser/JSXParser.ts (82%) rename lib/parser/MarkupTemplateParser.js => src/lib/parser/MarkupTemplateParser.ts (73%) rename lib/parser/MethodParser.js => src/lib/parser/MethodParser.ts (88%) rename lib/parser/ModelParser.js => src/lib/parser/ModelParser.ts (78%) rename lib/parser/NameParser.js => src/lib/parser/NameParser.ts (64%) create mode 100644 src/lib/parser/Parser.ts rename lib/parser/PropParser.js => src/lib/parser/PropParser.ts (85%) rename lib/parser/ScriptParser.js => src/lib/parser/ScriptParser.ts (68%) rename lib/parser/SetupParser.js => src/lib/parser/SetupParser.ts (63%) rename lib/parser/SlotParser.js => src/lib/parser/SlotParser.ts (80%) rename lib/utils/KeywordsUtils.js => src/lib/utils/KeywordsUtils.ts (61%) rename loaders/html.js => src/loaders/html.ts (55%) rename loaders/javascript.js => src/loaders/javascript.ts (56%) rename loaders/pug.js => src/loaders/pug.ts (82%) rename loaders/typescript.js => src/loaders/typescript.ts (100%) rename loaders/vue.js => src/loaders/vue.ts (84%) rename schema/options.js => src/schema/options.ts (100%) create mode 100644 tsconfig.json create mode 100644 types/index.d.ts create mode 100644 vitest.config.ts diff --git a/.eslintignore b/.eslintignore index 9acb038..d0f8b6e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,5 +2,5 @@ test/MethodParser.spec.js test/issues.spec.js test/examples test/tutorial -jest.config.js sample.js +esm/ diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 527d560..07fd666 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -2,14 +2,32 @@ root: true env: node: true +parser: '@typescript-eslint/parser' +plugins: + - import + - '@typescript-eslint' extends: - eslint:recommended - airbnb-base - plugin:import/recommended + - plugin:import/typescript + - plugin:@typescript-eslint/recommended + - plugin:security/recommended parserOptions: - ecmaVersion: 13 + ecmaVersion: 2019 sourceType: module rules: + '@typescript-eslint/ban-types': 'off' + '@typescript-eslint/indent': 'off' + '@typescript-eslint/explicit-function-return-type': 'off' + '@typescript-eslint/explicit-member-accessibility': 'off' + '@typescript-eslint/interface-name-prefix': 'off' + '@typescript-eslint/no-empty-function': 'off' + '@typescript-eslint/no-explicit-any': 'off' + '@typescript-eslint/no-empty-interface': 'off' + '@typescript-eslint/no-this-alias': 'off' + '@typescript-eslint/no-unused-vars': 'off' + '@typescript-eslint/no-var-requires': 'off' arrow-body-style: 'off' array-bracket-spacing: - error @@ -25,9 +43,6 @@ rules: imports: never exports: never functions: never - complexity: - - error - - max: 40 class-methods-use-this: 'off' default-case: 'off' global-require: 'off' @@ -44,7 +59,7 @@ rules: import/no-named-as-default: 'off' import/no-named-as-default-member: 'off' import/no-unresolved: - - 'error' + - 'off' import/order: 'off' import/prefer-default-export: 'off' jest/expect-expect: 'off' @@ -62,12 +77,13 @@ rules: no-use-before-define: 'off' no-restricted-syntax: 'off' no-shadow: 'off' + no-trailing-spaces: 'error' no-useless-constructor: 'off' no-var: error max-classes-per-file: 'off' max-len: - error - - code: 128 + - code: 148 comments: 128 tabWidth: 2 ignoreUrls: true @@ -127,6 +143,10 @@ rules: - error - single - avoidEscape: true + security/detect-non-literal-fs-filename: 'off' + security/detect-non-literal-regexp: 'off' + security/detect-object-injection: 'off' + security/detect-unsafe-regex: 'off' semi: - error - always diff --git a/.gitignore b/.gitignore index 00976db..aef7205 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,5 @@ jspm_packages *.odp *.pdf *.png +esm/ +sample.js \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c07ff2c..87321c6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ tests & coverage: stage: tests script: - yarn install - - yarn test + - yarn coverage coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' artifacts: paths: diff --git a/.npmignore b/.npmignore index 2afbbec..9455ea5 100644 --- a/.npmignore +++ b/.npmignore @@ -1,9 +1,11 @@ +src test coverage .* sample.js -jest.config.js +vitest.ts result.json +tsconfig.json *.tgz *.nix *.yml diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 2f5f5ca..0000000 --- a/index.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -declare module '@vuedoc/parser' { - export { Parser, Loader } from '@vuedoc/parser/parser/lib/Parser.js'; - import { Options as ParserOptions } from '@vuedoc/parser/parser/Parser.js'; - import { Definition } from '@vuedoc/parser/lib/Loader.js'; - import { Keyword } from '@vuedoc/parser/entity/Keyword'; - import { ComputedEntry } from '@vuedoc/parser/entity/ComputedEntry'; - import { DataEntry } from '@vuedoc/parser/entity/DataEntry'; - import { EventEntry } from '@vuedoc/parser/entity/EventEntry'; - import { MethodEntry } from '@vuedoc/parser/entity/MethodEntry'; - import { PropEntry } from '@vuedoc/parser/entity/PropEntry'; - import { SlotEntry } from '@vuedoc/parser/entity/SlotEntry'; - - export async function parseComponent(options: Options): Promise; - export async function parseOptions(options: Options): Promise; - export function synchronizeParsingResult(options: Options, component: ComponentAST): void; - - type FileOptions = { - /** - * The filename to parse - */ - filename: string; - }; - - type ContentOptions = { - /** - * The file content to parse - */ - filecontent: string; - }; - - export type Options = (FileOptions | ContentOptions) & Omit & { - /** - * The file encoding - * @default 'utf8' - */ - encoding?: string; - - /** - * Custom loaders - */ - loaders?: Definition[]; - }; - - type ComponentAST = { - name: string; // Component name - description?: string; // Component description - category?: string; - version?: string; - since?: string; - inheritAttrs: boolean; - keywords: Keyword[]; // Attached component keywords - slots: SlotEntry[]; // Component slots - props: PropEntry[]; // Component props - data: DataEntry[]; // Component data - computed: ComputedEntry[]; // Computed properties - events: EventEntry[]; // Events - methods: MethodEntry[]; // Component methods - errors: string[]; // Syntax and parsing errors - warnings: string[]; // Syntax and parsing warnings - }; -} diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 3193c8c..0000000 --- a/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -export default { - expand: true, - notify: false, - transform: {}, - testMatch: [ - '/test/**/*.spec.js', - ], - collectCoverageFrom: [ - 'index.js', - 'lib/**', - 'schema/**', - 'loader/**', - ], - moduleFileExtensions: [ - 'js', - 'json', - ], - watchPathIgnorePatterns: [ - '/test/examples', - ], -}; diff --git a/lib/Loader.d.ts b/lib/Loader.d.ts deleted file mode 100644 index fa62d7d..0000000 --- a/lib/Loader.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -declare module '@vuedoc/parser/lib/Loader.js' { - abstract class Loader { - static extend(name: LoaderName, loader: Loader): Definition; - static getFileContent(filename: string, options?: GetFileContentOptions): Promise; - - abstract load(data: ScriptData | TemplateData): Promise; - emitTemplate(data: TemplateData): void; - emitScript(data: ScriptData): void; - emitErrors(errors: any[]): void; - pipe(name: string, data: ScriptData | TemplateData): Promise; - } - - type GenericAttrs = { - lang: string; - }; - - type ScriptAttrs = GenericAttrs & { - setup: boolean; - }; - - type PipeData = { - content: string; - attrs: Attrs; - }; - - type ScriptData = PipeData; - type TemplateData = PipeData; - - /** - * The loader name, which is either the extension of the file or the value of - * the attribute `lang` - */ - type LoaderName = string; - - type Definition = { - name: LoaderName; - loader: Loader; - }; - - type GetFileContentOptions = { - /** - * The file encoding - */ - encoding?: string; - }; -} diff --git a/lib/Loader.js b/lib/Loader.js deleted file mode 100644 index 1db5813..0000000 --- a/lib/Loader.js +++ /dev/null @@ -1,52 +0,0 @@ -import { readFile } from 'node:fs/promises'; - -export class Loader { - static extend(name, loader) { - return { name, loader }; - } - - static get(name, options) { - const item = options.definitions.find((loader) => name === loader.name); - - if (!item) { - throw new Error(`Missing loader for ${name}`); - } - - return item.loader; - } - - static async getFileContent(filename, options) { - const buffer = await readFile(filename, options); - - return buffer.toString(); - } - - constructor(options) { - this.options = options; - } - - /* istanbul ignore next */ - /* eslint-disable-next-line class-methods-use-this */ - // eslint-disable-next-line no-unused-vars - async load(data) { - throw new Error('Cannot call abstract Loader.load() method'); - } - - emitTemplate(data) { - this.options.source.template = data; - } - - emitScript(data) { - this.options.source.script = data; - } - - emitErrors(errors) { - this.options.source.errors.push(...errors); - } - - async pipe(name, data) { - const LoaderClass = Loader.get(name, this.options); - - return new LoaderClass(this.options).load(data); - } -} diff --git a/lib/entity/AbstractCategorizeEntry.js b/lib/entity/AbstractCategorizeEntry.js deleted file mode 100644 index 582ec17..0000000 --- a/lib/entity/AbstractCategorizeEntry.js +++ /dev/null @@ -1,12 +0,0 @@ -import { AbstractDecorativeEntry } from './AbstractDecorativeEntry.js'; -import { Visibility } from '../Enum.js'; - -export class AbstractCategorizeEntry extends AbstractDecorativeEntry { - constructor(kind, description = undefined) { - super(kind, description); - - this.category = undefined; - this.version = undefined; - this.visibility = Visibility.public; - } -} diff --git a/lib/entity/AbstractCategorizeTypeEntry.js b/lib/entity/AbstractCategorizeTypeEntry.js deleted file mode 100644 index 4fc931b..0000000 --- a/lib/entity/AbstractCategorizeTypeEntry.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Value } from './Value.js'; -import { AbstractCategorizeEntry } from './AbstractCategorizeEntry.js'; - -export class AbstractCategorizeTypeEntry extends AbstractCategorizeEntry { - constructor(kind, type, description = undefined) { - super(kind, description); - - this.type = type instanceof Array ? type.map(Value.parseNativeType) : Value.parseNativeType(type); - } -} diff --git a/lib/entity/AbstractDecorativeEntry.js b/lib/entity/AbstractDecorativeEntry.js deleted file mode 100644 index 4eab1de..0000000 --- a/lib/entity/AbstractDecorativeEntry.js +++ /dev/null @@ -1,10 +0,0 @@ -import { AbstractEntry } from './AbstractEntry.js'; - -export class AbstractDecorativeEntry extends AbstractEntry { - constructor(kind, description = undefined) { - super(kind); - - this.description = description || undefined; - this.keywords = []; - } -} diff --git a/lib/entity/AbstractEntry.js b/lib/entity/AbstractEntry.js deleted file mode 100644 index e2b53f2..0000000 --- a/lib/entity/AbstractEntry.js +++ /dev/null @@ -1,5 +0,0 @@ -export class AbstractEntry { - constructor(kind) { - this.kind = kind; - } -} diff --git a/lib/entity/AbstractLiteralEntry.js b/lib/entity/AbstractLiteralEntry.js deleted file mode 100644 index 3f87241..0000000 --- a/lib/entity/AbstractLiteralEntry.js +++ /dev/null @@ -1,9 +0,0 @@ -import { AbstractEntry } from './AbstractEntry.js'; - -export class AbstractLiteralEntry extends AbstractEntry { - constructor(kind, value = undefined) { - super(kind); - - this.value = value; - } -} diff --git a/lib/entity/ComputedEntry.js b/lib/entity/ComputedEntry.js deleted file mode 100644 index 923fc46..0000000 --- a/lib/entity/ComputedEntry.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Type } from '../Enum.js'; -import { AbstractCategorizeTypeEntry } from './AbstractCategorizeTypeEntry.js'; - -export class ComputedEntry extends AbstractCategorizeTypeEntry { - constructor({ name, type = Type.unknown, dependencies = [] }) { - super('computed', type); - - this.name = name; - this.dependencies = dependencies; - } -} diff --git a/lib/entity/DataEntry.js b/lib/entity/DataEntry.js deleted file mode 100644 index 6af959b..0000000 --- a/lib/entity/DataEntry.js +++ /dev/null @@ -1,10 +0,0 @@ -import { AbstractCategorizeTypeEntry } from './AbstractCategorizeTypeEntry.js'; - -export class DataEntry extends AbstractCategorizeTypeEntry { - constructor(name, { type, value }) { - super('data', type); - - this.name = name; - this.initialValue = value; - } -} diff --git a/lib/entity/DescriptionEntry.js b/lib/entity/DescriptionEntry.js deleted file mode 100644 index 544e15d..0000000 --- a/lib/entity/DescriptionEntry.js +++ /dev/null @@ -1,7 +0,0 @@ -import { AbstractLiteralEntry } from './AbstractLiteralEntry.js'; - -export class DescriptionEntry extends AbstractLiteralEntry { - constructor(description) { - super('description', description); - } -} diff --git a/lib/entity/EventEntry.js b/lib/entity/EventEntry.js deleted file mode 100644 index 0dafbe8..0000000 --- a/lib/entity/EventEntry.js +++ /dev/null @@ -1,30 +0,0 @@ -import { AbstractCategorizeEntry } from './AbstractCategorizeEntry.js'; -import { Type } from '../Enum.js'; -import { Value } from './Value.js'; -import { toKebabCase } from '@b613/utils/lib/string.js'; - -export class EventEntry extends AbstractCategorizeEntry { - constructor(name, args = []) { - super('event'); - - this.name = toKebabCase(name, [':']); - this.arguments = args; - } -} - -export class EventArgument { - constructor(name, type = Type.unknown) { - this.name = name; - this.type = type instanceof Array ? type.map(Value.parseNativeType) : Value.parseNativeType(type); - this.description = undefined; - this.rest = false; - } -} - -export function* eventArgumentGenerator() { - while (true) { - yield new EventArgument(); - } -} - -export const EventArgumentGenerator = eventArgumentGenerator(); diff --git a/lib/entity/InheritAttrsEntry.js b/lib/entity/InheritAttrsEntry.js deleted file mode 100644 index e32d83c..0000000 --- a/lib/entity/InheritAttrsEntry.js +++ /dev/null @@ -1,7 +0,0 @@ -import { AbstractLiteralEntry } from './AbstractLiteralEntry.js'; - -export class InheritAttrsEntry extends AbstractLiteralEntry { - constructor(value) { - super('inheritAttrs', value); - } -} diff --git a/lib/entity/Keyword.js b/lib/entity/Keyword.js deleted file mode 100644 index a6e76b9..0000000 --- a/lib/entity/Keyword.js +++ /dev/null @@ -1,6 +0,0 @@ -export class Keyword { - constructor(name = '', description = undefined) { - this.name = name; - this.description = description; - } -} diff --git a/lib/entity/KeywordsEntry.js b/lib/entity/KeywordsEntry.js deleted file mode 100644 index cdf7d50..0000000 --- a/lib/entity/KeywordsEntry.js +++ /dev/null @@ -1,7 +0,0 @@ -import { AbstractLiteralEntry } from './AbstractLiteralEntry.js'; - -export class KeywordsEntry extends AbstractLiteralEntry { - constructor(keywords) { - super('keywords', keywords); - } -} diff --git a/lib/entity/ModelEntry.js b/lib/entity/ModelEntry.js deleted file mode 100644 index c0efa64..0000000 --- a/lib/entity/ModelEntry.js +++ /dev/null @@ -1,11 +0,0 @@ -import { AbstractDecorativeEntry } from './AbstractDecorativeEntry.js'; - -export class ModelEntry extends AbstractDecorativeEntry { - // By default, v-model on a component uses `value` as the prop and `input` as the event - constructor(prop = 'value', event = 'input') { - super('model'); - - this.prop = prop; - this.event = event; - } -} diff --git a/lib/entity/NameEntry.js b/lib/entity/NameEntry.js deleted file mode 100644 index a51170c..0000000 --- a/lib/entity/NameEntry.js +++ /dev/null @@ -1,7 +0,0 @@ -import { AbstractLiteralEntry } from './AbstractLiteralEntry.js'; - -export class NameEntry extends AbstractLiteralEntry { - constructor(value) { - super('name', value); - } -} diff --git a/lib/entity/PropEntry.js b/lib/entity/PropEntry.js deleted file mode 100644 index 72bfe00..0000000 --- a/lib/entity/PropEntry.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Type } from '../Enum.js'; -import { AbstractCategorizeTypeEntry } from './AbstractCategorizeTypeEntry.js'; -import { toKebabCase } from '@b613/utils/lib/string.js'; - -export class PropEntry extends AbstractCategorizeTypeEntry { - constructor(name, { type = Type.unknown, value = undefined, required = false, describeModel = false } = {}) { - super('prop', type); - - this.name = toKebabCase(name); - this.default = value; - this.required = required; - this.describeModel = describeModel; - } -} diff --git a/lib/parser/ClassComponentDataParser.js b/lib/parser/ClassComponentDataParser.js deleted file mode 100644 index d9e11a6..0000000 --- a/lib/parser/ClassComponentDataParser.js +++ /dev/null @@ -1,45 +0,0 @@ -import { DataParser } from './DataParser.js'; - -import { DataEntry } from '../entity/DataEntry.js'; -import { Syntax } from '../Enum.js'; - -export class ClassComponentDataParser extends DataParser { - parse(node, { type = null } = {}) { - switch (node.type) { - case Syntax.ClassProperty: - this.parseData(node, node.key, node.value, type); - break; - - default: - switch (node.expression.type) { - case Syntax.AssignmentExpression: - this.parseData( - node, - node.expression.left.property, - node.expression.right, - type - ); - break; - } - break; - } - } - - parseData(node, key, value, type = null) { - const ref = this.getValue(value); - const entry = new DataEntry(key.name, { - type: type || this.getTSType(node, ref.kind), - value: ref.raw, - }); - - this.root.setScopeValue(entry.name, value, ref); - this.parseEntryComment(entry, node); - DataParser.mergeEntryKeywords(entry); - - if (node.accessibility) { - entry.visibility = node.accessibility; - } - - this.emit(entry); - } -} diff --git a/lib/parser/Parser.d.ts b/lib/parser/Parser.d.ts deleted file mode 100644 index 595e427..0000000 --- a/lib/parser/Parser.d.ts +++ /dev/null @@ -1,159 +0,0 @@ -declare module '@vuedoc/parser/parser/Parser.js' { - import EventEmitter from 'node:events'; - - - export type Options = { - /** - * Source to parse - */ - source: { - script?: string; - template?: string; - }; - - /** - * The filename to parse - */ - filename?: string; - - /** - * The component features to parse and extract - * @default ['name', 'description', 'slots', 'props', 'data', 'computed', 'events', 'methods'] - */ - features?: Feature[]; - - /** - * List of ignored visibilities - * @default ['protected', 'private'] - */ - ignoredVisibilities?: Visibility[]; - - /** - * Additional composition tokens for advanced components - */ - composition?: { - data?: string[]; - props?: string[]; - computed?: string[]; - methods?: string[]; - }; - - /** - * Set to `true` to enable JSX parsing - * @default false - */ - jsx?: boolean; - }; - - export type Feature = 'name' | 'description' | 'slots' | 'props' | 'data' | 'computed' | 'events' | 'methods'; - export type Visibility = 'public' | 'protected' | 'private'; - - export declare class Parser extends EventEmitter { - static SUPPORTED_FEATURES: Feature[]; - constructor(options: Options); - walk(): Parser; - } - - export type ParsingEntry = ComputedEntry - | DataEntry - | DescriptionEntry - | EventEntry - | InheritAttrsEntry - | KeywordsEntry - | MethodEntry - | ModelEntry - | NameEntry - | PropEntry - | SlotEntry; - - export type Keyword = { - name: string; - description?: string; - }; - - export interface AbstractEntry { - kind: Kind; - description?: string; - category?: string; - version?: string; - visibility: Visibility; - keywords: Keyword[]; - } - - export interface ComputedEntry extends AbstractEntry<'computed'> { - type: string; - name: string; - dependencies: string[]; - } - - export interface DataEntry extends AbstractEntry<'data'> { - type: string; - name: string; - initialValue: string; - } - - export interface DescriptionEntry extends AbstractEntry<'description'> { - value: string; - } - - export interface EventEntry extends AbstractEntry<'event'> { - name: string; - arguments: Array<{ - name: string; - type: string; - description: string; - rest: boolean; - }>; - } - - export interface InheritAttrsEntry extends AbstractEntry<'inheritAttrs'> { - value: boolean; - } - - export interface KeywordsEntry extends AbstractEntry<'keywords'> { - value: Keyword[]; - } - - export interface MethodEntry extends AbstractEntry<'method'> { - name: string; - syntax: string[]; - params: Array<{ - name: string; - type: string; - description: string; - defaultValue: string; - rest: boolean; - }>; - returns: { - type: string; - description?: string; - }; - } - - export interface ModelEntry extends AbstractEntry<'model'> { - prop: string; - event: string; - } - - export interface NameEntry extends AbstractEntry<'name'> { - value: string; - } - - export interface PropEntry extends AbstractEntry<'prop'> { - type: string; - name: string; - default: string; - required: boolean; - describeModel: boolean; - } - - export interface SlotEntry extends AbstractEntry<'slot'> { - name: string; - description?: string; - props: Array<{ - name: string; - type: string; - description?: string; - }>; - } -} diff --git a/lib/parser/Parser.js b/lib/parser/Parser.js deleted file mode 100644 index d71c525..0000000 --- a/lib/parser/Parser.js +++ /dev/null @@ -1,109 +0,0 @@ -import path from 'path'; -import EventEmitter from 'events'; - -import { parseAst, ScriptParser } from './ScriptParser.js'; -import { MarkupTemplateParser } from './MarkupTemplateParser.js'; -import { CompositionParser } from './CompositionParser.js'; - -import { NameEntry } from '../entity/NameEntry.js'; -import { Feature, Features, DEFAULT_IGNORED_VISIBILITIES } from '../Enum.js'; - -export class Parser extends EventEmitter { - constructor(options) { - super(); - - this.options = options; - this.features = options.features || Features; - this.scope = {}; - this.hasNameEntry = false; - this.ignoredVisibilities = options.ignoredVisibilities || DEFAULT_IGNORED_VISIBILITIES; - } - - static validateOptions(options) { - if (!options.source) { - throw new Error('options.source is required'); - } - - if (options.features) { - if (!Array.isArray(options.features)) { - throw new TypeError('options.features must be an array'); - } - - options.features.forEach((feature) => { - if (!Features.includes(feature)) { - throw new Error(`Unknow '${feature}' feature. Supported features: ${JSON.stringify(Features)}`); - } - }); - } - } - - static getEventName(feature) { - return feature.endsWith('s') - ? feature.substring(0, feature.length - 1) - : feature; - } - - isIgnoredVisibility(item) { - return this.ignoredVisibilities.includes(item); - } - - walk() { - Parser.validateOptions(this.options); - - if (this.features.includes(Feature.name)) { - this.on(Feature.name, () => { - this.hasNameEntry = true; - }); - } - - process.nextTick(() => { - if (this.options.source.script?.content) { - const options = { - jsx: this.options.jsx || false, - composition: this.options.composition, - }; - - try { - const ast = parseAst(this.options.source.script.content, options); - - if (!this.options.source.script.attrs.setup) { - this.options.source.script.attrs.setup = CompositionParser.isCompositionScript(ast); - } - - if (this.options.source.script.attrs.setup) { - new CompositionParser(this, ast, this.options.source.script, options).parse(); - } else { - new ScriptParser(this, ast, this.options.source.script, options).parse(); - } - } catch (err) { - this.emit('error', err); - } - } - - if (this.options.source.template?.content) { - new MarkupTemplateParser(this, this.options.source.template).parse(); - } - - if (!this.hasNameEntry) { - if (this.features.includes(Feature.name)) { - this.parseComponentName(); - } - } - - setTimeout(() => this.emit('end'), 0); - }); - - return this; - } - - parseComponentName() { - if (this.options.filename) { - const name = path.parse(this.options.filename).name; - const entry = new NameEntry(name); - - this.emit(entry.kind, entry); - } - } -} - -Parser.SUPPORTED_FEATURES = Features; diff --git a/package.json b/package.json index 5e7f247..ac98dd4 100644 --- a/package.json +++ b/package.json @@ -4,36 +4,40 @@ "description": "Generate a JSON documentation for a Vue file", "type": "module", "exports": { - ".": "./index.js", - "./Enum": "./lib/Enum.js", - "./entity/ComputedEntry": "./lib/entity/ComputedEntry.js", - "./entity/DataEntry": "./lib/entity/DataEntry.js", - "./entity/DescriptionEntry": "./lib/entity/DescriptionEntry.js", - "./entity/EventEntry": "./lib/entity/EventEntry.js", - "./entity/InheritAttrsEntry": "./lib/entity/InheritAttrsEntry.js", - "./entity/Keyword": "./lib/entity/Keyword.js", - "./entity/KeywordsEntry": "./lib/entity/KeywordsEntry.js", - "./entity/MethodEntry": "./lib/entity/MethodEntry.js", - "./entity/ModelEntry": "./lib/entity/ModelEntry.js", - "./entity/NameEntry": "./lib/entity/NameEntry.js", - "./entity/PropEntry": "./lib/entity/PropEntry.js", - "./entity/RestValue": "./lib/entity/RestValue.js", - "./entity/SlotEntry": "./lib/entity/SlotEntry.js", - "./entity/Value": "./lib/entity/Value.js", - "./JSDoc": "./lib/JSDoc.js", - "./loaders/html": "./loaders/html.js", - "./loaders/javascript": "./loaders/javascript.js", - "./loaders/pug": "./loaders/pug.js", - "./loaders/typescript": "./loaders/typescript.js", - "./loaders/vue": "./loaders/vue.js", - "./schema/options": "./schema/options.js", - "./utils/KeywordsUtils": "./lib/utils/KeywordsUtils.js" + ".": "./esm/index.js", + "./types": "./types/index.d.ts", + "./Enum": "./esm/lib/Enum.js", + "./entity/ComputedEntry": "./esm/lib/entity/ComputedEntry.js", + "./entity/DataEntry": "./esm/lib/entity/DataEntry.js", + "./entity/DescriptionEntry": "./esm/lib/entity/DescriptionEntry.js", + "./entity/EventEntry": "./esm/lib/entity/EventEntry.js", + "./entity/InheritAttrsEntry": "./esm/lib/entity/InheritAttrsEntry.js", + "./entity/Keyword": "./esm/lib/entity/Keyword.js", + "./entity/KeywordsEntry": "./esm/lib/entity/KeywordsEntry.js", + "./entity/MethodEntry": "./esm/lib/entity/MethodEntry.js", + "./entity/ModelEntry": "./esm/lib/entity/ModelEntry.js", + "./entity/NameEntry": "./esm/lib/entity/NameEntry.js", + "./entity/PropEntry": "./esm/lib/entity/PropEntry.js", + "./entity/RestValue": "./esm/lib/entity/RestValue.js", + "./entity/SlotEntry": "./esm/lib/entity/SlotEntry.js", + "./entity/Value": "./esm/lib/entity/Value.js", + "./JSDoc": "./esm/lib/JSDoc.js", + "./loaders/html": "./esm/loaders/html.js", + "./loaders/javascript": "./esm/loaders/javascript.js", + "./loaders/pug": "./esm/loaders/pug.js", + "./loaders/typescript": "./esm/loaders/typescript.js", + "./loaders/vue": "./esm/loaders/vue.js", + "./schema/options": "./esm/schema/options.js", + "./utils/KeywordsUtils": "./esm/lib/utils/KeywordsUtils.js" }, - "typings": "index.d.ts", + "types": "./types/index.d.ts", "scripts": { "preversion": "yarn test", - "test": "node --experimental-vm-modules node_modules/.bin/jest --coverage", - "update-examples": "UPDATE_EXAMPLES_RESULTS=true node --experimental-vm-modules node_modules/.bin/jest --no-coverage Compatibility.spec.js", + "build": "tsc", + "test": "vitest", + "testui": "vitest --ui", + "coverage": "vitest run --coverage", + "update-examples": "UPDATE_EXAMPLES_RESULTS=true node_modules/.bin/vitest -- Compatibility.spec.js", "lint": "eslint .", "gimtoc": "gimtoc -f README.md -s 'Table of Contents' -o README.md" }, @@ -86,12 +90,22 @@ "vue-template-compiler": "^2.6.14" }, "devDependencies": { - "eslint": "^8.14.0", + "@types/pug": "^2.0.6", + "@typescript-eslint/eslint-plugin": "^5.30.7", + "@typescript-eslint/parser": "^5.30.7", + "@vitest/ui": "^0.18.1", + "c8": "^7.12.0", + "eslint": "^8.20.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", + "eslint-import-resolver-typescript": "^3.3.0", "eslint-plugin-import": "^2.26.0", + "eslint-plugin-security": "^1.5.0", "gimtoc": "^2.0.0", - "jest": "^28.0.3" + "pug": "^3.0.2", + "typescript": "^4.7.4", + "vite": "^3.0.2", + "vitest": "^0.18.1" }, "engines": { "node": ">=16.6" diff --git a/index.js b/src/index.ts similarity index 52% rename from index.js rename to src/index.ts index 7326b72..43d0595 100644 --- a/index.js +++ b/src/index.ts @@ -1,40 +1,53 @@ import path from 'path'; -import { Loader } from './lib/Loader.js'; +import { Loader, Options } from './lib/Loader.js'; import { Parser } from './lib/parser/Parser.js'; import { VueLoader } from './loaders/vue.js'; import { HtmlLoader } from './loaders/html.js'; import { JavaScriptLoader } from './loaders/javascript.js'; import { TypeScriptLoader } from './loaders/typescript.js'; -import { Feature, DEFAULT_IGNORED_VISIBILITIES, DEFAULT_ENCODING } from './lib/Enum.js'; +import { Feature, DEFAULT_IGNORED_VISIBILITIES, DEFAULT_ENCODING, FeatureEvent } from './lib/Enum.js'; import { KeywordsUtils } from './lib/utils/KeywordsUtils.js'; import { PropEntry } from './lib/entity/PropEntry.js'; +import { Vuedoc } from '../types/index.js'; export { Loader } from './lib/Loader.js'; export { Parser } from './lib/parser/Parser.js'; -const DEFAULT_LOADERS = [ +const DEFAULT_LOADERS: Vuedoc.Loader.Definition[] = [ Loader.extend('js', JavaScriptLoader), Loader.extend('ts', TypeScriptLoader), Loader.extend('html', HtmlLoader), Loader.extend('vue', VueLoader), ]; -export async function parseOptions(options) { +export async function parseOptions(options: Vuedoc.Index.Options) { if (!options) { - /* eslint-disable max-len */ throw new Error('Missing options argument'); } - if (!options.filename && !options.filecontent) { - /* eslint-disable max-len */ - throw new Error('One of options.filename or options.filecontent is required'); - } - if (!options.encoding) { options.encoding = DEFAULT_ENCODING; } + let filecontent: string | null = null; + + if ('filename' in options) { + if (options.filename) { + filecontent = await Loader.getFileContent(options.filename, options.encoding); + } else { + throw new Error('options.filename cannot be empty'); + } + } + + if ('filecontent' in options) { + filecontent = options.filecontent; + } + + if (filecontent === null) { + throw new Error('Missing options.filename of options.filecontent'); + } + if (!options.ignoredVisibilities) { options.ignoredVisibilities = DEFAULT_IGNORED_VISIBILITIES; } @@ -50,8 +63,8 @@ export async function parseOptions(options) { errors: [], }; - const loaderOptions = { - ...options, + const loaderOptions: Options = { + source: options.source, definitions: [ ...DEFAULT_LOADERS, ], @@ -61,21 +74,17 @@ export async function parseOptions(options) { loaderOptions.definitions.unshift(...options.loaders); } - if (options.filename) { + if ('filename' in options) { const ext = path.extname(options.filename); const loaderName = ext.substring(1); - const LoaderClass = Loader.get(loaderName, loaderOptions); - const source = await Loader.getFileContent(options.filename, { - encoding: options.encoding, - }); - + const LoaderClass: any = Loader.get(loaderName, loaderOptions); const loader = new LoaderClass(loaderOptions); await loader.load({ attrs: { lang: loaderName, }, - content: source, + content: filecontent, }); } else { const loader = new VueLoader(loaderOptions); @@ -84,28 +93,37 @@ export async function parseOptions(options) { attrs: { lang: 'vue', }, - content: options.filecontent, + content: filecontent, }); } + + return options as Vuedoc.Parser.ResolvedOptions; } -export function synchronizeParsingResult(options, component) { - const defaultModelProp = options.source.script.attrs.setup ? 'model-value' : 'value'; - const additionalProps = []; +type ExtendedParsingResult = Vuedoc.Index.ParsingResult & { + model?: Vuedoc.Entry.ModelEntry[]; +}; + +export function synchronizeParsingResult(options: Vuedoc.Index.Options, component: ExtendedParsingResult) { + const defaultModelProp = options.source.script?.attrs.setup ? 'model-value' : 'value'; + const additionalProps: Vuedoc.Entry.PropEntry[] = []; if (!component.props) { component.props = []; } - component.model.forEach((model) => { - const props = component.props.filter((prop) => prop.name === model.prop); + component.model?.forEach((model) => { + const props = component.props?.filter((prop) => prop.name === model.prop); - if (props.length) { - props.forEach((prop) => { + if (props?.length) { + for (const prop of props) { prop.describeModel = true; - }); + } } else { - const prop = new PropEntry(model.prop, { describeModel: true }); + const prop = new PropEntry({ + name: model.prop, + describeModel: true, + }); prop.description = model.description; prop.keywords = model.keywords; @@ -115,41 +133,41 @@ export function synchronizeParsingResult(options, component) { }); component.props.push(...additionalProps); - delete component.model; if (Feature.props in component) { - component.props.forEach((prop) => { + for (const prop of component.props) { if (prop.name === defaultModelProp) { prop.describeModel = true; } if (prop.describeModel) { prop.name = 'v-model'; - } else if (options.source.script.attrs.setup && Feature.events in component) { - const hasUpdateEvent = component.events.some((event) => event.name === `update:${prop.name}`); + } else if (options.source.script?.attrs.setup && Feature.events in component) { + const hasUpdateEvent = component.events?.some((event) => event.name === `update:${prop.name}`); if (hasUpdateEvent) { prop.name = `v-model:${prop.name}`; prop.describeModel = true; } } - }); + } } } -export async function parseComponent(options) { - await parseOptions(options); +export async function parseComponent(options: Vuedoc.Index.Options): Promise { + const resolvedOptions = await parseOptions(options); return new Promise((resolve) => { - const component = { + const component: ExtendedParsingResult = { inheritAttrs: true, errors: [...options.source.errors], warnings: [], keywords: [], + model: [], }; - const parser = new Parser(options); + const parser = new Parser(resolvedOptions); parser.on('error', ({ message }) => { component.errors.push(message); @@ -159,21 +177,29 @@ export async function parseComponent(options) { component.warnings.push(message); }); - parser.on('keywords', ({ value }) => { + parser.on(FeatureEvent.keywords, ({ value }) => { component.keywords.push(...value); KeywordsUtils.parseCommonEntryTags(component); }); + parser.on('inheritAttrs', ({ value }) => { + component.inheritAttrs = value; + }); + + parser.on('model', handleEventEntry(component.model)); + parser.on('end', () => { synchronizeParsingResult(options, component); - resolve(component); - }); - parser.on('inheritAttrs', ({ value }) => { - component.inheritAttrs = value; + // FIXME Handle options.hooks + // if (options.hooks?.handleParsingResult) { + // options.hooks.handleParsingResult(component); + // } + + resolve(component); }); - [...parser.features, 'model'].forEach((feature) => { + for (const feature of parser.features) { switch (feature) { case Feature.name: case Feature.description: @@ -182,24 +208,34 @@ export async function parseComponent(options) { }); break; - default: { - const eventName = Parser.getEventName(feature); + case Feature.keywords: + // already handled + break; - component[feature] = []; + default: { + const eventName = FeatureEvent[feature]; + const items: Vuedoc.Entry.TypeKeywords[] = []; - parser.on(eventName, (entry) => { - const index = component[feature].findIndex((item) => item.name === entry.name); + component[feature as string] = items; - if (index > -1) { - component[feature].splice(index, 1, entry); - } else { - component[feature].push(entry); - } - }); + parser.on(eventName, handleEventEntry(items)); + break; } } - }); + } parser.walk(); }); } + +function handleEventEntry(items: Vuedoc.Entry.TypeKeywords[]) { + return (entry: Vuedoc.Entry.TypeKeywords) => { + const index = items.findIndex((item) => item.name === entry.name); + + if (index > -1) { + items.splice(index, 1, entry); + } else { + items.push(entry); + } + }; +} diff --git a/lib/Enum.js b/src/lib/Enum.ts similarity index 89% rename from lib/Enum.js rename to src/lib/Enum.ts index 272f8a3..a1b4397 100644 --- a/lib/Enum.js +++ b/src/lib/Enum.ts @@ -1,4 +1,6 @@ -export const ScalarType = Object.freeze({ +import { Vuedoc } from '../../types/index.js'; + +export const ScalarType = { string: 'string', boolean: 'boolean', binary: 'binary', @@ -6,9 +8,9 @@ export const ScalarType = Object.freeze({ null: 'null', bigint: 'bigint', undefined: 'undefined', -}); +} as const; -export const Type = Object.freeze({ +export const Type = { ...ScalarType, any: 'any', never: 'never', @@ -20,16 +22,16 @@ export const Type = Object.freeze({ function: 'function', Promise: 'Promise', symbol: 'symbol', -}); +} as const; export const ScalarTypeList = Object.freeze(Object.values(ScalarType)); export const TypeList = Object.freeze(Object.values(Type)); -export const Visibility = Object.freeze({ +export const Visibility: Record = { public: 'public', protected: 'protected', private: 'private', -}); +}; export const JSDocTag = Object.freeze({ name: 'name', @@ -117,14 +119,14 @@ export const CommonTags = Object.freeze([ export const Visibilities = Object.freeze(Object.values(Visibility)); -export const DEFAULT_IGNORED_VISIBILITIES = Object.freeze([ +export const DEFAULT_IGNORED_VISIBILITIES = [ Visibility.protected, Visibility.private, -]); +]; export const DEFAULT_ENCODING = 'utf8'; -export const Feature = Object.freeze({ +export const Feature: Record = Object.freeze({ name: 'name', description: 'description', keywords: 'keywords', @@ -136,16 +138,28 @@ export const Feature = Object.freeze({ methods: 'methods', }); -export const Features = Object.freeze(Object.values(Feature)); +export const FeatureEvent: Record = Object.freeze({ + name: 'name', + description: 'description', + keywords: 'keyword', + slots: 'slot', + props: 'prop', + data: 'data', + computed: 'computed', + events: 'event', + methods: 'method', +}); + +export const Features = Object.values(Feature); -export const CompositionProperties = Object.freeze({ +export const CompositionProperties = { setup: 'setup', extends: 'extends', expose: 'expose', emits: 'emits', -}); +} as const; -export const Properties = Object.freeze({ +export const Properties = { ...CompositionProperties, name: 'name', model: 'model', @@ -168,9 +182,9 @@ export const Properties = Object.freeze({ render: 'render', methods: 'methods', watch: 'watch', -}); +} as const; -export const CompositionAPI = Object.freeze({ +export const CompositionAPI = { ref: 'ref', $ref: '$ref', unref: 'unref', @@ -187,13 +201,13 @@ export const CompositionAPI = Object.freeze({ markRaw: 'markRaw', toRef: 'toRef', $toRef: '$toRef', -}); +} as const; -export const RouterKeys = Object.freeze({ +export const RouterKeys = { useRoute: 'useRoute', useRouter: 'useRouter', useLink: 'useLink', -}); +} as const; export const LegacyHooks = [ Properties.beforeCreate, @@ -242,6 +256,7 @@ export const Syntax = Object.freeze({ ClassProperty: 'ClassProperty', ClassMethod: 'ClassMethod', ClassPrivateMethod: 'ClassPrivateMethod', + ClassPrivateProperty: 'ClassPrivateProperty', FunctionExpression: 'FunctionExpression', FunctionDeclaration: 'FunctionDeclaration', ImportDeclaration: 'ImportDeclaration', diff --git a/lib/JSDoc.js b/src/lib/JSDoc.ts similarity index 81% rename from lib/JSDoc.js rename to src/lib/JSDoc.ts index 13ec99a..d381291 100644 --- a/lib/JSDoc.js +++ b/src/lib/JSDoc.ts @@ -1,4 +1,6 @@ +import { Vuedoc } from '../../types/index.js'; import { Type, TagAlias } from './Enum.js'; +import type { AbstractParser } from './parser/AbstractParser.js'; import { KeywordsUtils } from './utils/KeywordsUtils.js'; const PARAM_NAME = '[a-zA-Z0-9$&\\.\\[\\]_\'"]+'; @@ -18,16 +20,16 @@ function parseDescriptionText(text) { : ''; } -function* paramGenerator() { +function* paramGenerator(): Generator { while (true) { - yield { name: null, type: Type.unknown, description: undefined }; + yield { name: '', type: Type.unknown }; } } const paramGeneratorInstance = paramGenerator(); export const JSDoc = { - parseTypeParam(type, param) { + parseTypeParam(type: Vuedoc.Parser.Type, param: Pick) { if (type.indexOf('|') > -1) { param.type = type.split('|').map((item) => item.trim()); } else if (type === '*') { @@ -39,11 +41,11 @@ export const JSDoc = { param.type = type; } }, - parseParams(parser, keywords, params, generator) { + parseParams(parser: AbstractParser, keywords: Vuedoc.Entry.Keyword[], params: Vuedoc.Entry.Param[], generator) { let initialParamsAltered = false; KeywordsUtils.extract(keywords, TagAlias.param, true).forEach(({ description }) => { - const param = JSDoc.parseParamKeyword(description, generator); + const param = JSDoc.parseParamKeyword(description || '', generator); if (param.name) { const entryIndex = params.findIndex((item) => item.name === param.name); @@ -78,7 +80,7 @@ export const JSDoc = { // Delete params like '{...}', '[...]', 'this.*' when dotlet has params if (initialParamsAltered) { - const indexToRemove = []; + const indexToRemove: number[] = []; params.forEach(({ name }, indexParam) => { if (name[0] === '{' || name[0] === '[' || name.startsWith('this.')) { @@ -89,13 +91,13 @@ export const JSDoc = { indexToRemove.reverse().forEach((index) => params.splice(index, 1)); } }, - parseReturns(keywords, returns) { + parseReturns(keywords: Vuedoc.Entry.Keyword[], returns: Vuedoc.Entry.MethodReturns) { KeywordsUtils.extract(keywords, TagAlias.returns, true).forEach(({ description }) => { - Object.assign(returns, JSDoc.parseReturnsKeyword(description, returns.type)); + Object.assign(returns, JSDoc.parseReturnsKeyword(description || '', returns.type)); }); }, - parseType(description) { - let type = `${description}`.trim(); + parseType(description: string) { + let type: Vuedoc.Parser.Type | Vuedoc.Parser.Type[] = `${description}`.trim(); const matches = TYPE_RE.exec(type); if (matches) { @@ -116,8 +118,7 @@ export const JSDoc = { break; } - // eslint-disable-next-line valid-typeof - if (typeof type === Type.string) { + if (typeof type === 'string') { if (type.indexOf('|') > -1) { type = type.split('|').map((item) => item.trim()); } else if (type === '*') { @@ -129,7 +130,7 @@ export const JSDoc = { return type; }, - parseParamKeyword(text, generator = paramGeneratorInstance, re = PARAM_RE) { + parseParamKeyword(text: string, generator = paramGeneratorInstance, re = PARAM_RE) { const param = generator.next().value; const matches = re.exec(`${text}\n`); @@ -167,8 +168,8 @@ export const JSDoc = { return param; }, - parseReturnsKeyword(text, type = Type.unknown) { - const output = { type, description: text }; + parseReturnsKeyword(text: string, type: Vuedoc.Parser.Type | Vuedoc.Parser.Type[] = Type.unknown) { + const output: Vuedoc.Entry.MethodReturns = { type, description: text }; const matches = RETURNS_RE.exec(`${text}\n`); if (matches) { diff --git a/src/lib/Loader.ts b/src/lib/Loader.ts new file mode 100644 index 0000000..d8fbf75 --- /dev/null +++ b/src/lib/Loader.ts @@ -0,0 +1,59 @@ +import { readFile } from 'node:fs/promises'; +import { Vuedoc } from '../../types/index.js'; + +export type Options = Pick & { + definitions: Vuedoc.Loader.Definition[]; +}; + +export class Loader implements Vuedoc.Loader.Interface { + options: Options; + + static extend(name: string, loader: any) { + return { name, loader }; + } + + static get(name: string, options: Options) { + const item = options.definitions.find((loader) => name === loader.name); + + if (!item) { + throw new Error(`Missing loader for ${name}`); + } + + return item.loader; + } + + static async getFileContent(filename: string, encoding?: string) { + const buffer = await readFile(filename, encoding as any); + + return buffer.toString(); + } + + constructor(options: Options) { + this.options = options; + } + + /* istanbul ignore next */ + /* eslint-disable-next-line class-methods-use-this */ + // eslint-disable-next-line no-unused-vars + async load(data: Vuedoc.Loader.TemplateData | Vuedoc.Loader.ScriptData) { + throw new Error('Cannot call abstract Loader.load() method'); + } + + emitTemplate(data: Vuedoc.Loader.TemplateData) { + this.options.source.template = data; + } + + emitScript(data: Vuedoc.Loader.ScriptData) { + this.options.source.script = data; + } + + emitErrors(errors: string[]) { + this.options.source.errors.push(...errors); + } + + async pipe(name: string, data: Vuedoc.Loader.TemplateData | Vuedoc.Loader.ScriptData) { + const LoaderClass: any = Loader.get(name, this.options); + + return new LoaderClass(this.options).load(data); + } +} diff --git a/src/lib/entity/AbstractCategorizeEntry.ts b/src/lib/entity/AbstractCategorizeEntry.ts new file mode 100644 index 0000000..59db993 --- /dev/null +++ b/src/lib/entity/AbstractCategorizeEntry.ts @@ -0,0 +1,15 @@ +import { AbstractDecorativeEntry } from './AbstractDecorativeEntry.js'; +import { Visibility } from '../Enum.js'; +import { Vuedoc } from '../../../types/index.js'; + +export class AbstractCategorizeEntry extends AbstractDecorativeEntry { + category?: string; + version?: string; + visibility: Vuedoc.Parser.Visibility; + + constructor(kind: Kind, description?: string) { + super(kind, description); + + this.visibility = Visibility.public; + } +} diff --git a/src/lib/entity/AbstractCategorizeTypeEntry.ts b/src/lib/entity/AbstractCategorizeTypeEntry.ts new file mode 100644 index 0000000..cbfe55f --- /dev/null +++ b/src/lib/entity/AbstractCategorizeTypeEntry.ts @@ -0,0 +1,13 @@ +import { Value } from './Value.js'; +import { AbstractCategorizeEntry } from './AbstractCategorizeEntry.js'; +import { Vuedoc } from '../../../types/index.js'; + +export class AbstractCategorizeTypeEntry extends AbstractCategorizeEntry { + type: Vuedoc.Parser.Type; + + constructor(kind: Kind, type: Vuedoc.Parser.Type | Vuedoc.Parser.Type[], description?: string) { + super(kind, description); + + this.type = type instanceof Array ? type.map(Value.parseNativeType) : Value.parseNativeType(type); + } +} diff --git a/src/lib/entity/AbstractDecorativeEntry.ts b/src/lib/entity/AbstractDecorativeEntry.ts new file mode 100644 index 0000000..39ca8cd --- /dev/null +++ b/src/lib/entity/AbstractDecorativeEntry.ts @@ -0,0 +1,14 @@ +import { Vuedoc } from '../../../types/index.js'; +import { AbstractEntry } from './AbstractEntry.js'; + +export class AbstractDecorativeEntry extends AbstractEntry { + description?: string; + keywords: Vuedoc.Entry.Keyword[]; + + constructor(kind: Kind, description?: string) { + super(kind); + + this.description = description; + this.keywords = []; + } +} diff --git a/src/lib/entity/AbstractEntry.ts b/src/lib/entity/AbstractEntry.ts new file mode 100644 index 0000000..9bd205e --- /dev/null +++ b/src/lib/entity/AbstractEntry.ts @@ -0,0 +1,9 @@ +import { Vuedoc } from '../../../types/index.js'; + +export class AbstractEntry { + kind: Kind; + + constructor(kind: Kind) { + this.kind = kind; + } +} diff --git a/src/lib/entity/AbstractLiteralEntry.ts b/src/lib/entity/AbstractLiteralEntry.ts new file mode 100644 index 0000000..fc1939a --- /dev/null +++ b/src/lib/entity/AbstractLiteralEntry.ts @@ -0,0 +1,12 @@ +import { Vuedoc } from '../../../types/index.js'; +import { AbstractEntry } from './AbstractEntry.js'; + +export class AbstractLiteralEntry extends AbstractEntry { + value: Value; + + constructor(kind: Kind, value: Value) { + super(kind); + + this.value = value; + } +} diff --git a/src/lib/entity/ComputedEntry.ts b/src/lib/entity/ComputedEntry.ts new file mode 100644 index 0000000..65ca98f --- /dev/null +++ b/src/lib/entity/ComputedEntry.ts @@ -0,0 +1,21 @@ +import { Vuedoc } from '../../../types/index.js'; +import { Type } from '../Enum.js'; +import { AbstractCategorizeTypeEntry } from './AbstractCategorizeTypeEntry.js'; + +export type Options = { + name: string; + type?: string | string[]; + dependencies?: string[]; +}; + +export class ComputedEntry extends AbstractCategorizeTypeEntry<'computed'> implements Vuedoc.Entry.ComputedEntry { + name: string; + dependencies: string[]; + + constructor({ name, type = Type.unknown, dependencies = [] }: Options) { + super('computed', type); + + this.name = name; + this.dependencies = dependencies; + } +} diff --git a/src/lib/entity/DataEntry.ts b/src/lib/entity/DataEntry.ts new file mode 100644 index 0000000..141d7c8 --- /dev/null +++ b/src/lib/entity/DataEntry.ts @@ -0,0 +1,20 @@ +import { Vuedoc } from '../../../types/index.js'; +import { AbstractCategorizeTypeEntry } from './AbstractCategorizeTypeEntry.js'; + +export type Options = { + name: string; + type: Vuedoc.Parser.Type | Vuedoc.Parser.Type[]; + initialValue: string; +}; + +export class DataEntry extends AbstractCategorizeTypeEntry<'data'> implements Vuedoc.Entry.DataEntry { + name: string; + initialValue: string; + + constructor({ name, type, initialValue }: Options) { + super('data', type); + + this.name = name; + this.initialValue = initialValue; + } +} diff --git a/src/lib/entity/DescriptionEntry.ts b/src/lib/entity/DescriptionEntry.ts new file mode 100644 index 0000000..cd6cc46 --- /dev/null +++ b/src/lib/entity/DescriptionEntry.ts @@ -0,0 +1,8 @@ +import { Vuedoc } from '../../../types/index.js'; +import { AbstractLiteralEntry } from './AbstractLiteralEntry.js'; + +export class DescriptionEntry extends AbstractLiteralEntry<'description'> implements Vuedoc.Entry.DescriptionEntry { + constructor(description: string) { + super('description', description); + } +} diff --git a/src/lib/entity/EventEntry.ts b/src/lib/entity/EventEntry.ts new file mode 100644 index 0000000..918f66f --- /dev/null +++ b/src/lib/entity/EventEntry.ts @@ -0,0 +1,39 @@ +import { AbstractCategorizeEntry } from './AbstractCategorizeEntry.js'; +import { Type } from '../Enum.js'; +import { Value } from './Value.js'; +import { toKebabCase } from '@b613/utils/lib/string.js'; +import { Vuedoc } from '../../../types/index.js'; + +export class EventEntry extends AbstractCategorizeEntry<'event'> implements Vuedoc.Entry.EventEntry { + name: string; + arguments: Vuedoc.Entry.Param[]; + + constructor(name: string, args: Vuedoc.Entry.Param[] = []) { + super('event'); + + this.name = toKebabCase(name, [':']); + this.arguments = args; + } +} + +export class EventArgument implements Vuedoc.Entry.Param { + name: string; + type: Vuedoc.Parser.Type; + description?: string; + rest: boolean; + + constructor(name: string, type: Vuedoc.Parser.Type | Vuedoc.Parser.Type[] = Type.unknown) { + this.name = name; + this.type = type instanceof Array ? type.map(Value.parseNativeType) : Value.parseNativeType(type); + this.description = undefined; + this.rest = false; + } +} + +export function* eventArgumentGenerator(): Generator { + while (true) { + yield new EventArgument(''); + } +} + +export const EventArgumentGenerator = eventArgumentGenerator(); diff --git a/src/lib/entity/InheritAttrsEntry.ts b/src/lib/entity/InheritAttrsEntry.ts new file mode 100644 index 0000000..9b5b57a --- /dev/null +++ b/src/lib/entity/InheritAttrsEntry.ts @@ -0,0 +1,8 @@ +import { Vuedoc } from '../../../types/index.js'; +import { AbstractLiteralEntry } from './AbstractLiteralEntry.js'; + +export class InheritAttrsEntry extends AbstractLiteralEntry<'inheritAttrs', boolean> implements Vuedoc.Entry.InheritAttrsEntry { + constructor(value: boolean) { + super('inheritAttrs', value); + } +} diff --git a/src/lib/entity/Keyword.ts b/src/lib/entity/Keyword.ts new file mode 100644 index 0000000..058cd68 --- /dev/null +++ b/src/lib/entity/Keyword.ts @@ -0,0 +1,11 @@ +import { Vuedoc } from '../../../types/index.js'; + +export class Keyword implements Vuedoc.Entry.Keyword { + name: string; + description: string | undefined; + + constructor(name?: string, description?: string) { + this.name = name || ''; + this.description = description; + } +} diff --git a/src/lib/entity/KeywordsEntry.ts b/src/lib/entity/KeywordsEntry.ts new file mode 100644 index 0000000..46d73e0 --- /dev/null +++ b/src/lib/entity/KeywordsEntry.ts @@ -0,0 +1,8 @@ +import { Vuedoc } from '../../../types/index.js'; +import { AbstractLiteralEntry } from './AbstractLiteralEntry.js'; + +export class KeywordsEntry extends AbstractLiteralEntry<'keyword', Vuedoc.Entry.Keyword[]> implements Vuedoc.Entry.KeywordsEntry { + constructor(keywords: Vuedoc.Entry.Keyword[]) { + super('keyword', keywords); + } +} diff --git a/lib/entity/MethodEntry.js b/src/lib/entity/MethodEntry.ts similarity index 65% rename from lib/entity/MethodEntry.js rename to src/lib/entity/MethodEntry.ts index 769d061..c4341f6 100644 --- a/lib/entity/MethodEntry.js +++ b/src/lib/entity/MethodEntry.ts @@ -1,11 +1,17 @@ import { AbstractCategorizeEntry } from './AbstractCategorizeEntry.js'; import { Type } from '../Enum.js'; import { Value } from './Value.js'; +import { Vuedoc } from '../../../types/index.js'; const ARRAY_BRAKET = '[]'; -export class MethodEntry extends AbstractCategorizeEntry { - constructor(name, params) { +export class MethodEntry extends AbstractCategorizeEntry<'method'> implements Vuedoc.Entry.MethodEntry { + name: string; + params: MethodParam[]; + syntax: string[]; + returns: MethodReturns; + + constructor(name: string, params: MethodParam[]) { super('method'); this.name = name; @@ -15,12 +21,16 @@ export class MethodEntry extends AbstractCategorizeEntry { } } -export class MethodParam { +export class MethodParam implements Vuedoc.Entry.Param { + name: string; + type: string; + description?: string; + defaultValue?: string; + rest: boolean; + constructor() { this.name = ''; this.type = Type.unknown; - this.description = undefined; - this.defaultValue = undefined; this.rest = false; } @@ -52,14 +62,17 @@ export class MethodParam { } } -export function* methodParamGenerator() { +export function* methodParamGenerator(): Generator { while (true) { yield new MethodParam(); } } export class MethodReturns { - constructor(type = Type.void) { + type: string; + description: undefined; + + constructor(type: Vuedoc.Parser.Type | Vuedoc.Parser.Type[] = Type.void) { this.type = type instanceof Array ? type.map(Value.parseNativeType) : Value.parseNativeType(type); this.description = undefined; } diff --git a/src/lib/entity/ModelEntry.ts b/src/lib/entity/ModelEntry.ts new file mode 100644 index 0000000..77f8927 --- /dev/null +++ b/src/lib/entity/ModelEntry.ts @@ -0,0 +1,26 @@ +import { toKebabCase } from '@b613/utils/lib/string.js'; +import { Vuedoc } from '../../../types/index.js'; +import { Visibility } from '../Enum.js'; +import { AbstractDecorativeEntry } from './AbstractDecorativeEntry.js'; + +export class ModelEntry extends AbstractDecorativeEntry<'model'> implements Vuedoc.Entry.ModelEntry { + name: string; + prop: string; + event: string; + category?: string | undefined; + version?: string | undefined; + visibility: Vuedoc.Parser.Visibility = Visibility.public; + + /** + * By default: + * - Vue 3: v-model on a component uses `modelValue` as the prop and `update:modelValue` as the event + * - Vue 2: v-model on a component uses `value` as the prop and `input` as the event + */ + constructor(propNameInCamelCase = 'value', event = 'input') { + super('model'); + + this.name = propNameInCamelCase; + this.prop = toKebabCase(propNameInCamelCase, [':']); + this.event = event; + } +} diff --git a/src/lib/entity/NameEntry.ts b/src/lib/entity/NameEntry.ts new file mode 100644 index 0000000..a077528 --- /dev/null +++ b/src/lib/entity/NameEntry.ts @@ -0,0 +1,8 @@ +import { Vuedoc } from '../../../types/index.js'; +import { AbstractLiteralEntry } from './AbstractLiteralEntry.js'; + +export class NameEntry extends AbstractLiteralEntry<'name'> implements Vuedoc.Entry.NameEntry { + constructor(value: string) { + super('name', value); + } +} diff --git a/src/lib/entity/PropEntry.ts b/src/lib/entity/PropEntry.ts new file mode 100644 index 0000000..76de6ca --- /dev/null +++ b/src/lib/entity/PropEntry.ts @@ -0,0 +1,29 @@ +import { Type } from '../Enum.js'; +import { AbstractCategorizeTypeEntry } from './AbstractCategorizeTypeEntry.js'; +import { toKebabCase } from '@b613/utils/lib/string.js'; +import { Vuedoc } from '../../../types/index.js'; + +export type Options = { + name: string; + type?: string | string[]; + defaultValue?: string; + required?: boolean; + describeModel?: boolean; +}; + +export class PropEntry extends AbstractCategorizeTypeEntry<'prop'> implements Vuedoc.Entry.PropEntry { + name: string; + default: string; + required: boolean; + describeModel: boolean; + function?: Vuedoc.Entry.PropFunction; + + constructor({ name, type = Type.unknown, defaultValue, required = false, describeModel = false }: Options) { + super('prop', type); + + this.name = toKebabCase(name); + this.default = defaultValue; + this.required = required; + this.describeModel = describeModel; + } +} diff --git a/lib/entity/RestValue.js b/src/lib/entity/RestValue.ts similarity index 53% rename from lib/entity/RestValue.js rename to src/lib/entity/RestValue.ts index 85e82c6..7ce4328 100644 --- a/lib/entity/RestValue.js +++ b/src/lib/entity/RestValue.ts @@ -1,7 +1,10 @@ +import { Vuedoc } from '../../../types/index.js'; import { Value } from './Value.js'; export class RestValue extends Value { - constructor(type, name) { + name: string; + + constructor(type: Vuedoc.Parser.Type, name: string) { super(type, {}, `{ ...${name} }`); this.name = name; diff --git a/lib/entity/SlotEntry.js b/src/lib/entity/SlotEntry.ts similarity index 52% rename from lib/entity/SlotEntry.js rename to src/lib/entity/SlotEntry.ts index 0e0e52e..d14eb3e 100644 --- a/lib/entity/SlotEntry.js +++ b/src/lib/entity/SlotEntry.ts @@ -1,11 +1,15 @@ import { AbstractCategorizeEntry } from './AbstractCategorizeEntry.js'; import { Type } from '../Enum.js'; import { Value } from './Value.js'; +import { Vuedoc } from '../../../types/index.js'; const DEFAULT_SLOT_NAME = 'default'; -export class SlotEntry extends AbstractCategorizeEntry { - constructor(name, description = undefined) { +export class SlotEntry extends AbstractCategorizeEntry<'slot'> implements Vuedoc.Entry.SlotEntry { + name: string; + props: SlotProp[]; + + constructor(name: string, description?: string) { super('slot', description); this.name = name || DEFAULT_SLOT_NAME; @@ -13,8 +17,12 @@ export class SlotEntry extends AbstractCategorizeEntry { } } -export class SlotProp { - constructor(name, type = Type.unknown, description = undefined) { +export class SlotProp implements Vuedoc.Entry.SlotProp { + name: string; + type: Vuedoc.Parser.Type; + description: string | undefined; + + constructor(name: string, type: Vuedoc.Parser.Type | Vuedoc.Parser.Type[] = Type.unknown, description: string = undefined) { this.name = name; this.type = type instanceof Array ? type.map(Value.parseNativeType) : Value.parseNativeType(type); this.description = description || undefined; @@ -23,7 +31,7 @@ export class SlotProp { export function* slotPropGenerator() { while (true) { - yield new SlotProp(); + yield new SlotProp(''); } } diff --git a/lib/entity/Value.js b/src/lib/entity/Value.ts similarity index 62% rename from lib/entity/Value.js rename to src/lib/entity/Value.ts index acb2241..09ca89a 100644 --- a/lib/entity/Value.js +++ b/src/lib/entity/Value.ts @@ -1,3 +1,4 @@ +import { Vuedoc } from '../../../types/index.js'; import { Type } from '../Enum.js'; const NATIVE_TYPES = [ @@ -14,11 +15,18 @@ const NATIVE_TYPES_PREFIX = [ 'Set<', ]; -export class Value { - constructor(type = Type.unknown, value = undefined, raw = value, rawObject = null) { - this.type = type; +export class Value { + type: Vuedoc.Parser.Type; + value: TValue; + raw: string; + rawObject: Record | null; + member: boolean; + $kind?: string; + + constructor(type?: Vuedoc.Parser.Type, value?: any, raw?: string, rawObject: object | null = null) { + this.type = type || Type.unknown; this.value = value; - this.raw = raw; + this.raw = raw || value; this.rawObject = rawObject; this.member = false; } @@ -39,13 +47,5 @@ export class Value { } } -function* undefinedValueGenerator() { - while (true) { - yield new Value(Type.unknown, undefined, Type.undefined); - } -} - -const UndefinedValueGenerator = undefinedValueGenerator(); - -export const UndefinedValue = UndefinedValueGenerator.next().value; -export const NullValue = new Value(Type.unknown, null, Type.null); +export const UndefinedValue = new Value(Type.unknown, undefined, Type.undefined); +export const NullValue = new Value(Type.unknown, null, Type.null); diff --git a/lib/parser/AbstractExpressionParser.js b/src/lib/parser/AbstractExpressionParser.ts similarity index 77% rename from lib/parser/AbstractExpressionParser.js rename to src/lib/parser/AbstractExpressionParser.ts index 17ccaf7..e32daf2 100644 --- a/lib/parser/AbstractExpressionParser.js +++ b/src/lib/parser/AbstractExpressionParser.ts @@ -1,8 +1,11 @@ import { AbstractParser } from './AbstractParser.js'; import { CommentParser } from './CommentParser.js'; import { Syntax, Visibility } from '../Enum.js'; +import { Vuedoc } from '../../../types/index.js'; +import * as Babel from '@babel/types'; +import { ScriptParser } from './ScriptParser.js'; -export class AbstractExpressionParser extends AbstractParser { +export class AbstractExpressionParser extends AbstractParser> { getSourceString(node) { const source = super.getSourceString(node); @@ -31,7 +34,7 @@ export class AbstractExpressionParser extends AbstractParser { } } - parseEntryComment(entry, node, defaultVisibility = Visibility.public) { + parseEntryComment(entry: Vuedoc.Entry.TypeKeywords, node, defaultVisibility = Visibility.public) { const comment = this.root.findComment(node); if (comment) { @@ -49,11 +52,11 @@ export class AbstractExpressionParser extends AbstractParser { return comment; } - parseSpreadElement(node) { + parseSpreadElement(node: Babel.SpreadElement) { this.parse(node.argument); } - parseObjectExpression(node) { + parseObjectExpression(node: Babel.ObjectExpression) { node.properties.forEach((property) => { switch (property.type) { case Syntax.SpreadElement: @@ -70,18 +73,18 @@ export class AbstractExpressionParser extends AbstractParser { /* istanbul ignore next */ /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ - parseObjectExpressionProperty(node) {} + parseObjectExpressionProperty(node: Babel.ObjectProperty | Babel.ObjectMethod) {} - parseBlockStatement(node) { + parseBlockStatement(node: Babel.BlockStatement) { for (const item of node.body) { this.parseBlockStatementItem(item); } } - parseBlockStatementItem(item) { + parseBlockStatementItem(item: Babel.Statement) { switch (item.type) { case Syntax.VariableDeclaration: - this.parseVariableDeclaration(item); + this.parseVariableDeclaration(item as any); break; case Syntax.ExpressionStatement: @@ -114,10 +117,7 @@ export class AbstractExpressionParser extends AbstractParser { } } - /** - * @param {IfStatement|SwitchCase} node - */ - parseConsequentStatement(node) { + parseConsequentStatement(node: Babel.Statement) { switch (node.type) { case Syntax.BlockStatement: this.parseBlockStatement(node); @@ -129,14 +129,11 @@ export class AbstractExpressionParser extends AbstractParser { } } - parseAlternameStatement(node) { + parseAlternameStatement(node: Babel.Statement) { this.parseConsequentStatement(node); } - /** - * @param {ForStatement|ForInStatement|ForOfStatement|WhileStatement|DoWhileStatement} node - */ - parseLoopStatement(node) { + parseLoopStatement(node: Babel.Loop) { switch (node.body.type) { case Syntax.BlockStatement: this.parseBlockStatement(node.body); @@ -144,13 +141,10 @@ export class AbstractExpressionParser extends AbstractParser { } } - /** - * @param {SwitchStatement} node - */ - parseSwitchStatement(node) { + parseSwitchStatement(node: Babel.SwitchStatement) { for (const caseNode of node.cases) { for (const consequent of caseNode.consequent) { - if (consequent.test) { + if ('test' in consequent) { this.parseConsequentStatement(consequent); } else { this.parseAlternameStatement(consequent); @@ -159,10 +153,7 @@ export class AbstractExpressionParser extends AbstractParser { } } - /** - * @param {TryStatement} node - */ - parseTryStatement(node) { + parseTryStatement(node: Babel.TryStatement) { this.parseBlockStatement(node.block); if (node.handler) { @@ -174,9 +165,6 @@ export class AbstractExpressionParser extends AbstractParser { } } - /** - * @param {FunctionExpression} node - */ parseFunctionExpression(node) { switch (node.type) { case Syntax.BlockStatement: diff --git a/lib/parser/AbstractLiteralParser.js b/src/lib/parser/AbstractLiteralParser.ts similarity index 51% rename from lib/parser/AbstractLiteralParser.js rename to src/lib/parser/AbstractLiteralParser.ts index 04cfb9e..632d747 100644 --- a/lib/parser/AbstractLiteralParser.js +++ b/src/lib/parser/AbstractLiteralParser.ts @@ -1,7 +1,10 @@ import { AbstractParser } from './AbstractParser.js'; import { Syntax } from '../Enum.js'; +import { Vuedoc } from '../../../types/index.js'; +import * as Babel from '@babel/types'; +import type { ScriptParser } from './ScriptParser.js'; -export class AbstractLiteralParser extends AbstractParser { +export class AbstractLiteralParser extends AbstractParser> { parse(node) { switch (node.type) { case Syntax.ObjectProperty: @@ -13,5 +16,5 @@ export class AbstractLiteralParser extends AbstractParser { /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ /* istanbul ignore next */ - parseObjectProperty(node) {} + parseObjectProperty(node: Babel.ObjectProperty) {} } diff --git a/lib/parser/AbstractParser.js b/src/lib/parser/AbstractParser.ts similarity index 56% rename from lib/parser/AbstractParser.js rename to src/lib/parser/AbstractParser.ts index c11cc06..00f8f6e 100644 --- a/lib/parser/AbstractParser.js +++ b/src/lib/parser/AbstractParser.ts @@ -1,16 +1,16 @@ -import EventEmitter from 'events'; - import { RestValue } from '../entity/RestValue.js'; import { Value, UndefinedValue, NullValue } from '../entity/Value.js'; -import { Syntax, Type, TypedocTag, JSDocTag, TypeList, CompositionAPIValues, ScalarTypeList } from '../Enum.js'; +import { Syntax, Type, TypeList, ScalarTypeList } from '../Enum.js'; import { get } from '@b613/utils/lib/object.js'; +import { Vuedoc } from '../../../types/index.js'; +import type { Parser } from './Parser.js'; +import * as Babel from '@babel/types'; const DUPLICATED_SPACES_RE = /\s+/g; const BOOLEAN_OPERATOR_RE = /(&&|\|\|)/; const BINARY_OPERATOR_RE = /(&|\|)/; -const IGNORED_KEYWORDS = [TypedocTag.hidden, JSDocTag.ignore]; const NUMERIC_OPERATORS = ['*', '+', '-', '/', '^']; -const IGNORED_SCOPED_TYPES = [Type.any, Type.unknown]; +const IGNORED_SCOPED_TYPES: string[] = [Type.any, Type.unknown]; const FUNCTION_EXPRESSIONS = [ Syntax.ObjectMethod, @@ -22,7 +22,7 @@ const FUNCTION_EXPRESSIONS = [ Syntax.FunctionDeclaration, ]; -function* generateName(prefix) { +function* generateName(prefix: string): Generator { let index = 0; while (true) { @@ -33,48 +33,44 @@ function* generateName(prefix) { const generateObjectName = generateName('object'); const generateArrayName = generateName('array'); -export class AbstractParser { - constructor(root, scope = root.scope, nodes = root.nodes) { +export class AbstractParser { + root: Root; + emitter: Parser; + source: Source; + features: Vuedoc.Parser.Feature[]; + parserOptions: Vuedoc.Parser.ResolvedOptions; + scope: Vuedoc.Parser.Scope; + + constructor(root: Root, emitter: Parser, source: Source, scope: Vuedoc.Parser.Scope) { this.root = root; - this.emitter = root instanceof EventEmitter ? root : root.root; - this.model = null; - this.source = this.root.source; - this.features = this.emitter.features; + this.emitter = emitter; + this.source = source; + this.parserOptions = emitter.options; + this.features = emitter.features; this.scope = { ...scope }; - this.nodes = { ...nodes }; - this.composition = root.options.composition - ? CompositionAPIValues.concat(...Object.values(root.options.composition).flat()) - : CompositionAPIValues; } static isFunction(node) { return FUNCTION_EXPRESSIONS.includes(node.type); } - emit(entry) { - if (this.emitter.isIgnoredVisibility(entry.visibility)) { - return; - } - - if (entry.keywords) { - const contentsAnIgnoredKeyword = entry.keywords.some(({ name }) => IGNORED_KEYWORDS.includes(name)); + emit(entry: Vuedoc.Entry.Type) { + // if ('parseExposedEntry' in this.root) { + // // TODO Handle this.root.parseExposedEntry(entry); + // // this.root.parseExposedEntry(entry); + // } - if (contentsAnIgnoredKeyword) { - return; - } - } - - if ('parseExposedEntry' in this.root) { - this.root.parseExposedEntry(entry); - } - - this.emitter.emit(entry.kind, entry); + this.emitter.emitEntry(entry); } - emitWarning(message) { + emitWarning(message: string) { this.emitter.emit('warning', message); } + emitError(message: string) { + this.emitter.emit('error', message); + } + parse(node) { switch (node.type) { case Syntax.ObjectProperty: @@ -116,201 +112,223 @@ export class AbstractParser { /* istanbul ignore next */ /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ - parseFunctionDeclaration(node) {} + parseFunctionDeclaration(node: Babel.FunctionDeclaration) {} /* istanbul ignore next */ /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ - parseEntryComment(entry, node) {} + parseEntryComment(entry: Vuedoc.Entry.Type, node) {} /* istanbul ignore next */ /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ - parseArrayExpression(node) {} + parseArrayExpression(node: Babel.ArrayExpression) {} /* istanbul ignore next */ /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ - parseCallExpression(node) {} + parseCallExpression(node: Babel.CallExpression) {} /* istanbul ignore next */ /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ - parseFunctionExpression(node) {} + parseFunctionExpression(node: Babel.FunctionExpression) {} /* istanbul ignore next */ /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ - parseExpressionStatement(node) {} + parseExpressionStatement(node: Babel.ExpressionStatement) {} /* istanbul ignore next */ /* eslint-disable no-unused-vars */ /* eslint-disable class-methods-use-this */ - parseObjectExpression(node) {} + parseObjectExpression(node: Babel.ObjectExpression) {} - parseObjectProperty(node) { + parseObjectProperty(node: Babel.ObjectProperty) { this.parse(node.value); } - parseIdentifier(node) { - if (node.name in this.nodes) { - this.parse(this.nodes[node.name].node); + parseIdentifier(node: Babel.Identifier) { + if (node.name in this.scope && this.scope[node.name].node) { + this.parse(this.scope[node.name].node.value); } } - parseVariableDeclarationArrayPattern(id, init) { - const array = new Value(Type.array, []); + parseVariableDeclarationArrayPattern(id: Vuedoc.Parser.AST.LVal, init: Babel.ArrayPattern | Babel.ArrayExpression) { + const array = new Value(Type.array, []); id.name = generateArrayName.next().value; - this.scope[id.name] = array; - id.elements.forEach((element, index) => { - if (element === null) { - return; - } - - let name = null; - let defaultValue; - let nodeDefaultValue = init; - - switch (element.type) { - case Syntax.Identifier: - name = element.name; - break; + this.setScopeValue(id.name, init, array); - case Syntax.AssignmentPattern: - name = element.left.name; - defaultValue = this.getValue(element.right); - nodeDefaultValue = element.right; - break; - } + if ('elements' in id) { + id.elements.forEach((element, index) => { + if (element === null) { + return; + } - switch (init.type) { - case Syntax.ArrayExpression: { - const node = init.elements[index] || init; + let name: string | undefined; + let defaultValue: Value | undefined; + let nodeDefaultValue: any = init; - if (defaultValue && defaultValue !== UndefinedValue) { - array.value.push(defaultValue.value); - this.setScopeValue(name, node, defaultValue); - } else { - const ref = init.elements[index] - ? this.getValue(init.elements[index]) - : UndefinedValue; + switch (element.type) { + case Syntax.Identifier: + name = element.name; + break; - array.value.push(ref.value); - this.setScopeValue(name, node, ref); - } - break; + case Syntax.AssignmentPattern: + if ('name' in element.left) { + name = element.left.name; + defaultValue = this.getValue(element.right); + nodeDefaultValue = element.right; + break; + } } - default: - array.value.push(defaultValue?.value); + if (name) { + switch (init.type) { + case Syntax.ArrayExpression: { + const node = init.elements[index] || init; + + if (defaultValue && defaultValue !== UndefinedValue) { + array.value.push(defaultValue.value); + this.setScopeValue(name, node, defaultValue); + } else { + const ref = init.elements[index] + ? this.getValue(init.elements[index]) + : UndefinedValue; + + array.value.push(ref.value); + this.setScopeValue(name, node, ref); + } + break; + } - if (defaultValue) { - this.setScopeValue(name, nodeDefaultValue, defaultValue); - } else { - this.setScopeValue(name, nodeDefaultValue, UndefinedValue); + default: + array.value.push(defaultValue?.value); + + if (defaultValue) { + this.setScopeValue(name, nodeDefaultValue, defaultValue); + } else { + this.setScopeValue(name, nodeDefaultValue, UndefinedValue); + } + break; } - break; - } - }); + } + }); + } array.raw = JSON.stringify(array.value); - - this.setScopeValue(id.name, init, array); } - parseVariableDeclarationObjectPattern(id, init) { - const object = new Value(Type.object, {}); + parseVariableDeclarationObjectPattern(id: Vuedoc.Parser.AST.LVal, init: Babel.ObjectPattern | Babel.ObjectExpression) { + const object = new Value(Type.object, {}); id.name = generateObjectName.next().value; - this.scope[id.name] = object; - - id.properties - .filter((property) => property.value) - .forEach((property) => { - let name = null; - let defaultValue; - switch (property.value.type) { - case Syntax.Identifier: - name = this.getValue(property.key).value; - break; - - case Syntax.AssignmentPattern: - name = this.getValue(property.value.left).value; - defaultValue = this.getValue(property.value.right); - break; - } - - switch (init.type) { - case Syntax.ObjectExpression: { - const propertyValue = init.properties.find(({ key }) => { - return this.getValue(key).value === name; - }); - - if (defaultValue && defaultValue !== UndefinedValue) { - object.value[name] = defaultValue.value; - this.setScopeValue(name, init, defaultValue); - } else { - const ref = propertyValue - ? this.getValue(propertyValue.value) - : UndefinedValue; + this.setScopeValue(id.name, init, object); - object.value[name] = ref.value; - this.setScopeValue(name, init, ref); + if ('properties' in id) { + id.properties + .filter((property) => 'value' in property) + .forEach((property) => { + if (property.type === Syntax.ObjectProperty) { + let name: string | undefined; + let defaultValue: Value | undefined; + + switch (property.value.type) { + case Syntax.Identifier: + name = this.getValue(property.key).value; + break; + + case Syntax.AssignmentPattern: + name = this.getValue(property.value.left).value; + defaultValue = this.getValue(property.value.right); + break; } - break; - } - - default: - object.value[name] = defaultValue?.value; - if (defaultValue) { - this.setScopeValue(name, init, defaultValue); - } else { - this.setScopeValue(name, init, UndefinedValue); + if (name) { + switch (init.type) { + case Syntax.ObjectExpression: { + const propertyValue = init.properties.find((item) => { + return 'key' in item && this.getValue(item.key).value === name; + }); + + if (defaultValue && defaultValue !== UndefinedValue) { + object.value[name] = defaultValue.value; + this.setScopeValue(name, init, defaultValue); + } else { + const ref = propertyValue && 'value' in propertyValue + ? this.getValue(propertyValue.value) + : UndefinedValue; + + object.value[name] = ref.value; + this.setScopeValue(name, init, ref); + } + break; + } + + default: + object.value[name] = defaultValue?.value; + + if (defaultValue) { + this.setScopeValue(name, init, defaultValue); + } else { + this.setScopeValue(name, init, UndefinedValue); + } + break; + } } - break; - } - }); + } else { + // TODO Implement property.type === Syntax.RestElement + } + }); + } object.raw = JSON.stringify(object.value); - - this.setScopeValue(id.name, init, object); } - setScopeValue(key, node, value, { nodeComment = node, nodeTyping = node, forceType = false } = {}) { - if (!forceType && key in this.scope && this.scope[key] && this.scope[key].type !== value.type) { - if (!IGNORED_SCOPED_TYPES.includes(this.scope[key].type)) { + setScopeValue(key: string, node, value: Value, { nodeComment = node, nodeTyping = node, forceType = false } = {}) { + if (!forceType && key in this.scope && this.scope[key].value.type !== value.type) { + if (!IGNORED_SCOPED_TYPES.includes(this.scope[key].value.type)) { value.type = Type.unknown; } } - this.scope[key] = value; - this.nodes[key] = { node, nodeComment, nodeTyping }; + this.scope[key] = { + value: value, + node: { + type: nodeTyping, + value: node, + comment: nodeComment, + }, + }; } - parseVariableDeclaration(node) { + parseVariableDeclaration(node: Vuedoc.Parser.AST.VariableDeclaration) { node.declarations .filter(({ type }) => type === Syntax.VariableDeclarator) .forEach((declaration) => { switch (declaration.id.type) { case Syntax.ArrayPattern: - this.parseVariableDeclarationArrayPattern(declaration.id, declaration.init); + this.parseVariableDeclarationArrayPattern(declaration.id as Vuedoc.Parser.AST.LVal, declaration.init as any); break; case Syntax.ObjectPattern: - this.parseVariableDeclarationObjectPattern(declaration.id, declaration.init); + this.parseVariableDeclarationObjectPattern(declaration.id as Vuedoc.Parser.AST.LVal, declaration.init as any); break; default: { - const hasBeenInitialized = !!declaration.init; - const initializedNode = hasBeenInitialized ? declaration.init : declaration; - const declarationValue = hasBeenInitialized ? this.getValue(declaration.init) : UndefinedValue; + const initializedNode = declaration.init ? declaration.init : declaration; + const declarationValue = declaration.init ? this.getValue(declaration.init) : UndefinedValue; + const nodeTyping = declaration.init && 'typeParameters' in declaration.init + ? declaration.init.typeParameters + : 'typeAnnotation' in declaration.id + ? declaration.id?.typeAnnotation + : declaration; this.setScopeValue(declaration.id.name, initializedNode, declarationValue, { - nodeTyping: declaration.init?.typeParameters || declaration.id?.typeAnnotation || declaration, + nodeTyping, nodeComment: node.declarations.length > 1 ? declaration : node, }); break; @@ -319,15 +337,14 @@ export class AbstractParser { }); } - /** - * @param {AssignmentExpression} node - */ - parseAssignmentExpression(node) { - this.setScopeValue(node.left.name, node.right, this.getValue(node.right)); + parseAssignmentExpression(node: Babel.AssignmentExpression) { + if ('name' in node.left) { + this.setScopeValue(node.left.name, node.right, this.getValue(node.right)); + } } - parseKey({ computed, id, key = id, value, body }) { - const keyName = key.type === Syntax.PrivateName + parseKey({ computed = false, id, key = id, value, body }) { + const keyName: string = key.type === Syntax.PrivateName ? key.id.name : key.name || key.value; @@ -358,13 +375,13 @@ export class AbstractParser { return keyName; } - parseType(node) { + parseType(node: any) { const ref = this.getValue(node); return ref.kind; } - getTSTypeRaw(node, defaultType = Type.unknown) { + getTSTypeRaw(node, defaultType: Vuedoc.Parser.Type = Type.unknown) { let type = defaultType; switch (node.type) { @@ -396,7 +413,7 @@ export class AbstractParser { return type; } - getTSType(node, defaultType = Type.unknown) { + getTSType(node, defaultType: Vuedoc.Parser.Type = Type.unknown) { const type = this.getTSTypeRaw(node, defaultType); return typeof type === 'string' @@ -404,7 +421,7 @@ export class AbstractParser { : type; } - getObjectExpressionValue(node) { + getObjectExpressionValue(node): Value { const value = {}; const raw = {}; @@ -418,7 +435,7 @@ export class AbstractParser { ? ref.value : ref.raw; - if (item.value.type === Syntax.Identifier && !(item.key.name in this.nodes)) { + if (item.value.type === Syntax.Identifier && !(item.key.name in this.scope)) { this.setScopeValue(item.key.name, item.value, ref); } break; @@ -459,9 +476,9 @@ export class AbstractParser { return new Value(type, value); } - getIdentifierValue(node) { + getIdentifierValue(node): Value { if (node.name in this.scope) { - return this.scope[node.name]; + return this.scope[node.name].value; } if (node.name === UndefinedValue.raw) { @@ -475,7 +492,7 @@ export class AbstractParser { return new Value(type, node.name, node.name); } - getUnaryExpression(node) { + getUnaryExpression(node): Value { let type; let raw; let value; @@ -519,20 +536,20 @@ export class AbstractParser { return new Value(type, value, raw); } - getMemberExpression(node) { + getMemberExpression(node: Babel.MemberExpression): Value { const exp = this.getSourceString(node); const type = exp.startsWith('Math.') || exp.startsWith('Number.') ? Type.number : Type.object; - if (type === Type.object && node.object.name in this.scope) { + if (type === Type.object && 'name' in node.object && node.object.name in this.scope) { const key = this.getValue(node.property); - const object = this.scope[node.object.name]; + const object = this.scope[node.object.name].value; const value = object.value instanceof Array ? object.value[key.value] : get(object.value, key.value); const valueType = value instanceof Array ? Type.array : typeof value; const isAnObject = valueType === Type.array || valueType === Type.object; - const isScalar = ScalarTypeList.includes(valueType) - || Object.keys(value).every((key) => ScalarTypeList.includes(typeof value[key])); + const isScalar = ScalarTypeList.includes(valueType as any) + || Object.keys(value).every((key) => ScalarTypeList.includes(typeof value[key] as any)); const raw = isScalar ? JSON.stringify(value) @@ -546,51 +563,53 @@ export class AbstractParser { return this.getSourceValue(node, type); } - getCallExpression(node) { + getCallExpression(node: Babel.CallExpression): Value { let value; - switch (node.callee.name) { - case 'useAttrs': - case 'useSlots': - case 'createApp': - value = UndefinedValue; - break; + if ('name' in node.callee) { + switch (node.callee.name) { + case 'useAttrs': + case 'useSlots': + case 'createApp': + value = UndefinedValue; + break; - case 'defineProps': - case 'defineEmits': - value = node.arguments.length ? this.getValue(node.arguments[0]) : UndefinedValue; - break; + case 'defineProps': + case 'defineEmits': + value = node.arguments.length ? this.getValue(node.arguments[0]) : UndefinedValue; + break; - case 'effectScope': - value = UndefinedValue; - break; + case 'effectScope': + value = UndefinedValue; + break; - case 'withDefaults': - value = node.arguments.length ? this.getValue(node.arguments[1]) : UndefinedValue; - break; + case 'withDefaults': + value = node.arguments.length ? this.getValue(node.arguments[1]) : UndefinedValue; + break; - default: - if (this.composition.includes(node.callee.name)) { - value = node.arguments.length ? this.getValue(node.arguments[0]) : UndefinedValue; - } - break; - } + default: + if (this.emitter.composition.includes(node.callee.name as any)) { + value = node.arguments.length ? this.getValue(node.arguments[0]) : UndefinedValue; + } + break; + } - if (value) { - value.$kind = node.callee.name; + if (value) { + value.$kind = node.callee.name; - return value; + return value; + } } - const type = node.callee && node.callee.name && TypeList.includes(node.callee.name.toLowerCase()) + const type = node.callee.type === Syntax.Identifier && TypeList.includes(node.callee.name.toLowerCase() as any) ? node.callee.name : Type.unknown; return this.getSourceValue(node, type); } - getAssignmentExpression(node) { - let type; + getAssignmentExpression(node: Babel.AssignmentExpression): Value { + let type: Vuedoc.Parser.Type; if (BOOLEAN_OPERATOR_RE.test(node.operator)) { type = Type.boolean; @@ -603,7 +622,7 @@ export class AbstractParser { return new Value(type, this.getSourceString(node)); } - getBinaryExpression(node) { + getBinaryExpression(node: Babel.BinaryExpression): Value { const type = NUMERIC_OPERATORS.includes(node.operator) ? this.parseType(node.left) : Type.binary; @@ -611,12 +630,12 @@ export class AbstractParser { return new Value(type, this.getSourceString(node)); } - getArrayExpression(node) { + getArrayExpression(node: Babel.ArrayExpression): Value { const value = node.elements.map((element) => this.getValue(element).value); const raw = node.elements.map((element) => { const ref = this.getValue(element); - return ScalarTypeList.includes(ref.type) + return ScalarTypeList.includes(ref.type as any) ? ref.raw : this.getInlineSourceString(element); }); @@ -624,7 +643,7 @@ export class AbstractParser { return new Value(Type.array, value, `[${raw.join(',')}]`, raw); } - getValue(node) { + getValue(node: Babel.Node | null): Value { if (node === null) { return NullValue; } @@ -633,9 +652,6 @@ export class AbstractParser { case Syntax.StringLiteral: return new Value(Type.string, node.value, JSON.stringify(node.value)); - case Syntax.Template: - return new Value(typeof node.value, node.value, `${node.value}`); - case Syntax.LogicalExpression: return new Value(Type.boolean, this.getSourceString(node)); @@ -643,13 +659,13 @@ export class AbstractParser { return new Value(Type.boolean, node.value, `${node.value}`); case Syntax.NumericLiteral: - return new Value(Type.number, node.value, node.extra.raw); + return new Value(Type.number, node.value, node.extra?.raw as string); case Syntax.BigIntLiteral: - return new Value(Type.bigint, node.extra.raw); + return new Value(Type.bigint, node.extra?.raw as string); case Syntax.RegExpLiteral: - return new Value(Type.regexp, node.extra.raw); + return new Value(Type.regexp, node.extra?.raw as string); case Syntax.ArrayExpression: return this.getArrayExpression(node); diff --git a/src/lib/parser/AbstractSourceParser.ts b/src/lib/parser/AbstractSourceParser.ts new file mode 100644 index 0000000..2805f57 --- /dev/null +++ b/src/lib/parser/AbstractSourceParser.ts @@ -0,0 +1,11 @@ +import { AbstractParser } from './AbstractParser.js'; +import { Vuedoc } from '../../../types/index.js'; + +import * as Babel from '@babel/types'; + +export abstract class AbstractSourceParser< + Source extends Vuedoc.Parser.Source, + Root = never +> extends AbstractParser { + abstract findComment(node: Babel.Node): string | null; +} diff --git a/lib/parser/ClassComponentComputedParser.js b/src/lib/parser/ClassComponentComputedParser.ts similarity index 60% rename from lib/parser/ClassComponentComputedParser.js rename to src/lib/parser/ClassComponentComputedParser.ts index b669d53..7763984 100644 --- a/lib/parser/ClassComponentComputedParser.js +++ b/src/lib/parser/ClassComponentComputedParser.ts @@ -1,9 +1,12 @@ import { ComputedParser } from './ComputedParser.js'; import { ComputedEntry } from '../entity/ComputedEntry.js'; -import { Syntax } from '../Enum.js'; +import { Syntax, Type } from '../Enum.js'; +import { Vuedoc } from '../../../types/index.js'; + +import * as Babel from '@babel/types'; export class ClassComponentComputedParser extends ComputedParser { - parse(node, { type = null, dependencies = [] } = {}) { + parseNode(node: Babel.ClassMethod | Babel.ClassPrivateMethod, type: Vuedoc.Parser.Type = Type.unknown, dependencies: string[] = []) { const name = node.key.type === Syntax.Identifier ? node.key.name : this.getValue(node.key).value; diff --git a/src/lib/parser/ClassComponentDataParser.ts b/src/lib/parser/ClassComponentDataParser.ts new file mode 100644 index 0000000..4373915 --- /dev/null +++ b/src/lib/parser/ClassComponentDataParser.ts @@ -0,0 +1,49 @@ +import { DataParser } from './DataParser.js'; + +import { DataEntry } from '../entity/DataEntry.js'; +import { Vuedoc } from '../../../types/index.js'; +import { Syntax } from '../Enum.js'; + +import * as Babel from '@babel/types'; + +export class ClassComponentDataParser extends DataParser { + parse(node: Babel.Node, type?: Vuedoc.Parser.Type) { + if (node.type === Syntax.ClassProperty) { + if ('name' in node.key) { + this.parseClassData(node, node.key.name, node.value, type); + } + } else if ('expression' in node && typeof node.expression === 'object') { + switch (node.expression.type) { + case Syntax.AssignmentExpression: + if ('property' in node.expression.left && 'name' in node.expression.left.property) { + this.parseClassData( + node, + node.expression.left.property.name, + node.expression.right, + type + ); + } + break; + } + } + } + + parseClassData(node: Babel.Node, name: string, value: Babel.Node, type?: Vuedoc.Parser.Type) { + const ref = this.getValue(value); + const entry = new DataEntry({ + name, + type: type || this.getTSType(node, ref.kind), + initialValue: ref.raw, + }); + + this.root.setScopeValue(entry.name, value, ref); + this.parseEntryComment(entry, node); + DataParser.mergeEntryKeywords(entry); + + if ('accessibility' in node && node.accessibility) { + entry.visibility = node.accessibility; + } + + this.emit(entry); + } +} diff --git a/lib/parser/ClassComponentParser.js b/src/lib/parser/ClassComponentParser.ts similarity index 51% rename from lib/parser/ClassComponentParser.js rename to src/lib/parser/ClassComponentParser.ts index 7275b31..b043e44 100644 --- a/lib/parser/ClassComponentParser.js +++ b/src/lib/parser/ClassComponentParser.ts @@ -1,6 +1,4 @@ -import { toKebabCase } from '@b613/utils/lib/string.js'; -// eslint-disable-next-line import/no-cycle -import { ScriptParser } from './ScriptParser.js'; +import { Options, ScriptParser } from './ScriptParser.js'; import { DataParser } from './DataParser.js'; import { PropParser } from './PropParser.js'; @@ -17,8 +15,14 @@ import { ComputedEntry } from '../entity/ComputedEntry.js'; import { Syntax, Type, Feature, Visibility, PropTypesTag } from '../Enum.js'; import { Value } from '../entity/Value.js'; +import { Vuedoc } from '../../../types/index.js'; +import type { Parser } from './Parser.js'; -function createFakeStringLiteralNode(value) { +import * as Babel from '@babel/types'; + +type ArgumentNode = Babel.Expression | Babel.SpreadElement | Babel.JSXNamespacedName | Babel.ArgumentPlaceholder; + +function createFakeStringLiteralNode(value: string): Babel.StringLiteral { return { type: Syntax.StringLiteral, start: 0, @@ -32,31 +36,39 @@ function createFakeStringLiteralNode(value) { }; } -export class ClassComponentParser extends ScriptParser { - constructor(root) { - super(root, root.ast, root.source, root.root.options); +export class ClassComponentParser extends ScriptParser { + constructor(parent: ScriptParser, root: Parser, ast: Vuedoc.Parser.AST.Result, source: Vuedoc.Parser.Script, options: Options) { + super(root, ast, source, options); + + this.root = parent; } - parse(node) { + parse(node: Babel.ClassDeclaration) { this.parseName(node); node.body.body.forEach((property) => { - if (property.static) { - return; // ignore static members - } + if ('static' in property) { + if (property.static) { + return; // ignore static members + } - switch (property.type) { - case Syntax.ClassProperty: - this.parseClassProperty(property); - break; + switch (property.type) { + case Syntax.ClassProperty: + this.parseClassProperty(property); + break; - case Syntax.ClassMethod: - this.parseClassMethod(property); - break; + case Syntax.ClassPrivateProperty: + this.parseClassProperty(property, Visibility.private); + break; - case Syntax.ClassPrivateMethod: - this.parseClassMethod(property, Visibility.private); - break; + case Syntax.ClassMethod: + this.parseClassMethod(property); + break; + + case Syntax.ClassPrivateMethod: + this.parseClassMethod(property, Visibility.private); + break; + } } }); @@ -68,22 +80,23 @@ export class ClassComponentParser extends ScriptParser { if (node.decorators) { node.decorators.forEach(({ expression }) => { if (expression.type === Syntax.CallExpression && expression.arguments.length) { - this.root.parseExportDefaultDeclaration(expression.arguments[0]); + this.parseExportDefaultDeclaration(expression.arguments[0]); } }); } } - parseClassProperty(node) { + parseClassProperty(node, defaultVisibility = Visibility.public) { const type = this.getTSType(node); const parsingResult = this.parseClassMember(type, node); if (!parsingResult && this.features.includes(Feature.data)) { - new ClassComponentDataParser(this.root).parse(node); + // TODO Handle defaultVisibility for private properties + new ClassComponentDataParser(this.root, this.emitter, this.source, this.scope).parse(node); } } - parseClassMethod(node, defaultVisibility = Visibility.public) { + parseClassMethod(node: Babel.ClassMethod | Babel.ClassPrivateMethod, defaultVisibility = Visibility.public) { switch (node.kind) { case 'constructor': this.parseContructor(node.body); @@ -91,7 +104,7 @@ export class ClassComponentParser extends ScriptParser { case 'get': if (this.features.includes(Feature.computed)) { - new ClassComponentComputedParser(this.root).parse(node); + new ClassComponentComputedParser(this.root, this.emitter, this.source, this.scope).parseNode(node); } break; @@ -100,7 +113,7 @@ export class ClassComponentParser extends ScriptParser { const ref = this.getValue(node.key); if (ref.value === Feature.data) { - new DataParser(this.root).parse(node); + new DataParser(this.root, this.emitter, this.source, this.scope).parse(node); } else { new MethodParser(this.root, { defaultVisibility }).parseMethodProperty(node, node.body); } @@ -109,55 +122,53 @@ export class ClassComponentParser extends ScriptParser { } } - parseClassMember(type, node) { + parseClassMember(type: Vuedoc.Parser.Type, node) { if (node.decorators) { - return node.decorators.some((decorator) => { - switch (decorator.expression.type) { - case Syntax.CallExpression: - switch (decorator.expression.callee.name) { - case PropTypesTag.Prop: - if (this.features.includes(Feature.props)) { - this.parsePropDecorator(node, decorator, type, node.key.name); - } + return node.decorators.some((decorator: Babel.Decorator) => { + if (decorator.expression.type === Syntax.CallExpression && 'name' in decorator.expression.callee) { + switch (decorator.expression.callee.name) { + case PropTypesTag.Prop: + if (this.features.includes(Feature.props)) { + this.parsePropDecorator(node, decorator.expression.arguments, type, node.key.name); + } - return true; + return true; - case PropTypesTag.PropSync: - this.parsePropSyncDecorator(node, decorator, type, node.key.name); + case PropTypesTag.PropSync: + this.parsePropSyncDecorator(node, decorator.expression.arguments, type, node.key.name); - return true; + return true; - case PropTypesTag.Model: - this.parseModelDecorator(node, decorator, type, node.key.name); + case PropTypesTag.Model: + this.parseModelDecorator(node, decorator.expression.arguments, type, node.key.name); - return true; + return true; - case PropTypesTag.ModelSync: - this.parseModelSyncDecorator(node, decorator, type); + case PropTypesTag.ModelSync: + this.parseModelSyncDecorator(node, decorator.expression.arguments, type); - return true; + return true; - case PropTypesTag.VModel: - this.parseModelVModelDecorator(node, decorator, type); + case PropTypesTag.VModel: + this.parseModelVModelDecorator(node, decorator.expression.arguments, type); - return true; + return true; - case PropTypesTag.Ref: - this.parseRefDecorator(node, decorator, type, node.key.name); + case PropTypesTag.Ref: + this.parseRefDecorator(node, decorator.expression.arguments, type); - return true; + return true; - case PropTypesTag.State: - this.parseStateDecorator(node, decorator, type, node.key.name); + case PropTypesTag.State: + this.parseStateDecorator(node, decorator.expression.arguments, type); - return true; + return true; - case PropTypesTag.Emit: - this.parseEmitDecorator(node, decorator, node.key.name); + case PropTypesTag.Emit: + this.parseEmitDecorator(node, decorator.expression.arguments, node.key.name); - return true; - } - break; + return true; + } } return false; @@ -193,7 +204,7 @@ export class ClassComponentParser extends ScriptParser { } if ('default' in ref.value) { - prop.defaultValue = ref.rawObject?.default || ref.raw.default; + prop.defaultValue = ref.rawObject?.default || JSON.stringify(ref.value.default); if (prop.type === Type.any) { prop.type = typeof ref.value.default; @@ -204,7 +215,7 @@ export class ClassComponentParser extends ScriptParser { } } - parsePropDecorator(node, decorator, type, name) { + parsePropDecorator(node, [argument]: ArgumentNode[], type: Vuedoc.Parser.Type, name: string) { if (this.features.includes(Feature.props)) { const prop = { type, @@ -212,24 +223,22 @@ export class ClassComponentParser extends ScriptParser { defaultValue: undefined, }; - if (decorator.expression.arguments.length) { - const argument = decorator.expression.arguments[0]; - + if (argument) { this.parsePropDecoratorArgument(argument, prop); } - const entry = new PropEntry(name, { + const entry = new PropEntry({ + name, type: prop.type, - value: prop.defaultValue, + defaultValue: prop.defaultValue, required: prop.required, }); - new PropParser(this.root).parseEntry(entry, node); + new PropParser(this.root, this.emitter, this.source, this.scope).parseEntry(entry, node, name); } } - parsePropSyncDecorator(node, decorator, type, computedName) { - const [propNameArg, propObjectArg] = decorator.expression.arguments; + parsePropSyncDecorator(node, [propNameArg, propObjectArg]: ArgumentNode[], type: Vuedoc.Parser.Type, computedName: string) { const propName = this.getValue(propNameArg); if (this.features.includes(Feature.props)) { @@ -243,13 +252,14 @@ export class ClassComponentParser extends ScriptParser { this.parsePropDecoratorArgument(propObjectArg, prop); } - const propEntry = new PropEntry(propName.value, { + const propEntry = new PropEntry({ + name: propName.value, type: prop.type || type, - value: prop.defaultValue, + defaultValue: prop.defaultValue, required: prop.required, }); - new PropParser(this.root).parseEntry(propEntry, node); + new PropParser(this.root, this.emitter, this.source, this.scope).parseEntry(propEntry, node, propName.value); } if (this.features.includes(Feature.computed)) { @@ -270,10 +280,8 @@ export class ClassComponentParser extends ScriptParser { } } - parseModelDecorator(node, decorator, type, propName) { + parseModelDecorator(node, [eventNameArg, propObjectArg]: ArgumentNode[], type: Vuedoc.Parser.Type, propName: string) { if (this.features.includes(Feature.props)) { - const [eventNameArg, propObjectArg] = decorator.expression.arguments; - const { value: eventName } = this.getValue(eventNameArg); const modelEntry = new ModelEntry(propName, eventName); @@ -289,88 +297,63 @@ export class ClassComponentParser extends ScriptParser { this.parsePropDecoratorArgument(propObjectArg, prop); } - const propEntry = new PropEntry(propName, { + const propEntry = new PropEntry({ + name: propName, type: prop.type, - value: prop.defaultValue, + defaultValue: prop.defaultValue, required: prop.required, describeModel: true, }); - new PropParser(this.root).parseEntry(propEntry, node); + new PropParser(this.root, this.emitter, this.source, this.scope).parseEntry(propEntry, node, propName); } } - parseModelSyncDecorator(node, decorator, type) { - const [propNameNode, eventNameArgNode, propObjectArgNode] = decorator.expression.arguments; + parseModelSyncDecorator(node, args: ArgumentNode[], type: Vuedoc.Parser.Type) { + const [propNameNode, eventNameArgNode, propObjectArgNode] = args; const { value: propName } = this.getValue(propNameNode); const { value: eventName } = this.getValue(eventNameArgNode); - const alteredDecorator = { - ...decorator, - expression: { - ...decorator.expression, - arguments: [eventNameArgNode, propObjectArgNode], - }, - }; - - this.parseModelDecorator(node, alteredDecorator, type, propName); + this.parseModelDecorator(node, [eventNameArgNode, propObjectArgNode], type, propName); if (this.features.includes(Feature.computed)) { - new ClassComponentComputedParser(this.root).parse(node, { type, dependencies: [propName] }); + new ClassComponentComputedParser(this.root, this.emitter, this.source, this.scope).parseNode(node, type, [propName]); } if (this.features.includes(Feature.events)) { - const eventNameKebab = toKebabCase(eventName); - const params = [ + this.setScopeValue('value', node, new Value(type, 'value')); + new EventParser(this.root, this.emitter, this.source, this.scope).parseEventNode(node, eventName, [ { type: Syntax.Identifier, start: node.start, end: node.end, range: [node.start, node.end], - leadingComments: undefined, - trailingComments: undefined, - innerComments: undefined, - extra: undefined, name: 'value', }, - ]; - - this.scope.value = new Value(type, 'value'); - - new EventParser(this.root, this.scope) - .parseEventNode(node, eventNameKebab, params); + ]); } } - parseModelVModelDecorator(node, decorator, type) { - const [propObjectArgNode] = decorator.expression.arguments; + parseModelVModelDecorator(node, [propObjectArgNode]: ArgumentNode[], type: Vuedoc.Parser.Type) { const propNameNode = createFakeStringLiteralNode('value'); const eventNameArgNode = createFakeStringLiteralNode('input'); - const alteredDecorator = { - ...decorator, - expression: { - ...decorator.expression, - arguments: [propNameNode, eventNameArgNode, propObjectArgNode], - }, - }; - - this.parseModelSyncDecorator(node, alteredDecorator, type); + this.parseModelSyncDecorator(node, [propNameNode, eventNameArgNode, propObjectArgNode], type); } - parseRefDecorator(node, decorator, type) { + parseRefDecorator(node, args: ArgumentNode[], type: Vuedoc.Parser.Type) { if (this.features.includes(Feature.data)) { - new ClassComponentDataParser(this.root).parse(node, { type }); + new ClassComponentDataParser(this.root, this.emitter, this.source, this.scope).parse(node, type); } } - parseStateDecorator(node, decorator, type) { + parseStateDecorator(node, args: ArgumentNode[], type: Vuedoc.Parser.Type) { if (this.features.includes(Feature.data)) { - new ClassComponentDataParser(this.root).parse(node, { type }); + new ClassComponentDataParser(this.root, this.emitter, this.source, this.scope).parse(node, type); } } - parseEmitDecorator(node, decorator, methodName) { + parseEmitDecorator(node, [eventNameArg]: ArgumentNode[], methodName: string) { const { params } = node; if (this.features.includes(Feature.methods)) { @@ -380,15 +363,12 @@ export class ClassComponentParser extends ScriptParser { } if (this.features.includes(Feature.events)) { - const [eventNameArg] = decorator.expression.arguments; const eventName = eventNameArg ? this.getValue(eventNameArg).value : methodName; - const eventNameKebab = toKebabCase(eventName); - - new EventParser(this.root, this.scope) - .parseEventNode(node, eventNameKebab, params); + new EventParser(this.root, this.emitter, this.source, this.scope) + .parseEventNode(node, eventName, params); } } @@ -396,7 +376,7 @@ export class ClassComponentParser extends ScriptParser { body.forEach((node) => { switch (node.expression.type) { case Syntax.CallExpression: - new EventParser(this.root).parse(node); + new EventParser(this.root, this.emitter, this.source, this.scope).parse(node); break; case Syntax.AssignmentExpression: @@ -420,13 +400,8 @@ export class ClassComponentParser extends ScriptParser { } parseAssignmentMemberExpression(node, parent = node) { - switch (node.left.object.type) { - case Syntax.ThisExpression: - new ClassComponentDataParser(this.root).parse(parent); - break; - - default: - super.parseAssignmentMemberExpression(node); + if (node.left.object.type === Syntax.ThisExpression) { + new ClassComponentDataParser(this.root, this.emitter, this.source, this.scope).parse(parent); } } } diff --git a/lib/parser/CommentParser.js b/src/lib/parser/CommentParser.ts similarity index 84% rename from lib/parser/CommentParser.js rename to src/lib/parser/CommentParser.ts index 398b8e2..1070d38 100644 --- a/lib/parser/CommentParser.js +++ b/src/lib/parser/CommentParser.ts @@ -1,3 +1,4 @@ +import { Vuedoc } from '../../../types/index.js'; import { Keyword } from '../entity/Keyword.js'; import { Visibility, Visibilities } from '../Enum.js'; @@ -5,9 +6,9 @@ const RE_KEYWORDS = /^\s*@\**\s*([a-z0-9_.$-]+)(\s+(-\s+)?(.+))?/i; const RE_VISIBILITY = new RegExp(Visibilities.join('|')); export const CommentParser = { - parse(text, defaultVisibility = Visibility.public) { + parse(text: string, defaultVisibility = Visibility.public) { const result = { - keywords: [], + keywords: [] as Vuedoc.Entry.Keyword[], visibility: defaultVisibility, description: '', }; @@ -19,7 +20,7 @@ export const CommentParser = { .replace(/\s*\**\/$/, '') .replace(/^\s*\* /, '')); - let keyword = null; + let keyword: Keyword; lines.forEach((line) => { const matches = RE_KEYWORDS.exec(line); @@ -50,15 +51,15 @@ export const CommentParser = { result.keywords.push(keyword); }); - if (keyword === null) { - result.description = lines.join('\n').trim(); - } else { + if (keyword) { result.description = result.description.trim(); keyword.description = keyword.description.trim(); if (!keyword.description) { delete keyword.description; } + } else { + result.description = lines.join('\n').trim(); } if (!result.description) { @@ -70,7 +71,7 @@ export const CommentParser = { return result; }, - getVisibility(keywords, defaultValue = Visibility.public) { + getVisibility(keywords: Keyword[], defaultValue = Visibility.public): Vuedoc.Parser.Visibility { const index = keywords.findIndex((keyword) => RE_VISIBILITY.test(keyword.name)); if (index > -1) { @@ -78,13 +79,13 @@ export const CommentParser = { keywords.splice(index, 1); - return keywordVisibility.name; + return keywordVisibility.name as Vuedoc.Parser.Visibility; } return defaultValue; }, - format(comment) { + format(comment: string) { const parsedComment = comment.trim(); if (parsedComment.startsWith('') && parsedLines.length) { @@ -90,9 +83,9 @@ export class MarkupTemplateParser extends AbstractParser { parseEvents(node) { Object.values(node.attrsMap) - .map((value) => EVENT_EMIT_RE.exec(value)) + .map((value: any) => EVENT_EMIT_RE.exec(value)) .filter((result) => result !== null) - .map((matches) => matches[1]) + .map((matches: any) => matches[1]) .forEach((name) => { const entry = new EventEntry(name); const comment = this.parseEntryComment(entry, node); @@ -131,6 +124,20 @@ export class MarkupTemplateParser extends AbstractParser { return; } - compile(this.source.content, this.options); + compile(this.source.content, { + outputSourceRange: true, + modules: [ + { + preTransformNode: (node: ASTElement) => { + this.parseNode(node); + + return node; + }, + transformNode: (node: ASTElement) => node, + postTransformNode: (node: ASTElement) => node, + genData: (node: ASTElement) => '', + }, + ], + }); } } diff --git a/lib/parser/MethodParser.js b/src/lib/parser/MethodParser.ts similarity index 88% rename from lib/parser/MethodParser.js rename to src/lib/parser/MethodParser.ts index b0a4352..3f434f5 100644 --- a/lib/parser/MethodParser.js +++ b/src/lib/parser/MethodParser.ts @@ -1,16 +1,19 @@ import { JSDoc } from '../JSDoc.js'; import { KeywordsUtils } from '../utils/KeywordsUtils.js'; +import { Vuedoc } from '../../../types/index.js'; import { AbstractExpressionParser } from './AbstractExpressionParser.js'; +import { ScriptParser } from './ScriptParser.js'; import { EventParser } from './EventParser.js'; import { MethodEntry, MethodParam, MethodParamGenerator } from '../entity/MethodEntry.js'; import { Syntax, Feature, Type, Visibility, Tag, LegacyHooks } from '../Enum.js'; import { Value } from '../entity/Value.js'; +import * as Babel from '@babel/types'; const hasReturnStatementCallback = (node) => node.type === Syntax.ReturnStatement; -function getBlockStatement(node) { +function getBlockStatement(node): any { let block = null; switch (node.type) { @@ -43,13 +46,15 @@ function parseMethodName(entry) { } export class MethodParser extends AbstractExpressionParser { - constructor(root, { defaultVisibility = Visibility.public } = {}) { - super(root); + defaultVisibility: Vuedoc.Parser.Visibility; + + constructor(root: ScriptParser, { defaultVisibility = Visibility.public } = {}) { + super(root, root.emitter, root.source, root.scope); this.defaultVisibility = defaultVisibility; } - static parseEntrySyntax(entry, node, { syntaxPrefix = '' } = {}) { + static parseEntrySyntax(entry: Vuedoc.Entry.PropFunction, node, { syntaxPrefix = '' } = {}) { KeywordsUtils.mergeEntryKeyword(entry, Tag.syntax, Type.array); if (entry.syntax.length === 0) { @@ -118,7 +123,7 @@ export class MethodParser extends AbstractExpressionParser { return param; } - parseObjectExpressionProperty(property) { + parseObjectExpressionProperty(property: Babel.ObjectProperty | Babel.ObjectMethod) { switch (property.type) { case Syntax.ObjectProperty: this.parseMethodProperty(property, property.value); @@ -131,10 +136,12 @@ export class MethodParser extends AbstractExpressionParser { } parseMethodProperty(node, property, nodeComment = node, { parseEvents = true, hooks = LegacyHooks } = {}) { + this.scope = { ...this.root.scope }; + if (this.root.features.includes(Feature.methods)) { const name = this.parseKey(node); - if (!hooks.includes(name)) { // ignore hooks + if (!hooks.includes(name as any)) { // ignore hooks const paramsNode = property.params ? property : node; const paramsList = paramsNode.params ? paramsNode.params.map((item) => this.getParam(item)) : []; const params = paramsList.filter(({ name }) => name); @@ -159,7 +166,7 @@ export class MethodParser extends AbstractExpressionParser { } if (parseEvents && this.features.includes(Feature.events)) { - new EventParser(this.root, this.scope).parse(property); + new EventParser(this.root, this.emitter, this.source, this.scope).parse(property); } } diff --git a/lib/parser/ModelParser.js b/src/lib/parser/ModelParser.ts similarity index 78% rename from lib/parser/ModelParser.js rename to src/lib/parser/ModelParser.ts index 9abe89f..fdcec96 100644 --- a/lib/parser/ModelParser.js +++ b/src/lib/parser/ModelParser.ts @@ -1,12 +1,15 @@ import { AbstractExpressionParser } from './AbstractExpressionParser.js'; import { ModelEntry } from '../entity/ModelEntry.js'; import { UndefinedValue } from '../entity/Value.js'; +import * as Babel from '@babel/types'; export class ModelParser extends AbstractExpressionParser { - parseObjectProperty(node) { + parseObjectProperty(node: Babel.ObjectProperty) { const { value: { prop, event } } = this.getValue(node.value); const entry = new ModelEntry(prop, event); + this.root.defaultModelPropName = prop; + this.root.setScopeValue(entry.prop, prop, UndefinedValue); this.parseEntryComment(entry, node); this.emit(entry); diff --git a/lib/parser/NameParser.js b/src/lib/parser/NameParser.ts similarity index 64% rename from lib/parser/NameParser.js rename to src/lib/parser/NameParser.ts index d757825..905ad28 100644 --- a/lib/parser/NameParser.js +++ b/src/lib/parser/NameParser.ts @@ -2,9 +2,11 @@ import { AbstractLiteralParser } from './AbstractLiteralParser.js'; import { NameEntry } from '../entity/NameEntry.js'; import { UndefinedValue } from '../entity/Value.js'; import { Type } from '../Enum.js'; +import { Vuedoc } from '../../../types/index.js'; +import * as Babel from '@babel/types'; -export class NameParser extends AbstractLiteralParser { - parseObjectProperty(node) { +export class NameParser extends AbstractLiteralParser { + parseObjectProperty(node: Babel.ObjectProperty) { const ref = this.getValue(node.value); const name = ref.type === Type.string ? ref.value : UndefinedValue.value; const entry = new NameEntry(name); diff --git a/src/lib/parser/Parser.ts b/src/lib/parser/Parser.ts new file mode 100644 index 0000000..6c7c62f --- /dev/null +++ b/src/lib/parser/Parser.ts @@ -0,0 +1,148 @@ +import path from 'path'; +import EventEmitter from 'events'; + +import { Options, parseAst, ScriptParser } from './ScriptParser.js'; +import { MarkupTemplateParser } from './MarkupTemplateParser.js'; +import { CompositionParser } from './CompositionParser.js'; + +import { NameEntry } from '../entity/NameEntry.js'; +import { Feature, Features, DEFAULT_IGNORED_VISIBILITIES, CompositionAPIValues, FeatureEvent, TypedocTag, JSDocTag } from '../Enum.js'; +import { Vuedoc } from '../../../types/index.js'; + +type AsyncOperation = () => Promise; + +const IGNORED_KEYWORDS = [TypedocTag.hidden, JSDocTag.ignore]; + +export class Parser extends EventEmitter { + options: Vuedoc.Parser.ResolvedOptions; + features: Vuedoc.Parser.Feature[]; + scope: Vuedoc.Parser.Scope; + ignoredVisibilities: Vuedoc.Parser.Visibility[]; + eventsEmmited: string[]; + composition: readonly ('computed' | 'ref' | '$ref' | 'unref' | 'reactive' | '$computed' | 'readonly' | 'shallowRef' | '$shallowRef' | 'shallowReactive' | 'shallowReadonly' | 'triggerRef' | 'toRaw' | 'markRaw' | 'toRef' | '$toRef')[]; + protected asyncOperations: Promise[]; + static SUPPORTED_FEATURES: readonly Vuedoc.Parser.Feature[] = Features; + + constructor(options: Vuedoc.Parser.ResolvedOptions) { + super(); + + this.options = options; + this.features = options.features || Features; + this.scope = {}; + this.ignoredVisibilities = options.ignoredVisibilities || DEFAULT_IGNORED_VISIBILITIES; + this.eventsEmmited = []; + this.asyncOperations = []; + // FIXME Check options.composition + this.composition = options.composition + ? CompositionAPIValues.concat(...Object.values(options.composition).flat() as any) + : CompositionAPIValues; + } + + static validateOptions(options) { + if (!options.source) { + throw new Error('options.source is required'); + } + + if (options.features) { + if (!Array.isArray(options.features)) { + throw new TypeError('options.features must be an array'); + } + + options.features.forEach((feature) => { + if (!Features.includes(feature)) { + throw new Error(`Unknow '${feature}' feature. Supported features: ${JSON.stringify(Features)}`); + } + }); + } + } + + emitEntry(entry: Vuedoc.Entry.Type) { + // FIXME Fix Parser.options.hooks.beforeEntryEmit(entry); + // if (this.options.hooks?.beforeEntryEmit && entry) { + // this.options.hooks.beforeEntryEmit(entry); + // } + + if ('visibility' in entry && this.isIgnoredVisibility(entry.visibility)) { + return; + } + + if ('keywords' in entry && entry.keywords.length) { + const contentsAnIgnoredKeyword = entry.keywords.some(({ name }) => IGNORED_KEYWORDS.includes(name)); + + if (contentsAnIgnoredKeyword) { + return; + } + } + + this.emit(entry.kind, entry); + } + + protected isIgnoredVisibility(visibility: Vuedoc.Parser.Visibility) { + return this.ignoredVisibilities.includes(visibility); + } + + execAsync(fn: AsyncOperation) { + this.asyncOperations.push(fn()); + } + + walk() { + Parser.validateOptions(this.options); + + let hasNameEntry = false; + + if (this.features.includes(Feature.name)) { + this.on(Feature.name, () => { + hasNameEntry = true; + }); + } + + if (this.options.source.script?.content) { + const options: Options = { + jsx: this.options.jsx || false, + composition: this.options.composition, + }; + + try { + const ast = parseAst(this.options.source.script.content, options); + + if (!this.options.source.script.attrs.setup) { + this.options.source.script.attrs.setup = CompositionParser.isCompositionScript(ast); + } + + const parser = this.options.source.script.attrs.setup + ? new CompositionParser(this, ast, this.options.source.script, options) + : new ScriptParser(this, ast, this.options.source.script, options); + + parser.parse(); + + this.scope = parser.scope; + } catch (err) { + this.emit('error', err); + } + } + + if (this.options.source.template?.content) { + new MarkupTemplateParser(this, this.options.source.template).parse(); + } + + if (!hasNameEntry) { + if (this.features.includes(Feature.name)) { + this.parseComponentName(); + } + } + + setTimeout(async () => { + await Promise.all(this.asyncOperations); + this.emit('end'); + }, 0); + } + + parseComponentName() { + if (this.options.filename) { + const name = path.parse(this.options.filename).name; + const entry = new NameEntry(name); + + this.emitEntry(entry); + } + } +} diff --git a/lib/parser/PropParser.js b/src/lib/parser/PropParser.ts similarity index 85% rename from lib/parser/PropParser.js rename to src/lib/parser/PropParser.ts index 11e280f..9d265a7 100644 --- a/lib/parser/PropParser.js +++ b/src/lib/parser/PropParser.ts @@ -1,4 +1,3 @@ -import { toKebabCase } from '@b613/utils/lib/string.js'; import { AbstractExpressionParser } from './AbstractExpressionParser.js'; import { Value, UndefinedValue } from '../entity/Value.js'; import { PropEntry } from '../entity/PropEntry.js'; @@ -7,8 +6,9 @@ import { JSDoc } from '../JSDoc.js'; import { Syntax, Type, Tag } from '../Enum.js'; import { KeywordsUtils } from '../utils/KeywordsUtils.js'; -import { MethodReturns, MethodParamGenerator } from '../entity/MethodEntry.js'; +import { MethodReturns, MethodParamGenerator, MethodEntry } from '../entity/MethodEntry.js'; import { MethodParser } from './MethodParser.js'; +import * as Babel from '@babel/types'; const PROP_TYPES_OBJECT_NAME = 'PropTypes'; const PROP_TYPES_ONE_OF = 'oneOf'; @@ -37,9 +37,10 @@ function parsePropTypeMemberProperty(property) { export class PropParser extends AbstractExpressionParser { static parseEntryCommentType(entry) { const items = KeywordsUtils.extract(entry.keywords, Tag.type); + const item = items.pop(); - if (items.length) { - entry.type = JSDoc.parseType(items.pop().description); + if (item && item.description) { + entry.type = JSDoc.parseType(item.description); } } @@ -51,7 +52,7 @@ export class PropParser extends AbstractExpressionParser { } } - parseTSTypeParameterInstantiation(node) { + parseTSTypeParameterInstantiation(node: Babel.TSTypeParameterInstantiation) { const param = node.params[0]; switch (param?.type) { @@ -60,10 +61,12 @@ export class PropParser extends AbstractExpressionParser { break; case Syntax.TSTypeReference: - if (param.typeName.name in this.root.types) { - const type = this.root.types[param.typeName.name]; + if ('name' in param.typeName && param.typeName.name in this.scope) { + const node = this.scope[param.typeName.name].node; - this.parseTSTypeReference(type); + if (node) { + this.parseTSTypeReference(node.type); + } } break; } @@ -105,24 +108,24 @@ export class PropParser extends AbstractExpressionParser { } parseClassMember(member) { - const name = toKebabCase(member.key.name); const type = this.getTSType(member); - const raw = this.root.scope[member.key.name]; - const value = raw?.type === type - ? raw - : this.getPropValueNode(this.root.nodes[member.key.name]?.node?.value, type); + const variable = this.scope[member.key.name]; + const value = variable?.value.type === type + ? variable.value + : this.getPropValueNode(variable?.node?.value, type); - const entry = new PropEntry(name, { + const entry = new PropEntry({ type, + name: member.key.name, required: !member.optional, - value: value?.raw, + defaultValue: value?.raw, }); if (value) { value.member = true; } - if (value === raw) { + if (value === variable?.value) { this.root.setScopeValue(member.key.name, member, value); } @@ -143,7 +146,7 @@ export class PropParser extends AbstractExpressionParser { } } - parseEntryComment(entry, property, camelName) { + parsePropEntryComment(entry: PropEntry, property, camelName: string) { super.parseEntryComment(entry, property); const kinds = KeywordsUtils.extract(entry.keywords, Tag.kind, true); @@ -151,16 +154,20 @@ export class PropParser extends AbstractExpressionParser { if (kinds.length) { const kind = kinds.pop(); - if (kind.description === 'function') { + if (kind?.description === 'function') { entry.function = { - name: camelName, params: [], syntax: [], - keywords: entry.keywords, + name: camelName, description: entry.description, + keywords: entry.keywords, returns: new MethodReturns(Type.unknown), }; + entry.function.keywords = entry.keywords; + entry.function.description = entry.description; + entry.function.returns.type = Type.unknown; + JSDoc.parseParams(this, entry.keywords, entry.function.params, MethodParamGenerator); JSDoc.parseReturns(entry.keywords, entry.function.returns); MethodParser.parseEntrySyntax(entry.function, property, { @@ -174,20 +181,19 @@ export class PropParser extends AbstractExpressionParser { KeywordsUtils.parseCommonEntryTags(entry); } - parseEntry(entry, node, camelName) { - if (this.source.attrs.setup) { - entry.describeModel = camelName === 'modelValue'; + parseEntry(entry: PropEntry, node, camelName: string) { + if (entry.describeModel === false) { + entry.describeModel = camelName === this.root.defaultModelPropName; } - this.parseEntryComment(entry, node, camelName); + this.parsePropEntryComment(entry, node, camelName); KeywordsUtils.mergeEntryKeyword(entry, Tag.type); this.emit(entry); } parseArrayExpression(node) { node.elements.forEach((item) => { - const name = toKebabCase(item.value); - const entry = new PropEntry(name, { required: true }); + const entry = new PropEntry({ name: item.value, required: true }); this.root.setScopeValue(entry.name, item, UndefinedValue); this.parseEntry(entry, item, item.value); @@ -204,9 +210,10 @@ export class PropParser extends AbstractExpressionParser { const name = this.parseKey(property); const type = this.getPropType(property.value); const value = this.getPropValue(property.value, type); - const entry = new PropEntry(name, { + const entry = new PropEntry({ + name, type: type.value, - value: value?.raw, + defaultValue: value?.raw, required: type.required, }); @@ -369,7 +376,7 @@ export class PropParser extends AbstractExpressionParser { } else if (nodeValue.body) { value = this.getValue(nodeValue.body); } - } else { + } else if (nodeValue.type !== Syntax.CallExpression) { value = this.getValue(nodeValue); } } @@ -381,7 +388,7 @@ export class PropParser extends AbstractExpressionParser { return this.getFunctionExpressionStringValue(node); } - getFunctionExpressionStringValue(node) { + getFunctionExpressionStringValue(node: Babel.ArrowFunctionExpression | Babel.FunctionExpression) { const declaration = node.type === Syntax.ArrowFunctionExpression ? this.getInlineSourceString(node) : 'function() ' + this.getSourceString(node.body); diff --git a/lib/parser/ScriptParser.js b/src/lib/parser/ScriptParser.ts similarity index 68% rename from lib/parser/ScriptParser.js rename to src/lib/parser/ScriptParser.ts index 63ba2f8..0f87d11 100644 --- a/lib/parser/ScriptParser.js +++ b/src/lib/parser/ScriptParser.ts @@ -1,6 +1,6 @@ -import { parse as BabelParser } from '@babel/parser'; +import { parse as BabelParser, ParseResult, ParserPlugin } from '@babel/parser'; -import { AbstractParser } from './AbstractParser.js'; +import { AbstractSourceParser } from './AbstractSourceParser.js'; import { SlotParser } from './SlotParser.js'; import { NameParser } from './NameParser.js'; import { ModelParser } from './ModelParser.js'; @@ -16,10 +16,22 @@ import { InlineTemplateParser } from './InlineTemplateParser.js'; import { NameEntry } from '../entity/NameEntry.js'; import { DescriptionEntry } from '../entity/DescriptionEntry.js'; import { KeywordsEntry } from '../entity/KeywordsEntry.js'; +import { UndefinedValue } from '../entity/Value.js'; import { Syntax, Properties, Feature, Tag, PropTypesTag, CompositionAPIValues } from '../Enum.js'; import { KeywordsUtils } from '../utils/KeywordsUtils.js'; import { JSXParser } from './JSXParser.js'; +import type { Parser } from './Parser.js'; +import { Vuedoc } from '../../../types/index.js'; + +import * as Babel from '@babel/types'; + +import { + TSTypeAliasDeclaration, + TSInterfaceDeclaration, + TSEnumDeclaration, + ImportDeclaration +} from '@babel/types'; const EXCLUDED_KEYWORDS = [Tag.name, Tag.slot, Tag.mixin]; @@ -28,7 +40,7 @@ const ALLOWED_EXPORTED_PROPERTIES = [ { object: 'exports', property: 'default' }, ]; -const BABEL_DEFAULT_PLUGINS = [ +const BABEL_DEFAULT_PLUGINS: ParserPlugin[] = [ 'asyncGenerators', 'bigInt', 'classPrivateMethods', @@ -59,7 +71,7 @@ const BABEL_DEFAULT_PLUGINS = [ 'typescript', ]; -export function parseAst(content, options) { +export function parseAst(content: string, options: Options) { return BabelParser(content, { allowImportExportEverywhere: true, allowAwaitOutsideFunction: true, @@ -78,28 +90,35 @@ export function parseAst(content, options) { }); } -export class ScriptParser extends AbstractParser { - /** - * @param {Parser} parser - The Parser object - */ - constructor(root, ast, data, options = {}) { - super(root); +export type Options = { + jsx: boolean; + composition: Vuedoc.Parser.ParsingComposition; +}; + +export class ScriptParser extends AbstractSourceParser { + source: Vuedoc.Parser.Script; + options: Options; + ast: Vuedoc.Parser.AST.Result; + ignoredMothods: never[]; + features: Vuedoc.Parser.Feature[]; + scope: Vuedoc.Parser.Scope; + defaultModelPropName = 'value'; + + constructor(emitter: Parser, ast: Vuedoc.Parser.AST.Result, source: Vuedoc.Parser.Script, options: Options) { + super(null as never, emitter, source, emitter.scope); - this.source = data; + this.source = source; this.options = options; this.ast = ast; - this.types = {}; - this.eventsEmmited = {}; this.ignoredMothods = []; - this.effectScopes = []; } - findComment({ trailingComments = [], leadingComments = trailingComments, ...node }) { - const leadingComment = leadingComments.reverse()[0]; + findComment({ trailingComments = [], leadingComments = trailingComments, ...node }: Babel.Node) { + const leadingComment = leadingComments?.reverse()[0]; if (leadingComment) { - return leadingComment.start > node.end ? null : leadingComment.value; + return leadingComment.start && node.end && leadingComment.start > node.end ? null : leadingComment.value; } return null; @@ -117,11 +136,11 @@ export class ScriptParser extends AbstractParser { return false; } - parse() { + parse(node: ParseNode) { this.parseAst(this.ast.program); } - parseCommentNode(node) { + parseCommentNode(node: Babel.Node) { const comment = this.findComment(node); if (comment) { @@ -131,8 +150,8 @@ export class ScriptParser extends AbstractParser { } } - parseComment(description, keywords) { - if (this.root.features.includes(Feature.name)) { + parseComment(description: string, keywords: Vuedoc.Entry.Keyword[]) { + if (this.features.includes(Feature.name)) { if (keywords.length) { const nameKeywords = KeywordsUtils.extract(keywords, Tag.name, true); const nameKeyword = nameKeywords.pop(); @@ -143,20 +162,16 @@ export class ScriptParser extends AbstractParser { } } - if (this.root.features.includes(Feature.description)) { - const entry = { description, keywords }; - - if (entry.description) { - this.emit(new DescriptionEntry(entry.description)); - } + if (this.features.includes(Feature.description) && description) { + this.emit(new DescriptionEntry(description)); } if (keywords.length) { - if (this.root.features.includes(Feature.slots)) { + if (this.features.includes(Feature.slots)) { SlotParser.extractSlotKeywords(keywords).forEach((slot) => this.emit(slot)); } - if (this.root.features.includes(Feature.keywords)) { + if (this.features.includes(Feature.keywords)) { this.emit(new KeywordsEntry(keywords.filter(({ name }) => !EXCLUDED_KEYWORDS.includes(name)))); } } @@ -167,27 +182,25 @@ export class ScriptParser extends AbstractParser { this.parseCommentNode(node); } - parseAst(node) { - node.body.forEach((item) => this.parseAstItem(item)); + parseAst(node: Vuedoc.Parser.AST.Program) { + node.body.forEach((item) => this.parseAstStatement(item)); } - parseAstItem(item) { + parseAstStatement(item: Vuedoc.Parser.AST.Statement) { switch (item.type) { case Syntax.ClassDeclaration: this.setScopeValue(item.id.name, item, this.getValue(item)); - - this.types[item.id.name] = item; break; case Syntax.VariableDeclaration: - this.parseVariableDeclaration(item); + this.parseVariableDeclaration(item as any); break; case Syntax.ExportNamedDeclaration: if (this.hasExplicitMixinKeyword(item)) { this.parseExplicitMixinDeclaration(item); - } else { - this.parseAstItem(item.declaration); + } else if (item.declaration) { + this.parseAstStatement(item.declaration); } break; @@ -221,27 +234,26 @@ export class ScriptParser extends AbstractParser { } } - parseTSTypeAliasDeclaration(item) { - this.types[item.id.name] = item.typeAnnotation; + parseTSTypeAliasDeclaration(item: TSTypeAliasDeclaration) { + this.setScopeValue(item.id.name, item.typeAnnotation, UndefinedValue); } - parseTSInterfaceDeclaration(item) { - this.types[item.id.name] = item.body; + parseTSInterfaceDeclaration(item: TSInterfaceDeclaration) { + this.setScopeValue(item.id.name, item.body, UndefinedValue); } - parseTSEnumDeclaration(item) { - this.types[item.id.name] = item; + parseTSEnumDeclaration(item: TSEnumDeclaration) { + this.setScopeValue(item.id.name, item, UndefinedValue); } - parseImportDeclaration(item) { - if (this.source.attrs.setup) { - if (item.importKind === 'value' && item.source.value === 'vue') { - item.specifiers - .filter(({ imported }) => CompositionAPIValues.includes(imported.name)) - .forEach(() => {}); - // console.log('>>>', item.specifiers) - } - } + parseImportDeclaration(item: ImportDeclaration) { + // TODO Handle ImportDeclaration + // if (item.importKind === 'value' && item.source.value === 'vue') { + // item.specifiers + // .filter(({ imported }) => CompositionAPIValues.includes(imported.name)) + // .forEach(() => {}); + // // console.log('>>>', item.specifiers) + // } } parseExplicitMixinDeclaration(node) { @@ -351,7 +363,7 @@ export class ScriptParser extends AbstractParser { .forEach((property) => this.parseFeature(property)); } - parseExportDefaultDeclaration(node) { + parseExportDefaultDeclaration(node: Babel.Node) { switch (node.type) { case Syntax.ObjectExpression: this.parseObjectExpression(node); @@ -372,13 +384,13 @@ export class ScriptParser extends AbstractParser { break; case Syntax.Identifier: - if (node.name in this.nodes) { - this.parseExportDefaultDeclaration(this.nodes[node.name].node); + if (node.name in this.scope && this.scope[node.name].node) { + this.parseExportDefaultDeclaration(this.scope[node.name].node.value); } break; case Syntax.NewExpression: - if (node.callee.name === 'Vue') { + if ('name' in node.callee && node.callee.name === 'Vue') { // Vue Instance if (node.arguments.length) { this.parseExportDefaultDeclaration(node.arguments[0]); @@ -394,33 +406,31 @@ export class ScriptParser extends AbstractParser { } } - async parseBaseClassComponent(node) { - // eslint-disable-next-line import/no-cycle - const { ClassComponentParser } = await import('./ClassComponentParser.js'); + parseBaseClassComponent(node: Babel.ClassDeclaration) { + this.emitter.execAsync(async () => { + const { ClassComponentParser } = await import('./ClassComponentParser.js'); - new ClassComponentParser(this).parse(node); + new ClassComponentParser(this, this.emitter, this.ast, this.source, this.options).parse(node); + }); } - parseDecorator(node) { - switch (node.type) { + parseDecorator(node: Babel.Decorator) { + switch (node.expression.type) { case Syntax.CallExpression: { - switch (node.callee.name) { - case PropTypesTag.Component: - if (node.arguments.length) { - this.parseExportDefaultDeclaration(node.arguments[0]); - } - break; + if ('name' in node.expression.callee && node.expression.callee.name === PropTypesTag.Component) { + if (node.expression.arguments.length) { + this.parseExportDefaultDeclaration(node.expression.arguments[0]); + } } - break; } } } - parseClassComponent(node) { + parseClassComponent(node: Babel.ClassDeclaration) { this.parseBaseClassComponent(node); if (node.decorators) { - node.decorators.forEach((node) => this.parseDecorator(node.expression)); + node.decorators.forEach((node) => this.parseDecorator(node)); } } @@ -428,33 +438,33 @@ export class ScriptParser extends AbstractParser { switch (property.key.name) { case Properties.name: if (this.features.includes(Feature.name)) { - new NameParser(this).parse(property); + new NameParser(this, this.emitter, this.source, this.scope).parse(property); } break; case Properties.inheritAttrs: - new InheritAttrsParser(this).parse(property); + new InheritAttrsParser(this, this.emitter, this.source, this.scope).parse(property); break; case Properties.model: if (this.features.includes(Feature.props)) { - new ModelParser(this).parse(property); + new ModelParser(this, this.emitter, this.source, this.scope).parse(property); } break; case Properties.data: - new DataParser(this).parse(property); + new DataParser(this, this.emitter, this.source, this.scope).parse(property); break; case Properties.props: if (this.features.includes(Feature.props)) { - new PropParser(this).parse(property); + new PropParser(this, this.emitter, this.source, this.scope).parse(property); } break; case Properties.computed: if (this.features.includes(Feature.computed)) { - new ComputedParser(this).parse(property); + new ComputedParser(this, this.emitter, this.source, this.scope).parse(property); } break; @@ -462,12 +472,12 @@ export class ScriptParser extends AbstractParser { if (this.features.includes(Feature.events)) { property.value.properties .filter((property) => ('value' in property && ScriptParser.isFunction(property.value)) || ScriptParser.isFunction(property)) - .forEach((watcher) => new EventParser(this).parse(watcher)); + .forEach((watcher) => new EventParser(this, this.emitter, this.source, this.scope).parse(watcher)); } break; case Properties.template: - new InlineTemplateParser(this).parse(property); + new InlineTemplateParser(this, this.emitter, this.source, this.scope).parse(property); break; case Properties.methods: @@ -475,12 +485,12 @@ export class ScriptParser extends AbstractParser { break; case Properties.render: - new JSXParser(this).parse(property); + new JSXParser(this, this.emitter, this.source, this.scope).parse(property); break; default: if (this.features.includes(Feature.events)) { - new EventParser(this).parse(property); + new EventParser(this, this.emitter, this.source, this.scope).parse(property); } break; } diff --git a/lib/parser/SetupParser.js b/src/lib/parser/SetupParser.ts similarity index 63% rename from lib/parser/SetupParser.js rename to src/lib/parser/SetupParser.ts index f2df49a..f91e578 100644 --- a/lib/parser/SetupParser.js +++ b/src/lib/parser/SetupParser.ts @@ -1,11 +1,12 @@ -import { DataParser } from './DataParser.js'; +import { DataParser, ParseDataValueOptions } from './DataParser.js'; import { ComputedParser } from './ComputedParser.js'; import { CompositionHooks, Feature, Syntax, Type } from '../Enum.js'; import { MethodParser } from './MethodParser.js'; +import * as Babel from '@babel/types'; export class SetupParser extends DataParser { - parseDataValue({ name, value, nodeTyping, nodeComment }) { - if (this.compositionComputedKeys.includes(value.$kind)) { + parseDataValue({ name, value, nodeTyping, nodeComment }: ParseDataValueOptions) { + if (this.compositionComputedKeys.includes(value.$kind as any)) { this.parseDataValueComputed({ name, value, nodeTyping, nodeComment }); } else if (value.type === Type.function) { this.parseDataValueMethod({ name, value, nodeTyping, nodeComment }); @@ -14,20 +15,20 @@ export class SetupParser extends DataParser { } } - parseDataValueComputed({ name, nodeTyping, nodeComment }) { + parseDataValueComputed({ name, nodeTyping, nodeComment }: ParseDataValueOptions) { if (this.features.includes(Feature.computed)) { - new ComputedParser(this.root).parseComputedValue({ + new ComputedParser(this.root, this.emitter, this.source, this.scope).parseComputedValue({ name, - node: this.nodes[name].node, + node: this.scope[name].node?.value, nodeTyping, nodeComment, }); } } - parseDataValueMethod({ name, nodeTyping, nodeComment }) { + parseDataValueMethod({ name, nodeTyping, nodeComment }: ParseDataValueOptions) { if (this.features.includes(Feature.methods)) { - const { node = nodeTyping, nodeComment: nComment = nodeComment } = this.nodes[name] || {}; + const { value: node = nodeTyping, comment: nComment = nodeComment } = this.scope[name]?.node || {}; if (!node.key) { node.key = node.id || nodeComment.key || nodeTyping.id; @@ -35,7 +36,8 @@ export class SetupParser extends DataParser { new MethodParser(this.root).parseMethodProperty(node, node, nComment, { parseEvents: false, - hooks: CompositionHooks, + // FIXME Handle hooks: CompositionHooks, + // hooks: CompositionHooks, }); } } @@ -47,9 +49,9 @@ export class SetupParser extends DataParser { if (exposedValue.type === Type.object) { Object.keys(exposedValue.value).forEach((name) => this.parseDataValue({ name, - value: this.scope[name], - nodeComment: this.nodes[name].nodeComment, - nodeTyping: this.nodes[name].nodeTyping, + value: this.scope[name].value, + nodeComment: this.scope[name].node?.comment, + nodeTyping: this.scope[name].node?.type, })); } } diff --git a/lib/parser/SlotParser.js b/src/lib/parser/SlotParser.ts similarity index 80% rename from lib/parser/SlotParser.js rename to src/lib/parser/SlotParser.ts index 31d8a8f..2b4dab2 100644 --- a/lib/parser/SlotParser.js +++ b/src/lib/parser/SlotParser.ts @@ -3,6 +3,7 @@ import { SlotEntry, SlotProp, SlotPropGenerator } from '../entity/SlotEntry.js'; import { Tag } from '../Enum.js'; import { KeywordsUtils } from '../utils/KeywordsUtils.js'; import { JSDoc, TYPE_LIMIT, TYPE_MIDLE } from '../JSDoc.js'; +import { Vuedoc } from '../../../types/index.js'; const PARAM_NAME = '[a-zA-Z0-9:$&\\.\\[\\]_-]+'; const TYPE = `[${TYPE_LIMIT}]*|[${TYPE_LIMIT}][${TYPE_LIMIT}${TYPE_MIDLE}]*[${TYPE_LIMIT}]`; @@ -12,7 +13,7 @@ const VBindPrefix = 'v-bind:'; const VBindName = VBindPrefix + 'name'; export const SlotParser = { - parseTemplateSlots(entry, attrsList, comment) { + parseTemplateSlots(entry: Vuedoc.Entry.SlotEntry, attrsList: Array<{ name: string }>, comment) { JSDoc.parseParams(this, comment.keywords, entry.props, SlotPropGenerator); const keywordsProps = entry.props.map(({ name }) => name); @@ -25,9 +26,9 @@ export const SlotParser = { entry.props.push(...definedProps); }, - extractSlotKeywords(keywords) { + extractSlotKeywords(keywords: Vuedoc.Entry.Keyword[]) { return KeywordsUtils.extract(keywords, Tag.slot).map((keyword) => { - const param = JSDoc.parseParamKeyword(keyword.description, SlotPropGenerator, SLOT_RE); + const param = JSDoc.parseParamKeyword(keyword.description || '', SlotPropGenerator, SLOT_RE); const entry = new SlotEntry(param.name, param.description); KeywordsUtils.parseCommonEntryTags(entry); diff --git a/lib/utils/KeywordsUtils.js b/src/lib/utils/KeywordsUtils.ts similarity index 61% rename from lib/utils/KeywordsUtils.js rename to src/lib/utils/KeywordsUtils.ts index 66b4843..74c574a 100644 --- a/lib/utils/KeywordsUtils.js +++ b/src/lib/utils/KeywordsUtils.ts @@ -1,7 +1,8 @@ +import { Vuedoc } from '../../../types/index.js'; import { CommonTags, Type } from '../Enum.js'; export const KeywordsUtils = { - extract(keywords, keywordNames, withNotEmptyDesc = false) { + extract(keywords: Vuedoc.Entry.Keyword[], keywordNames: string | string[], withNotEmptyDesc = false) { const keywordNamesArray = keywordNames instanceof Array ? keywordNames : [keywordNames]; const items = keywords.filter(({ name }) => keywordNamesArray.includes(name)); @@ -19,8 +20,8 @@ export const KeywordsUtils = { return items; }, - mergeEntryKeyword(entry, tag, type = Type.unknown) { - const items = this.extract(entry.keywords, tag, true); + mergeEntryKeyword(entry: Pick, tag: string, type: Vuedoc.Parser.Type = Type.unknown) { + const items = KeywordsUtils.extract(entry.keywords, tag, true); if (items.length) { switch (type) { @@ -31,7 +32,7 @@ export const KeywordsUtils = { default: { const item = items.pop(); - if (item.description) { + if (item?.description) { entry[tag] = item.description; } break; @@ -40,7 +41,7 @@ export const KeywordsUtils = { } }, - parseCommonEntryTags(entry) { - CommonTags.forEach(({ tag, type }) => this.mergeEntryKeyword(entry, tag, type)); + parseCommonEntryTags(entry: Pick) { + CommonTags.forEach(({ tag, type }) => KeywordsUtils.mergeEntryKeyword(entry, tag, type)); }, }; diff --git a/loaders/html.js b/src/loaders/html.ts similarity index 55% rename from loaders/html.js rename to src/loaders/html.ts index 19ec685..e2d6991 100644 --- a/loaders/html.js +++ b/src/loaders/html.ts @@ -1,7 +1,8 @@ import { Loader } from '../lib/Loader.js'; +import { Vuedoc } from '../../types/index.js'; export class HtmlLoader extends Loader { - async load(data) { + async load(data: Vuedoc.Loader.TemplateData) { this.emitTemplate(data); } } diff --git a/loaders/javascript.js b/src/loaders/javascript.ts similarity index 56% rename from loaders/javascript.js rename to src/loaders/javascript.ts index 6d81444..277ff68 100644 --- a/loaders/javascript.js +++ b/src/loaders/javascript.ts @@ -1,7 +1,8 @@ import { Loader } from '../lib/Loader.js'; +import { Vuedoc } from '../../types/index.js'; export class JavaScriptLoader extends Loader { - async load(data) { + async load(data: Vuedoc.Loader.ScriptData) { this.emitScript(data); } } diff --git a/loaders/pug.js b/src/loaders/pug.ts similarity index 82% rename from loaders/pug.js rename to src/loaders/pug.ts index 593f54a..b8645b7 100644 --- a/loaders/pug.js +++ b/src/loaders/pug.ts @@ -1,12 +1,13 @@ // eslint-disable-next-line import/no-unresolved import pug from 'pug'; import { Loader } from '../lib/Loader.js'; +import { Vuedoc } from '../../types/index.js'; /** * @note Install the [pug](https://www.npmjs.com/package/pug) dependency */ export class PugLoader extends Loader { - async load(data) { + async load(data: Vuedoc.Loader.TemplateData) { this.emitTemplate({ ...data, content: pug.render(data.content, { diff --git a/loaders/typescript.js b/src/loaders/typescript.ts similarity index 100% rename from loaders/typescript.js rename to src/loaders/typescript.ts diff --git a/loaders/vue.js b/src/loaders/vue.ts similarity index 84% rename from loaders/vue.js rename to src/loaders/vue.ts index be874be..8d6610e 100644 --- a/loaders/vue.js +++ b/src/loaders/vue.ts @@ -1,12 +1,13 @@ import { parseComponent } from 'vue-template-compiler'; import { Loader } from '../lib/Loader.js'; +import { Vuedoc } from '../../types/index.js'; const DEFAULT_TEMPLATE_LANG = 'html'; const DEFAULT_SCRIPT_LANG = 'js'; export class VueLoader extends Loader { - async load(data) { - const result = parseComponent(data.content); + async load(data: Vuedoc.Loader.TemplateData) { + const result: any = parseComponent(data.content); const template = result.template || { attrs: { diff --git a/schema/options.js b/src/schema/options.ts similarity index 100% rename from schema/options.js rename to src/schema/options.ts diff --git a/test/fixtures/checkbox.ts b/test/fixtures/checkbox.ts index 366fd37..3b5ab86 100644 --- a/test/fixtures/checkbox.ts +++ b/test/fixtures/checkbox.ts @@ -1,4 +1,4 @@ /** * A ts component */ -export default {} +export default {}; diff --git a/test/lib/TestUtils.js b/test/lib/TestUtils.js index 29475b7..945bb44 100644 --- a/test/lib/TestUtils.js +++ b/test/lib/TestUtils.js @@ -1,8 +1,8 @@ -import { parseComponent } from '../../index.js'; -import { beforeAll, describe, expect, it } from '@jest/globals'; +import { parseComponent } from '../../src/index.ts'; +import { beforeAll, describe, expect, it } from 'vitest'; export const ComponentTestCase = ({ name, description, expected, options }) => { - describe(description ? `${name}: ${description}` : name, () => { + describe.concurrent(description ? `${name}: ${description}` : name, () => { let component = null; beforeAll(async () => { diff --git a/test/lib/VueDocExample.js b/test/lib/VueDocExample.js index 03a0880..9597f97 100644 --- a/test/lib/VueDocExample.js +++ b/test/lib/VueDocExample.js @@ -1,7 +1,7 @@ import { join } from 'path'; import { readFile, stat, writeFile } from 'fs/promises'; -import { beforeAll, describe, expect, it } from '@jest/globals'; -import { parseComponent } from '../../index.js'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { parseComponent } from '../../src/index.ts'; async function loadFileContent(path) { try { diff --git a/test/spec/ClassComponent.spec.js b/test/spec/ClassComponent.spec.js index 9b4d154..a5cc66b 100644 --- a/test/spec/ClassComponent.spec.js +++ b/test/spec/ClassComponent.spec.js @@ -1,4 +1,4 @@ -import { describe } from '@jest/globals'; +import { describe } from 'vitest'; import { ComponentTestCase } from '../lib/TestUtils.js'; describe('Class Component', () => { diff --git a/test/spec/CommentParser.spec.js b/test/spec/CommentParser.spec.js index 9348c1c..e9edd6a 100644 --- a/test/spec/CommentParser.spec.js +++ b/test/spec/CommentParser.spec.js @@ -1,5 +1,5 @@ -import { describe, expect, it } from '@jest/globals'; -import { CommentParser } from '../../lib/parser/CommentParser.js'; +import { describe, expect, it } from 'vitest'; +import { CommentParser } from '../../src/lib/parser/CommentParser.ts'; const comment = ` /** diff --git a/test/spec/Compatibility.spec.js b/test/spec/Compatibility.spec.js index 7b42b62..c712433 100644 --- a/test/spec/Compatibility.spec.js +++ b/test/spec/Compatibility.spec.js @@ -1,5 +1,5 @@ import { readdir } from 'fs/promises'; -import { describe } from '@jest/globals'; +import { describe } from 'vitest'; import { fileURLToPath } from 'url'; import { join, dirname, basename } from 'path'; import { VueDocExample } from '../lib/VueDocExample.js'; diff --git a/test/spec/ComputedParser.spec.js b/test/spec/ComputedParser.spec.js index 58cea06..e96f642 100644 --- a/test/spec/ComputedParser.spec.js +++ b/test/spec/ComputedParser.spec.js @@ -1,4 +1,4 @@ -import { describe } from '@jest/globals'; +import { describe } from 'vitest'; import { ComponentTestCase } from '../lib/TestUtils.js'; describe('ComputedParser', () => { diff --git a/test/spec/DataParser.spec.js b/test/spec/DataParser.spec.js index 38dba4b..850bb3b 100644 --- a/test/spec/DataParser.spec.js +++ b/test/spec/DataParser.spec.js @@ -1,4 +1,4 @@ -import { describe } from '@jest/globals'; +import { describe } from 'vitest'; import { ComponentTestCase } from '../lib/TestUtils.js'; describe('DataParser', () => { diff --git a/test/spec/ECMAScript.spec.js b/test/spec/ECMAScript.spec.js index da1e31c..06857b1 100644 --- a/test/spec/ECMAScript.spec.js +++ b/test/spec/ECMAScript.spec.js @@ -1,5 +1,5 @@ -import { describe, expect, it } from '@jest/globals'; -import { parseComponent, Parser } from '../../index.js'; +import { describe, expect, it } from 'vitest'; +import { parseComponent, Parser } from '../../src/index.ts'; /** * Code samples from lukehoban/es6features @@ -425,7 +425,7 @@ const Feature = { function testPropertyFunction(property) { describe(`should parse ${property} without errors`, () => { describe('es2015', () => { - Object.keys(Feature).forEach((feature) => it(feature, (done) => { + Object.keys(Feature).forEach((feature) => it(feature, () => new Promise((done) => { const script = ` export default { ${property}: function () { @@ -441,11 +441,11 @@ function testPropertyFunction(property) { parser.on('end', done); parser.walk(); - })); + }))); }); describe('es6', () => { - Object.keys(Feature).forEach((feature) => it(feature, (done) => { + Object.keys(Feature).forEach((feature) => it(feature, () => new Promise((done) => { const script = ` export default { ${property} () { @@ -461,11 +461,11 @@ function testPropertyFunction(property) { parser.on('end', done); parser.walk(); - })); + }))); }); describe('arrow', () => { - Object.keys(Feature).forEach((feature) => it(feature, (done) => { + Object.keys(Feature).forEach((feature) => it(feature, () => new Promise((done) => { const script = ` export default { ${property}: () => { @@ -481,7 +481,7 @@ function testPropertyFunction(property) { parser.on('end', done); parser.walk(); - })); + }))); }); }); } @@ -489,7 +489,7 @@ function testPropertyFunction(property) { function testPropertyObject(property) { describe(`should parse ${property} without errors`, () => { describe('es2015', () => { - Object.keys(Feature).forEach((feature) => it(feature, (done) => { + Object.keys(Feature).forEach((feature) => it(feature, () => new Promise((done) => { const script = ` export default { ${property}: { @@ -507,11 +507,11 @@ function testPropertyObject(property) { parser.on('end', done); parser.walk(); - })); + }))); }); describe('es6', () => { - Object.keys(Feature).forEach((feature) => it(feature, (done) => { + Object.keys(Feature).forEach((feature) => it(feature, () => new Promise((done) => { const script = ` export default { ${property}: { @@ -529,11 +529,11 @@ function testPropertyObject(property) { parser.on('end', done); parser.walk(); - })); + }))); }); describe('arrow', () => { - Object.keys(Feature).forEach((feature) => it(feature, (done) => { + Object.keys(Feature).forEach((feature) => it(feature, () => new Promise((done) => { const script = ` export default { ${property}: { @@ -551,14 +551,14 @@ function testPropertyObject(property) { parser.on('end', done); parser.walk(); - })); + }))); }); }); } describe('ECMAScript Feature Parsing', () => { describe('should parse without errors', () => { - Object.keys(Feature).forEach((feature) => it(feature, (done) => { + Object.keys(Feature).forEach((feature) => it(feature, () => new Promise((done) => { const script = Feature[feature]; const source = { script }; const options = { source }; @@ -566,7 +566,7 @@ describe('ECMAScript Feature Parsing', () => { parser.on('end', done); parser.walk(); - })); + }))); }); testPropertyFunction('name'); diff --git a/test/spec/EventParser.spec.js b/test/spec/EventParser.spec.js index 42a0e7c..71d67be 100644 --- a/test/spec/EventParser.spec.js +++ b/test/spec/EventParser.spec.js @@ -1,4 +1,4 @@ -import { describe } from '@jest/globals'; +import { describe } from 'vitest'; import { ComponentTestCase } from '../lib/TestUtils.js'; describe('EventParser', () => { diff --git a/test/spec/InheritAttrs.spec.js b/test/spec/InheritAttrs.spec.js index 4265c86..db8123b 100644 --- a/test/spec/InheritAttrs.spec.js +++ b/test/spec/InheritAttrs.spec.js @@ -1,5 +1,5 @@ -import { describe, expect, it } from '@jest/globals'; -import { parseComponent } from '../../index.js'; +import { describe, expect, it } from 'vitest'; +import { parseComponent } from '../../src/index.ts'; describe('#43 - InheritAttrs Field', () => { it('should successfully parse inheritAttrs === true', () => { diff --git a/test/spec/InlineTemplate.spec.js b/test/spec/InlineTemplate.spec.js index 48f5d56..0d2b503 100644 --- a/test/spec/InlineTemplate.spec.js +++ b/test/spec/InlineTemplate.spec.js @@ -1,5 +1,5 @@ -import { describe, expect, it } from '@jest/globals'; -import { parseComponent } from '../../index.js'; +import { describe, expect, it } from 'vitest'; +import { parseComponent } from '../../src/index.ts'; describe('#44 - Inline Template', () => { it('should successfully parse component with inline template', () => { diff --git a/test/spec/JSDoc.spec.js b/test/spec/JSDoc.spec.js index dbe124d..b140575 100644 --- a/test/spec/JSDoc.spec.js +++ b/test/spec/JSDoc.spec.js @@ -1,7 +1,7 @@ -import { JSDoc } from '../../lib/JSDoc.js'; +import { JSDoc } from '../../src/lib/JSDoc.ts'; import { ComponentTestCase } from '../lib/TestUtils.js'; import { JSDocTypeSpec } from '../lib/JSDocTypeSpec.js'; -import { describe, expect, it } from '@jest/globals'; +import { describe, expect, it } from 'vitest'; describe('JSDoc', () => { describe('parseTypeParam(type)', () => { @@ -115,7 +115,7 @@ describe('JSDoc', () => { it('should parse @param keyword with malformated input', () => { const comment = '{ !x=> The x value.'; - const expected = { type: 'unknown', name: null, description: undefined }; + const expected = { type: 'unknown', name: '', description: undefined }; const result = JSDoc.parseParamKeyword(comment); expect(result).toEqual(expected); diff --git a/test/spec/JSXParser.spec.js b/test/spec/JSXParser.spec.js index d0754ac..62fac7a 100644 --- a/test/spec/JSXParser.spec.js +++ b/test/spec/JSXParser.spec.js @@ -1,5 +1,5 @@ import { ComponentTestCase } from '../lib/TestUtils.js'; -import { describe } from '@jest/globals'; +import { describe } from 'vitest'; describe('JSXParser', () => { ComponentTestCase({ diff --git a/test/spec/MarkupTemplateParser.spec.js b/test/spec/MarkupTemplateParser.spec.js index 204b5f0..3f7d4f5 100644 --- a/test/spec/MarkupTemplateParser.spec.js +++ b/test/spec/MarkupTemplateParser.spec.js @@ -1,5 +1,5 @@ import { ComponentTestCase } from '../lib/TestUtils.js'; -import { describe } from '@jest/globals'; +import { describe } from 'vitest'; describe('MarkupTemplateParser', () => { ComponentTestCase({ diff --git a/test/spec/MethodParser.spec.js b/test/spec/MethodParser.spec.js index eef5dcd..e4c1eee 100644 --- a/test/spec/MethodParser.spec.js +++ b/test/spec/MethodParser.spec.js @@ -1,5 +1,5 @@ import { ComponentTestCase } from '../lib/TestUtils.js'; -import { describe } from '@jest/globals'; +import { describe } from 'vitest'; // [paramName, paramDefaultValue, expectedParamType, expectedDefaultValue = paramDefaultValue] const defaultParams = [ diff --git a/test/spec/Model.spec.js b/test/spec/Model.spec.js index 0a06809..c5edd48 100644 --- a/test/spec/Model.spec.js +++ b/test/spec/Model.spec.js @@ -1,5 +1,5 @@ -import { parseComponent } from '../../index.js'; -import { describe, expect, it } from '@jest/globals'; +import { parseComponent } from '../../src/index.ts'; +import { describe, expect, it } from 'vitest'; const filecontent = `