import ITsccSpec , { ExternalModuleData } from './ITsccSpec' ; import ITsccSpecJSON , { IModule , INamedModuleSpecs , IDebugOptions } from './ITsccSpecJSON' ; import { DirectedTree , CycleError } from './shared/Graph' ; import path = require ( 'path' ) ; import fs = require ( 'fs' ) ; import process = require ( 'process' ) ; import { readJsonSync } from 'fs-extra' ; import fg = require ( 'fast-glob' ) ; interface IInputTsccSpecJSONWithSpecFile extends Partial < ITsccSpecJSON > { /**
     * If exists, the plugin will first load the spec from the specified path,
     * and then override it with properties provided in this object.
     */ specFile : string ; } interface IInputTsccSpecJSONWithOptionalSpecFile extends ITsccSpecJSON { specFile ? : string ; } export type IInputTsccSpecJSON = IInputTsccSpecJSONWithOptionalSpecFile | IInputTsccSpecJSONWithSpecFile ; function hasSpecFileKey ( json ) { return typeof json . specFile === 'string' ; } export default class TsccSpec implements ITsccSpec { protected static readonly SPEC_FILE = 'tscc.spec.json' ; private static PATH_SEP = '[\\\/' + ( path . sep === '/' ? '' : '\\\\' ) + ']' ; private static readonly RE_DOT_PATH = new RegExp ( '^[\\.]{1,2}' + TsccSpec . PATH_SEP ) ; private static readonly RE_ENDS_WITH_SEP = new RegExp ( TsccSpec . PATH_SEP + '$' ) ; private static isDotPath ( p ) { return TsccSpec . RE_DOT_PATH . test ( p ) ; } private static endsWithSep ( p ) { return TsccSpec . RE_ENDS_WITH_SEP . test ( p ) ; } /**
     * Follows the behavior of Typescript CLI.
     * 1. If --project argument is supplied,
     *   1-1. If it is a file, use it.
     *   1-2. If it is a directory, use directory/tsconfig.json.
     *   1-3. If it is not a file nor a directory, throw an error.
     * 2. If it is not supplied (and file arguments are not supplied which is always the case for
     *    tscc) it calls ts.findConfigFile to search for tsconfig.json from the current working
     *    directory.
     */ /**
     * At least one among searchPath and defaultLocation must be non-null. This cannot be expressed
     * well with function overloads, because for example when one tries to call it with variables
     * satisfying the same contract, TS thinks that the call signature is not visible.
     */ protected static resolveSpecFile ( searchPath , specFileName , defaultLocation ? ) { if ( typeof searchPath === 'string' ) { try { let stat = fs . statSync ( searchPath ) ; if ( stat . isFile ( ) ) return path . resolve ( searchPath ) ; if ( stat . isDirectory ( ) ) { let specPath = path . resolve ( searchPath , specFileName ) ; let specStat = fs . statSync ( specPath ) ; if ( specStat . isFile ( ) ) return specPath ; } } catch ( e ) { } return ; } let nextPath = defaultLocation ! ; while ( nextPath !== searchPath ) { searchPath = nextPath ; try { let specPath = path . resolve ( searchPath , specFileName ) ; let stat = fs . statSync ( specPath ) ; if ( stat . isFile ( ) ) return specPath ; } catch ( e ) { } nextPath = path . dirname ( searchPath ) ; } return ; } protected static toDisplayedPath ( p ) { const relPath = path . relative ( '.' , p ) ; if ( TsccSpec . isDotPath ( relPath ) ) return path . resolve ( p ) ; if ( relPath === '.' ) return "the current working directory" ; return relPath ; } private static findTsccSpecAndThrow ( root ) { const specPath = TsccSpec . resolveSpecFile ( root , TsccSpec . SPEC_FILE , process . cwd ( ) ) ; if ( specPath === undefined ) { let displayedPath = TsccSpec . toDisplayedPath ( root || process . cwd ( ) ) ; throw new TsccSpecError ( `No spec file was found from ${ displayedPath }.` ) ; } return specPath ; } protected static loadSpecRaw ( tsccSpecJSONOrItsPath ) { const tsccSpecJSONPath = typeof tsccSpecJSONOrItsPath === 'string' ? TsccSpec . findTsccSpecAndThrow ( tsccSpecJSONOrItsPath ) : typeof tsccSpecJSONOrItsPath === 'object' ? hasSpecFileKey ( tsccSpecJSONOrItsPath ) ? TsccSpec . findTsccSpecAndThrow ( tsccSpecJSONOrItsPath . specFile ) : path . join ( process . cwd ( ) , TsccSpec . SPEC_FILE ) : TsccSpec . findTsccSpecAndThrow ( undefined ) ; const readSpecJSON = ( ) : ITsccSpecJSON => { try { return readJsonSync ( tsccSpecJSONPath ) ; } catch ( e ) { throw new TsccSpecError ( `Spec file is an invalid JSON: ${ TsccSpec . toDisplayedPath ( tsccSpecJSONPath ) }.` ) ; } } ; const tsccSpecJSON = typeof tsccSpecJSONOrItsPath === 'object' ? hasSpecFileKey ( tsccSpecJSONOrItsPath ) ? Object . assign ( readSpecJSON ( ) , tsccSpecJSONOrItsPath ) : tsccSpecJSONOrItsPath : readSpecJSON ( ) ; return { tsccSpecJSON , tsccSpecJSONPath } ; } static loadSpec < T extends typeof TsccSpec > ( this , tsccSpecJSONOrItsPath ) { let { tsccSpecJSON , tsccSpecJSONPath } = TsccSpec . loadSpecRaw ( tsccSpecJSONOrItsPath ) ; return < InstanceType < T > > new this ( tsccSpecJSON , tsccSpecJSONPath ) ; } constructor ( protected readonly tsccSpec , protected basePath ) { this . computeOrderedModuleSpecs ( ) ; this . resolveRelativeExternalModuleNames ( ) ; } private orderedModuleSpecs ! : Required < INamedModuleSpecs > [ ] ; private computeOrderedModuleSpecs ( ) { const modules = this . tsccSpec . modules ; if ( Array . isArray ( modules ) ) { this . orderedModuleSpecs = modules . map ( module => this . interopModuleSpecs ( module . moduleName , module ) ) ; return ; } const graph = new DirectedTree < string > ( ) ; for ( let moduleName in modules ) { graph . addNodeById ( moduleName ) ; let moduleSpecOrModuleEntryFile = modules [ moduleName ] ; if ( typeof moduleSpecOrModuleEntryFile === 'string' ) continue ; let deps = moduleSpecOrModuleEntryFile . dependencies ; if ( ! deps ) continue ; for ( let dep of deps ) { graph . addEdgeById ( dep , moduleName ) ; } } let sorted ; try { sorted = graph . sort ( ) ; } catch ( e ) { if ( e instanceof CycleError ) { throw new TsccSpecError ( `Circular dependency in modules ${ [ ... e . cycle ] }` ) ; } throw e ; } this . orderedModuleSpecs = sorted . map ( moduleName => { return this . interopModuleSpecs ( moduleName , modules [ moduleName ] ) ; } ) ; } getOrderedModuleSpecs ( ) { return this . orderedModuleSpecs ; } private interopModuleSpecs ( moduleName , moduleSpec ) { let spec = typeof moduleSpec === 'string' ? { entry : moduleSpec } : moduleSpec ; if ( ! ( 'dependencies' in spec ) ) spec . dependencies = [ ] ; if ( ! ( 'extraSources' in spec ) ) spec . extraSources = [ ] ; spec . moduleName = moduleName ; spec . entry = this . absolute ( spec . entry ) ; return < Required < INamedModuleSpecs > > spec ; } /**
     * Paths specified in TSCC spec are resolved with following strategy:
     *  - If starts with "./" or "../", resolve relative to the spec file's path.
     *  - If it is still not an absolute path, resolve relative to the current working directory.
     *    as if cwd is in the PATH.
     *  - Otherwise, use the absolute path as is.
     *  Also, it preserves the trailing path separator. This, for example, has semantic difference
     *  in closure compiler's 'chunk_output_path_prefix' option.
     */ protected absolute ( filePath ) { if ( path . isAbsolute ( filePath ) ) return path . normalize ( filePath ) ; let endsWithSep = TsccSpec . endsWithSep ( filePath ) || filePath . length === 0 ; let base = TsccSpec . isDotPath ( filePath ) ? path . dirname ( this . basePath ) : process . cwd ( ) ; return path . resolve ( base , filePath ) + ( endsWithSep ? path . sep : '' ) ; } /**
     * Resolves with TSCC's convention, but as a relative path from current working directory.
     */ protected relativeFromCwd ( filePath ) { let absolute = this . absolute ( filePath ) ; let endsWithSep = absolute . endsWith ( path . sep ) ; const relative = path . relative ( process . cwd ( ) , absolute ) ; return relative + ( endsWithSep && relative . length > 0 ? path . sep : '' ) ; } protected getOutputPrefix ( target ) { let prefix = this . tsccSpec . prefix ; if ( typeof prefix === 'undefined' ) return '' ; if ( typeof prefix === 'string' ) return prefix ; return prefix [ target ] ; } private external : Map < string , ExternalModuleData > = new Map ( ) ; private resolveRelativeExternalModuleNames ( ) { if ( ! ( 'external' in this . tsccSpec ) ) return ; for ( let [ moduleName , globalName ] of Object . entries ( this . tsccSpec . external ! ) ) { if ( TsccSpec . isDotPath ( moduleName ) ) { this . external . set ( this . absolute ( moduleName ) , { globalName , isFilePath : true } ) ; } else { this . external . set ( moduleName , { globalName , isFilePath : false } ) ; } } } getExternalModuleNames ( ) { return [ ... this . external . keys ( ) ] ; } getExternalModuleDataMap ( ) { return this . external ; } getJsFiles ( ) { let jsFiles = this . tsccSpec . jsFiles ; if ( ! jsFiles ) return [ ] ; if ( typeof jsFiles === 'string' ) { jsFiles = [ jsFiles ] ; } jsFiles = jsFiles . map ( this . absolute , this ) ; return < string [ ] > fg . sync ( jsFiles ) ; } debug ( ) { return this . tsccSpec . debug || { } ; } } export class TsccSpecError extends Error { }