import * as jconAst from '@quenk/jcon/lib/ast'; import * as rclAst from '@quenk/rcl/lib/ast'; import * as jcon from './jcon'; import * as rcl from './rcl'; import { EOL } from 'os'; import { Path, exists, isDirectory, isFile, readTextFile, listDirsAbs, writeTextFile } from '@quenk/noni/lib/io/file'; import { Future, pure, raise, parallel, doFuture } from '@quenk/noni/lib/control/monad/future'; import { noop } from '@quenk/noni/lib/data/function'; import { transformTree } from './jcon/transform'; import { toCode } from './common/imports'; import { merge } from '@quenk/noni/lib/data/record'; export const FILE_CONF = 'conf'; export const FILE_ROUTE = 'routes'; export const FILE_INDEX = 'index.ts'; export const FILE_START = 'start.ts'; export const DEFAULT_MAIN = '@quenk/tendril/lib/app#App'; export const TDC_MERGE_IMPORTS = /\/\*\s*tdc-output-imports\s*\*\//; export const TDC_MERGE_EXPORTS = /\/\*\s*tdc-output-exports\s*\*\//; type ParsedFiles = [JCONFile, RCLFile]; type Parser = (src: string) => Future; type TypeScript = string; type JCONFile = jconAst.File; type RCLFile = rclAst.File; export interface Arguments { '': string, '--no-recurse': boolean, '--no-start': boolean, '--ignore': string[], '--main': string, '--root-dir': string } export interface Options { module: string, noRecurse: boolean, noStart: boolean, ignore: RegExp[], main: string, rootDir: string } const newContext = (root: Path, path: Path) => ({ path, root, locals: [], loader: (target: Path) => readTextFile(`${path}/${target}`), jcon: jcon.parse, rcl: rcl.parse, tendril: '@quenk/tendril', EOL }); /** * args2Opts function. */ export const args2Opts = (args: Arguments): Options => ({ module: args[''], noRecurse: args['--no-recurse'], noStart: args['--no-start'], ignore: ['node_modules'].concat(args['--ignore']).map(p => new RegExp(p)), main: (args['--main'] != null) ? args['--main'] : DEFAULT_MAIN, rootDir: (args['--root-dir'] != null) ? args['--root-dir'] : process.cwd() }) /** * startTemplate provides the contant of the start.js file. */ export const startTemplate = (opts: Options): [TypeScript, TypeScript] => [ [getMainImport(opts) + `import {template} from './';${EOL}` ].join(EOL), [ `let app = new App(template);${EOL}` + `app.start().fork(e => { throw e; }, ()=>{});` ].join(EOL) ]; const getMainImport = (opts: Options) => { let [mod, exp] = opts.main.split('#'); return `import {${exp} as App} from '${mod}';${EOL}`; } /** * isModule tests whether a directory is a module or not. * * A directory is a module if it has either a conf or routes file or both. */ export const isModule = (path: Path): Future => doFuture(function*() { let isfile = yield exists(path); if (isfile) { let isdir = yield isDirectory(path); if (isdir) { let targets = [ isFile(`${path}/${FILE_CONF}`), isFile(`${path}/${FILE_ROUTE}`) ]; let results: boolean[] = yield parallel(targets); return pure(results.filter(y => y).length > 0); } } return pure(false); }); /** * exec the program. */ export const exec = (path: Path, opts: Options): Future => doFuture(function*() { let pathExists = yield exists(path); if (!pathExists) yield raise(pathNotExistsErr(path)); let isdir = yield isDirectory(path); if (!isdir) yield raise(pathNotDirErr(path)); let files = yield getTDCFiles(path); let ts = yield compile(files, opts, path); yield writeIndexFile(path, ts); if (!opts.noRecurse) yield execR(path, opts); if (!opts.noStart) yield writeStartFile(path, opts); return pure(undefined); }); const pathNotExistsErr = (path: Path) => new Error(`The path ${path} does not exist!`); const pathNotDirErr = (path: Path) => new Error(`The path ${path} is not a directory!`); /** * getFiles provides the parsed conf file and routes file. * * If they don't exist, an empty string is passed to the relevant parser. */ export const getTDCFiles = (path: Path): Future => doFuture(function*() { let conf = yield getParsedFile(`${path}/${FILE_CONF}`, jcon.parse); let routes = yield getParsedFile(`${path}/${FILE_ROUTE}`, rcl.parse); return pure([conf, routes]); }); const getParsedFile = (path: Path, parser: Parser): Future => exists(path) .chain(yes => yes ? readTextFile(path) : pure('')) .chain(parser) /** * @private */ export const compile = ([conf, routes]: ParsedFiles, opts: Options, path: Path) => doFuture<[TypeScript, TypeScript]>(function*() { let ctx = newContext(opts.rootDir, path); let rclCode = yield rcl.file2TS(ctx, routes); let jconTree = yield transformTree( ctx, addCreate(addRoutes(conf, rclCode)) ); let imports = merge( jcon.getAllImports(jconTree.directives), rcl.getAllImports(routes) ); let jconCode = jcon.file2TS(ctx, jconTree); return pure([ [ toCode(imports), `//@ts-ignore: 6133`, `import {System} from '@quenk/potoo/lib/actor/system';`, `//@ts-ignore: 6133`, `import * as _json from '@quenk/noni/lib/data/jsonx';`, `//@ts-ignore: 6133`, `import {Template} from '@quenk/tendril/lib/app/module/template';`, `//@ts-ignore: 6133`, `import {Module} from '@quenk/tendril/lib/app/module';`, `//@ts-ignore: 6133`, `import {Request} from '@quenk/tendril/lib/app/api/request;'`, `//@ts-ignore: 6133`, `import {RouteConf as $RouteConf} from '@quenk/tendril/lib/app/module';`, getMainImport(opts), ctx.EOL ].join(ctx.EOL), [`//@ts-ignore: 6133`, `export const template = ($app: App): Template => ` + `(${ctx.EOL} ${jconCode})` ].join(EOL) ]); }); const addRoutes = (f: JCONFile, routes: string): JCONFile => { let loc = {}; let path = [ new jconAst.Identifier('app', loc), new jconAst.Identifier('routes', loc) ]; let prop = new jconAst.Property(path, new jconAst.Function(routes, loc), loc); f.directives.push(prop); return f; } const addCreate = (f: JCONFile): JCONFile => { let loc = {}; let path = [ new jconAst.Identifier('create', loc), ]; let prop = new jconAst.Property(path, new jconAst.Function(`${EOL}//@ts-ignore: 6133 ${EOL}` + `(s:System) => new Module(s)`, loc), loc); f.directives.unshift(prop); return f; } /** * execR executes recursively on a path. * * All directories under the given path will be checked for * TDC files, if any are found they will be turned into modules. */ export const execR = (path: Path, opts: Options): Future => listDirsAbs(path) .chain(paths => parallel(paths.map(recurse(opts)))); const recurse = (opts: Options) => (path: Path): Future => isIgnored(opts, path) ? pure(undefined) : isModule(path) .chain(yes => yes ? getTDCFiles(path) .chain(files => compile(files, opts, path)) .chain(ts => writeIndexFile(path, ts)) : pure(undefined)) .chain(() => listDirsAbs(path)) .chain(paths => parallel(paths.map(recurse(opts)))) .map(noop); const isIgnored = (opts: Options, path: Path): boolean => opts.ignore.reduce((p, c) => (p === true) ? p : c.test(path), false); /** * writeIndexFile writes out compiled typescript to the index file of a path. * * If the file already exists we attempt to merge it with the output via * the TDC_MERGE_COMMENT */ export const writeIndexFile = (path: Path, ts: [TypeScript, TypeScript]) => writeOutput(`${path}/${FILE_INDEX}`, ts); /** * writeStartFile writes out the start script to a destination/ */ export const writeStartFile = (path: Path, opts: Options): Future => writeOutput(`${path}/${FILE_START}`, startTemplate(opts)); /** * writeOutput writes out compiled typescript to the specified path. * * If the file already exists we attempt to merge it with the output via * the TDC_MERGE_IMPORTS and TDC_MERGE_EXPORTS comment. */ const writeOutput = (path: Path, [iports, eports]: [TypeScript, TypeScript]) => doFuture(function*() { let ts = ''; if (yield isFile(path)) { let contents = yield readTextFile(path); contents = contents.replace(TDC_MERGE_IMPORTS, iports); contents = contents.replace(TDC_MERGE_EXPORTS, eports); ts = contents; } else { ts = [iports, eports].join(''); } return writeTextFile(path, ts); });