chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { json } from '@angular-devkit/core';
|
||||
import { SchemaFlattener } from './schema-flattener';
|
||||
|
||||
export enum OptionType {
|
||||
Any = 'any',
|
||||
Array = 'array',
|
||||
Boolean = 'boolean',
|
||||
Number = 'number',
|
||||
String = 'string',
|
||||
}
|
||||
|
||||
function _getEnumFromValue<E, T extends E[keyof E]>(
|
||||
value: json.JsonValue,
|
||||
enumeration: E,
|
||||
defaultValue: T
|
||||
): T {
|
||||
if (typeof value !== 'string') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (Object.values(enumeration).indexOf(value) !== -1) {
|
||||
return value as unknown as T;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export async function parseJsonSchemaToOptions(
|
||||
flattener: SchemaFlattener,
|
||||
schema: json.JsonObject
|
||||
): Promise<any[]> {
|
||||
const options: any[] = [];
|
||||
|
||||
function visitor(
|
||||
current: json.JsonObject | json.JsonArray,
|
||||
pointer: json.schema.JsonPointer,
|
||||
parentSchema?: json.JsonObject | json.JsonArray
|
||||
) {
|
||||
if (!parentSchema) {
|
||||
// Ignore root.
|
||||
return;
|
||||
} else if (pointer.split(/\/(?:properties|definitions)\//g).length > 2) {
|
||||
// Ignore subitems (objects or arrays).
|
||||
return;
|
||||
} else if (json.isJsonArray(current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pointer.indexOf('/not/') != -1) {
|
||||
// We don't support anyOf/not.
|
||||
throw new Error('The "not" keyword is not supported in JSON Schema.');
|
||||
}
|
||||
|
||||
const ptr = json.schema.parseJsonPointer(pointer); // eg: /properties/commands => [ 'properties', 'commands' ]
|
||||
const name = ptr[ptr.length - 1]; // eg: 'commands'
|
||||
|
||||
if (ptr[ptr.length - 2] != 'properties') {
|
||||
// Skip any non-property items.
|
||||
return;
|
||||
}
|
||||
|
||||
const typeSet = json.schema.getTypesOfSchema(current); // eg: array
|
||||
|
||||
if (typeSet.size == 0) {
|
||||
throw new Error('Cannot find type of schema.');
|
||||
}
|
||||
|
||||
// We only support number, string or boolean (or array of those), so remove everything else.
|
||||
|
||||
const types = Array.from(typeSet)
|
||||
.filter((x) => {
|
||||
switch (x) {
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'array':
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map((x) => _getEnumFromValue(x, OptionType, OptionType.String));
|
||||
|
||||
if (types.length == 0) {
|
||||
// This means it's not usable on the command line. e.g. an Object.
|
||||
return;
|
||||
}
|
||||
|
||||
// Only keep enum values we support (booleans, numbers and strings).
|
||||
const enumValues = (
|
||||
(json.isJsonArray(current.enum) && current.enum) ||
|
||||
[]
|
||||
).filter((x) => {
|
||||
switch (typeof x) {
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}) as any[];
|
||||
|
||||
let defaultValue: string | number | boolean | undefined = undefined;
|
||||
if (current.default !== undefined) {
|
||||
switch (types[0]) {
|
||||
case 'string':
|
||||
if (typeof current.default == 'string') {
|
||||
defaultValue = current.default;
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
if (typeof current.default == 'number') {
|
||||
defaultValue = current.default;
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
if (typeof current.default == 'boolean') {
|
||||
defaultValue = current.default;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const type = types[0];
|
||||
const $default = current.$default;
|
||||
const $defaultIndex =
|
||||
json.isJsonObject($default) && $default['$source'] == 'argv'
|
||||
? $default['index']
|
||||
: undefined;
|
||||
const positional: number | undefined =
|
||||
typeof $defaultIndex == 'number' ? $defaultIndex : undefined;
|
||||
|
||||
const required =
|
||||
json.isJsonObject(parentSchema) && json.isJsonArray(parentSchema.required)
|
||||
? parentSchema.required.indexOf(name) != -1
|
||||
: false;
|
||||
const aliases = json.isJsonArray(current.aliases)
|
||||
? [...current.aliases].map((x) => `${x}`)
|
||||
: current.alias
|
||||
? [`${current.alias}`]
|
||||
: [];
|
||||
const format =
|
||||
typeof current.format == 'string' ? current.format : undefined;
|
||||
const visible = current.visible === undefined || current.visible === true;
|
||||
const hidden = !!current.hidden || !visible;
|
||||
|
||||
// Deprecated is set only if it's true or a string.
|
||||
const xDeprecated = current['x-deprecated'];
|
||||
const deprecated =
|
||||
xDeprecated === true || typeof xDeprecated == 'string'
|
||||
? xDeprecated
|
||||
: undefined;
|
||||
|
||||
const option: any = {
|
||||
name,
|
||||
description:
|
||||
current.description === undefined ? '' : `${current.description}`,
|
||||
...(types.length == 1 ? { type } : { type, types }),
|
||||
...(defaultValue !== undefined ? { default: defaultValue } : {}),
|
||||
...(enumValues && enumValues.length > 0 ? { enum: enumValues } : {}),
|
||||
required,
|
||||
aliases,
|
||||
...(format !== undefined ? { format } : {}),
|
||||
hidden,
|
||||
...(deprecated !== undefined ? { deprecated } : {}),
|
||||
...(positional !== undefined ? { positional } : {}),
|
||||
};
|
||||
|
||||
if (current.type === 'array' && current.items) {
|
||||
const items = current.items as {
|
||||
additionalProperties: boolean;
|
||||
properties: any;
|
||||
required?: string[];
|
||||
type: string;
|
||||
};
|
||||
|
||||
if (items.properties) {
|
||||
option.arrayOfType = items.type;
|
||||
option.arrayOfValues = Object.keys(items.properties).map((key) => ({
|
||||
name: key,
|
||||
...items.properties[key],
|
||||
isRequired: items.required && items.required.includes(key),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
options.push(option);
|
||||
}
|
||||
|
||||
const flattenedSchema = flattener.flatten(schema);
|
||||
json.schema.visitJsonSchema(flattenedSchema, visitor);
|
||||
|
||||
// Sort by positional.
|
||||
return options.sort((a, b) => {
|
||||
if (a.positional) {
|
||||
if (b.positional) {
|
||||
return a.positional - b.positional;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else if (b.positional) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "documentation-map-schema",
|
||||
"title": "JSON schema for documentation map",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Title of the document"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the document"
|
||||
},
|
||||
"content": {
|
||||
"type": "array",
|
||||
"description": "Dictionary map section",
|
||||
"items": {
|
||||
"$ref": "#/definitions/entry"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"entry": {
|
||||
"type": "object",
|
||||
"required": ["name", "id"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name for the current item"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Identifier for the current item"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description for the item"
|
||||
},
|
||||
"mediaImage": {
|
||||
"type": "string",
|
||||
"description": "Path to an alternate open graph image, relative to /docs"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "Path to the markdown file"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"description": "Tags are used on nx.dev to link related piece of content together (e.g: Related Documentation)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Custom path or URL to find the item on nx.dev"
|
||||
},
|
||||
"isExternal": {
|
||||
"type": "boolean",
|
||||
"description": "Is the path provided is external to nx.dev?"
|
||||
},
|
||||
"itemList": {
|
||||
"type": "array",
|
||||
"description": "Children for the item",
|
||||
"items": {
|
||||
"$ref": "#/definitions/entry"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,238 @@
|
||||
import { writeFileSync } from 'fs';
|
||||
import axios from 'axios';
|
||||
|
||||
interface Interval {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}
|
||||
|
||||
interface PluginRegistry {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const packagesJson = require('../../nx-dev/nx-dev/public/documentation/generated/manifests/new-nx-api.json');
|
||||
const officialPlugins = Object.keys(packagesJson)
|
||||
.filter(
|
||||
(m: any) =>
|
||||
packagesJson[m].name !== 'add-nx-to-monorepo' &&
|
||||
packagesJson[m].name !== 'cra-to-nx' &&
|
||||
packagesJson[m].name !== 'create-nx-plugin' &&
|
||||
packagesJson[m].name !== 'create-nx-workspace' &&
|
||||
packagesJson[m].name !== 'make-angular-cli-faster' &&
|
||||
packagesJson[m].name !== 'tao'
|
||||
)
|
||||
.map((k) => ({
|
||||
name: packagesJson[k].name === 'nx' ? 'nx' : '@nx/' + packagesJson[k].name,
|
||||
description: packagesJson[k].description,
|
||||
url: packagesJson[k].githubRoot,
|
||||
}));
|
||||
|
||||
const plugins =
|
||||
require('../../astro-docs/src/content/approved-community-plugins.json') as PluginRegistry[];
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const qualityIndicators: any = {};
|
||||
for (let i = 0; i < officialPlugins.length; i++) {
|
||||
const plugin = officialPlugins[i];
|
||||
console.log(`Fetching data for ${plugin.name}`);
|
||||
const npmData = await getNpmData(plugin, true);
|
||||
const npmDownloads = await getNpmDownloads(plugin);
|
||||
qualityIndicators[plugin.name] = {
|
||||
lastPublishedDate: npmData.lastPublishedDate,
|
||||
npmDownloads,
|
||||
githubRepo: `nrwl/nx`,
|
||||
};
|
||||
}
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
const plugin = plugins[i];
|
||||
console.log(`Fetching data for ${plugin.name}`);
|
||||
const npmData = await getNpmData(plugin);
|
||||
const npmDownloads = await getNpmDownloads(plugin);
|
||||
qualityIndicators[plugin.name] = {
|
||||
lastPublishedDate: npmData.lastPublishedDate,
|
||||
npmDownloads,
|
||||
githubRepo: npmData.githubRepo,
|
||||
nxVersion: npmData.nxVersion,
|
||||
};
|
||||
}
|
||||
const repos = Object.keys(qualityIndicators).map((pluginName) => {
|
||||
const [owner, repo] =
|
||||
qualityIndicators[pluginName].githubRepo?.split('/');
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
};
|
||||
});
|
||||
const starData = await getGithubStars(repos);
|
||||
Object.keys(qualityIndicators).forEach((key) => {
|
||||
qualityIndicators[key].githubStars =
|
||||
starData[qualityIndicators[key].githubRepo.replace(/[\-\/#]/g, '')]
|
||||
?.stargazers?.totalCount || -1;
|
||||
delete qualityIndicators[key].githubRepo;
|
||||
});
|
||||
|
||||
writeFileSync(
|
||||
'./nx-dev/nx-dev/pages/quality-indicators.json',
|
||||
JSON.stringify(qualityIndicators, null, 2)
|
||||
);
|
||||
} catch (ex) {
|
||||
console.warn('Failed to load quality indicators!');
|
||||
console.warn(ex);
|
||||
// Don't overwrite quality-indicators.json if the script fails
|
||||
}
|
||||
}
|
||||
main();
|
||||
|
||||
// Publish date (and github directory, readme content)
|
||||
// i.e. https://registry.npmjs.org/@nxkit/playwright
|
||||
async function getNpmData(plugin: PluginRegistry, skipNxVersion = false) {
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`https://registry.npmjs.org/${plugin.name}`
|
||||
);
|
||||
const lastPublishedDate = data.time[data['dist-tags'].latest];
|
||||
const nxVersion = skipNxVersion || (await getNxVersion(data));
|
||||
if (!data.repository) {
|
||||
console.warn('- No repository defined in package.json!');
|
||||
return { lastPublishedDate, nxVersion, githubRepo: '' };
|
||||
}
|
||||
const url: String = data.repository.url;
|
||||
const indexOfTree = url.indexOf('/tree/');
|
||||
const githubRepo = url
|
||||
.slice(0, indexOfTree === -1 ? undefined : indexOfTree)
|
||||
.slice(0, url.indexOf('#') === -1 ? undefined : url.indexOf('#'))
|
||||
.slice(url.indexOf('github.com/') + 11)
|
||||
.replace('.git', '');
|
||||
return {
|
||||
lastPublishedDate,
|
||||
githubRepo,
|
||||
nxVersion,
|
||||
// readmeContent: plugin.name
|
||||
};
|
||||
} catch (ex) {
|
||||
console.warn('Failed to load npm data for ' + plugin.name, ex);
|
||||
return {
|
||||
lastPublishedData: '',
|
||||
githubRepo: '',
|
||||
nxVersion: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Download count
|
||||
// i.e. https://api.npmjs.org/downloads/point/2023-06-01:2023-07-01/@nxkit/playwright
|
||||
async function getNpmDownloads(plugin: PluginRegistry) {
|
||||
const lastMonth = getLastMonth();
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`https://api.npmjs.org/downloads/point/${stringifyIntervalForUrl(
|
||||
lastMonth
|
||||
)}/${plugin.name}`
|
||||
);
|
||||
return data.downloads;
|
||||
} catch (ex) {
|
||||
console.warn('Failed to load npm downloads for ' + plugin.name, ex);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLastMonth() {
|
||||
const now = new Date();
|
||||
const oneMonthAgo = new Date();
|
||||
oneMonthAgo.setMonth(now.getMonth() - 1);
|
||||
return {
|
||||
start: oneMonthAgo,
|
||||
end: now,
|
||||
};
|
||||
}
|
||||
|
||||
export function stringifyIntervalForUrl(interval: Interval): string {
|
||||
return `${stringifyDate(interval.start)}:${stringifyDate(interval.end)}`;
|
||||
}
|
||||
|
||||
export function stringifyDate(date: Date) {
|
||||
// yyyy-MM-dd
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
// Stars
|
||||
// i.e. https://api.github.com/graphql
|
||||
async function getGithubStars(repos: { owner: string; repo: string }[]) {
|
||||
const query = `
|
||||
fragment repoProperties on Repository {
|
||||
nameWithOwner
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
${repos
|
||||
.filter(({ owner, repo }) => owner && repo && !owner.includes('.'))
|
||||
.map(
|
||||
({ owner, repo }, index) =>
|
||||
`${owner.replace(/[\-#]/g, '')}${repo.replace(
|
||||
/[\-#]/g,
|
||||
''
|
||||
)}: repository(owner: "${owner}", name: "${repo}") {
|
||||
...repoProperties
|
||||
}`
|
||||
)
|
||||
.join('\n')}
|
||||
}`;
|
||||
|
||||
const result = await axios.post(
|
||||
'https://api.github.com/graphql',
|
||||
{
|
||||
query,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return result.data.data;
|
||||
}
|
||||
|
||||
async function getNxVersion(data: any) {
|
||||
const latest = data['dist-tags'].latest;
|
||||
const nxPackages = ['@nx/devkit', '@nx/workspace'];
|
||||
let devkitVersion = '';
|
||||
for (let i = 0; i < nxPackages.length && !devkitVersion; i++) {
|
||||
const packageName = nxPackages[i];
|
||||
if (data.versions[latest]?.dependencies) {
|
||||
devkitVersion = data.versions[latest]?.dependencies[packageName];
|
||||
if (devkitVersion) {
|
||||
return await findNxRange(devkitVersion);
|
||||
}
|
||||
}
|
||||
if (!devkitVersion && data.versions[latest]?.peerDependencies) {
|
||||
devkitVersion = data.versions[latest]?.peerDependencies[packageName];
|
||||
if (devkitVersion) {
|
||||
return await findNxRange(devkitVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.warn('- No dependency on @nx/devkit!');
|
||||
return devkitVersion;
|
||||
}
|
||||
|
||||
async function findNxRange(devkitVersion: string) {
|
||||
devkitVersion = devkitVersion
|
||||
.replace('^', '')
|
||||
.replace('>=', '')
|
||||
.replace('>', '');
|
||||
const { data: devkitData } = await axios.get(
|
||||
`https://registry.npmjs.org/@nx/devkit`
|
||||
);
|
||||
if (!devkitData.versions[devkitVersion]?.peerDependencies) {
|
||||
const dependencies = devkitData.versions[devkitVersion]?.dependencies;
|
||||
return dependencies?.nx;
|
||||
}
|
||||
return devkitData.versions[devkitVersion]?.peerDependencies.nx;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Prebuild script that fetches banner configuration from a Framer URL
|
||||
* and saves it to a local JSON file for use at build time.
|
||||
*
|
||||
* The Framer page renders JSON inside a <pre> tag which we extract and parse.
|
||||
*/
|
||||
|
||||
import { writeFileSync, mkdirSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
|
||||
// Support configurable env var and output path for use by both nx-dev and astro-docs
|
||||
const BANNER_ENV_VAR = process.env.BANNER_ENV_VAR || 'NEXT_PUBLIC_BANNER_URL';
|
||||
const BANNER_URL = process.env[BANNER_ENV_VAR];
|
||||
const OUTPUT_PATH = process.env.BANNER_OUTPUT_PATH || 'lib/banner.json';
|
||||
|
||||
/**
|
||||
* Extract JSON from a Framer HTML page.
|
||||
* Framer renders the banner config as JSON inside a <pre> tag.
|
||||
*/
|
||||
function extractJsonFromFramerHtml(html) {
|
||||
// Look for JSON in a <pre> tag (Framer renders it this way)
|
||||
const preMatch = html.match(/<pre[^>]*>([\s\S]*?)<\/pre>/i);
|
||||
if (preMatch && preMatch[1]) {
|
||||
try {
|
||||
// Clean up any HTML entities and parse
|
||||
const jsonStr = preMatch[1]
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, "'")
|
||||
.trim();
|
||||
return JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse <pre> content as JSON:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to parse the entire response as JSON (for direct JSON endpoints)
|
||||
try {
|
||||
return JSON.parse(html);
|
||||
} catch {
|
||||
// Not valid JSON
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the config has all required fields
|
||||
*/
|
||||
function validateBannerConfig(config) {
|
||||
if (!config || typeof config !== 'object') return false;
|
||||
if (typeof config.title !== 'string' || !config.title) return false;
|
||||
if (typeof config.description !== 'string') return false;
|
||||
if (typeof config.primaryCtaUrl !== 'string' || !config.primaryCtaUrl)
|
||||
return false;
|
||||
if (typeof config.primaryCtaText !== 'string' || !config.primaryCtaText)
|
||||
return false;
|
||||
// activeUntil is required for determining when the banner expires
|
||||
if (typeof config.activeUntil !== 'string' || !config.activeUntil)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip optional fields that are present but malformed, so a bad optional
|
||||
* value never drops the whole banner.
|
||||
*/
|
||||
function normalizeBannerConfig(config) {
|
||||
if (config.artwork !== undefined) {
|
||||
if (typeof config.artwork !== 'string' || !config.artwork) {
|
||||
console.warn('Ignoring invalid artwork value:', config.artwork);
|
||||
delete config.artwork;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Empty array for when no banner is configured
|
||||
const emptyCollection = [];
|
||||
|
||||
if (!BANNER_URL) {
|
||||
console.log(`${BANNER_ENV_VAR} not set, writing empty banner collection`);
|
||||
mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
||||
writeFileSync(OUTPUT_PATH, JSON.stringify(emptyCollection, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Fetching banner config from: ${BANNER_URL}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(BANNER_URL, {
|
||||
headers: {
|
||||
Accept: 'text/html,application/xhtml+xml,application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const config = extractJsonFromFramerHtml(text);
|
||||
|
||||
if (!config) {
|
||||
throw new Error('No valid JSON found in banner page');
|
||||
}
|
||||
|
||||
if (!validateBannerConfig(config)) {
|
||||
throw new Error('Invalid banner configuration format');
|
||||
}
|
||||
|
||||
// Wrap in array for collection format, add id for Astro file loader
|
||||
const collection = [{ id: 'banner', ...normalizeBannerConfig(config) }];
|
||||
|
||||
console.log('Banner config fetched successfully:', config.title);
|
||||
mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
||||
writeFileSync(OUTPUT_PATH, JSON.stringify(collection, null, 2) + '\n');
|
||||
console.log(`Written to ${OUTPUT_PATH}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch banner config:', error.message);
|
||||
console.log('Writing empty banner collection as fallback');
|
||||
mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
||||
writeFileSync(OUTPUT_PATH, JSON.stringify(emptyCollection, null, 2) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,5 @@
|
||||
import { json } from '@angular-devkit/core';
|
||||
|
||||
export interface SchemaFlattener {
|
||||
flatten: (schema) => json.JsonObject;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import { MenuItem } from '@nx/nx-dev-models-menu';
|
||||
import { outputFileSync } from 'fs-extra';
|
||||
import {
|
||||
bold,
|
||||
code,
|
||||
h2,
|
||||
lines as mdLines,
|
||||
strikethrough,
|
||||
table,
|
||||
} from 'markdown-factory';
|
||||
import { join } from 'path';
|
||||
import { format, resolveConfig } from 'prettier';
|
||||
import { CommandModule } from 'yargs';
|
||||
import { stripVTControlCharacters } from 'node:util';
|
||||
|
||||
const importFresh = require('import-fresh');
|
||||
|
||||
export function sortAlphabeticallyFunction(a: string, b: string): number {
|
||||
const nameA = a.toUpperCase(); // ignore upper and lowercase
|
||||
const nameB = b.toUpperCase(); // ignore upper and lowercase
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
// names must be equal
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function sortByBooleanFunction(a: boolean, b: boolean): number {
|
||||
if (a && !b) {
|
||||
return -1;
|
||||
}
|
||||
if (!a && b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function generateMarkdownFile(
|
||||
outputDirectory: string,
|
||||
templateObject: { name: string; template: string }
|
||||
): Promise<void> {
|
||||
const filePath = join(outputDirectory, `${templateObject.name}.md`);
|
||||
outputFileSync(
|
||||
filePath,
|
||||
await formatWithPrettier(
|
||||
filePath,
|
||||
stripVTControlCharacters(templateObject.template)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateJsonFile(
|
||||
filePath: string,
|
||||
json: unknown
|
||||
): Promise<void> {
|
||||
outputFileSync(
|
||||
filePath,
|
||||
await formatWithPrettier(filePath, JSON.stringify(json)),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
}
|
||||
|
||||
function menuItemToStrings(item: MenuItem, pathPrefix = '/'): string[] {
|
||||
if (item.isExternal) {
|
||||
return [];
|
||||
}
|
||||
const line = item.path ? `- [${item.name}](${item.path})` : `- ${item.name}`;
|
||||
const padding = item.path
|
||||
.replace(pathPrefix, '')
|
||||
.split('/')
|
||||
.map(() => ' ')
|
||||
.join('');
|
||||
const childLines = item.children.flatMap((child) =>
|
||||
menuItemToStrings(child, pathPrefix)
|
||||
);
|
||||
return [padding + line, ...childLines];
|
||||
}
|
||||
|
||||
function deduplicate<T>(array: T[]): T[] {
|
||||
return Array.from(new Set(array));
|
||||
}
|
||||
|
||||
export async function generateIndexMarkdownFile(
|
||||
filePath: string,
|
||||
json: { id: string; menu: MenuItem[] }[]
|
||||
): Promise<void> {
|
||||
function capitalize(word: string) {
|
||||
const [firstLetter, ...rest] = word;
|
||||
return firstLetter.toLocaleUpperCase() + rest.join('');
|
||||
}
|
||||
const idToPathPrefix = {
|
||||
nx: undefined,
|
||||
recipes: `/recipes/`,
|
||||
plugins: `/plugins/`,
|
||||
packages: `/packages/`,
|
||||
ci: `/ci/`,
|
||||
};
|
||||
const content = json
|
||||
.map(
|
||||
({ id, menu }) =>
|
||||
deduplicate(
|
||||
[
|
||||
`- ${capitalize(id)}`,
|
||||
...menu.flatMap((item) =>
|
||||
menuItemToStrings(item, idToPathPrefix[id])
|
||||
),
|
||||
].filter((line) => line.length > 0)
|
||||
).join('\n') + '\n'
|
||||
)
|
||||
.join(`\n`);
|
||||
outputFileSync(filePath, await formatWithPrettier(filePath, content), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
export async function formatWithPrettier(filePath: string, content: string) {
|
||||
let options: any = {
|
||||
filepath: filePath,
|
||||
};
|
||||
const resolvedOptions = await resolveConfig(filePath);
|
||||
if (resolvedOptions) {
|
||||
options = {
|
||||
...options,
|
||||
...resolvedOptions,
|
||||
};
|
||||
}
|
||||
|
||||
return format(content, options);
|
||||
}
|
||||
|
||||
export function wrapLinks(content: string): string {
|
||||
const urlRegex = /(https?:\/\/)[^\s]+[a-zA-Z][a-zA-Z]/g;
|
||||
const links = content.match(urlRegex) || [];
|
||||
for (const link of links) {
|
||||
const wrappedLink = `[${link}](${link.replace('https://nx.dev', '')})`;
|
||||
content = content.replace(link, wrappedLink);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export function formatDescription(
|
||||
description: string,
|
||||
deprecated: boolean | string
|
||||
) {
|
||||
const updatedDescription = wrapLinks(description);
|
||||
if (!deprecated) {
|
||||
return updatedDescription;
|
||||
}
|
||||
if (!description) {
|
||||
return `${bold('Deprecated:')} ${deprecated}`;
|
||||
}
|
||||
return deprecated === true
|
||||
? `${bold('Deprecated:')} ${updatedDescription}`
|
||||
: mdLines(`${bold('Deprecated:')} ${deprecated}`, updatedDescription);
|
||||
}
|
||||
|
||||
export function getCommands(command: any) {
|
||||
return command.getInternalMethods().getCommandInstance().getCommandHandlers();
|
||||
}
|
||||
|
||||
export interface ParsedCommandOption {
|
||||
name: string[];
|
||||
type: string;
|
||||
description: string;
|
||||
default: string;
|
||||
deprecated: boolean | string;
|
||||
hidden: boolean;
|
||||
choices?: string[];
|
||||
}
|
||||
|
||||
export interface ParsedCommand {
|
||||
name: string;
|
||||
commandString: string;
|
||||
description: string;
|
||||
deprecated: string;
|
||||
options?: Array<ParsedCommandOption>;
|
||||
subcommands?: Array<ParsedCommand>;
|
||||
}
|
||||
|
||||
const YargsTypes = ['array', 'count', 'string', 'boolean', 'number'];
|
||||
|
||||
export async function parseCommand(
|
||||
name: string,
|
||||
command: any
|
||||
): Promise<ParsedCommand> {
|
||||
// It is not a function return a strip down version of the command
|
||||
if (
|
||||
!(
|
||||
command.builder &&
|
||||
command.builder.constructor &&
|
||||
command.builder.call &&
|
||||
command.builder.apply
|
||||
)
|
||||
) {
|
||||
return {
|
||||
name,
|
||||
commandString: command.original,
|
||||
deprecated: command.deprecated,
|
||||
description: command.description,
|
||||
};
|
||||
}
|
||||
|
||||
// Show all the options we can get from yargs
|
||||
const builder = await command.builder(
|
||||
importFresh('yargs')().getInternalMethods().reset()
|
||||
);
|
||||
const builderDescriptions = builder
|
||||
.getInternalMethods()
|
||||
.getUsageInstance()
|
||||
.getDescriptions();
|
||||
const builderOptions = builder.getOptions();
|
||||
const builderDefaultOptions = builderOptions.default;
|
||||
const builderAutomatedOptions = builderOptions.defaultDescription;
|
||||
const builderDeprecatedOptions = builder.getDeprecatedOptions();
|
||||
const builderOptionsChoices = builderOptions.choices;
|
||||
const builderOptionTypes = YargsTypes.reduce((acc, type) => {
|
||||
builderOptions[type].forEach(
|
||||
(option: any) => (acc = { ...acc, [option]: type })
|
||||
);
|
||||
return acc;
|
||||
}, {});
|
||||
const subcommands = await Promise.all(
|
||||
Object.entries(getCommands(builder))
|
||||
.filter(([, subCommandConfig]) => {
|
||||
const c = subCommandConfig as CommandModule;
|
||||
// These are all supported yargs fields for description, even though the types don't reflect that
|
||||
// @ts-ignore
|
||||
return c.description || c.describe || c.desc;
|
||||
})
|
||||
.map(([subCommandName, subCommandConfig]) =>
|
||||
parseCommand(subCommandName, subCommandConfig)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
name,
|
||||
description: command.description,
|
||||
commandString: command.original.replace('$0', name),
|
||||
deprecated: command.deprecated,
|
||||
options:
|
||||
Object.keys(builderDescriptions).map((key) => ({
|
||||
name: [key, ...(builderOptions.alias[key] || [])],
|
||||
description: builderDescriptions[key]
|
||||
? builderDescriptions[key].replace('__yargsString__:', '')
|
||||
: '',
|
||||
default: builderDefaultOptions[key] ?? builderAutomatedOptions[key],
|
||||
type: (<any>builderOptionTypes)[key],
|
||||
choices: builderOptionsChoices[key],
|
||||
deprecated: builderDeprecatedOptions[key],
|
||||
hidden: builderOptions.hiddenOptions.includes(key),
|
||||
})) || null,
|
||||
subcommands,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateOptionsMarkdown(
|
||||
command: ParsedCommand,
|
||||
extraHeadingLevels = 0
|
||||
): string {
|
||||
type FieldName = 'name' | 'type' | 'description';
|
||||
const items: Record<FieldName, string>[] = [];
|
||||
const optionsField = command.subcommands?.length ? 'Shared Option' : 'Option';
|
||||
const fields: { field: FieldName; label: string }[] = [
|
||||
{ field: 'name', label: optionsField },
|
||||
{ field: 'type', label: 'Type' },
|
||||
{ field: 'description', label: 'Description' },
|
||||
];
|
||||
if (Array.isArray(command.options) && !!command.options.length) {
|
||||
command.options
|
||||
.sort((a, b) => sortAlphabeticallyFunction(a.name[0], b.name[0]))
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.forEach((option) => {
|
||||
function nameAliases(aliases) {
|
||||
return aliases.map((alias) => code('--' + alias)).join(', ');
|
||||
}
|
||||
const name = option.deprecated
|
||||
? strikethrough(nameAliases(option.name))
|
||||
: nameAliases(option.name);
|
||||
let description = formatDescription(
|
||||
option.description,
|
||||
option.deprecated
|
||||
);
|
||||
let type = option.type;
|
||||
if (option.choices !== undefined) {
|
||||
type = option.choices
|
||||
.map((c: any) => '`' + JSON.stringify(c).replace(/"/g, '') + '`')
|
||||
.join(', ');
|
||||
}
|
||||
if (option.default !== undefined) {
|
||||
description += ` (Default: \`${JSON.stringify(option.default).replace(
|
||||
/"/g,
|
||||
''
|
||||
)}\`)`;
|
||||
}
|
||||
if (
|
||||
(option.name[0] === 'version' &&
|
||||
option.description === 'Show version number') ||
|
||||
(option.name[0] === 'help' && option.description === 'Show help')
|
||||
) {
|
||||
// Add . to the end of the built-in description for consistency with our other descriptions
|
||||
description = `${description}.`;
|
||||
}
|
||||
items.push({ name, type, description });
|
||||
});
|
||||
}
|
||||
return h2('Options', table(items, fields));
|
||||
}
|
||||
Reference in New Issue
Block a user