diff --git a/packages/angular-demo/.storybook/main.js b/packages/angular-demo/.storybook/main.ts similarity index 64% rename from packages/angular-demo/.storybook/main.js rename to packages/angular-demo/.storybook/main.ts index 266a8cb..f6ac63d 100644 --- a/packages/angular-demo/.storybook/main.js +++ b/packages/angular-demo/.storybook/main.ts @@ -1,14 +1,18 @@ -const rootMain = require('../../../.storybook/main'); +import { StorybookConfig } from '@storybook/angular'; + const WebpackAngularTypesPlugin = require('../../../dist/packages/storybook-webpack-angular-types-plugin/index').WebpackAngularTypesPlugin; -module.exports = { - ...rootMain, - - core: { builder: 'webpack5' }, - +const config: StorybookConfig = { + framework: { + name: '@storybook/angular', + options: {}, + }, + core: { + builder: '@storybook/builder-webpack5', + }, stories: ['../src/app/**/*.mdx', '../src/app/**/*.stories.@(js|jsx|ts|tsx)'], - addons: ['@storybook/addon-essentials'], + addons: ['@storybook/addon-docs'], webpackFinal: async (config, { configType }) => { // add your own webpack tweaks if needed config.plugins.push( @@ -20,3 +24,5 @@ module.exports = { return config; }, }; + +export default config; diff --git a/packages/angular-demo/src/app/app.component.ts b/packages/angular-demo/src/app/app.component.ts index e06a841..2644de9 100644 --- a/packages/angular-demo/src/app/app.component.ts +++ b/packages/angular-demo/src/app/app.component.ts @@ -23,6 +23,7 @@ export class AppComponent { amount: 5.99, unit: '€', }, + hiddenProperty: '', }; /** diff --git a/packages/angular-demo/src/app/parent.directive.ts b/packages/angular-demo/src/app/parent.directive.ts index 1adfad7..35e1529 100644 --- a/packages/angular-demo/src/app/parent.directive.ts +++ b/packages/angular-demo/src/app/parent.directive.ts @@ -3,13 +3,14 @@ import { Directive, EventEmitter, Input } from '@angular/core'; import { TestInterface } from './internal-types'; +import { GreatGrandParentDirective } from '@scope/entrypoint'; interface X { x: T; } @Directive() -export abstract class GrandParentDirective { +export abstract class GrandParentDirective extends GreatGrandParentDirective { @Input() nestedGenericTypeParameterInput?: EventEmitter>; } diff --git a/packages/angular-demo/src/app/stories/interface.mdx b/packages/angular-demo/src/app/stories/interface.mdx index 9e03661..fc5dc66 100644 --- a/packages/angular-demo/src/app/stories/interface.mdx +++ b/packages/angular-demo/src/app/stories/interface.mdx @@ -13,6 +13,20 @@ Interfaces need to be annotated with `@include-docs` for the type information to > > Add at least one **used** constant or function declaration to the file containing the interface. +To exclude certain properties, annotate them with the `@exclude-docs` annotation: + +```typescript +/** + * @include-docs Product + */ +export interface Product { + /** + * @exclude-docs + */ + hiddenProperty: unknown; +} +``` + ## Aliases diff --git a/packages/angular-demo/src/app/types.ts b/packages/angular-demo/src/app/types.ts index 9085603..ca1630e 100644 --- a/packages/angular-demo/src/app/types.ts +++ b/packages/angular-demo/src/app/types.ts @@ -7,6 +7,11 @@ export interface UndocumentedSecret { */ export interface Product extends Item { price: Price; + + /** + * @exclude-docs + */ + hiddenProperty?: unknown; } export interface Item { diff --git a/packages/angular-demo/src/entrypoint/great-grand-parent.directive.ts b/packages/angular-demo/src/entrypoint/great-grand-parent.directive.ts new file mode 100644 index 0000000..346f8a8 --- /dev/null +++ b/packages/angular-demo/src/entrypoint/great-grand-parent.directive.ts @@ -0,0 +1,19 @@ +import { Directive, input, output } from '@angular/core'; + +@Directive({}) +export abstract class GreatGrandParentDirective { + /** + * An input from a Directive imported from a path alias + */ + libInput = input(); + + /** + * An output from a Directive imported from a path alias + */ + libOutput = output(); + + /** + * A property from a Directive imported from a path alias + */ + libProperty = 42; +} diff --git a/packages/angular-demo/src/entrypoint/index.ts b/packages/angular-demo/src/entrypoint/index.ts new file mode 100644 index 0000000..5cb4cba --- /dev/null +++ b/packages/angular-demo/src/entrypoint/index.ts @@ -0,0 +1 @@ +export { GreatGrandParentDirective } from './great-grand-parent.directive'; diff --git a/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/plugin.ts b/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/plugin.ts index d183c95..64e4e5e 100644 --- a/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/plugin.ts +++ b/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/plugin.ts @@ -1,8 +1,8 @@ -import { getTsconfig } from 'get-tsconfig'; +import { getTsconfig, TsConfigJsonResolved } from 'get-tsconfig'; import * as micromatch from 'micromatch'; import * as path from 'path'; import * as process from 'process'; -import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from 'ts-morph'; +import { Project } from 'ts-morph'; import { Compiler, Module } from 'webpack'; import { DEFAULT_TS_CONFIG_PATH, PLUGIN_NAME } from '../constants'; import { @@ -23,14 +23,24 @@ import { } from './templating/arg-code-block-templates'; import { getPrototypeClassIdCodeBlock } from './templating/prototype-class-id-code-block-template'; import { generateTypeInformation } from './type-extraction/type-extraction'; +import { parseModuleKind, parseModuleResolution, parseScriptTarget } from './ts-morph-helpers'; export class WebpackAngularTypesPlugin { // A queue for modules, that should be processed by the plugin in the next seal-hook private moduleQueue: Module[] = []; - private readonly tsconfigPaths: string[] = this.getTsconfigPaths(); + private readonly tsconfigPath: string; + private readonly tsconfig: TsConfigJsonResolved; + + private readonly includedPaths: string[]; + + constructor(private options: WebpackAngularTypesPluginOptions = {}) { + this.tsconfigPath = this.options.tsconfigPath ?? DEFAULT_TS_CONFIG_PATH; + this.tsconfig = getTsconfig(this.tsconfigPath)?.config ?? {}; + + this.includedPaths = this.getIncludedPathsFromTsconfig(this.tsconfigPath, this.tsconfig); + } - constructor(private options: WebpackAngularTypesPluginOptions = {}) {} apply(compiler: Compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { compilation.dependencyTemplates.set(CodeDocDependency, new CodeDocDependencyTemplate()); @@ -43,11 +53,13 @@ export class WebpackAngularTypesPlugin { compilation.hooks.seal.tap(PLUGIN_NAME, () => { const smallTsProject = new Project({ - // TODO this should be taken from the specified storybook tsconfig in the future compilerOptions: { - module: ModuleKind.ES2020, - target: ScriptTarget.ESNext, - moduleResolution: ModuleResolutionKind.NodeJs, + module: parseModuleKind(this.tsconfig.compilerOptions?.module), + target: parseScriptTarget(this.tsconfig.compilerOptions?.target), + moduleResolution: parseModuleResolution( + this.tsconfig.compilerOptions?.moduleResolution, + ), + paths: this.tsconfig.compilerOptions?.paths ?? {}, }, }); const modulesToProcess = this.moduleQueue @@ -129,10 +141,7 @@ export class WebpackAngularTypesPlugin { const codeDocDependency = new CodeDocDependency( alias, uniqueId, - getNonClassArgCodeBlock( - alias, - interfaceInformation.entitiesByCategory, - ), + getNonClassArgCodeBlock(alias, interfaceInformation.entitiesByCategory), ); module.addDependency(codeDocDependency); } @@ -161,7 +170,7 @@ export class WebpackAngularTypesPlugin { } private isPathIncludedInTsConfig(pathToCheck: string): boolean { - const res = micromatch([pathToCheck], this.tsconfigPaths, { + const res = micromatch([pathToCheck], this.includedPaths, { format: this.toUnixPath, }); return res.length === 1; @@ -179,13 +188,15 @@ export class WebpackAngularTypesPlugin { }; } - private getTsconfigPaths(): string[] { - const tsconfigPath = this.options.tsconfigPath ?? DEFAULT_TS_CONFIG_PATH; - const tsconfigResult = getTsconfig(tsconfigPath); - // + private getIncludedPathsFromTsconfig( + tsconfigPath: string, + tsconfig: TsConfigJsonResolved, + ): string[] { const tsConfigRootDir = path.join(process.cwd(), tsconfigPath, '..'); - const includedPaths = tsconfigResult?.config.include || []; - const excludedPaths = tsconfigResult?.config.exclude || []; + + const includedPaths = tsconfig.include || []; + const excludedPaths = tsconfig.exclude || []; + return [ ...this.transformToAbsolutePaths(tsConfigRootDir, includedPaths), ...this.transformToAbsolutePaths(tsConfigRootDir, excludedPaths).map(this.negateGlob), diff --git a/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/ts-morph-helpers.ts b/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/ts-morph-helpers.ts index b3afbf5..3ca5ce3 100644 --- a/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/ts-morph-helpers.ts +++ b/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/ts-morph-helpers.ts @@ -1,7 +1,7 @@ // These are ts-morph helper functions that are no publicly exported but should // bet rather used to get insights/help during development. -import { Type, TypeFormatFlags } from 'ts-morph'; +import { ModuleKind, ModuleResolutionKind, ScriptTarget, Type, TypeFormatFlags } from 'ts-morph'; /** * Prints a type with all available formatters. This helps to understand what @@ -59,3 +59,89 @@ export function typeSummary(type: Type): object { data.push({ check: 'Has alias symbol', result: !!type.getAliasSymbol() }); return data; } + +export function parseModuleKind(moduleString: string | undefined): ModuleKind { + switch (moduleString?.toLowerCase()) { + case 'commonjs': + return ModuleKind.CommonJS; + case 'amd': + return ModuleKind.AMD; + case 'umd': + return ModuleKind.UMD; + case 'system': + return ModuleKind.System; + case 'es6': + case 'es2015': + return ModuleKind.ES2015; + case 'es2020': + return ModuleKind.ES2020; + case 'es2022': + return ModuleKind.ES2022; + case 'esnext': + return ModuleKind.ESNext; + case 'node16': + return ModuleKind.Node16; + case 'nodenext': + return ModuleKind.NodeNext; + default: + console.warn( + `[WebpackAngularTypesPlugin]: Invalid or unknown "compilerOptions.module" retrieved from tsconfig: ${moduleString}. Defaulting to "es2020".`, + ); + return ModuleKind.ES2020; + } +} +export function parseScriptTarget(targetString: string | undefined): ScriptTarget { + switch (targetString?.toLowerCase()) { + case 'es3': + return ScriptTarget.ES3; + case 'es5': + return ScriptTarget.ES5; + case 'es6': + case 'es2015': + return ScriptTarget.ES2015; + case 'es2016': + return ScriptTarget.ES2016; + case 'es2017': + return ScriptTarget.ES2017; + case 'es2018': + return ScriptTarget.ES2018; + case 'es2019': + return ScriptTarget.ES2019; + case 'es2020': + return ScriptTarget.ES2020; + case 'es2021': + return ScriptTarget.ES2021; + case 'es2022': + return ScriptTarget.ES2022; + case 'esnext': + return ScriptTarget.ESNext; + default: + console.warn(` + [WebpackAngularTypesPlugin]: Invalid or unknown "compilerOptions.target" retrieved from tsconfig: ${targetString}. Defaulting to "esnext". + `); + return ScriptTarget.ESNext; + } +} + +export function parseModuleResolution( + moduleResolutionString: string | undefined, +): ModuleResolutionKind { + switch (moduleResolutionString?.toLowerCase()) { + case 'classic': + return ModuleResolutionKind.Classic; + case 'node': + case 'nodejs': + return ModuleResolutionKind.NodeJs; + case 'node16': + return ModuleResolutionKind.Node16; + case 'nodenext': + return ModuleResolutionKind.NodeNext; + case 'bundler': + return ModuleResolutionKind.Bundler; + default: + console.warn( + `[WebpackAngularTypesPlugin]: Invalid or unknown "compilerOptions.moduleResolution" retrieved from tsconfig: ${moduleResolutionString}. Defaulting to "nodejs".`, + ); + return ModuleResolutionKind.NodeJs; + } +} diff --git a/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/type-extraction/interface-type-extraction.ts b/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/type-extraction/interface-type-extraction.ts index c94bc5e..c1b54c0 100644 --- a/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/type-extraction/interface-type-extraction.ts +++ b/packages/storybook-webpack-angular-types-plugin/src/lib/webpack-angular-types-plugin/type-extraction/interface-type-extraction.ts @@ -1,7 +1,7 @@ import { EntitiesByCategory, Entity, InterfaceInformation } from '../../types'; import { InterfaceDeclaration } from 'ts-morph'; -import { groupBy } from '../utils'; -import { collectBaseInterfaces, getJsDocsIncludeDocsAliases } from './ast-utils'; +import { EXCLUDE_DOCS_JS_DOCS_PARAM, groupBy } from '../utils'; +import { collectBaseInterfaces, getJsDocsIncludeDocsAliases, hasJsDocsTag } from './ast-utils'; import { mapSignatureToEntity } from './signature-mappers'; import { getterOrSetterInputExists, mergeEntities } from './utils'; @@ -17,6 +17,10 @@ function getInterfaceEntities( const entities = new Map(); for (const signature of [...properties, ...methods]) { + if (hasJsDocsTag(signature, EXCLUDE_DOCS_JS_DOCS_PARAM)) { + continue; + } + // do not include the property if it passes the exclusion test if (propertiesToExclude?.test(signature.getName())) { continue; diff --git a/tsconfig.base.json b/tsconfig.base.json index bad612a..5bdbc7f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,7 +20,8 @@ ], "storybook-webpack-angular-types-plugin/extract-arg-types": [ "packages/storybook-webpack-angular-types-plugin/extract-arg-types.ts" - ] + ], + "@scope/entrypoint": ["packages/angular-demo/src/entrypoint/index.ts"] } }, "exclude": ["node_modules", "tmp"]