@softvisio/cli
Version:
Softisio CLI Tool
397 lines (303 loc) • 12.3 kB
JavaScript
import childProcess from "node:child_process";
import fs from "node:fs";
import url from "node:url";
import ansi from "#core/ansi";
import GitHub from "#core/api/github";
import ejs from "#core/ejs";
import env from "#core/env";
import File from "#core/file";
import FileTree from "#core/file-tree";
import { exists } from "#core/fs";
import { glob } from "#core/glob";
import * as utils from "#core/utils";
import { resolve } from "#core/utils";
import { lintFile } from "#lib/lint";
import Markdown from "#lib/markdown";
import Options from "./docs/options.js";
export default class Docs {
#package;
constructor ( pkg ) {
this.#package = pkg;
}
// properties
get package () {
return this.#package;
}
get location () {
return this.#package.cliConfig?.docs?.location;
}
get locationPath () {
if ( !this.location ) {
return null;
}
else if ( this.location === "/" ) {
return this.#package.root;
}
else {
return this.#package.root + this.location;
}
}
get isEnabled () {
return Boolean( this.location );
}
get isInitialized () {
return Boolean( this.isEnabled && fs.existsSync( this.locationPath ) );
}
// public
async init () {
var res;
process.stdout.write( "Initializing documentation ... " );
try {
// check docs config
if ( !this.location ) throw result( [ 404, "Documentation config was not found" ] );
const options = await new Options( this.#package ).init();
await this.#init( options );
await this.#initGitHubPages( options );
res = result( 200 );
}
catch ( e ) {
res = result.catch( e );
}
console.log( res + "" );
return res;
}
async build () {
var res, warn;
process.stdout.write( "Building documentation ... " );
try {
// check docs config
if ( !this.location ) throw result( [ 404, "Documentation config was not found" ] );
// check docs initialized
if ( !this.isInitialized ) throw result( [ 404, "Documentation is not initialized" ] );
const options = await new Options( this.#package ).init();
// build API schemas
await this.#buildAppSchemas( options, { "lint": false } );
// check markdown files
warn = await this.#checkMarkdown( options, { "lint": true } );
// build index.html
await this.#buildDefault( options );
// build README.md
await this.#buildReadme( options );
res = result( 200 );
}
catch ( e ) {
res = result.catch( e, { "log": false } );
}
console.log( res + "" );
if ( warn ) console.log( warn );
return res;
}
// private
async #init ( options ) {
const fileTree = new FileTree();
// generate README.md
if ( !( await exists( this.locationPath + "/README.md" ) ) ) {
const readmeTmpl = utils.resolve( "#resources/templates/docs/README-default.md", import.meta.url );
if ( this.location !== "/" ) {
// default readme
fileTree.add( {
"path": "README.md",
"buffer": await ejs.renderFile( readmeTmpl, options ),
} );
}
else {
// copy root readme
if ( await exists( this.#package.root + "/README.md" ) ) {
fileTree.add( {
"path": "README.md",
"buffer": fs.readFileSync( this.#package.root + "/README.md" ),
} );
}
// default readme
else {
fileTree.add( {
"path": "README.md",
"buffer": await ejs.renderFile( readmeTmpl, options ),
} );
}
}
}
// generate default sidebar
if ( !( await exists( this.locationPath + "/_sidebar.md" ) ) ) {
fileTree.add( {
"path": "_sidebar.md",
"buffer": await ejs.renderFile( utils.resolve( "#resources/templates/docs/_sidebar.md", import.meta.url ), options ),
} );
}
await fileTree.write( this.locationPath );
// copy .nojekyll
this.#createNoJekyll( options );
}
async #buildDefault ( options ) {
// generate index.html
let index = await ejs.renderFile( utils.resolve( "#resources/templates/docs/index.html", import.meta.url ), options );
// lint index.html
const res = await lintFile( new File( {
"path": this.locationPath + "/" + "index.html",
"buffer": index,
} ) );
if ( !res.ok ) throw res;
index = res.data;
// write index.html
fs.writeFileSync( this.locationPath + "/" + "index.html", index );
// create .nojekyll
this.#createNoJekyll( options );
}
async #buildAppSchemas ( options, { lint } = {} ) {
if ( !options.app ) return;
const builder = resolve( "#resources/docs-builder.js", import.meta.url ),
cwd = this.#package.root,
appUrl = url.pathToFileURL( cwd + "/lib/app.js" ),
args = {
"componentsUrl": resolve( "@softvisio/core/app/components", appUrl, { "url": true } ),
appUrl,
"types": options.app,
};
const proc = childProcess.spawn( process.argv[ 0 ], [ "--preserve-symlinks", "--preserve-symlinks-main", builder, JSON.stringify( args ) ], {
cwd,
"stdio": [ "ignore", "pipe", "inherit" ],
} );
var data = "";
proc.stdout.on( "data", buffer => ( data += buffer.toString() ) );
await new Promise( resolve => proc.on( "close", resolve ) );
if ( proc.exitCode ) throw result( 500 );
data = JSON.parse( data );
fs.rmSync( this.locationPath + "/api", { "force": true, "recursive": true } );
fs.rmSync( this.locationPath + "/rpc", { "force": true, "recursive": true } );
const fileTree = new FileTree();
for ( const [ path, buffer ] of Object.entries( data ) ) {
fileTree.add( new File( { path, buffer } ) );
}
if ( fileTree.isEmpty ) return;
// write files
await fileTree.write( this.locationPath );
// lint
if ( lint ) {
for ( const file of fileTree ) {
const res = await lintFile(
new File( {
"path": this.locationPath + "/" + file.path,
} ),
{
"write": true,
}
);
if ( !res.ok ) throw res;
}
}
}
// TODO: replace relative urls
async #buildReadme ( options ) {
const readmePath = this.locationPath + "/README.md";
if ( this.location === "/" || options.generateReadme === false || !( await exists( readmePath ) ) ) return;
const template = utils.resolve( "#resources/templates/docs/README-wrapper.md.ejs", import.meta.url ),
fileTree = new FileTree();
options.readmeContent = fs.readFileSync( readmePath, "utf8" ).trim();
// TODO: replace relative urls
fileTree.add( {
"path": "README.md",
"buffer": await ejs.renderFile( template, options ),
} );
await fileTree.write( this.#package.root );
}
#createNoJekyll ( options ) {
fs.writeFileSync( this.locationPath + "/.nojekyll", "" );
}
async #checkMarkdown ( options, { lint } = {} ) {
const files = await glob( "**/*.md", {
"cwd": this.locationPath,
"directories": false,
} ),
types = options.getTypes(),
warn = [];
for ( const file of files ) {
const source = fs.readFileSync( this.locationPath + "/" + file, "utf8" );
const markdown = new Markdown( source );
const content = markdown
.traverse(
( node, index, parent ) => {
if ( node.lang ) {
const language = markdown.getCodeLanguage( node.lang )?.language || node.lang;
if ( !options.addLanguage( language ) ) {
throw result( [ 500, `Code block in file "${ file }" has unsupported code language identifier "${ language }"` ] );
}
node.lang = language;
}
return markdown.CONTINUE;
},
{
"test": "code",
}
)
.toMarkdown( {
"handlers": {
text ( node, parent, state, info ) {
const value = markdown.nodeToString( node );
for ( const match of value.matchAll( /{([\w.[\]|]+)}/g ) ) {
for ( let type of match[ 1 ].split( "|" ) ) {
type = type.replace( /\[]$/, "" );
if ( type in types ) {
options.addType( type );
}
else {
warn.push( `${ ansi.warn( " warn " ) } Type "${ type }" in file "${ file }" is undefined` );
}
}
}
return markdown.defaultHandlers.text( node, parent, state, info );
},
},
} );
if ( content !== markdown.source ) fs.writeFileSync( this.locationPath + "/" + file, content );
}
// lint
if ( lint ) {
for ( const file of files ) {
const res = await lintFile(
new File( {
"path": this.locationPath + "/" + file,
} ),
{
"write": true,
}
);
if ( !res.ok ) return res;
}
}
return warn.join( "\n" );
}
async #initGitHubPages ( options ) {
const branch = "main",
path = this.location;
process.stdout.write( "Initializing GitHub pages ... " );
var res;
try {
// GitHub pages init
const upstream = this.#package.git.upstream;
env.loadUserEnv();
if ( upstream.hosting !== "github" ) throw result( [ 500, "Upstream is not GitHub" ] );
if ( !process.env.GITHUB_TOKEN ) throw result( [ 500, "GitHub API token is not configured" ] );
const github = new GitHub( process.env.GITHUB_TOKEN );
res = await github.getPages( upstream.repositorySlug );
// pages created
if ( res.ok ) {
if ( path === res.data.source.path ) {
throw result( [ 200, `OK, branch: "${ res.data.source.branch }", path: "${ res.data.source.path }"` ] );
}
// update location
else {
res = await github.updatePages( upstream.repositorySlug, { "source": { branch, path } } );
if ( res.ok ) throw result( [ 200, `Updated, branch: "${ branch }", path: "${ path }"` ] );
}
}
// create pages
else {
res = await github.createPages( upstream.repositorySlug, branch, path );
}
}
catch ( e ) {
res = result.catch( e );
}
console.log( res + "" );
}
}