Skip to content

Commit

Permalink
feat: implement @docsId and @docsIncludeIds JSDoc tags to capture…
Browse files Browse the repository at this point in the history
… documentation relationships in the manifest (#3063)
  • Loading branch information
Blackbaud-SteveBrush authored Jan 29, 2025
1 parent 847c970 commit ba33999
Show file tree
Hide file tree
Showing 23 changed files with 478 additions and 43 deletions.
1 change: 1 addition & 0 deletions .github/workflows/validate-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ jobs:
components/list-builder-view-grids
components/lists
components/lookup
components/manifest
components/modals
components/navbar
components/packages
Expand Down
3 changes: 2 additions & 1 deletion libs/components/manifest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"tslib": "^2.8.1"
},
"optionalDependencies": {
"typedoc": "~0.26.11"
"typedoc": "~0.26.11",
"typescript": "5.6.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`get-public-api should return the public API for a specific docs ID 1`] = `
Object {
"packages": Object {
"@company/components": Array [
Object {
"docsId": "bar",
"docsIncludeIds": Array [
"foo",
"FooTesting",
"FooTestingInternal",
],
"isInternal": false,
},
Object {
"docsId": "foo",
"isInternal": false,
},
],
"@company/components/testing": Array [
Object {
"docsId": "FooTesting",
"isInternal": false,
},
],
},
}
`;

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions libs/components/manifest/src/generator/assign-docs-include-ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import fsPromises from 'node:fs/promises';
import * as ts from 'typescript';

import { type PackagesMap } from './get-public-api';

/**
* Assign `docsIncludeIds` for each entry-point module, based on the types
* included in its `exports` array.
*/
export async function assignDocsIncludeIds(
packages: PackagesMap,
): Promise<void> {
for (const [, definitions] of packages) {
for (const definition of definitions) {
if (
definition.kind !== 'module' ||
definition.docsIncludeIds ||
definition.isInternal
) {
continue;
}

const contents = await fsPromises.readFile(definition.filePath, 'utf-8');

const sourceFile = ts.createSourceFile(
definition.filePath,
contents,
ts.ScriptTarget.Latest,
);

let moduleExports: string[] = [];

// Get the exports from the module.
ts.forEachChild(sourceFile, (node) => {
if (
ts.isClassDeclaration(node) &&
node.name?.escapedText === definition.name
) {
ts.getDecorators(node)?.[0].expression.forEachChild((child) => {
if (ts.isObjectLiteralExpression(child)) {
child.properties.forEach((property) => {
if (
ts.isPropertyAssignment(property) &&
ts.isIdentifier(property.name) &&
property.name.escapedText === 'exports' &&
ts.isArrayLiteralExpression(property.initializer)
) {
property.initializer.elements.forEach((element) => {
if (ts.isIdentifier(element)) {
const exportName = element.escapedText.toString();

if (
exportName.startsWith('Sky') &&
definitions.some(
(definition) => definition.name === exportName,
)
) {
moduleExports.push(element.escapedText.toString());
}
}
});
}
});
}
});
}
});

moduleExports = moduleExports.map((exportName) => {
// Map the export name to its corresponding docsId.
const definition = definitions.find(
(definition) => definition.name === exportName,
);

/* istanbul ignore next: safety check */
if (!definition?.docsId) {
throw new Error(`Missing @docsId for ${exportName}.`);
}

return definition.docsId;
});

definition.docsIncludeIds = moduleExports;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ function setup(options: { outDirExists: boolean }): {
});

jest.mock('node:fs/promises', () => {
const originalModule = jest.requireActual('node:fs/promises');

return {
...originalModule,
mkdir: mkdirMock,
writeFile: writeFileMock,
};
Expand Down Expand Up @@ -67,7 +70,7 @@ describe('generate-manifest', () => {
});

expect(writeFileMock).toMatchSnapshot();
});
}, 60000);

it('should create the out directory if it does not exist', async () => {
const { mkdirMock } = setup({
Expand All @@ -83,5 +86,5 @@ describe('generate-manifest', () => {
});

expect(mkdirMock).toHaveBeenCalledWith('/dist');
});
}, 60000);
});
7 changes: 6 additions & 1 deletion libs/components/manifest/src/generator/get-public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type DeclarationReflection, ReflectionKind } from 'typedoc';
import type { SkyManifestParentDefinition } from '../types/base-def';
import type { SkyManifestPublicApi } from '../types/manifest';

import { assignDocsIncludeIds } from './assign-docs-include-ids';
import { getEntryPointsReflections } from './get-entry-points-reflections';
import { ProjectDefinition } from './get-project-definitions';
import { getClass } from './utility/get-class';
Expand Down Expand Up @@ -123,7 +124,9 @@ export async function getPublicApi(
const items = packages.get(entryName) ?? [];

for (const child of reflection.children) {
const filePath = child.sources?.[0].fileName;
const filePath = child.sources?.[0].fullFileName
.replace(process.cwd(), '')
.slice(1);

/* istanbul ignore if: safety check */
if (!filePath || filePath.endsWith('/index.ts')) {
Expand All @@ -137,6 +140,8 @@ export async function getPublicApi(
}
}

await assignDocsIncludeIds(packages);

return {
packages: Object.fromEntries(packages),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { Foo1, FooWithOnlyLetters, ____ } from './lib/anchor-id';
export { ____, Foo1, FooWithOnlyLetters } from './lib/anchor-id';
export {
shouldHaveCodeExampleDefaultLanguageComment,
shouldHaveCodeExampleMarkupLanguageComment,
Expand All @@ -13,19 +13,25 @@ export {
FooBasicTypeParamClass,
FooBasicTypeParamDefaultValueClass,
FooClass,
FooWithDocsIdOverrideClass,
FooWithEmptyDocsIncludeIdsClass,
FooWithStaticPropertiesClass,
} from './lib/foo.class';
export { FooComponent as λ1 } from './lib/foo.component';
export {
SkyFooNonStandaloneComponent,
SkyFooStandaloneComponent,
FooComponent as λ1,
} from './lib/foo.component';
export { FOO_BREAKPOINTS, FooBreakpoint } from './lib/foo.const-assertion';
export {
λ2,
FooDirective,
FooWithInputsOutputsDirective,
λ2,
} from './lib/foo.directive';
export { FooEnum } from './lib/foo.enum';
export { createFoo } from './lib/foo.function';
export { FooEmptyInterface, FooInterface } from './lib/foo.interface';
export { FooModule } from './lib/foo.module';
export { FooModule, FooWithExportsModule } from './lib/foo.module';
export { FooPipe } from './lib/foo.pipe';
export { FooService } from './lib/foo.service';
export { FooAlias, FooReferenceTypeAlias } from './lib/foo.type-alias';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,14 @@ export abstract class FooWithStaticPropertiesClass {
*/
public abstract someAbstractMethod(): void;
}

/**
* @docsId docs-id-override
* @docsIncludeIds foo, bar, baz
*/
export class FooWithDocsIdOverrideClass {}

/**
* @docsIncludeIds
*/
export class FooWithEmptyDocsIncludeIdsClass {}
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,26 @@ export class FooComponent {
@Output()
public onTouch = new EventEmitter<void>();
}

/**
* This is a non-standalone component with a Sky prefix. We need to include the
* prefix so that it's included in the owning module's docsIncludeIds array.
*/
@Component({
selector: 'sky-foo-non-standalone',
standalone: false,
template: ``,
})
export class SkyFooNonStandaloneComponent {}

/**
* This is a standalone component with a Sky prefix and an overridden docsId.
* We need to include the prefix so that it's included in the owning module's
* docsIncludeIds array.
* @docsId sky-foo-standalone-override
*/
@Component({
selector: 'sky-foo-standalone',
template: ``,
})
export class SkyFooStandaloneComponent {}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { ModuleWithProviders, NgModule } from '@angular/core';

import {
FooComponent,
SkyFooNonStandaloneComponent,
SkyFooStandaloneComponent,
} from './foo.component';

@NgModule({})
export class FooModule {
public forRoot(): ModuleWithProviders<FooModule> {
Expand All @@ -9,3 +15,14 @@ export class FooModule {
};
}
}

/**
* This module should automatically generate values for docsIncludeIds based on
* its exports.
*/
@NgModule({
declarations: [SkyFooNonStandaloneComponent],
imports: [FooComponent, SkyFooStandaloneComponent],
exports: [SkyFooStandaloneComponent, SkyFooNonStandaloneComponent],
})
export class FooWithExportsModule {}
4 changes: 4 additions & 0 deletions libs/components/manifest/src/generator/utility/get-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export function getClass(
codeExampleLanguage,
deprecationReason,
description,
docsId,
docsIncludeIds,
isDeprecated,
isInternal,
isPreview,
Expand All @@ -135,6 +137,8 @@ export function getClass(
codeExampleLanguage,
deprecationReason,
description,
docsId: docsId ?? reflection.name,
docsIncludeIds,
filePath,
isDeprecated,
isInternal,
Expand Down
31 changes: 31 additions & 0 deletions libs/components/manifest/src/generator/utility/get-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface SkyManifestComment {
defaultValue?: string;
deprecationReason?: string;
description?: string;
docsId?: string;
docsIncludeIds?: string[];
isDeprecated?: boolean;
isInternal?: boolean;
isPreview?: boolean;
Expand Down Expand Up @@ -62,6 +64,14 @@ function getCodeExample(comment: CommentTag): {
return { codeExample, codeExampleLanguage };
}

function getDocsIncludeIds(tag: CommentTag): string[] {
return (
getCommentTagText(tag.content)
?.split(',')
.map((id) => id.trim()) || []
);
}

/**
* Gets information about the reflection's JSDoc comment block.
*/
Expand All @@ -74,6 +84,8 @@ export function getComment(reflection: {
let defaultValue: string | undefined;
let deprecationReason: string | undefined;
let description: string | undefined;
let docsId: string | undefined;
let docsIncludeIds: string[] | undefined;
let isDeprecated: boolean | undefined;
let isInternal: boolean | undefined;
let isPreview: boolean | undefined;
Expand Down Expand Up @@ -105,6 +117,23 @@ export function getComment(reflection: {
break;
}

case '@docsId': {
const docsIdFromComment = getCommentTagText(tag.content);

/* istanbul ignore next: safety check */
if (!docsIdFromComment) {
throw new Error(`A @docsId tag must have a value.`);
}

docsId = docsIdFromComment;
break;
}

case '@docsIncludeIds': {
docsIncludeIds = getDocsIncludeIds(tag);
break;
}

case '@example': {
const example = getCodeExample(tag);
codeExample = example.codeExample;
Expand Down Expand Up @@ -140,6 +169,8 @@ export function getComment(reflection: {
defaultValue,
deprecationReason,
description,
docsId,
docsIncludeIds,
isDeprecated,
isInternal,
isPreview,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export function getDirective(
codeExampleLanguage,
deprecationReason,
description,
docsId,
docsIncludeIds,
isDeprecated,
isInternal,
isPreview,
Expand All @@ -133,6 +135,8 @@ export function getDirective(
codeExampleLanguage,
deprecationReason,
description,
docsId: docsId ?? directiveName,
docsIncludeIds,
filePath,
isDeprecated,
isInternal,
Expand Down
Loading

0 comments on commit ba33999

Please sign in to comment.