Skip to content

Commit 3619464

Browse files
authored
docs: fix api generation (#332)
1 parent 0c62e1e commit 3619464

File tree

9 files changed

+233
-162
lines changed

9 files changed

+233
-162
lines changed

scripts/api-generator.mts

Lines changed: 75 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,26 @@
1-
import { fileURLToPath } from 'url';
21
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
32
import { basename, dirname, join, normalize, relative } from 'path';
3+
import { fileURLToPath } from 'url';
44

55
const root = fileURLToPath(new URL('../', import.meta.url));
66
const documentation = JSON.parse(readFileSync(join(root, `/src/docs/documentation.json`), 'utf8'));
77
const modulesWithLegacySubmodules = ['checkbox', 'link-list', 'radio-button'];
8+
const ignoredFolders = ['core'];
89

910
/**
10-
* The configuration object used to merge different `api` files in a single one for the angular project.
11-
*/
12-
const mergeConfigAngular = {
13-
alert: ['alert', 'alert-group'],
14-
breadcrumb: ['breadcrumb', 'breadcrumb-group'],
15-
button: [
16-
'button',
17-
'button-link',
18-
'button-static',
19-
'accent-button',
20-
'accent-button-link',
21-
'accent-button-static',
22-
'secondary-button',
23-
'secondary-button-link',
24-
'secondary-button-static',
25-
'transparent-button',
26-
'transparent-button-link',
27-
'transparent-button-static',
28-
'mini-button',
29-
'mini-button-group',
30-
],
31-
card: ['card', 'card-button', 'card-link', 'card-badge'],
32-
carousel: ['carousel', 'carousel-list', 'carousel-item'],
33-
chip: ['chip', 'chip-group'],
34-
container: ['container', 'sticky-bar'],
35-
datepicker: ['datepicker', 'datepicker-toggle', 'datepicker-previous-day', 'datepicker-next-day'],
36-
dialog: ['dialog', 'dialog-title', 'dialog-content', 'dialog-actions', 'dialog-close-button'],
37-
'expansion-panel': ['expansion-panel', 'expansion-panel-header', 'expansion-panel-content'],
38-
'file-selector': ['file-selector', 'file-selector-dropzone'],
39-
'flip-card': ['flip-card', 'flip-card-summary', 'flip-card-details'],
40-
'form-field': ['form-field', 'form-field-clear', 'error'],
41-
header: ['header', 'header-button', 'header-link', 'header-environment'],
42-
link: [
43-
'link',
44-
'link-button',
45-
'link-static',
46-
'block-link',
47-
'block-link-button',
48-
'block-link-static',
49-
],
50-
menu: ['menu', 'menu-button', 'menu-link'],
51-
'mini-calendar': ['mini-calendar', 'mini-calendar-month', 'mini-calendar-day'],
52-
navigation: [
53-
'navigation',
54-
'navigation-section',
55-
'navigation-list',
56-
'navigation-marker',
57-
'navigation-link',
58-
'navigation-button',
59-
],
60-
option: ['option', 'optgroup', 'option-hint'],
61-
paginator: ['paginator', 'compact-paginator'],
62-
sidebar: [
63-
'sidebar',
64-
'sidebar-container',
65-
'sidebar-content',
66-
'sidebar-title',
67-
'sidebar-close-button',
68-
'icon-sidebar',
69-
'icon-sidebar-container',
70-
'icon-sidebar-content',
71-
'icon-sidebar-button',
72-
'icon-sidebar-link',
73-
],
74-
stepper: ['stepper', 'step', 'step-label'],
75-
table: ['table', 'table-wrapper', 'sort'],
76-
tab: ['tab', 'tab-group', 'tab-label'],
77-
tag: ['tag', 'tag-group'],
78-
teaser: ['teaser', 'teaser-hero', 'teaser-product', 'teaser-product-static'],
79-
'timetable-form': [
80-
'timetable-form',
81-
'timetable-form-field',
82-
'timetable-form-details',
83-
'timetable-form-swap-button',
84-
],
85-
timetable: [
86-
'train-formation',
87-
'train',
88-
'train-wagon',
89-
'train-blocked-passage',
90-
'timetable-occupancy',
91-
'timetable-occupancy-icon',
92-
],
93-
toggle: ['toggle', 'toggle-option', 'toggle-check'],
94-
};
95-
96-
/**
97-
* The configuration object used to merge different `api` files in a single one for the angular-experimental project.
11+
* Reads the module names for a given package from meta.ts.
12+
* For each module listed in PACKAGES (from meta.ts), all api files are merged into a single file.
9813
*/
99-
const mergeConfigAngularExperimental = {
100-
'autocomplete-grid': [
101-
'autocomplete-grid',
102-
'autocomplete-grid-row',
103-
'autocomplete-grid-optgroup',
104-
'autocomplete-grid-option',
105-
'autocomplete-grid-cell',
106-
'autocomplete-grid-button',
107-
],
108-
'seat-reservation': [
109-
'seat-reservation-area',
110-
'seat-reservation-graphic',
111-
'seat-reservation-navigation-coach',
112-
'seat-reservation-navigation-services',
113-
'seat-reservation-place-control',
114-
'seat-reservation-scoped',
115-
],
116-
};
117-
118-
const mergeConfig: Record<string, Record<string, string[]>> = {
119-
angular: mergeConfigAngular,
120-
'angular-experimental': mergeConfigAngularExperimental,
14+
const getModuleNamesFromMeta = async (projectFolder: string): Promise<string[]> => {
15+
const { PACKAGES } = await import('../src/docs/app/shared/meta.js');
16+
const pkg = PACKAGES[projectFolder];
17+
if (!pkg) {
18+
return [];
19+
}
20+
return pkg.sections
21+
.flatMap((s) => s.entries)
22+
.map((e) => e.link.split('/').at(-1)!)
23+
.filter(Boolean);
12124
};
12225

12326
/**
@@ -130,68 +33,93 @@ const mergeConfig: Record<string, Record<string, string[]>> = {
13033
*
13134
* @param projectFolder the name of the package (angular / angular-experimental / ...)
13235
*/
133-
const generateApiFiles = (projectFolder: string) => {
36+
const generateApiFiles = async (projectFolder: string) => {
13437
const projectPath = join(root, 'src', projectFolder);
13538
const outputPath = join(root, 'src/docs/app', projectFolder, 'api');
13639
if (existsSync(outputPath)) {
13740
rmSync(outputPath, { recursive: true, force: true });
13841
}
13942
mkdirSync(outputPath, { recursive: true });
140-
scanFoldersAndWriteFiles(projectPath, outputPath);
141-
mergeFilesInModule(outputPath, mergeConfig[projectFolder]);
43+
await mergeFilesInModule(projectPath, outputPath, projectFolder);
14244
};
14345

14446
/**
14547
* Recursive function which reaches the innermost folder and creates a `.md` file
14648
* with the documentation of the objects from compodoc that matches the final path.
49+
* Returns the list of generated api file names.
14750
*
14851
* @param projectPath path of the source package
14952
* @param apiFolder path of the output folder
15053
*/
151-
const scanFoldersAndWriteFiles = (projectPath: string, apiFolder: string) => {
54+
const scanFoldersAndWriteFiles = (projectPath: string, apiFolder: string): string[] => {
15255
const folders = readdirSync(projectPath, { withFileTypes: true });
15356
const subFolders = folders.filter((e) => e.isDirectory());
154-
155-
// Inner folder reached
156-
if (
157-
subFolders.length === 0 ||
158-
modulesWithLegacySubmodules.some((m) => projectPath.endsWith(`/${m}`))
159-
) {
160-
// Scan the documentation file and possibly create the docs file.
57+
const currentName = basename(projectPath);
58+
const isLegacy = modulesWithLegacySubmodules.some((m) => projectPath.endsWith(`/${m}`));
59+
const hasSameNamedSubFolder = subFolders.some((e) => e.name === currentName);
60+
const generated: string[] = [];
61+
62+
// Scan the current folder unless it has a same-named sub-folder whose docs would
63+
// duplicate it – except for legacy modules which always own their own docs directly.
64+
if (isLegacy || !hasSameNamedSubFolder) {
16165
const readmeContent = createReadmeAPI(relative(root, normalize(projectPath)));
16266
if (readmeContent) {
163-
const apiFileName = `${basename(projectPath)}-api.md`;
164-
const outPath = join(apiFolder, apiFileName);
165-
writeFileSync(outPath, readmeContent, { encoding: 'utf-8', flag: 'a' });
67+
const apiFileName = `${currentName}-api.md`;
68+
writeFileSync(join(apiFolder, apiFileName), readmeContent, { encoding: 'utf-8', flag: 'a' });
69+
generated.push(apiFileName);
16670
}
167-
return;
16871
}
16972

170-
// Inner folder not reached, go deeper recursively
73+
// Stop recursion at leaf folders or legacy submodule roots
74+
if (subFolders.length === 0 || isLegacy) {
75+
return generated;
76+
}
77+
78+
// Go deeper recursively into all sub-folders, skipping ignored ones
17179
for (const sub of subFolders) {
172-
const subPath = join(projectPath, sub.name);
173-
scanFoldersAndWriteFiles(subPath, apiFolder);
80+
if (!ignoredFolders.includes(sub.name)) {
81+
generated.push(...scanFoldersAndWriteFiles(join(projectPath, sub.name), apiFolder));
82+
}
17483
}
84+
85+
return generated;
17586
};
17687

17788
/**
178-
* Based on the provided `config` object, it creates a single file from several ones.
179-
* The config's values are mapped as `<config.value[i]>-api.md` files,
180-
* then these files are read and joined as a single file, named as `<config.key>-api.md`.
181-
* @param path the project path
182-
* @param config the key-values object used to generate the file
89+
* For each module listed in meta.ts for the given package, merges all generated
90+
* `*-api.md` files that belong to that module's folder into a single `<module>-api.md`.
91+
*
92+
* The belonging files are determined by re-scanning `src/<projectFolder>/<moduleName>/`
93+
* – which is identical to what scanFoldersAndWriteFiles already did – so we reuse it
94+
* in dry-run mode (no apiFolder writing needed, we only need the file names).
18395
*/
184-
const mergeFilesInModule = (path: string, config: Record<string, string[]>): void => {
185-
Object.entries(config).forEach(([mainFile, subFiles]: [string, string[]]) => {
186-
let outputDoc = '';
187-
subFiles
188-
.map((fileName) => join(path, `${fileName}-api.md`))
189-
.forEach((file) => {
190-
outputDoc += readFileSync(file, 'utf8');
191-
rmSync(file, { force: true });
192-
writeFileSync(join(path, `${mainFile}-api.md`), outputDoc, { encoding: 'utf-8' });
193-
});
194-
});
96+
const mergeFilesInModule = async (
97+
projectPath: string,
98+
outputPath: string,
99+
projectFolder: string,
100+
): Promise<void> => {
101+
const moduleNames = await getModuleNamesFromMeta(projectFolder);
102+
103+
for (const moduleName of moduleNames) {
104+
const moduleDir = join(projectPath, moduleName);
105+
if (!existsSync(moduleDir)) {
106+
continue;
107+
}
108+
109+
// Scan this module's folder – writes the individual api files and returns their names
110+
const belongingFiles = scanFoldersAndWriteFiles(moduleDir, outputPath);
111+
112+
if (belongingFiles.length <= 1) {
113+
continue; // nothing to merge
114+
}
115+
116+
const outputDoc = belongingFiles.map((f) => readFileSync(join(outputPath, f), 'utf8')).join('');
117+
118+
for (const f of belongingFiles) {
119+
rmSync(join(outputPath, f), { force: true });
120+
}
121+
writeFileSync(join(outputPath, `${moduleName}-api.md`), outputDoc, { encoding: 'utf-8' });
122+
}
195123
};
196124

197125
/**
@@ -419,5 +347,5 @@ const createParametersForTable = (args: any[]): string => {
419347
return '-';
420348
};
421349

422-
generateApiFiles('angular');
423-
generateApiFiles('angular-experimental');
350+
await generateApiFiles('angular');
351+
await generateApiFiles('angular-experimental');
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Component } from '@angular/core';
2+
import { type ComponentFixture, TestBed } from '@angular/core/testing';
3+
4+
import { SbbHeaderScrollOrigin } from './header-scroll-origin';
5+
6+
describe(`sbb-header-scroll-origin`, () => {
7+
describe('attribute usage', () => {
8+
let fixture: ComponentFixture<TestComponent>, component: TestComponent;
9+
10+
beforeEach(async () => {
11+
fixture = TestBed.createComponent(TestComponent);
12+
component = fixture.componentInstance;
13+
fixture.detectChanges();
14+
});
15+
16+
it('should create', async () => {
17+
expect(component).toBeDefined();
18+
expect(
19+
fixture.nativeElement.querySelector('div').hasAttribute('sbb-header-scroll-origin'),
20+
).toBe(true);
21+
});
22+
});
23+
24+
describe('host directive usage', () => {
25+
let fixture: ComponentFixture<TestComponentWithHostDirectiveApplied>,
26+
component: TestComponentWithHostDirectiveApplied;
27+
28+
beforeEach(async () => {
29+
fixture = TestBed.createComponent(TestComponentWithHostDirectiveApplied);
30+
component = fixture.componentInstance;
31+
fixture.detectChanges();
32+
});
33+
34+
it('should create', async () => {
35+
expect(component).toBeDefined();
36+
expect(
37+
fixture.nativeElement
38+
.querySelector('sbb-test-div')
39+
.hasAttribute('sbb-header-scroll-origin'),
40+
).toBe(true);
41+
});
42+
});
43+
});
44+
45+
@Component({
46+
template: `<div sbb-header-scroll-origin>Label</div>`,
47+
imports: [SbbHeaderScrollOrigin],
48+
})
49+
class TestComponent {}
50+
51+
@Component({
52+
selector: 'sbb-test-div',
53+
template: `<ng-content></ng-content>`,
54+
hostDirectives: [SbbHeaderScrollOrigin],
55+
})
56+
class TestComponentWithHostDirective {}
57+
58+
@Component({
59+
template: `<sbb-test-div>Label</sbb-test-div>`,
60+
imports: [TestComponentWithHostDirective],
61+
})
62+
class TestComponentWithHostDirectiveApplied {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Directive } from '@angular/core';
2+
3+
/**
4+
* Directive to mark a scroll container as source of scrolling to the `sbb-header`.
5+
* Can be placed on any scrollable element.
6+
*/
7+
@Directive({
8+
selector: '[sbb-header-scroll-origin]',
9+
host: {
10+
'[attr.sbb-header-scroll-origin]': '""',
11+
},
12+
})
13+
export class SbbHeaderScrollOrigin {}

src/angular/header/header.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { SbbHeader } from './header/header';
44
import { SbbHeaderButton } from './header-button/header-button';
55
import { SbbHeaderEnvironment } from './header-environment/header-environment';
66
import { SbbHeaderLink } from './header-link/header-link';
7+
import { SbbHeaderScrollOrigin } from './header-scroll-origin/header-scroll-origin';
78

89
const SBB_HEADER_EXPORTED_DECLARATIONS = [
910
SbbHeader,
1011
SbbHeaderButton,
1112
SbbHeaderEnvironment,
1213
SbbHeaderLink,
14+
SbbHeaderScrollOrigin,
1315
];
1416

1517
@NgModule({

src/angular/header/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export * from './header/header';
22
export * from './header-button/header-button';
33
export * from './header-environment/header-environment';
44
export * from './header-link/header-link';
5+
export * from './header-scroll-origin/header-scroll-origin';
56
export * from './header.module';

0 commit comments

Comments
 (0)