import path from 'path' ; import * as acorn from 'acorn' ; import * as t from 'io-ts' ; import { failure } from 'io-ts/lib/PathReporter' ; import * as walk from 'acorn-walk' ; import execa from 'execa' ; import importFresh from 'import-fresh' ; import makeDir from 'make-dir' ; import resolve from 'resolve' ; import readPkgUp from 'read-pkg-up' ; import { debug , time } from './util' ; export const findRequiredErrors = { parseError : 'Could not parse provided code' , invalidArguments : 'Arguments of a `require` call must be a ' + 'single string literal, e.g. require(\'got\')' , invalidArgumentFormat : '`require` argument must be in one of the forms of: ' + [ '<name>' , '<scope>/<name>' , '<name>@<version>' , '<scope>/<name>@<version>' ] . join ( ', ' ) } ; export interface Resolved { location : 'relative' | 'node_modules' | 'global cache' ; filepath : string ; meta ? : { name : string ; version : string ; } ; } export interface Required { name : PackageMeta [ 'name' ] ; version ? : PackageMeta [ 'version' ] ; path ? : string ; } const InstalledIO = t . type ( { elapsed : t . number , added : t . array ( t . type ( { action : t . string , name : t . string , version : t . string , path : t . string } ) ) } ) ; export type Installed = t . TypeOf < typeof InstalledIO > ; export interface FindRequiredOutput { errors : string [ ] ; packages : Required [ ] ; } export interface PackageMeta { name : string ; version : string ; maintainers : string [ ] ; published : string ; description ? : string ; homepage ? : string ; license ? : string ; dependencies : { [ name ] : string ; } ; } type RegistryErrorCodes = 'NOT_FOUND' | 'UNKNOWN' ; export class RegistryError extends Error { code : string ; constructor ( code , message ) { super ( message ) ; this . code = code ; this . message = message ; } } const moduleIdRegex = /^((?:[^@\s/]+)|(?:@[^@\s/]+\/[^@\s/]+))(?:@([^@\s/]+))?(\/[^@\s]*)?$/ ; export function stripVersion ( id ) { const res = moduleIdRegex . exec ( id ) ; if ( ! res ) { throw new Error ( `Invalid module id: ${ id }` ) ; } const [ , name , , _path ] = res ; return name + ( _path ? _path : '' ) ; } export function isLocalModuleId ( id ) { return id . startsWith ( '.' ) || id . startsWith ( '/' ) ; } export function findRequired ( code ) { const astTime = time ( 'ast' ) ; const ast = ( code => { try { return acorn . parse ( code ) ; } catch { return null ; } } ) ( code ) ; debug ( astTime ( ) ) ; if ( ! ast ) { return { errors : [ findRequiredErrors . parseError ] , packages : [ ] } ; } const walkTime = time ( 'walk' ) ; const visitors = { CallExpression ( node , state ) { if ( node . callee . name !== 'require' ) { return ; } if ( node . arguments . length !== 1 || node . arguments [ 0 ] . type !== 'Literal' || typeof node . arguments [ 0 ] . value !== 'string' ) { state . errors . push ( findRequiredErrors . invalidArguments ) ; return ; } const { value } = node . arguments [ 0 ] ; const res = moduleIdRegex . exec ( value ) ; if ( ! res ) { state . errors . push ( findRequiredErrors . invalidArgumentFormat ) ; return ; } const [ , name , version , _path ] = res ; if ( ! resolve . isCore ( name ) && ! isLocalModuleId ( name ) ) { state . packages . push ( { name , ... ( version !== undefined ) && { version } , ... ( _path !== undefined ) && { path : _path } } ) ; } } } ; const state = { errors : [ ] , packages : [ ] } ; walk . simple ( ast , visitors , null , state ) ; debug ( walkTime ( ) ) ; return state ; } export interface Moduler { info ( name , version ? ) : Promise < PackageMeta > ; install ( name , version ? ) : Promise < Installed | undefined > ; resolve ( id ) : Resolved ; require ( id ) : unknown ; } export function createModuler ( basePath ) { makeDir . sync ( basePath ) ; function _resolve ( id ) { if ( isLocalModuleId ( id ) ) { return { location : 'relative' , filepath : resolve . sync ( id , { basedir : process . cwd ( ) } ) } ; } const strippedId = stripVersion ( id ) ; const Pkg = t . type ( { package : t . intersection ( [ t . type ( { name : t . string , version : t . string } ) , t . record ( t . string , t . unknown ) ] ) , path : t . string } ) ; const decodePkg = ( filepath ) => Pkg . decode ( readPkgUp . sync ( { cwd : path . dirname ( filepath ) } ) ) . getOrElseL ( ( ) => { throw new Error ( `\`${ id }\` has an invalid \`package.json\` file` ) ; } ) ; try { const filepath = resolve . sync ( strippedId , { basedir : process . cwd ( ) } ) ; const pkg = decodePkg ( filepath ) . package ; return { location : 'node_modules' , filepath , meta : { name : pkg . name , version : pkg . version } } ; } catch { } const filepath = resolve . sync ( strippedId , { basedir : path . join ( path . join ( basePath , 'lib' ) ) } ) ; const pkg = decodePkg ( filepath ) . package ; return { location : 'global cache' , filepath , meta : { name : pkg . name , version : pkg . version } } ; } function _require ( id ) { return importFresh ( _resolve ( id ) . filepath ) ; } async function install ( name , version = 'latest' ) { const id = name + ( version ? '@' + version : '' ) ; const args = [ 'install' , id , '--prefix' , basePath , '--global' , '--loglevel' , 'error' , '--color' , 'always' , '--json' ] ; const { stdout } = await execa ( 'npm' , args , { stderr : 'inherit' } ) ; try { return t . intersection ( [ InstalledIO , t . record ( t . string , t . unknown ) ] ) . decode ( JSON . parse ( stdout ) ) . getOrElseL ( errors => { throw new Error ( failure ( errors ) . join ( '\n' ) ) ; } ) ; } catch { } return undefined ; } async function info ( name , version = 'latest' ) { const id = name + ( version ? '@' + version : '' ) ; const args = [ 'info' , id , '--json' ] ; const { code , stdout } = await execa ( 'npm' , args , { reject : false , stderr : 'pipe' } ) ; const optional = < T > ( x ) => t . union ( [ x , t . undefined ] ) ; const OutputSuccess = t . intersection ( [ t . type ( { name : t . string , version : t . string , time : t . record ( t . string , t . string ) , maintainers : t . array ( t . string ) , description : optional ( t . string ) , homepage : optional ( t . string ) , dependencies : optional ( t . record ( t . string , t . string ) ) , license : optional ( t . string ) } ) , t . record ( t . string , t . unknown ) ] ) ; const OutputError = t . type ( { error : t . type ( { code : t . string , summary : t . string , detail : t . string } ) } ) ; const parsed = JSON . parse ( stdout ) ; if ( code !== 0 ) { const { error } = OutputError . decode ( parsed ) . getOrElseL ( errors => { throw new Error ( failure ( errors ) . join ( '\n' ) ) ; } ) ; if ( error . code === 'E404' ) { throw new RegistryError ( 'NOT_FOUND' , `\`${ id }\` was not found on the npm registry.` ) ; } throw new RegistryError ( 'UNKNOWN' , error . summary ) ; } const output = OutputSuccess . decode ( parsed ) . getOrElseL ( errors => { console . log ( parsed ) ; throw new Error ( failure ( errors ) . join ( '\n' ) ) ; } ) ; const published = output . time [ output . version ] ; return { name : output . name , version : output . version , maintainers : output . maintainers , published , description : output . description , homepage : output . homepage , license : output . license , dependencies : output . dependencies || { } } ; } return { require : _require , resolve : _resolve , install , info } ; }