shoelace/scripts/make-metadata.js

291 wiersze
9.0 KiB
JavaScript
Czysty Zwykły widok Historia

2021-02-26 14:09:13 +00:00
//
// This script runs TypeDoc and uses its output to generate metadata files used by the docs
2021-02-26 14:09:13 +00:00
//
2021-06-17 21:38:48 +00:00
import chalk from 'chalk';
import { execSync } from 'child_process';
import fs from 'fs';
import mkdirp from 'mkdirp';
import path from 'path';
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
2021-02-26 14:09:13 +00:00
function getTagName(className) {
return className.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`).replace(/^-/, '');
}
// Takes a prop or param and returns type info as a string and, if applicable, an array of possible values
2021-02-26 14:09:13 +00:00
function getTypeInfo(item) {
const values = [];
2021-02-26 14:09:13 +00:00
let type = item.type.name || '';
if (item.type.type === 'union') {
const types = item.type.types.map(t => {
if (t.type === 'literal' || t.type === 'reference') {
values.push(t.value);
2021-02-26 14:09:13 +00:00
type = `'${item.type.types.map(t => t.value).join(`' | '`)}'`;
}
if (t.type === 'intrinsic') {
values.push(t.name);
2021-02-26 14:09:13 +00:00
type = item.type.types.map(t => t.name).join(' | ');
}
});
}
2021-05-30 13:46:09 +00:00
if (item.type.type === 'reflection' && item.type.declaration?.children) {
const args = item.type.declaration.children.map(prop => {
const name = prop.name;
const type = prop.type.name;
const isOptional = prop.flags.isOptional === true;
return `${name}${isOptional ? '?' : ''}: ${type}`;
});
// Display as an object
type += `{ ${args.join(', ')} }`;
}
return {
type,
values: values.length ? values : undefined
};
2021-02-26 14:09:13 +00:00
}
// Splits a string of tag text into a { name, description } object
function splitText(text) {
const shouldSplit = text.indexOf(' - ') > -1;
let name = '';
let description = '';
if (shouldSplit) {
const split = text.split(' - ');
name = split[0].trim();
description = split.slice(1).join(' - ').replace(/^- /, '');
} else {
description = text.trim().replace(/^-\s/, '');
}
return { name, description };
}
// Run typedoc
console.log(chalk.cyan('Generating type data with TypeDoc'));
mkdirp.sync('./.cache');
execSync(
2021-03-04 14:08:43 +00:00
'typedoc --json .cache/typedoc.json --entryPoints src/shoelace.ts --exclude "**/*+(index|.spec|.e2e).ts" --excludeExternals --excludeProtected --excludeInternal',
{ stdio: 'inherit' }
2021-02-26 14:09:13 +00:00
);
const data = JSON.parse(fs.readFileSync('.cache/typedoc.json', 'utf8'));
const modules = data.children;
const components = modules.filter(module => module.kindString === 'Class');
const metadata = {
2021-06-17 21:38:48 +00:00
name: packageData.name,
description: packageData.description,
version: packageData.version,
author: packageData.author,
homepage: packageData.homepage,
license: packageData.license,
2021-02-26 14:09:13 +00:00
components: []
};
components.map(async component => {
const api = {
className: component.name,
tag: getTagName(component.name),
file: component.sources[0].fileName,
since: '',
status: '',
props: [],
methods: [],
events: [],
slots: [],
cssCustomProperties: [],
parts: [],
dependencies: []
};
// Metadata
if (component.comment) {
const tags = component.comment.tags;
const dependencies = tags.filter(item => item.tag === 'dependency');
const slots = tags.filter(item => item.tag === 'slot');
const parts = tags.filter(item => item.tag === 'part');
const customProperties = tags.filter(item => item.tag === 'customproperty');
const animations = tags.filter(item => item.tag === 'animation');
2021-02-26 14:09:13 +00:00
api.since = tags.find(item => item.tag === 'since').text.trim();
api.status = tags.find(item => item.tag === 'status').text.trim();
api.dependencies = dependencies.map(tag => tag.text.trim());
api.slots = slots.map(tag => splitText(tag.text));
api.parts = parts.map(tag => splitText(tag.text));
api.cssCustomProperties = customProperties.map(tag => splitText(tag.text));
api.animations = animations.map(tag => splitText(tag.text));
2021-02-26 14:09:13 +00:00
} else {
console.error(chalk.yellow(`Missing comment block for ${component.name} - skipping metadata`));
}
// Props
const props = component.children
.filter(child => child.kindString === 'Property' && !child.flags.isStatic)
2021-03-06 17:01:39 +00:00
.filter(child => child.type.name !== 'EventEmitter')
2021-02-26 14:09:13 +00:00
.filter(child => child.comment && child.comment.shortText); // only with comments
props.map(prop => {
const { type, values } = getTypeInfo(prop);
2021-05-24 20:07:41 +00:00
let attribute;
// Look for an attribute in the @property decorator
if (Array.isArray(prop.decorators)) {
const decorator = prop.decorators.find(d => d.name === 'property');
if (decorator) {
try {
// We trust TypeDoc <3
const options = eval(`(${decorator.arguments.options})`);
// If an attribute is specified, it will always be a string
if (options && typeof options.attribute === 'string') {
attribute = options.attribute;
}
} catch (err) {
console.log(err);
}
}
}
2021-02-26 14:09:13 +00:00
api.props.push({
name: prop.name,
2021-05-24 20:07:41 +00:00
attribute: attribute,
2021-02-26 14:09:13 +00:00
description: prop.comment.shortText,
type,
values,
2021-02-26 14:09:13 +00:00
defaultValue: prop.defaultValue
});
});
2021-03-06 17:01:39 +00:00
// Events
const events = component.children
.filter(child => child.kindString === 'Property' && !child.flags.isStatic)
.filter(child => child.type.name === 'EventEmitter')
.filter(child => child.comment && child.comment.shortText); // only with comments
events.map(event => {
const decorator = event.decorators.filter(dec => dec.name === 'event')[0];
const name = (decorator ? decorator.arguments.eventName : event.name).replace(/['"`]/g, '');
2021-03-08 13:21:13 +00:00
// TODO: This logic is used to gather event details in a developer-friendly format. It could be improved as it may
// not cover all types yet. The output is used to populate the Events table of each component in the docs.
const params = event.type.typeArguments.map(param => {
if (param.type === 'intrinsic') {
return param.name;
}
if (param.type === 'literal') {
return param.value;
}
if (param.type === 'reflection') {
return (
'{ ' +
param.declaration.children
.map(child => {
2021-03-10 13:33:50 +00:00
// Component exports aren't named, so they appear as "default" in the type data. However, we can use the
// id to link them to the right class.
if (child.type.name === 'default') {
const component = components.find(component => component.id === child.type.id);
if (component) {
child.type.name = component.name;
} else {
child.type.name = 'unknown';
}
}
2021-03-08 13:21:13 +00:00
if (child.type.type === 'intrinsic' || child.type.type === 'reference') {
return `${child.name}: ${child.type.name}`;
} else if (child.name) {
if (child.type.type === 'array') {
return `${child.name}: ${child.type.elementType.name}[]`;
} else {
return `${child.name}: ${child.type.elementType.name} (${child.type.type})`;
}
} else {
return child.type.type;
}
})
.join(', ') +
' }'
);
}
return '';
});
const details = params.join(', ');
2021-03-06 17:01:39 +00:00
api.events.push({
name,
description: event.comment.shortText,
details
});
});
2021-02-26 14:09:13 +00:00
// Methods
const methods = component.children
.filter(child => child.kindString === 'Method' && !child.flags.isStatic)
.filter(child => child.signatures[0].comment && child.signatures[0].comment.shortText); // only with comments
methods.map(method => {
const signature = method.signatures[0];
const params = Array.isArray(signature.parameters)
? signature.parameters.map(param => {
const { type, values } = getTypeInfo(param);
2021-02-26 14:09:13 +00:00
return {
name: param.name,
type,
values,
2021-05-29 14:54:55 +00:00
isOptional: param.flags?.isOptional,
2021-02-26 14:09:13 +00:00
defaultValue: param.defaultValue
};
})
: [];
api.methods.push({
name: method.name,
description: signature.comment.shortText,
params
});
});
metadata.components.push(api);
2021-02-26 14:09:13 +00:00
});
2021-03-25 12:15:26 +00:00
// Generate metadata.json
2021-02-26 14:09:13 +00:00
(async () => {
2021-03-25 12:15:26 +00:00
const filename = path.join('./dist/metadata.json');
const json = JSON.stringify(metadata, null, 2);
await mkdirp(path.dirname(filename));
fs.writeFileSync(filename, json, 'utf8');
})();
// Generate vscode.html-custom-data.json (for IntelliSense)
(async () => {
const filename = path.join('./dist/vscode.html-custom-data.json');
const customData = {
tags: metadata.components.map(component => ({
name: component.tag,
description: component.description,
attributes: component.props.map(prop => ({
name: prop.name,
description: prop.description,
values: prop.values ? prop.values.map(value => ({ name: value })) : undefined
}))
}))
};
const json = JSON.stringify(customData, null, 2);
2021-02-26 14:09:13 +00:00
await mkdirp(path.dirname(filename));
fs.writeFileSync(filename, json, 'utf8');
2021-02-26 14:09:13 +00:00
})();
console.log(chalk.green(`Successfully generated metadata 🏷\n`));