nx/scripts/documentation/package-schemas/package-metadata.ts
Isaac Mann 84387f3611
feat(nx-dev): generate migration detail pages (#29580)
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.
2025-01-27 13:17:36 -05:00

195 lines
6.1 KiB
TypeScript

import {
convertToDocumentMetadata,
DocumentMetadata,
} from '@nx/nx-dev/models-document';
import { readFileSync, existsSync } from 'fs';
import { readJsonSync } from 'fs-extra';
import { sync } from 'glob';
import { join, resolve } from 'path';
import * as DocumentationMap from '../../../docs/map.json';
import {
JsonSchema1,
PackageData,
SchemaMetadata,
} from '@nx/nx-dev/models-package';
function createSchemaMetadata(
name: string,
data: Record<string, any>,
paths: {
absoluteRoot: string;
folderName: string;
root: string;
},
type: 'executor' | 'generator' | 'migration'
): SchemaMetadata {
const path = join(paths.root, data?.schema || '');
// "factory" is for Angular support, this is the same as "implementation"
if (!data['implementation'] && data['factory'])
data.implementation = data.factory;
const schemaMetadata: SchemaMetadata = {
name,
...data,
aliases: data.aliases ?? [],
description: data.description ?? '',
hidden: data.hidden ?? false,
implementation: data.implementation
? join(paths.root, data.implementation) + '.ts'
: '',
path, // Switching property for less confusing naming conventions
schema: data.schema
? readJsonSync(join(paths.absoluteRoot, paths.root, data.schema))
: null,
type,
};
if (schemaMetadata.schema && !schemaMetadata.schema.presets) {
schemaMetadata.schema.presets = [];
}
return schemaMetadata;
}
function getSchemaList(
paths: {
absoluteRoot: string;
folderName: string;
root: string;
},
collectionFileName: string,
collectionEntries: string[]
): SchemaMetadata[] {
const targetPath = join(paths.absoluteRoot, paths.root, collectionFileName);
// We assume the type of the collection of schema is the name of the collection file (executors, generators or migrations)
const type = collectionFileName.replace('.json', '').replace('s', '') as
| 'executor'
| 'generator'
| 'migration';
try {
const metadata: SchemaMetadata[] = [];
const collectionFile = readJsonSync(targetPath, 'utf8');
for (const entry of collectionEntries) {
if (!collectionFile[entry]) {
continue;
}
metadata.push(
...Object.entries<JsonSchema1>(collectionFile[entry])
.filter(([name]) => !metadata.find((x) => x.name === name))
.map(([name, schema]: [string, JsonSchema1]) =>
createSchemaMetadata(name, schema, paths, type)
)
);
}
return metadata;
} catch (e) {
console.log(
`SchemaMetadata "${paths.root
.split('/')
.pop()}" resolution skipped: no file found at "${targetPath}".`
);
return [];
}
}
/**
* Generate the package metadata by exploring the directory path given.
* This function will look for all the packages in the given directory under packagesDirectory.
* It will then look for the package.json file and read the description and name of the package.
* It will also look for the generators.json and executors.json files and read the schema of each generator and executor.
* It will also look for the documents.json file and read the documents of each package.
* If the package is private and NODE_ENV is not development, it will not be included in the metadata.
* @param absoluteRoot
* @param packagesDirectory
* @returns Configuration
*/
export function findPackageMetadataList(
absoluteRoot: string,
packagesDirectory: string = 'packages',
prefix = ''
): PackageData[] {
const packagesDir = resolve(join(absoluteRoot, packagesDirectory));
/**
* Get all the custom overview information on each package if available
*/
const additionalApiReferences: DocumentMetadata[] = DocumentationMap.content
.find((data) => data.id === 'additional-api-references')!
.itemList.map((item) => convertToDocumentMetadata(item));
// Do not use map.json, but add a documentation property on the package.json directly that can be easily resolved
return sync(`${packagesDir}/${prefix}*`, {
ignore: [`${packagesDir}/cli`, `${packagesDir}/*-e2e`],
})
.map((folderPath: string): PackageData => {
const folderName = folderPath.substring(packagesDir.length + 1);
const relativeFolderPath = folderPath.replace(absoluteRoot, '');
if (!existsSync(join(folderPath, 'package.json'))) {
return null;
}
const packageJson = readJsonSync(
join(folderPath, 'package.json'),
'utf8'
);
const isPrivate =
packageJson.private && process.env.NODE_ENV !== 'development'; // skip this check in dev mode
const hasDocumentation = additionalApiReferences.find(
(pkg) => pkg.id === folderName
);
return isPrivate
? null
: {
githubRoot: 'https://github.com/nrwl/nx/blob/master',
name: folderName,
packageName: packageJson.name,
description: packageJson.description,
root: relativeFolderPath,
source: join(relativeFolderPath, '/src'),
documents: !!hasDocumentation
? hasDocumentation.itemList.map((item) => ({
...item,
path: item.path,
file: item.file,
content: readFileSync(
join('docs', item.file + '.md'),
'utf8'
),
}))
: [],
generators: getSchemaList(
{
absoluteRoot,
folderName,
root: relativeFolderPath,
},
'generators.json',
['generators']
),
migrations: getSchemaList(
{
absoluteRoot,
folderName,
root: relativeFolderPath,
},
'migrations.json',
['generators', 'packageJsonUpdates']
),
executors: getSchemaList(
{
absoluteRoot,
folderName,
root: relativeFolderPath,
},
'executors.json',
['executors', 'builders']
),
};
})
.filter(Boolean);
}