From 2e959ffbf9e7eb88729d34fb8fb430bf7028dd81 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rousseau Date: Mon, 30 Nov 2020 14:04:41 -0500 Subject: [PATCH 1/5] feat(validation): Validate options passed to the parse function - Validate input - Validate encoding - Validate ignoredVisibilities - Add typings for parse options --- index.js | 95 +++++++++++++++++++++++++++++++-- lib/utils.js | 14 ++++- typings.d.ts | 146 ++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 236 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 939c621..1018947 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,87 @@ const { loadFileStructureFromOptions } = require('./lib/helpers'); const SvelteVersionDetector = require('./lib/detector'); - +const { isString, printArray, isVisibilitySupported } = require('./lib/utils'); + +/** + * @typedef {import("./typings").SvelteParserOptions} SvelteParserOptions + * @typedef {import('./typings').SymbolVisibility} SymbolVisibility + */ + +/** @type {BufferEncoding[]} */ +const ENCODINGS = [ + 'ascii', + 'utf8', + 'utf-8', + 'utf16le', + 'ucs2', + 'ucs-2', + 'base64', + 'latin1', + 'binary', + 'hex' +]; + +/** @type {SymbolVisibility[]} */ +const VISIBILITIES = ['public', 'protected', 'private']; + +/** @type {BufferEncoding} */ const DEFAULT_ENCODING = 'utf8'; + +/** @type {SymbolVisibility[]} */ const DEFAULT_IGNORED_VISIBILITIES = ['protected', 'private']; +const ERROR_OPTIONS_REQUIRED = 'An options object is required.'; +const ERROR_INPUT_REQUIRED = 'One of options.filename or options.fileContent is required.'; +const ERROR_ENCODING_NOT_SUPPORTED = + 'options.encoding must be one of: ' + printArray(ENCODINGS); + +const ERROR_IGNORED_VISIBILITIES_FORMAT = + 'options.ignoredVisibilities must be an array of those strings: ' + + printArray(VISIBILITIES); + +const ERROR_IGNORED_VISIBILITIES_NOT_SUPPORTED = + `options.ignoredVisibilities expected any of [${printArray(VISIBILITIES)}] ` + + 'but found these instead: '; + +/** + * @param {SvelteParserOptions} options + * @throws an error if any option is invalid + */ function validateOptions(options) { - if (!options || (!options.filename && !options.fileContent)) { - throw new Error('One of options.filename or options.filecontent is required'); + if (!options) { + throw new Error(ERROR_OPTIONS_REQUIRED); + } + + if (!isString(options.filename) && !isString(options.fileContent)) { + throw new Error(ERROR_INPUT_REQUIRED); + } + + if (options.encoding && !ENCODINGS.includes(options.encoding)) { + throw new Error(ERROR_ENCODING_NOT_SUPPORTED); + } + + if (options.ignoredVisibilities) { + if (!Array.isArray(options.ignoredVisibilities)) { + throw new Error(ERROR_IGNORED_VISIBILITIES_FORMAT); + } + + if (!options.ignoredVisibilities.every(isVisibilitySupported)) { + const notSupported = options.ignoredVisibilities.filter( + (iv) => !isVisibilitySupported(iv) + ); + + throw new Error( + ERROR_IGNORED_VISIBILITIES_NOT_SUPPORTED + + printArray(notSupported) + ); + } } } +/** + * Applies default values to options. + * @param {SvelteParserOptions} options + */ function normalizeOptions(options) { options.encoding = options.encoding || DEFAULT_ENCODING; options.ignoredVisibilities = options.ignoredVisibilities || DEFAULT_IGNORED_VISIBILITIES; @@ -179,6 +251,20 @@ function subscribeOnParserEvents(parser, options, version, resolve, reject) { }); } +/** + * Main parse function. + * @param {SvelteParserOptions} options + * @example + * const { parse } = require('sveltedoc-parser'); + * const doc = await parse({ + * filename: 'main.svelte', + * encoding: 'ascii', + * features: ['data', 'computed', 'methods'], + * ignoredVisibilities: ['private'], + * includeSourceLocations: true, + * version: 3 + * }); + */ module.exports.parse = (options) => new Promise((resolve, reject) => { try { validateOptions(options); @@ -198,6 +284,9 @@ module.exports.parse = (options) => new Promise((resolve, reject) => { } }); +/** + * @param {SvelteParserOptions} options + */ module.exports.detectVersion = (options) => { validateOptions(options); diff --git a/lib/utils.js b/lib/utils.js index ebf2912..1686b22 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,10 +1,13 @@ -const RE_VISIBILITY = /(public|protected|private)/; +const VISIBILITIES = ['public', 'protected', 'private']; +const RE_VISIBILITY = new RegExp(`^(${VISIBILITIES.join('|')})$`); const RE_KEYWORDS = /@\**\s*([a-z0-9_-]+)(\s+(-\s+)?([\wÀ-ÿ\s*{}[\]()='"`_^$#&²~|\\£¤€%µ,?;.:/!§<>+¨-]+))?/ig; const DEFAULT_VISIBILITY = 'public'; +const isVisibilitySupported = (v) => RE_VISIBILITY.test(v); + const getVisibility = (keywords, defaultVisibility) => { - const keyword = keywords.find((keyword) => RE_VISIBILITY.test(keyword.name)); + const keyword = keywords.find((keyword) => isVisibilitySupported(keyword.name)); if (keyword) { return keyword.name; @@ -284,7 +287,12 @@ const isTopLevelComment = (comment) => { return comment.keywords.some((keyword) => keyword.name === 'component'); }; +const isString = (x) => typeof x === 'string' || x instanceof String; + +const printArray = (array) => array.map(s => `'${s}'`).join(', '); + module.exports.DEFAULT_VISIBILITY = DEFAULT_VISIBILITY; +module.exports.isVisibilitySupported = isVisibilitySupported; module.exports.getVisibility = getVisibility; module.exports.parseComment = parseComment; module.exports.getCommentFromSourceCode = getCommentFromSourceCode; @@ -300,3 +308,5 @@ module.exports.escapeImportKeyword = escapeImportKeyword; module.exports.inferTypeFromVariableDeclaration = inferTypeFromVariableDeclaration; module.exports.isTopLevelComment = isTopLevelComment; module.exports.buildCamelCase = buildCamelCase; +module.exports.isString = isString; +module.exports.printArray = printArray; diff --git a/typings.d.ts b/typings.d.ts index 62da62b..d1e2b4e 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -18,7 +18,7 @@ export interface JSDocType { /** * Kind of this type. */ - kind: 'type'|'union'|'const', + kind: 'type' | 'union' | 'const', /** * The text representation of this type. */ @@ -28,7 +28,7 @@ export interface JSDocType { * @see `'type'|'const'` in `kind` field, then this field provide the name of JS type. * @see `'union'` in `kind` field, then this field provide the list of @see JSDocType */ - type: string|JSDocType[], + type: string | JSDocType[], /** * The constant value related to this type, if can be provided. */ @@ -50,7 +50,7 @@ export interface JSDocTypeElement extends JSDocType { /** * Kind of this type. */ - kind: 'type'|'const', + kind: 'type' | 'const', /** * The type representation of this item. */ @@ -87,11 +87,11 @@ export interface ISvelteItem { /** * The description of the item, provided from related comment. */ - description?: string|null; + description?: string | null; /** * The visibility of item. */ - visibility?: 'public'|'protected'|'private'; + visibility?: 'public' | 'protected' | 'private'; /** * The list of parsed JSDoc keywords from related comment. */ @@ -120,7 +120,7 @@ export interface SvelteDataItem extends ISvelteItem { * @since Svelte V3 * @since {2.0.0} */ - kind?: 'var'|'let'|'const'; + kind?: 'var' | 'let' | 'const'; /** * Provides information about property binding. * @since Svelte V3 @@ -179,7 +179,7 @@ export interface SvelteComputedItem extends ISvelteItem { /** * The list of data or computed properties names, marked as depended to this property. */ - dependencies: string[] + dependencies: string[] } export interface SvelteMethodParamItem { @@ -259,13 +259,13 @@ export interface SvelteComponentItem extends ISvelteItem { * @since Svelte V3 * @since {2.0.0} */ -export type SvelteEventModificator = 'preventDefault'|'stopPropagation'|'passive'|'capture'|'once'; +export type SvelteEventModificator = 'preventDefault' | 'stopPropagation' | 'passive' | 'capture' | 'once'; export interface SvelteEventItem extends ISvelteItem { /** * The name of HTML element if propagated standart JS Dom event or null. */ - parent?: string|null; + parent?: string | null; /** * The list of event modificators. @@ -295,7 +295,7 @@ export interface SvelteRefItem extends ISvelteItem { /** * The name of HTML element or component that binded with this ref name. */ - parent?: string|null; + parent?: string | null; } /** @@ -305,7 +305,7 @@ export interface SvelteComponentDoc { /** * The name of the parsed component. */ - name?: string|null; + name?: string | null; /** * The Svelte compiler version that used for this document. */ @@ -313,7 +313,7 @@ export interface SvelteComponentDoc { /** * The component description. */ - description?: string|null; + description?: string | null; /** * The list of defined model properties. @@ -323,7 +323,7 @@ export interface SvelteComponentDoc { * The list of defined computed properties of component. */ computed?: SvelteComputedItem[]; - + /** * The list of included components. */ @@ -364,4 +364,122 @@ export interface SvelteComponentDoc { * @since {2.1.0} */ dispatchers?: SvelteMethodItem[]; -} \ No newline at end of file +} + +/** + * Features supported by the Svelte 2 parser. + */ +export enum Svelte2Feature { + name = 'name', + data = 'data', + computed = 'computed', + methods = 'methods', + actions = 'actions', + helpers = 'helpers', + components = 'components', + description = 'description', + keywords = 'keywords', + events = 'events', + slots = 'slots', + transitions = 'transitions', + refs = 'refs', + store = 'store', +}; + +/** + * Features supported by the Svelte 3 parser. + */ +export enum Svelte3Feature { + name = 'name', + data = 'data', + computed = 'computed', + methods = 'methods', + components = 'components', + description = 'description', + keywords = 'keywords', + events = 'events', + slots = 'slots', + refs = 'refs', +}; + +type Svelte2FeatureKeys = keyof typeof Svelte2Feature; +type Svelte3FeatureKeys = keyof typeof Svelte3Feature; +type SvelteFeatureKeys = Svelte2FeatureKeys & Svelte3FeatureKeys; +type Svelte2ExclusiveFeature = Exclude; +type Svelte3ExclusiveFeature = Exclude; + +/** + * Visibility of a Svelte item. + */ +export type SymbolVisibility = 'private' | 'protected' | 'public'; + +/** + * Supported Svelte versions. + */ +export type SvelteVersion = 2 | 3; + +export interface ParserOptions { + /** + * The filename to parse. Required, unless fileContent is passed. + */ + filename?: string; + + /** + * The file content to parse. Required, unless filename is passed. + */ + fileContent?: string; + + /** + * @default 'utf8' + */ + encoding?: BufferEncoding; + + /** + * The component features to parse and extract. + * Uses all supported features by default. + * @see Svelte2Feature + * @see Svelte3Feature + */ + features?: F[]; + + /** + * The list of ignored visibilities. Use an empty array to export all + * visibilities. + * @default ['private','protected'] + */ + ignoredVisibilities?: SymbolVisibility[]; + + /** + * Indicates that source locations should be provided for component symbols. + * @default false + */ + includeSourceLocations?: boolean + + /** + * Optional. Use 2 or 3 to specify which svelte syntax should be used. + * When version is not provided, the parser tries to detect which version + * of the syntax to use. + */ + version?: V; + + /** + * Optional. Specify which version of svelte syntax to fallback to if the + * parser can't identify the version used. + */ + defaultVersion?: V; +} + +/** + * Options to pass to the main parse function. + * + * @example + * const options = { + * filename: 'main.svelte', + * encoding: 'ascii', + * features: ['data', 'computed', 'methods'], + * ignoredVisibilities: ['private'], + * includeSourceLocations: true, + * version: 3 + * }); + */ +export type SvelteParserOptions = ParserOptions<3, Svelte3FeatureKeys> | ParserOptions<2, Svelte2FeatureKeys>; From 7640af749e4aa37b9ce7f24143c7b4263709f8d8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rousseau Date: Tue, 1 Dec 2020 19:21:57 -0500 Subject: [PATCH 2/5] test(options): Add tests and refactor - Refactor options in a separate module - Add tests (12) for options module - Fix typings for options - Set options.structure instead of passing param - Add documentation to lib/helpers.js --- CHANGELOG.md | 15 ++ README.md | 36 +---- examples/Button.json | 2 +- index.js | 133 ++++------------ lib/helpers.js | 37 ++++- lib/jsdoc.js | 14 +- lib/options.js | 150 ++++++++++++++++++ lib/parser.js | 3 +- lib/utils.js | 3 + lib/v3/parser.js | 4 +- package.json | 3 +- .../integration/overall/overall.main.doc.json | 1 - test/unit/jsdoc/jsdoc.spec.js | 6 +- test/unit/options/options.spec.js | 103 ++++++++++++ typings.d.ts | 76 +++++---- 15 files changed, 390 insertions(+), 196 deletions(-) create mode 100644 lib/options.js create mode 100644 test/unit/options/options.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8e523..226e170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to the "svelte-intellisense" extension will be documented in Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [4.0.0] UNRELEASED + +- 🛠 **[Fixed]** Fix [Issue #42](https://github.com/alexprey/sveltedoc-parser/issues/42) +- ✔ **[Added]** Support to complete parsing of component method arguments [Issue #39](https://github.com/alexprey/sveltedoc-parser/issues/39). Thanks for [@soft-decay](https://github.com/soft-decay) +- ✔ **[Added]** Support to parse return types and description for methods in component [Issue #37](https://github.com/alexprey/sveltedoc-parser/issues/37). Thanks for [@soft-decay](https://github.com/soft-decay) +- 🔥 **[Breaking]** API rework for component methods description: + - `args` property was renamed to `params`; + - Change the structure of `return` item for methods: + - `desc` property was renamed to `description`; + - `type` property now contains the `JSDocType` object, instead of `string` type with text representation of type. This can be gets from `text` property of `JSDocType` object; + - [Svelte2]: method arguments was presented with plain array with names, now that replaced with objects of `SvelteMethodParamItem` type; +- 🔥 **[Breaking]** Cleanup depricated code: + - `loc` property was removed, please use `locations` instead, if you late with upgrade; + - `value` property of `SvelteComponentItem` was removed, please use `importPath` instead + ## [3.0.5] 28.11.2020 - 🛠 **[Fixed]** Fix [Issue #35](https://github.com/alexprey/sveltedoc-parser/issues/35): Object literals not supported in @type keyword. Thanks for @soft-decay diff --git a/README.md b/README.md index e87e3c6..7bcb329 100644 --- a/README.md +++ b/README.md @@ -7,41 +7,7 @@ Generate a JSON documentation for a Svelte file ## Changelog -## [3.0.5] 28.11.2020 - -- 🛠 **[Fixed]** Fix [Issue #35](https://github.com/alexprey/sveltedoc-parser/issues/35): Object literals not supported in @type keyword. Thanks for @soft-decay - -### [3.0.4] 25.08.2020 - -- 🛠 **[Fixed]** Fix [issue #5](https://github.com/alexprey/sveltedoc-parser/issues/5) (slots items have a private access level by default) - -### [3.0.3] 25.08.2020 - -- 🛠 **[Fixed]** Fix [issue #28](https://github.com/alexprey/sveltedoc-parser/issues/28) (Inline event handlers in markup cause errors when used without quotes) - - Change dependency from `htmlparser2` to [htmlparser2-svelte](https://www.npmjs.com/package/htmlparser2-svelte) special fork of [htmlparser2](https://www.npmjs.com/package/htmlparser2) to handle Svelte specific cases to parse markup - -### [3.0.2] 24.08.2020 - -- 🛠 **[Fixed]** Fix issue #6 (Build a correct component name from a file name) -``` -round.button.svelte -> RoundButton -``` -- 🛠 **[Fixed]** Fix issue #27 (Events is not exposed from exported functions and arrow functions) -- 🛠 **[Fixed]** Fix issue #31 (Propogated events in markup should be parsed even it was before handled) -- 🛠 **[Fixed]** Fix issue #32 (Event is not registered when dispatched from functions used as a parameters of another functions) - -### [3.0.1] 17.08.2020 - -- 🛠 **[Fixed]** Solve issue #26, support `export { variables as var }` statement. -- ✔ **[Added]** now interface `SvelteDataItem` provides a new property `localName` with information about internal name of component property. - -### [3.0.0] 08.08.2020 - -- 🛠 **[Fixed]** Solve vulnerability issues: - - Update `espree` to `7.2.0` - - Update `htmlparser2` to `3.9.2` - - Add dependency to `eslint` to fix issues after upgrading to new versions -- 🔥 **[Breaking]** Increase requirement of Node.js to `10.0.0`, Node.js v8 now is not supported, this is related with security isssues above. Please let me know if it still required. +...TBD... Full changelog of release versions can be found [here](/CHANGELOG.md) diff --git a/examples/Button.json b/examples/Button.json index 34b1de2..de9a43d 100644 --- a/examples/Button.json +++ b/examples/Button.json @@ -151,7 +151,7 @@ }, "name": "question", "optional": true, - "default": "Why?", + "defaultValue": "Why?", "description": "a question about life, the universe, everything" } ], diff --git a/index.js b/index.js index 1018947..9b98509 100644 --- a/index.js +++ b/index.js @@ -1,123 +1,44 @@ const { loadFileStructureFromOptions } = require('./lib/helpers'); +const { validate, retrieveFileOptions } = require('./lib/options'); const SvelteVersionDetector = require('./lib/detector'); -const { isString, printArray, isVisibilitySupported } = require('./lib/utils'); /** * @typedef {import("./typings").SvelteParserOptions} SvelteParserOptions - * @typedef {import('./typings').SymbolVisibility} SymbolVisibility */ -/** @type {BufferEncoding[]} */ -const ENCODINGS = [ - 'ascii', - 'utf8', - 'utf-8', - 'utf16le', - 'ucs2', - 'ucs-2', - 'base64', - 'latin1', - 'binary', - 'hex' -]; - -/** @type {SymbolVisibility[]} */ -const VISIBILITIES = ['public', 'protected', 'private']; - -/** @type {BufferEncoding} */ -const DEFAULT_ENCODING = 'utf8'; - -/** @type {SymbolVisibility[]} */ -const DEFAULT_IGNORED_VISIBILITIES = ['protected', 'private']; - -const ERROR_OPTIONS_REQUIRED = 'An options object is required.'; -const ERROR_INPUT_REQUIRED = 'One of options.filename or options.fileContent is required.'; -const ERROR_ENCODING_NOT_SUPPORTED = - 'options.encoding must be one of: ' + printArray(ENCODINGS); - -const ERROR_IGNORED_VISIBILITIES_FORMAT = - 'options.ignoredVisibilities must be an array of those strings: ' + - printArray(VISIBILITIES); - -const ERROR_IGNORED_VISIBILITIES_NOT_SUPPORTED = - `options.ignoredVisibilities expected any of [${printArray(VISIBILITIES)}] ` + - 'but found these instead: '; - -/** - * @param {SvelteParserOptions} options - * @throws an error if any option is invalid - */ -function validateOptions(options) { - if (!options) { - throw new Error(ERROR_OPTIONS_REQUIRED); - } - - if (!isString(options.filename) && !isString(options.fileContent)) { - throw new Error(ERROR_INPUT_REQUIRED); - } - - if (options.encoding && !ENCODINGS.includes(options.encoding)) { - throw new Error(ERROR_ENCODING_NOT_SUPPORTED); - } - - if (options.ignoredVisibilities) { - if (!Array.isArray(options.ignoredVisibilities)) { - throw new Error(ERROR_IGNORED_VISIBILITIES_FORMAT); - } - - if (!options.ignoredVisibilities.every(isVisibilitySupported)) { - const notSupported = options.ignoredVisibilities.filter( - (iv) => !isVisibilitySupported(iv) - ); - - throw new Error( - ERROR_IGNORED_VISIBILITIES_NOT_SUPPORTED + - printArray(notSupported) - ); - } - } -} - -/** - * Applies default values to options. - * @param {SvelteParserOptions} options - */ -function normalizeOptions(options) { - options.encoding = options.encoding || DEFAULT_ENCODING; - options.ignoredVisibilities = options.ignoredVisibilities || DEFAULT_IGNORED_VISIBILITIES; -} - -function buildSvelte2Parser(structure, options) { +function buildSvelte2Parser(options) { const Parser = require('./lib/parser'); // Convert structure object to old version source options - const hasScript = structure.scripts && structure.scripts.length > 0; - const hasStyle = structure.styles && structure.styles.length > 0; + const { scripts, styles, template } = options.structure; + + const hasScript = !!scripts && scripts.length > 0; + const hasStyle = !!styles && styles.length > 0; options.source = { - template: structure.template, - script: hasScript ? structure.scripts[0].content : '', - scriptOffset: hasScript ? structure.scripts[0].offset : 0, - style: hasStyle ? structure.styles[0].content : '', - styleOffset: hasStyle ? structure.styles[0].offset : 0, + template: template, + script: hasScript ? scripts[0].content : '', + scriptOffset: hasScript ? scripts[0].offset : 0, + style: hasStyle ? styles[0].content : '', + styleOffset: hasStyle ? styles[0].offset : 0, }; return new Parser(options); } -function buildSvelte3Parser(structure, options) { +function buildSvelte3Parser(options) { const Parser = require('./lib/v3/parser'); - return new Parser(structure, options); + return new Parser(options); } -function buildSvelteParser(structure, options, version) { +function buildSvelteParser(options, version) { if (version === SvelteVersionDetector.SVELTE_VERSION_3) { - return buildSvelte3Parser(structure, options); + return buildSvelte3Parser(options); } if (version === SvelteVersionDetector.SVELTE_VERSION_2) { - return buildSvelte2Parser(structure, options); + return buildSvelte2Parser(options); } if (version) { @@ -188,9 +109,9 @@ function mergeItems(itemType, currentItem, newItem, ignoreLocations) { return currentItem; } -function subscribeOnParserEvents(parser, options, version, resolve, reject) { +function subscribeOnParserEvents(parser, ignoredVisibilities, detectedVersion, resolve, reject) { const component = { - version: version + version: detectedVersion }; parser.features.forEach((feature) => { @@ -238,7 +159,7 @@ function subscribeOnParserEvents(parser, options, version, resolve, reject) { parser.features.forEach((feature) => { if (component[feature] instanceof Array) { component[feature] = component[feature].filter((item) => { - return !options.ignoredVisibilities.includes(item.visibility); + return !ignoredVisibilities.includes(item.visibility); }); } }); @@ -256,6 +177,7 @@ function subscribeOnParserEvents(parser, options, version, resolve, reject) { * @param {SvelteParserOptions} options * @example * const { parse } = require('sveltedoc-parser'); + * // basic usage only requires 'filename' to be set. * const doc = await parse({ * filename: 'main.svelte', * encoding: 'ascii', @@ -267,16 +189,17 @@ function subscribeOnParserEvents(parser, options, version, resolve, reject) { */ module.exports.parse = (options) => new Promise((resolve, reject) => { try { - validateOptions(options); - normalizeOptions(options); + validate(options); + + const fileOptions = retrieveFileOptions(options); - const structure = loadFileStructureFromOptions(options); + options.structure = loadFileStructureFromOptions(fileOptions); - const version = options.version || SvelteVersionDetector.detectVersionFromStructure(structure, options.defaultVersion); + const detectedVersion = options.version || SvelteVersionDetector.detectVersionFromStructure(structure, options.defaultVersion); - const parser = buildSvelteParser(structure, options, version); + const parser = buildSvelteParser(options, detectedVersion); - subscribeOnParserEvents(parser, options, version, resolve, reject); + subscribeOnParserEvents(parser, options.ignoredVisibilities, detectedVersion, resolve, reject); parser.walk(); } catch (error) { @@ -288,7 +211,7 @@ module.exports.parse = (options) => new Promise((resolve, reject) => { * @param {SvelteParserOptions} options */ module.exports.detectVersion = (options) => { - validateOptions(options); + validate(options); return SvelteVersionDetector.detectVersionFromOptions(options); }; diff --git a/lib/helpers.js b/lib/helpers.js index 63546e5..31fdd19 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,6 +1,21 @@ const fs = require('fs'); const path = require('path'); +/** + * @typedef {import('../typings').SvelteParserOptions} SvelteParserOptions + * @typedef {'fileContent' | 'filename' | 'encoding'} FileOptionKeys + * @typedef {{content?: string; template?: string, scripts?: unknown[], styles?: unknown[] }} FileStructure + * @typedef {Pick & { structure?: FileStructure }} FileOptions + */ + +/** + * @typedef {{ offset: number, outerPosition: { start: number, end: number }, content: string, attributes: string }} HtmlBlock + */ + +/** + * + * @param {FileOptions} options + */ function loadFileStructureFromOptions(options) { if (options.structure) { return options.structure; @@ -28,9 +43,17 @@ function loadFileStructureFromOptions(options) { } } - throw new Error('Can not load source from options, because nothing is not provided'); + // With options validation, this should never happen + throw new Error('Internal Error: Cannot load FileStructure because input is missing.'); } +/** + * + * @param {string} content + * @param {string} blockName + * @param {number} searchStartIndex + * @returns {{ block: HtmlBlock | null }} + */ function extractHtmlBlock(content, blockName, searchStartIndex) { const blockOuterStartIndex = content.indexOf(`<${blockName}`, searchStartIndex); @@ -65,7 +88,14 @@ function extractHtmlBlock(content, blockName, searchStartIndex) { }; } +/** + * + * @param {string} content + * @param {string} blockName + * @returns {{ blocks: HtmlBlock[] }} + */ function extractAllHtmlBlocks(content, blockName) { + /** @type {HtmlBlock[]} */ const blocks = []; let searchResult = extractHtmlBlock(content, blockName); @@ -83,6 +113,11 @@ function extractAllHtmlBlocks(content, blockName) { }; } +/** + * + * @param {string} fileContent + * @returns {FileStructure} + */ function parseFileStructureFromContent(fileContent) { const scriptBlocksSearchResult = extractAllHtmlBlocks(fileContent, 'script'); const styleBlocksSearchResult = extractAllHtmlBlocks(fileContent, 'style'); diff --git a/lib/jsdoc.js b/lib/jsdoc.js index 2329d4a..bde1caf 100644 --- a/lib/jsdoc.js +++ b/lib/jsdoc.js @@ -121,8 +121,8 @@ function parseParamKeyword(text) { const param = { type: getDefaultJSDocType(), name: null, - optional: false, - default: null + description: null, + optional: false }; const match = PARAM_RE.exec(text); @@ -152,7 +152,7 @@ function parseParamKeyword(text) { param.optional = true; if (match[3]) { - param.default = match[3].trim(); + param.defaultValue = match[3].trim(); } } @@ -175,16 +175,18 @@ function parseReturnKeyword(text) { type: getDefaultJSDocType(), description: null }; + const matches = RETURN_RE.exec(text); const typeMatch = matches[2]; // type, excluding curly braces - const descMatch = matches[4]; // description, excluding prefix if (typeMatch) { output.type = parseJSDocType(typeMatch); } - if (descMatch) { - output.description = descMatch.trim(); + const descriptionMatch = matches[4]; // description, excluding prefix + + if (descriptionMatch) { + output.description = descriptionMatch.trim(); } return output; diff --git a/lib/options.js b/lib/options.js new file mode 100644 index 0000000..c0cc6e4 --- /dev/null +++ b/lib/options.js @@ -0,0 +1,150 @@ +const { isString, printArray, isVisibilitySupported } = require('./utils'); + +/** + * @typedef {import("../typings").SvelteParserOptions} SvelteParserOptions + * @typedef {import('../typings').JSVisibilityScope} JSVisibilityScope + */ + +/** @type {BufferEncoding[]} */ +const ENCODINGS = [ + 'ascii', + 'utf8', + 'utf-8', + 'utf16le', + 'ucs2', + 'ucs-2', + 'base64', + 'latin1', + 'binary', + 'hex' +]; + +/** @type {SymbolVisibility[]} */ +const VISIBILITIES = [ + 'public', + 'protected', + 'private' +]; + +const ERROR_OPTIONS_REQUIRED = 'An options object is required.'; +const ERROR_INPUT_REQUIRED = 'One of options.filename or options.fileContent is required.'; +const ERROR_IGNORED_VISIBILITIES_FORMAT = + 'options.ignoredVisibilities must be an array containing only these strings: ' + + printArray(VISIBILITIES); +const ERROR_IGNORED_VISIBILITIES_EXPECTED = + `options.ignoredVisibilities expected any of ${printArray(VISIBILITIES)}, ` + + 'but found these unsupported visibilities: '; + +const ErrorMessage = Object.freeze({ + OptionsRequired: ERROR_OPTIONS_REQUIRED, + InputRequired: ERROR_INPUT_REQUIRED, + EncodingFormat: 'Expected options.encoding to be a string. Use one of: ' + printArray(ENCODINGS), + EncodingNotSupported: (enc) => `encoding '${isString(enc) ? "'" + enc + "'" : enc}' is not supported. options.encoding must be one of: ` + printArray(ENCODINGS), + IgnoredVisibilitiesFormat: ERROR_IGNORED_VISIBILITIES_FORMAT, + IgnoredVisibilitiesNotSupported: (arr) => ERROR_IGNORED_VISIBILITIES_EXPECTED + printArray(arr), +}); + +/** @type {BufferEncoding} */ +const DEFAULT_ENCODING = 'utf8'; + +/** @type {SymbolVisibility[]} */ +const DEFAULT_IGNORED_VISIBILITIES = ['protected', 'private']; + +/** @returns {SvelteParserOptions} */ +function getDefaultOptions() { + return { + encoding: DEFAULT_ENCODING, + ignoredVisibilities: [...DEFAULT_IGNORED_VISIBILITIES], + }; +} + +function retrieveFileOptions(options) { + return { + structure: options.structure, + fileContent: options.fileContent, + filename: options.filename, + encoding: options.encoding, + }; +} + +/** + * Applies default values to options. + * @param {SvelteParserOptions} options + */ +function normalize(options) { + const defaults = getDefaultOptions(); + + Object.keys(defaults).forEach((optionKey) => { + /** + * If the key was not set by the user, apply default value. + * This is better than checking for falsy values because it catches + * use cases were a user tried to do something not intended with + * an option (e.g. putting a value of 'false' or an empty string) + */ + if (!(optionKey in options)) { + options[optionKey] = defaults[optionKey]; + } + }); +} + +/** + * @param {SvelteParserOptions} options + * @throws an error if any options are invalid + */ +function validate(options) { + if (!options) { + throw new Error(ErrorMessage.OptionsRequired); + } + + normalize(options); + + const hasFilename = + ('filename' in options) && + isString(options.filename) && + options.filename.length > 0; + + // Don't check length for fileContent because it could be an empty file. + const hasFileContent = + ('fileContent' in options) && + isString(options.fileContent); + + if (!hasFilename && !hasFileContent) { + throw new Error(ErrorMessage.InputRequired); + } + + if ('encoding' in options) { + if (!isString(options.encoding)) { + throw new Error(ErrorMessage.EncodingFormat); + } + + if (!ENCODINGS.includes(options.encoding)) { + throw new Error(ErrorMessage.EncodingNotSupported(options.encoding)); + } + } else { + // Sanity check. At this point, 'encoding' should be set. + throw new Error(ErrorMessage.EncodingMissing); + } + + if ('ignoredVisibilities' in options) { + if (!Array.isArray(options.ignoredVisibilities)) { + throw new Error(ErrorMessage.IgnoredVisibilitiesFormat); + } + + if (!options.ignoredVisibilities.every(isVisibilitySupported)) { + const notSupported = options.ignoredVisibilities.filter( + (iv) => !isVisibilitySupported(iv) + ); + + throw new Error(ErrorMessage.IgnoredVisibilitiesNotSupported(notSupported)); + } + } else { + // Sanity check. At this point, 'ignoredVisibilities' should be set. + throw new Error(ErrorMessage.IgnoredVisibilitiesMissing); + } +} + +module.exports = { + ErrorMessage: ErrorMessage, + validate: validate, + retrieveFileOptions: retrieveFileOptions, +}; diff --git a/lib/parser.js b/lib/parser.js index 2dd24a4..bf17524 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -6,7 +6,8 @@ const path = require('path'); const utils = require('./utils'); const jsdoc = require('./jsdoc'); -const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); +// TODO: Remove this and use from utils directly +const hasOwnProperty = utils.hasOwnProperty; const DEFAULT_OPTIONS = { /** diff --git a/lib/utils.js b/lib/utils.js index 1686b22..f1252d5 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -291,6 +291,8 @@ const isString = (x) => typeof x === 'string' || x instanceof String; const printArray = (array) => array.map(s => `'${s}'`).join(', '); +const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); + module.exports.DEFAULT_VISIBILITY = DEFAULT_VISIBILITY; module.exports.isVisibilitySupported = isVisibilitySupported; module.exports.getVisibility = getVisibility; @@ -310,3 +312,4 @@ module.exports.isTopLevelComment = isTopLevelComment; module.exports.buildCamelCase = buildCamelCase; module.exports.isString = isString; module.exports.printArray = printArray; +module.exports.hasOwnProperty = hasOwnProperty; diff --git a/lib/v3/parser.js b/lib/v3/parser.js index 16aca41..7f423b7 100644 --- a/lib/v3/parser.js +++ b/lib/v3/parser.js @@ -40,12 +40,12 @@ const RETURN_ALIASES = { }; class Parser extends EventEmitter { - constructor(structure, options) { + constructor(options) { super(); this._options = Object.assign({}, DEFAULT_OPTIONS, options); - this.structure = structure; + this.structure = options.structure; this.features = options.features || SUPPORTED_FEATURES; this.includeSourceLocations = options.includeSourceLocations; this.componentName = null; diff --git a/package.json b/package.json index 0345c71..5510858 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "sveltedoc-parser", - "version": "3.0.5", + "version": "4.0.0", "description": "Generate a JSON documentation for a Svelte file", "main": "index.js", + "types": "./typings.d.ts", "scripts": { "lint": "eslint ./**/*.js", "test": "mocha ./test/**/*.spec.js", diff --git a/test/svelte2/integration/overall/overall.main.doc.json b/test/svelte2/integration/overall/overall.main.doc.json index 6af59ad..b7f7363 100644 --- a/test/svelte2/integration/overall/overall.main.doc.json +++ b/test/svelte2/integration/overall/overall.main.doc.json @@ -245,7 +245,6 @@ }, "name": "actionName", "optional": false, - "default": null, "description": "The name of action that should be performed." } ] diff --git a/test/unit/jsdoc/jsdoc.spec.js b/test/unit/jsdoc/jsdoc.spec.js index ccbfd00..179cd28 100644 --- a/test/unit/jsdoc/jsdoc.spec.js +++ b/test/unit/jsdoc/jsdoc.spec.js @@ -188,7 +188,7 @@ describe('JSDoc parser module tests', () => { expect(param).is.exist; expect(param.name).is.equal('parameter'); - expect(param.default).is.not.exist; + expect(param.defaultValue).is.not.exist; expect(param.optional).is.true; }); @@ -197,7 +197,7 @@ describe('JSDoc parser module tests', () => { expect(param).is.exist; expect(param.name).is.equal('parameter'); - expect(param.default).is.not.exist; + expect(param.defaultValue).is.not.exist; expect(param.optional).is.true; expect(param.type).is.exist; @@ -210,7 +210,7 @@ describe('JSDoc parser module tests', () => { expect(param).is.exist; expect(param.name).is.equal('parameter'); - expect(param.default).is.equal('Default value'); + expect(param.defaultValue).is.equal('Default value'); expect(param.optional).is.true; expect(param.type).is.exist; diff --git a/test/unit/options/options.spec.js b/test/unit/options/options.spec.js new file mode 100644 index 0000000..5225566 --- /dev/null +++ b/test/unit/options/options.spec.js @@ -0,0 +1,103 @@ +const expect = require('chai').expect; + +const { + validate, + retrieveFileOptions, + ErrorMessage +} = require('../../../lib/options'); + +const baseOptions = { filename: 'empty.svelte' }; + +describe('Options Module', () => { + describe('options.validate', () => { + describe('Should throw when', () => { + it('options object is missing', () => { + expect(() => validate()).to.throw(ErrorMessage.OptionsRequired); + }); + + it('input is missing, not a string, or an empty filename', () => { + expect(() => validate({})).to.throw(ErrorMessage.InputRequired); + expect(() => validate({ filename: {} })).to.throw(ErrorMessage.InputRequired); + expect(() => validate({ filename: '' })).to.throw(ErrorMessage.InputRequired); + expect(() => validate({ fileContent: {} })).to.throw(ErrorMessage.InputRequired); + }); + + it('encoding is not a string', () => { + const options = { ...baseOptions, encoding: true }; + + expect(() => validate(options)).to.throw(ErrorMessage.EncodingFormat); + }); + + it('encoding is not supported', () => { + const unsupported = 'l33t-enc'; + const options = { ...baseOptions, encoding: unsupported }; + + expect(() => validate(options)).to.throw( + ErrorMessage.EncodingNotSupported(unsupported) + ); + }); + + it('ignoreVisibilities is not an array', () => { + const unsupported = 'unsupported'; + const mixed = ['private', unsupported]; + const options = { ...baseOptions, ignoredVisibilities: mixed }; + + expect(() => validate(options)).to.throw( + ErrorMessage.IgnoredVisibilitiesNotSupported([unsupported]) + ); + }); + + it('ignoreVisibilities contains at least one unsupported visibility', () => { + const unsupported = 'unsupported'; + const mixed = ['private', unsupported]; + const options = { ...baseOptions, ignoredVisibilities: mixed }; + + expect(() => validate(options)).to.throw( + ErrorMessage.IgnoredVisibilitiesNotSupported([unsupported]) + ); + }); + }); + + describe('Should pass when', () => { + it('just filename is present', () => { + expect(() => validate(baseOptions)).to.not.throw(); + }); + + it('just fileContent is present', () => { + expect(() => validate({ fileContent: 'content' })).to.not.throw(); + }); + + it('fileContent is empty', () => { + expect(() => validate({ fileContent: '' })).to.not.throw(); + }); + + it('ignoreVisibilities is an empty array', () => { + const options = { ...baseOptions, ignoredVisibilities: [] }; + + expect(() => validate(options)).to.not.throw(); + }); + + it('ignoreVisibilities is an array of supported visibilities', () => { + const options = { + ...baseOptions, + ignoredVisibilities: ['protected', 'public'] + }; + + expect(() => validate(options)).to.not.throw(); + }); + }); + }); + + describe('options.retrieveFileOptions', () => { + it('Should return all file-related keys from options', () => { + const options = { + ...baseOptions, + ignoredVisibilities: ['protected', 'public'] + }; + + expect(retrieveFileOptions(options)).to.have.keys( + 'filename', 'fileContent', 'structure', 'encoding' + ); + }); + }); +}); diff --git a/typings.d.ts b/typings.d.ts index d1e2b4e..8f8fdc3 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -14,49 +14,43 @@ export interface JSDocKeyword { description: string; } -export interface JSDocType { - /** - * Kind of this type. - */ - kind: 'type' | 'union' | 'const', +export interface JSDocTypeBase { /** * The text representation of this type. */ text: string, +} + +export interface JSDocTypeElement extends JSDocTypeBase { + kind: 'type', /** - * The type representation of this item. - * @see `'type'|'const'` in `kind` field, then this field provide the name of JS type. - * @see `'union'` in `kind` field, then this field provide the list of @see JSDocType - */ - type: string | JSDocType[], - /** - * The constant value related to this type, if can be provided. + * The name of JS type. */ - value?: any + type: string, } -export interface JSDocTypeUnion extends JSDocType { +export interface JSDocTypeConst extends JSDocTypeBase { + kind: 'const', /** - * Kind of this type. + * The name of JS type. */ - kind: 'union', + type: string, /** - * The list of types of this item. + * The constant value related to this type, if can be provided. */ - type: JSDocType[], + value?: any, } -export interface JSDocTypeElement extends JSDocType { - /** - * Kind of this type. - */ - kind: 'type' | 'const', +export interface JSDocTypeUnion extends JSDocTypeBase { + kind: 'union', /** - * The type representation of this item. + * The list of possible types. */ - type: string, + type: JSDocType[] } +export type JSDocType = JSDocTypeElement | JSDocTypeConst | JSDocTypeUnion; + /** * Represents a source location of symbol. */ @@ -72,6 +66,10 @@ export interface SourceLocation { end: number; } +export type JSVisibilityScope = 'public' | 'protected' | 'private'; + +export type JSVariableDeclarationKind = 'var' | 'let' | 'const'; + export interface ISvelteItem { /** * The name of the item. @@ -91,7 +89,7 @@ export interface ISvelteItem { /** * The visibility of item. */ - visibility?: 'public' | 'protected' | 'private'; + visibility?: JSVisibilityScope; /** * The list of parsed JSDoc keywords from related comment. */ @@ -120,7 +118,7 @@ export interface SvelteDataItem extends ISvelteItem { * @since Svelte V3 * @since {2.0.0} */ - kind?: 'var' | 'let' | 'const'; + kind?: JSVariableDeclarationKind; /** * Provides information about property binding. * @since Svelte V3 @@ -143,7 +141,7 @@ export interface SvelteDataItem extends ISvelteItem { /** * The default value of property, if provided. */ - value?: any; + defaultValue?: any; /** * The original name of the imported item. @@ -201,10 +199,12 @@ export interface SvelteMethodParamItem { optional?: boolean; /** * The default value of optional parameter. + * @since {4.0.0} */ - default?: string; + defaultValue?: string; /** * The description of the parameter. + * @since {4.0.0} */ description?: string; /** @@ -216,14 +216,12 @@ export interface SvelteMethodParamItem { static?: boolean; } -export type SvelteArgItem = SvelteMethodParamItem; -export type SvelteArgumentItem = SvelteMethodParamItem; - export interface SvelteMethodReturnItem { /** * The JSDocType of the return value. */ type: JSDocType; + /** * The description of the return value. */ @@ -233,11 +231,14 @@ export interface SvelteMethodReturnItem { export interface SvelteMethodItem extends ISvelteItem { /** * The list of parameter items of the method. + * @since {4.0.0} */ params?: SvelteMethodParamItem[]; + /** * The return item of the method. This exists if an item with 'name' equal * to 'returns' or 'return' exists in 'keywords'. + * @since {4.0.0} */ return?: SvelteMethodReturnItem; } @@ -259,7 +260,7 @@ export interface SvelteComponentItem extends ISvelteItem { * @since Svelte V3 * @since {2.0.0} */ -export type SvelteEventModificator = 'preventDefault' | 'stopPropagation' | 'passive' | 'capture' | 'once'; +export type SvelteEventModificator = 'preventDefault'|'stopPropagation'|'passive'|'capture'|'once'|'nonpassive'; export interface SvelteEventItem extends ISvelteItem { /** @@ -384,7 +385,7 @@ export enum Svelte2Feature { transitions = 'transitions', refs = 'refs', store = 'store', -}; +} /** * Features supported by the Svelte 3 parser. @@ -408,11 +409,6 @@ type SvelteFeatureKeys = Svelte2FeatureKeys & Svelte3FeatureKeys; type Svelte2ExclusiveFeature = Exclude; type Svelte3ExclusiveFeature = Exclude; -/** - * Visibility of a Svelte item. - */ -export type SymbolVisibility = 'private' | 'protected' | 'public'; - /** * Supported Svelte versions. */ @@ -447,7 +443,7 @@ export interface ParserOptions Date: Wed, 2 Dec 2020 02:11:52 -0500 Subject: [PATCH 3/5] fix(options): update typings + refactor error messages --- lib/options.js | 43 +++++++++++++++++++++---------------------- lib/utils.js | 7 ++++++- typings.d.ts | 14 ++++++++------ 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/lib/options.js b/lib/options.js index c0cc6e4..6f2b71e 100644 --- a/lib/options.js +++ b/lib/options.js @@ -1,8 +1,7 @@ -const { isString, printArray, isVisibilitySupported } = require('./utils'); +const { isString, printArray, isVisibilitySupported, VISIBILITIES } = require('./utils'); /** * @typedef {import("../typings").SvelteParserOptions} SvelteParserOptions - * @typedef {import('../typings').JSVisibilityScope} JSVisibilityScope */ /** @type {BufferEncoding[]} */ @@ -19,29 +18,29 @@ const ENCODINGS = [ 'hex' ]; -/** @type {SymbolVisibility[]} */ -const VISIBILITIES = [ - 'public', - 'protected', - 'private' -]; +const ERROR_ENCODING_FORMAT = 'Expected options.encoding to be a string. '; +const ERROR_VISIBILITIES_FORMAT = 'Expected options.ignoredVisibilities to be an array of strings. '; +const INFO_ENCODING_SUPPORTED = `Supported encodings: ${printArray(ENCODINGS)}.`; +const INFO_VISIBILITIES_SUPPORTED = `Supported visibilities: ${printArray(VISIBILITIES)}.`; -const ERROR_OPTIONS_REQUIRED = 'An options object is required.'; -const ERROR_INPUT_REQUIRED = 'One of options.filename or options.fileContent is required.'; -const ERROR_IGNORED_VISIBILITIES_FORMAT = - 'options.ignoredVisibilities must be an array containing only these strings: ' + - printArray(VISIBILITIES); -const ERROR_IGNORED_VISIBILITIES_EXPECTED = - `options.ignoredVisibilities expected any of ${printArray(VISIBILITIES)}, ` + - 'but found these unsupported visibilities: '; +function getUnsupportedEncodingString(enc) { + return `encoding ${printArray([enc])} not supported. ` + + INFO_ENCODING_SUPPORTED; +} + +function getUnsupportedVisibilitiesString(arr) { + return `Visibilities [${printArray(arr)}] in ` + + 'options.ignoredVisibilities are not supported. ' + + INFO_VISIBILITIES_SUPPORTED; +} const ErrorMessage = Object.freeze({ - OptionsRequired: ERROR_OPTIONS_REQUIRED, - InputRequired: ERROR_INPUT_REQUIRED, - EncodingFormat: 'Expected options.encoding to be a string. Use one of: ' + printArray(ENCODINGS), - EncodingNotSupported: (enc) => `encoding '${isString(enc) ? "'" + enc + "'" : enc}' is not supported. options.encoding must be one of: ` + printArray(ENCODINGS), - IgnoredVisibilitiesFormat: ERROR_IGNORED_VISIBILITIES_FORMAT, - IgnoredVisibilitiesNotSupported: (arr) => ERROR_IGNORED_VISIBILITIES_EXPECTED + printArray(arr), + OptionsRequired: 'An options object is required.', + InputRequired: 'One of options.filename or options.fileContent is required.', + EncodingFormat: ERROR_ENCODING_FORMAT + INFO_ENCODING_SUPPORTED, + EncodingNotSupported: (enc) => getUnsupportedEncodingString(enc), + IgnoredVisibilitiesFormat: ERROR_VISIBILITIES_FORMAT + INFO_VISIBILITIES_SUPPORTED, + IgnoredVisibilitiesNotSupported: (arr) => `${getUnsupportedVisibilitiesString(arr)}`, }); /** @type {BufferEncoding} */ diff --git a/lib/utils.js b/lib/utils.js index f1252d5..7df98bd 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,8 @@ -const VISIBILITIES = ['public', 'protected', 'private']; +/** + * @typedef {import('../typings').JSVisibilityScope} JSVisibilityScope + * @type {Readonly} + */ +const VISIBILITIES = Object.freeze(['public', 'protected', 'private']); const RE_VISIBILITY = new RegExp(`^(${VISIBILITIES.join('|')})$`); const RE_KEYWORDS = /@\**\s*([a-z0-9_-]+)(\s+(-\s+)?([\wÀ-ÿ\s*{}[\]()='"`_^$#&²~|\\£¤€%µ,?;.:/!§<>+¨-]+))?/ig; @@ -293,6 +297,7 @@ const printArray = (array) => array.map(s => `'${s}'`).join(', '); const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); +module.exports.VISIBILITIES = VISIBILITIES; module.exports.DEFAULT_VISIBILITY = DEFAULT_VISIBILITY; module.exports.isVisibilitySupported = isVisibilitySupported; module.exports.getVisibility = getVisibility; diff --git a/typings.d.ts b/typings.d.ts index 8f8fdc3..81861a2 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -405,7 +405,7 @@ export enum Svelte3Feature { type Svelte2FeatureKeys = keyof typeof Svelte2Feature; type Svelte3FeatureKeys = keyof typeof Svelte3Feature; -type SvelteFeatureKeys = Svelte2FeatureKeys & Svelte3FeatureKeys; +type SvelteFeatureKeys = Svelte2FeatureKeys | Svelte3FeatureKeys; type Svelte2ExclusiveFeature = Exclude; type Svelte3ExclusiveFeature = Exclude; @@ -414,14 +414,14 @@ type Svelte3ExclusiveFeature = Exclude; */ export type SvelteVersion = 2 | 3; -export interface ParserOptions { +interface BaseParserOptions { /** - * The filename to parse. Required, unless fileContent is passed. + * The filename to parse. Required unless fileContent is passed. */ filename?: string; /** - * The file content to parse. Required, unless filename is passed. + * The file content to parse. Required unless filename is passed. */ fileContent?: string; @@ -476,6 +476,8 @@ export interface ParserOptions | ParserOptions<2, Svelte2FeatureKeys>; +export type SvelteParserOptions = + | BaseParserOptions<3, Svelte3FeatureKeys> + | BaseParserOptions<2, Svelte2FeatureKeys>; From f5d0d3969a3b42c63b469f1904f2f8b2293dd053 Mon Sep 17 00:00:00 2001 From: soft-decay Date: Wed, 2 Dec 2020 02:39:33 -0500 Subject: [PATCH 4/5] fix(options): Add missing error messages - Add default value to printArray --- lib/options.js | 2 ++ lib/utils.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/options.js b/lib/options.js index 6f2b71e..3fc3933 100644 --- a/lib/options.js +++ b/lib/options.js @@ -37,8 +37,10 @@ function getUnsupportedVisibilitiesString(arr) { const ErrorMessage = Object.freeze({ OptionsRequired: 'An options object is required.', InputRequired: 'One of options.filename or options.fileContent is required.', + EncodingMissing: 'Internal Error: options.encoding is not set.', EncodingFormat: ERROR_ENCODING_FORMAT + INFO_ENCODING_SUPPORTED, EncodingNotSupported: (enc) => getUnsupportedEncodingString(enc), + IgnoredVisibilitiesMissing: 'Internal Error: options.ignoredVisibilities is not set.', IgnoredVisibilitiesFormat: ERROR_VISIBILITIES_FORMAT + INFO_VISIBILITIES_SUPPORTED, IgnoredVisibilitiesNotSupported: (arr) => `${getUnsupportedVisibilitiesString(arr)}`, }); diff --git a/lib/utils.js b/lib/utils.js index 7df98bd..59035f3 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -293,7 +293,7 @@ const isTopLevelComment = (comment) => { const isString = (x) => typeof x === 'string' || x instanceof String; -const printArray = (array) => array.map(s => `'${s}'`).join(', '); +const printArray = (array = []) => array.map(s => `'${s}'`).join(', '); const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); From a438a40627bb81b83474465fdf0b63b856e53958 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rousseau Date: Wed, 2 Dec 2020 22:19:06 -0500 Subject: [PATCH 5/5] fix(options): Add integration tests for version auto-detect - revert detectedVersion => version - Fix typedef for FileStructure - Small code changes - Small test changes --- index.js | 10 ++++---- lib/helpers.js | 2 +- lib/options.js | 2 +- lib/parser.js | 1 - test/integration/parse/basicV2.svelte | 3 +++ test/integration/parse/basicV3.svelte | 3 +++ test/integration/parse/parse.spec.js | 35 +++++++++++++++++++++++++++ test/unit/options/options.spec.js | 7 +----- 8 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 test/integration/parse/basicV2.svelte create mode 100644 test/integration/parse/basicV3.svelte create mode 100644 test/integration/parse/parse.spec.js diff --git a/index.js b/index.js index 9b98509..dc5cfd9 100644 --- a/index.js +++ b/index.js @@ -109,9 +109,9 @@ function mergeItems(itemType, currentItem, newItem, ignoreLocations) { return currentItem; } -function subscribeOnParserEvents(parser, ignoredVisibilities, detectedVersion, resolve, reject) { +function subscribeOnParserEvents(parser, ignoredVisibilities, version, resolve, reject) { const component = { - version: detectedVersion + version: version }; parser.features.forEach((feature) => { @@ -195,11 +195,11 @@ module.exports.parse = (options) => new Promise((resolve, reject) => { options.structure = loadFileStructureFromOptions(fileOptions); - const detectedVersion = options.version || SvelteVersionDetector.detectVersionFromStructure(structure, options.defaultVersion); + const version = options.version || SvelteVersionDetector.detectVersionFromStructure(options.structure, options.defaultVersion); - const parser = buildSvelteParser(options, detectedVersion); + const parser = buildSvelteParser(options, version); - subscribeOnParserEvents(parser, options.ignoredVisibilities, detectedVersion, resolve, reject); + subscribeOnParserEvents(parser, options.ignoredVisibilities, version, resolve, reject); parser.walk(); } catch (error) { diff --git a/lib/helpers.js b/lib/helpers.js index 31fdd19..2362d87 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -4,7 +4,7 @@ const path = require('path'); /** * @typedef {import('../typings').SvelteParserOptions} SvelteParserOptions * @typedef {'fileContent' | 'filename' | 'encoding'} FileOptionKeys - * @typedef {{content?: string; template?: string, scripts?: unknown[], styles?: unknown[] }} FileStructure + * @typedef {{content?: string; template?: string, scripts?: HtmlBlock[], styles?: HtmlBlock[] }} FileStructure * @typedef {Pick & { structure?: FileStructure }} FileOptions */ diff --git a/lib/options.js b/lib/options.js index 3fc3933..dbbe408 100644 --- a/lib/options.js +++ b/lib/options.js @@ -42,7 +42,7 @@ const ErrorMessage = Object.freeze({ EncodingNotSupported: (enc) => getUnsupportedEncodingString(enc), IgnoredVisibilitiesMissing: 'Internal Error: options.ignoredVisibilities is not set.', IgnoredVisibilitiesFormat: ERROR_VISIBILITIES_FORMAT + INFO_VISIBILITIES_SUPPORTED, - IgnoredVisibilitiesNotSupported: (arr) => `${getUnsupportedVisibilitiesString(arr)}`, + IgnoredVisibilitiesNotSupported: (arr) => getUnsupportedVisibilitiesString(arr), }); /** @type {BufferEncoding} */ diff --git a/lib/parser.js b/lib/parser.js index bf17524..6137d8d 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -6,7 +6,6 @@ const path = require('path'); const utils = require('./utils'); const jsdoc = require('./jsdoc'); -// TODO: Remove this and use from utils directly const hasOwnProperty = utils.hasOwnProperty; const DEFAULT_OPTIONS = { diff --git a/test/integration/parse/basicV2.svelte b/test/integration/parse/basicV2.svelte new file mode 100644 index 0000000..215ded0 --- /dev/null +++ b/test/integration/parse/basicV2.svelte @@ -0,0 +1,3 @@ + diff --git a/test/integration/parse/basicV3.svelte b/test/integration/parse/basicV3.svelte new file mode 100644 index 0000000..7534c7d --- /dev/null +++ b/test/integration/parse/basicV3.svelte @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/integration/parse/parse.spec.js b/test/integration/parse/parse.spec.js new file mode 100644 index 0000000..46e67c2 --- /dev/null +++ b/test/integration/parse/parse.spec.js @@ -0,0 +1,35 @@ +const path = require('path'); +const chai = require('chai'); +const expect = chai.expect; + +const parser = require('./../../../index'); + +describe('parse - Integration', () => { + it('should correctly auto-detect svelte V2 component', (done) => { + parser.parse({ + filename: path.resolve(__dirname, 'basicV2.svelte'), + }).then((doc) => { + expect(doc, 'Document should exist').to.exist; + // v3-parser converts component name to kebab-case + expect(doc.name).to.equal('basic-v2'); + + done(); + }).catch(e => { + done(e); + }); + }); + + it('should correctly auto-detect svelte V3 component', (done) => { + parser.parse({ + filename: path.resolve(__dirname, 'basicV3.svelte'), + }).then((doc) => { + expect(doc, 'Document should be provided').to.exist; + // v3-parser converts component name to PascalCase + expect(doc.name).to.equal('BasicV3'); + + done(); + }).catch(e => { + done(e); + }); + }); +}); diff --git a/test/unit/options/options.spec.js b/test/unit/options/options.spec.js index 5225566..76d2f0a 100644 --- a/test/unit/options/options.spec.js +++ b/test/unit/options/options.spec.js @@ -90,12 +90,7 @@ describe('Options Module', () => { describe('options.retrieveFileOptions', () => { it('Should return all file-related keys from options', () => { - const options = { - ...baseOptions, - ignoredVisibilities: ['protected', 'public'] - }; - - expect(retrieveFileOptions(options)).to.have.keys( + expect(retrieveFileOptions(baseOptions)).to.have.keys( 'filename', 'fileContent', 'structure', 'encoding' ); });