Generates list of migrations on the plugin overview page and a standalone `/migrations` page. To add sample code changes for a migration that has an implementation file, create a `.md` file with the same name as the implementation file in the same folder as the implementation file. i.e. `move-cache-directory.md` for `move-cache-directory.ts`. Migrations that have `packages` defined will have a table generated with the package updates listed. Separate PRs will be created to add sample code changes for each migration with an implementation. The migration list on the plugin overview page: [Angular migrations](https://nx-dev-git-docs-migration-details-nrwl.vercel.app/nx-api/angular#migrations) Standalone migration list page: [Angular migrations](https://nx-dev-git-docs-migration-details-nrwl.vercel.app/nx-api/angular/migrations) Sample migration with added markdown file details: [17.0.0-move-cache-directory](https://nx-dev-git-docs-migration-details-nrwl.vercel.app/nx-api/nx#1700movecachedirectory) Sample migration with only package updates: [Angular 20.4.0](https://nx-dev-git-docs-migration-details-nrwl.vercel.app/nx-api/angular#2040packageupdates) Sample migration without any markdown file details: [update-angular-cli-version-19-1-0](https://nx-dev-git-docs-migration-details-nrwl.vercel.app/nx-api/angular#updateangularcliversion1910) - This last sample is very bare-bones and the reason why we need these pages in the first place. People don't know what migrations are actually doing. Follow up PRs will address pages like this.
498 lines
13 KiB
TypeScript
498 lines
13 KiB
TypeScript
import * as chalk from 'chalk';
|
|
import { readJsonSync } from 'fs-extra';
|
|
import { resolve } from 'path';
|
|
import {
|
|
convertToDocumentMetadata,
|
|
createDocumentMetadata,
|
|
DocumentMetadata,
|
|
} from '@nx/nx-dev/models-document';
|
|
import { MenuItem } from '@nx/nx-dev/models-menu';
|
|
import {
|
|
PackageMetadata,
|
|
ProcessedPackageMetadata,
|
|
} from '@nx/nx-dev/models-package';
|
|
import { generateIndexMarkdownFile, generateJsonFile } from '../utils';
|
|
import { convertToDictionary } from './utils-generator/convert-to-dictionary';
|
|
|
|
interface DocumentSection {
|
|
name: string;
|
|
content: Partial<DocumentMetadata>[];
|
|
prefix: string;
|
|
}
|
|
|
|
interface DocumentManifest {
|
|
id: string;
|
|
records: Record<string, DocumentMetadata>;
|
|
}
|
|
|
|
interface PackageManifest {
|
|
id: string;
|
|
records: Record<string, ProcessedPackageMetadata>;
|
|
}
|
|
|
|
type Manifest = DocumentManifest | PackageManifest;
|
|
|
|
const isDocument = (
|
|
e: DocumentMetadata | ProcessedPackageMetadata
|
|
): e is DocumentMetadata => !('packageName' in e);
|
|
const isPackage = (
|
|
e: DocumentMetadata | ProcessedPackageMetadata
|
|
): e is ProcessedPackageMetadata => 'packageName' in e;
|
|
|
|
export function generateManifests(workspace: string): Promise<void[]> {
|
|
console.log(`${chalk.blue('i')} Generating Manifests`);
|
|
const documentationPath = resolve(workspace, 'docs');
|
|
const generatedDocumentationPath = resolve(documentationPath, 'generated');
|
|
const generatedExternalDocumentationPath = resolve(
|
|
documentationPath,
|
|
'external-generated'
|
|
);
|
|
const targetFolder: string = resolve(generatedDocumentationPath, 'manifests');
|
|
const documents: Partial<DocumentMetadata>[] = readJsonSync(
|
|
`${documentationPath}/map.json`,
|
|
{
|
|
encoding: 'utf8',
|
|
}
|
|
).content;
|
|
const packages: PackageMetadata[] = [
|
|
...readJsonSync(`${generatedDocumentationPath}/packages-metadata.json`, {
|
|
encoding: 'utf8',
|
|
}),
|
|
...readJsonSync(
|
|
`${generatedExternalDocumentationPath}/packages-metadata.json`,
|
|
{
|
|
encoding: 'utf8',
|
|
}
|
|
),
|
|
];
|
|
|
|
/**
|
|
* We are starting by selecting what section of the map.json we want to work with.
|
|
* @type {DocumentSection[]}
|
|
*/
|
|
const documentSections = createDocumentSections(documents);
|
|
|
|
/**
|
|
* Once we have the DocumentSections we can start creating our DocumentManifests.
|
|
* @type {Manifest[]}
|
|
*/
|
|
const manifests = getDocumentManifests(documentSections);
|
|
|
|
/**
|
|
* Packages are not Documents and need to be handled in a custom way.
|
|
* @type {{id: string, records: Record<string, ProcessedPackageMetadata>}}
|
|
*/
|
|
const packagesManifest = createPackagesManifest(packages);
|
|
|
|
/**
|
|
* Add the packages manifest to the manifest collection for simplicity.
|
|
*/
|
|
manifests.push(packagesManifest);
|
|
|
|
/**
|
|
* We can easily infer all Documents menus but need a custom way to handle them
|
|
* for the packages.
|
|
* @type {{id: string, menu: MenuItem[]}[]}
|
|
*/
|
|
const menus: {
|
|
id: string;
|
|
menu: MenuItem[];
|
|
}[] = getDocumentMenus(
|
|
manifests.filter((m): m is DocumentManifest =>
|
|
isDocument(m.records[Object.keys(m.records)[0]])
|
|
)
|
|
);
|
|
|
|
/**
|
|
* Creating packages menu with custom package logic.
|
|
* @type {{id: string, menu: MenuItem[]}}
|
|
*/
|
|
const packagesMenu: {
|
|
id: string;
|
|
menu: MenuItem[];
|
|
} = createPackagesMenu(packagesManifest);
|
|
|
|
/**
|
|
* Add the packages menu to the main menu collection for simplicity.
|
|
*/
|
|
menus.push(packagesMenu);
|
|
|
|
/**
|
|
* We can easily get all associated existing tags from each manifest.
|
|
* @type {Record<string, {description: string, file: string, id: string, name: string, path: string}[]>}
|
|
*/
|
|
const tags: Record<
|
|
string,
|
|
{
|
|
description: string;
|
|
file: string;
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
}[]
|
|
> = generateTags(manifests);
|
|
|
|
/**
|
|
* We can now create manifest files.
|
|
*/
|
|
const fileGenerationPromises: Promise<any>[] = [];
|
|
manifests.forEach((manifest) =>
|
|
fileGenerationPromises.push(
|
|
generateJsonFile(
|
|
resolve(targetFolder + `/${manifest.id}.json`),
|
|
manifest.records
|
|
)
|
|
)
|
|
);
|
|
fileGenerationPromises.push(
|
|
generateJsonFile(resolve(targetFolder, `tags.json`), tags)
|
|
);
|
|
fileGenerationPromises.push(
|
|
generateJsonFile(resolve(targetFolder, `menus.json`), menus)
|
|
);
|
|
fileGenerationPromises.push(
|
|
generateIndexMarkdownFile(
|
|
resolve(documentationPath, `shared`, `reference`, `sitemap.md`),
|
|
menus
|
|
)
|
|
);
|
|
|
|
return Promise.all(fileGenerationPromises);
|
|
}
|
|
|
|
function generateTags(manifests: Manifest[]) {
|
|
const tags: Record<
|
|
string,
|
|
{
|
|
description: string;
|
|
file: string;
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
}[]
|
|
> = {};
|
|
manifests.map((manifest) => {
|
|
for (let key in manifest.records) {
|
|
const item: DocumentMetadata | ProcessedPackageMetadata =
|
|
manifest.records[key];
|
|
|
|
if (isDocument(item))
|
|
item.tags.forEach((t) => {
|
|
const tagData = {
|
|
description: item.description,
|
|
file: item.file,
|
|
id: item.id,
|
|
name: item.name,
|
|
path: item.path,
|
|
};
|
|
!tags[t] ? (tags[t] = [tagData]) : tags[t].push(tagData);
|
|
});
|
|
|
|
if (isPackage(item))
|
|
Object.values(item.documents).forEach(
|
|
(documentMetadata: DocumentMetadata) => {
|
|
documentMetadata.tags.forEach((t: string) => {
|
|
const filePath = documentMetadata.file.startsWith(
|
|
'generated/packages'
|
|
)
|
|
? documentMetadata.file
|
|
: ['generated', 'packages', documentMetadata.file].join('/');
|
|
|
|
const tagData = {
|
|
description: documentMetadata.description,
|
|
file: filePath,
|
|
id: documentMetadata.id,
|
|
name: documentMetadata.name,
|
|
path: documentMetadata.path,
|
|
};
|
|
!tags[t] ? (tags[t] = [tagData]) : tags[t].push(tagData);
|
|
});
|
|
}
|
|
);
|
|
}
|
|
});
|
|
|
|
return tags;
|
|
}
|
|
|
|
function createPackagesMenu(packages: PackageManifest): {
|
|
id: string;
|
|
menu: MenuItem[];
|
|
} {
|
|
const packagesMenu: MenuItem[] = Object.values(packages.records).map((p) => {
|
|
const item: MenuItem = {
|
|
id: p.name,
|
|
path: '/nx-api/' + p.name,
|
|
name: p.name,
|
|
children: [],
|
|
isExternal: false,
|
|
disableCollapsible: false,
|
|
};
|
|
|
|
if (!!Object.values(p.documents).length) {
|
|
// Might need to remove the path set in the "additional api resources" items
|
|
item.children.push({
|
|
id: 'documents',
|
|
path: '/' + ['nx-api', p.name, 'documents'].join('/'),
|
|
name: 'documents',
|
|
children: Object.values(p.documents).map((d) =>
|
|
menuItemRecurseOperations(d)
|
|
),
|
|
isExternal: false,
|
|
disableCollapsible: false,
|
|
});
|
|
}
|
|
|
|
if (!!Object.values(p.executors).length) {
|
|
item.children.push({
|
|
id: 'executors',
|
|
path: '/' + ['nx-api', p.name, 'executors'].join('/'),
|
|
name: 'executors',
|
|
children: Object.values(p.executors).map((e) => ({
|
|
id: e.name,
|
|
path: '/' + ['nx-api', p.name, 'executors', e.name].join('/'),
|
|
name: e.name,
|
|
children: [],
|
|
isExternal: false,
|
|
disableCollapsible: false,
|
|
})),
|
|
isExternal: false,
|
|
disableCollapsible: false,
|
|
});
|
|
}
|
|
|
|
if (!!Object.values(p.generators).length) {
|
|
item.children.push({
|
|
id: 'generators',
|
|
path: '/' + ['nx-api', p.name, 'generators'].join('/'),
|
|
name: 'generators',
|
|
children: Object.values(p.generators).map((g) => ({
|
|
id: g.name,
|
|
path: '/' + ['nx-api', p.name, 'generators', g.name].join('/'),
|
|
name: g.name,
|
|
children: [],
|
|
isExternal: false,
|
|
disableCollapsible: false,
|
|
})),
|
|
isExternal: false,
|
|
disableCollapsible: false,
|
|
});
|
|
}
|
|
|
|
if (!!Object.values(p.migrations).length) {
|
|
item.children.push({
|
|
id: 'migrations',
|
|
path: '/' + ['nx-api', p.name, 'migrations'].join('/'),
|
|
name: 'migrations',
|
|
children: [],
|
|
isExternal: false,
|
|
disableCollapsible: false,
|
|
});
|
|
}
|
|
return item;
|
|
});
|
|
return { id: 'nx-api', menu: packagesMenu };
|
|
}
|
|
|
|
function getDocumentMenus(manifests: DocumentManifest[]): {
|
|
id: string;
|
|
menu: MenuItem[];
|
|
}[] {
|
|
return manifests.map((record) => ({
|
|
id: record.id,
|
|
menu: Object.values(record.records)
|
|
.map((item: any) => convertToDocumentMetadata(item))
|
|
.map((item: DocumentMetadata) => menuItemRecurseOperations(item)),
|
|
}));
|
|
}
|
|
|
|
function createPackagesManifest(packages: PackageMetadata[]): {
|
|
id: string;
|
|
records: Record<string, ProcessedPackageMetadata>;
|
|
} {
|
|
const packagesManifest: {
|
|
id: string;
|
|
records: Record<string, ProcessedPackageMetadata>;
|
|
} = { id: 'nx-api', records: {} };
|
|
|
|
packages.forEach((p) => {
|
|
packagesManifest.records[p.name] = {
|
|
githubRoot: p.githubRoot,
|
|
name: p.name,
|
|
packageName: p.packageName,
|
|
description: p.description,
|
|
documents: convertToDictionary(
|
|
p.documents.map((d) =>
|
|
documentRecurseOperations(
|
|
d,
|
|
createDocumentMetadata({ id: p.name, path: 'nx-api/' })
|
|
)
|
|
),
|
|
'path'
|
|
),
|
|
root: p.root,
|
|
source: p.source,
|
|
executors: convertToDictionary(
|
|
p.executors.map((e) => ({
|
|
...e,
|
|
path: generatePath({ id: e.name, path: e.path }, 'nx-api'),
|
|
})),
|
|
'path'
|
|
),
|
|
generators: convertToDictionary(
|
|
p.generators.map((g) => ({
|
|
...g,
|
|
path: generatePath({ id: g.name, path: g.path }, 'nx-api'),
|
|
})),
|
|
'path'
|
|
),
|
|
migrations: convertToDictionary(
|
|
p.migrations.map((g) => ({
|
|
...g,
|
|
path: generatePath({ id: g.name, path: g.path }, 'nx-api'),
|
|
})),
|
|
'path'
|
|
),
|
|
path: generatePath({ id: p.name, path: '' }, 'nx-api'),
|
|
};
|
|
});
|
|
|
|
return packagesManifest;
|
|
}
|
|
|
|
function getDocumentManifests(sections: DocumentSection[]): Manifest[] {
|
|
return sections.map((section) => {
|
|
const records: Record<string, DocumentMetadata> = {};
|
|
section.content
|
|
.map((item: any) => convertToDocumentMetadata(item))
|
|
.map((item: DocumentMetadata) =>
|
|
documentRecurseOperations(
|
|
item,
|
|
createDocumentMetadata({ id: section.name, path: section.prefix })
|
|
)
|
|
)
|
|
.forEach((item: DocumentMetadata) => {
|
|
populateDictionary(item, records);
|
|
});
|
|
|
|
return {
|
|
id: section.name,
|
|
records,
|
|
};
|
|
});
|
|
}
|
|
|
|
function createDocumentSections(
|
|
documents: Partial<DocumentMetadata>[]
|
|
): DocumentSection[] {
|
|
return [
|
|
{
|
|
name: 'nx',
|
|
content: documents.find((x) => x.id === 'nx-documentation')!
|
|
.itemList as Partial<DocumentMetadata>[],
|
|
prefix: '',
|
|
},
|
|
{
|
|
name: 'extending-nx',
|
|
content: documents.find((x) => x.id === 'extending-nx')!
|
|
.itemList as Partial<DocumentMetadata>[],
|
|
prefix: 'extending-nx',
|
|
},
|
|
{
|
|
name: 'ci',
|
|
content: documents.find((x) => x.id === 'ci')!
|
|
.itemList as Partial<DocumentMetadata>[],
|
|
prefix: 'ci',
|
|
},
|
|
];
|
|
}
|
|
|
|
function generatePath(
|
|
item: { path: string; id: string },
|
|
prefix: string = ''
|
|
): string {
|
|
const isLinkExternal: (p: string) => boolean = (p: string) =>
|
|
p.startsWith('http');
|
|
const isLinkAbsolute: (p: string) => boolean = (p: string) =>
|
|
p.startsWith('/');
|
|
|
|
if (!item.path)
|
|
return '/' + [...prefix.split('/'), item.id].filter(Boolean).join('/');
|
|
if (isLinkAbsolute(item.path) || isLinkExternal(item.path)) return item.path;
|
|
return (
|
|
'/' +
|
|
[...prefix.split('/'), ...item.path.split('/')].filter(Boolean).join('/')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle data interpolation for all items and their children.
|
|
* @param target
|
|
* @param parent
|
|
*/
|
|
function documentRecurseOperations(
|
|
target: DocumentMetadata,
|
|
parent: DocumentMetadata
|
|
): DocumentMetadata {
|
|
const document: DocumentMetadata = structuredClone(target);
|
|
|
|
/**
|
|
* Calculate `path`
|
|
*/
|
|
if (!!parent) document.path = generatePath(target, parent.path);
|
|
else document.path = generatePath(document);
|
|
|
|
/**
|
|
* Calculate `isExternal`
|
|
*/
|
|
if (document['isExternal'] === undefined) {
|
|
document.isExternal = document.path.startsWith('http');
|
|
}
|
|
|
|
if (!!target.itemList.length) {
|
|
document.itemList = target.itemList.map((i) =>
|
|
documentRecurseOperations(i, document)
|
|
);
|
|
}
|
|
|
|
return document;
|
|
}
|
|
|
|
function populateDictionary(
|
|
document: DocumentMetadata,
|
|
dictionary: Record<string, DocumentMetadata>
|
|
) {
|
|
if (document.path.startsWith('http')) return;
|
|
dictionary[document.path] = document;
|
|
|
|
document.itemList.forEach((item: DocumentMetadata) =>
|
|
populateDictionary(item, dictionary)
|
|
);
|
|
}
|
|
|
|
// Creates menus dictionary mapping
|
|
function menuItemRecurseOperations(target: DocumentMetadata): MenuItem {
|
|
const menuItem: MenuItem = {
|
|
name: target.name,
|
|
path: target.path,
|
|
id: target.id,
|
|
isExternal: target.isExternal,
|
|
children: [],
|
|
disableCollapsible: false,
|
|
};
|
|
/**
|
|
* Calculate `isExternal`
|
|
*/
|
|
if (menuItem['isExternal'] === undefined) {
|
|
menuItem.isExternal = menuItem.path.startsWith('http');
|
|
}
|
|
|
|
if (!!target.itemList.length) {
|
|
menuItem.children = target.itemList.map((i) =>
|
|
menuItemRecurseOperations(i)
|
|
);
|
|
}
|
|
|
|
return menuItem;
|
|
}
|