feat: initial build

This commit is contained in:
calebboyd
2017-08-28 00:06:46 -05:00
parent 5df6840972
commit c6d057ac43
26 changed files with 1355 additions and 350 deletions
-62
View File
@@ -1,62 +0,0 @@
import { join, dirname } from 'path'
import { readdir, unlink } from 'fs'
import { readFileAsync, writeFileAsync, isDirectoryAsync } from './util'
import { promisify, map } from 'bluebird'
import * as mkdirp from 'mkdirp'
import { NexeCompiler } from './compiler'
const mkdirpAsync = promisify(mkdirp)
const unlinkAsync = (promisify(unlink) as any) as (path: string) => PromiseLike<void>
const readdirAsync = promisify(readdir)
function readDirAsync(dir: string): PromiseLike<string[]> {
return readdirAsync(dir)
.map((file: string) => {
const path = join(dir, file)
return isDirectoryAsync(path).then((x: boolean) => (x ? readDirAsync(path) : path as any))
})
.reduce((a: string[], b: string[] | string) => a.concat(b), [])
}
function maybeReadFileContentsAsync(file: string) {
return readFileAsync(file, 'utf-8').catch(e => {
if (e.code === 'ENOENT') {
return ''
}
throw e
})
}
/**
* The artifacts step is where source patches are committed, or written as "artifacts"
* Steps:
* - A temporary directory is created in the downloaded source
* - On start, any files in that directory are restored into the source tree
* - After the patch functions have run, the temporary directory is emptied
* - Original versions of sources to be patched are written to the temporary directory
* - Finally, The patched files are written into source.
*
*/
export default async function artifacts(compiler: NexeCompiler, next: () => Promise<void>) {
const { src } = compiler
const temp = join(src, 'nexe')
await mkdirpAsync(temp)
const tmpFiles = await readDirAsync(temp)
await map(tmpFiles, async path => {
return compiler.writeFileAsync(path.replace(temp, ''), await readFileAsync(path, 'utf-8'))
})
await next()
await map(tmpFiles, x => unlinkAsync(x))
return map(compiler.files, async file => {
const sourceFile = join(src, file.filename)
const tempFile = join(temp, file.filename)
const fileContents = await maybeReadFileContentsAsync(sourceFile)
await mkdirpAsync(dirname(tempFile))
await writeFileAsync(tempFile, fileContents)
await compiler.writeFileAsync(file.filename, file.contents)
})
}
+18 -6
View File
@@ -1,9 +1,18 @@
import { NexeCompiler } from '../compiler'
import { FuseBox, JSONPlugin, CSSPlugin, HTMLPlugin, SourceMapPlainJsPlugin } from 'fuse-box'
import { FuseBox, JSONPlugin, CSSPlugin, HTMLPlugin, QuantumPlugin } from 'fuse-box'
//import NativeModulePlugin from './fuse-native-module-plugin'
function bundleProducer(filename: string, name: string) {
console.error(process.cwd())
function bundleProducer(filename: string, options: { name: string; minify: any }) {
const plugins: any = [JSONPlugin(), CSSPlugin(), HTMLPlugin()]
if (options.minify) {
plugins.push(
QuantumPlugin({
target: 'server',
uglify: true,
bakeApiIntoBundle: options.name
})
)
}
const fuse = FuseBox.init({
cache: false,
log: Boolean(process.env.NEXE_BUNDLE_DEBUG) || false,
@@ -12,9 +21,9 @@ function bundleProducer(filename: string, name: string) {
writeBundles: false,
output: '$name.js',
target: 'server',
plugins: [JSONPlugin(), CSSPlugin(), HTMLPlugin(), SourceMapPlainJsPlugin()]
plugins
})
fuse.bundle(name).instructions(`> ${filename}`)
fuse.bundle(options.name).instructions(`> ${filename}`)
return fuse.run().then(x => {
let output = ''
x.bundles.forEach(y => (output = y.context.output.lastPrimaryOutput.content!.toString()))
@@ -32,6 +41,9 @@ export default async function bundle(compiler: NexeCompiler, next: any) {
producer = require(compiler.options.bundle).bundleProducer
}
compiler.input = await producer(compiler.options.input, compiler.options.name)
compiler.input = await producer(compiler.options.input, {
name: compiler.options.name,
minify: compiler.options.compress
})
return next()
}
+14 -15
View File
@@ -1,14 +1,13 @@
import { normalize, join } from 'path'
import * as Bluebird from 'bluebird'
import { Buffer } from 'buffer'
import { createHash } from 'crypto'
import { createReadStream } from 'fs'
import { Readable } from 'stream'
import { spawn } from 'child_process'
import { Stream as Needle } from 'nigel'
import { Logger } from './logger'
import { readFileAsync, writeFileAsync, pathExistsAsync, dequote, isWindows } from './util'
import { NexeOptions } from './options'
import { NexeOptions, nexeVersion } from './options'
import { NexeTarget } from './target'
const isBsd = Boolean(~process.platform.indexOf('bsd'))
const make = isWindows ? 'vcbuild.bat' : isBsd ? 'gmake' : 'make'
@@ -51,7 +50,7 @@ export class NexeCompiler {
constructor(public options: NexeOptions) {
const { python } = (this.options = options)
this.log.step('nexe ' + nexeVersion, 'info')
if (python) {
if (isWindows) {
this.env.PATH = '"' + dequote(normalize(python)) + '";' + this.env.PATH
@@ -67,7 +66,10 @@ export class NexeCompiler {
cachedFile = {
absPath,
filename: file,
contents: await readFileAsync(absPath, 'utf-8').catch({ code: 'ENOENT' }, () => '')
contents: await readFileAsync(absPath, 'utf-8').catch(x => {
if (x.code === 'ENOENT') return ''
throw x
})
}
this.files.push(cachedFile)
}
@@ -84,21 +86,21 @@ export class NexeCompiler {
}
}
quit(code = 0) {
quit() {
const time = Date.now() - this.start
this.log.write(`Finsihed in ${time / 1000}s`)
return this.log.flush().then(x => process.exit(code))
return this.log.flush()
}
private _getNodeExecutableLocation(target?: string | null) {
private _getNodeExecutableLocation(target?: NexeTarget) {
if (target) {
return join(this.options.temp, target)
return join(this.options.temp, target.toString())
}
return this.nodeSrcBinPath
}
private _runBuildCommandAsync(command: string, args: string[]) {
return new Bluebird((resolve, reject) => {
return new Promise((resolve, reject) => {
spawn(command, args, {
cwd: this.src,
env: this.env,
@@ -123,7 +125,7 @@ export class NexeCompiler {
: '...'}`
)
await this._configureAsync()
const buildOptions = isWindows ? this.options.vcBuild : this.options.make
const buildOptions = this.options.make
this.compileStep.log(
`Compiling Node${buildOptions.length ? ' with arguments: ' + buildOptions : '...'}`
)
@@ -152,16 +154,13 @@ export class NexeCompiler {
return `process.__nexe=${JSON.stringify(header)};`
}
async compileAsync() {
async compileAsync(target: NexeTarget) {
const step = (this.compileStep = this.log.step('Compiling result'))
let target = this.options.targets.slice().shift()
const location = this._getNodeExecutableLocation(target)
let binary = (await pathExistsAsync(location)) ? createReadStream(location) : null
const header = this._generateHeader()
step.log(`Scanning existing binary...`)
if (target && !binary) {
//throw new Error('\nNot Implemented, use --build during beta\n')
binary = await this._fetchPrebuiltBinaryAsync()
}
+5 -2
View File
@@ -46,14 +46,17 @@ export class Logger {
this.ora.color = color
}
step(text: string): LogStep {
step(text: string, method: string = 'succeed'): LogStep {
if (this.silent) {
return { modify() {}, log() {} }
}
if (!this.ora.id) {
this.ora.start().text = text
if (method !== 'succeed') {
this.ora[method]()
}
} else {
this.ora.succeed().text = text
this.ora[method]().text = text
this.ora.start()
}
+19 -11
View File
@@ -1,18 +1,18 @@
import { compose, PromiseConfig, Middleware } from 'app-builder'
import resource from './resource'
import resource from './steps/resource'
import { NexeCompiler } from './compiler'
import { argv, normalizeOptionsAsync, NexeOptions } from './options'
import cli from './cli'
import { argv, normalizeOptionsAsync, NexeOptions, NexePatch } from './options'
import cli from './steps/cli'
import bundle from './bundling/fuse'
import download from './download'
import artifacts from './artifacts'
import download from './steps/download'
import artifacts from './steps/artifacts'
import patches from './patches'
import { rimrafAsync } from './util'
import * as Bluebird from 'bluebird'
PromiseConfig.constructor = Bluebird
async function compile(compilerOptions: NexeOptions, callback?: (err: Error | null) => void) {
async function compile(
compilerOptions?: Partial<NexeOptions>,
callback?: (err: Error | null) => void
) {
const options = await normalizeOptionsAsync(compilerOptions)
const compiler = new NexeCompiler(options)
const build = compiler.options.build
@@ -25,9 +25,17 @@ async function compile(compilerOptions: NexeOptions, callback?: (err: Error | nu
return compiler.quit()
}
const buildSteps = build ? [download, artifacts, ...patches, ...options.patches] : []
const buildSteps = build
? [download, artifacts, ...patches, ...(options.patches as NexePatch[])]
: []
const nexe = compose(resource, bundle, cli, buildSteps)
return nexe(compiler).asCallback(callback)
return callback
? void nexe(compiler).then(() => callback && callback(null)).catch((e: Error) => {
if (callback) {
callback(e)
} else throw e
})
: nexe(compiler)
}
export { argv, compile }
+39 -57
View File
@@ -1,72 +1,49 @@
import * as parseArgv from 'minimist'
import { NexeCompiler } from './compiler'
import { basename, extname, join } from 'path'
import * as Bluebird from 'bluebird'
import { isWindows } from './util'
import { basename, extname, join, isAbsolute } from 'path'
import { getTarget, NexeTarget } from './target'
import { EOL } from 'os'
export const nexeVersion = '2.0.0-rc.1'
export interface NexePatch {
(compiler: NexeCompiler, next: () => Promise<void>): Promise<void>
}
export interface NexeOptions {
build: boolean
/**
* Entrypoint filepath
*/
input: string
output: string
targets: string[]
/**
* Build name, Used for executable, and stacktraces
*/
compress: boolean
targets: (string | NexeTarget)[]
name: string
/**
* The node version to be built
*/
version: string
python?: string
/**
* Node flags e.g. "--expose-gc" baked into the executable
*/
flags: string[]
/**
* Pass configuration options to node build configure script
*/
configure: string[]
/**
* Pass make options to node make script
*/
make: string[]
vcBuild: string[]
make: string[]
snapshot?: string
/**
* Array of glob strings describing resources to pull into the bundle
*/
resources: string[]
/**
* Temporary directory where nexe artifacts will be cached
* TODO dot folder in home dir
*/
temp: string
ico?: string
sourceMaps?: boolean
rc: { [key: string]: string }
/**
* Causes nexe to remove all temporary files for current configuration
*/
clean: boolean
enableNodeCli: boolean
sourceUrl?: string
bundle: boolean | string
patches: (string | NexePatch)[]
empty: boolean
sourceUrl?: string
python?: string
loglevel: 'info' | 'silent' | 'verbose'
silent?: boolean
verbose?: boolean
info?: boolean
patches: NexePatch[]
empty: boolean
ico?: string
warmup?: string
downloadOptions?: any
clean?: boolean
/**
* Api Only
*/
downloadOptions: any
}
function padRight(str: string, l: number) {
@@ -80,10 +57,11 @@ const defaults = {
configure: [],
make: [],
targets: [],
vcBuild: ['nosign', 'release', process.arch],
vcBuild: isWindows ? ['nosign', 'release', process.arch] : [],
enableNodeCli: false,
compress: false,
build: false,
bundle: true,
build: true,
patches: []
}
const alias = {
@@ -98,9 +76,7 @@ const alias = {
f: 'flag',
c: 'configure',
m: 'make',
vc: 'vcBuild',
s: 'snapshot',
cli: 'enableNodeCli',
h: 'help',
l: 'loglevel'
}
@@ -117,9 +93,8 @@ nexe --help CLI OPTIONS
-v --version =${padRight(process.version.slice(1), 23)}-- node version
-p --python =/path/to/python2 -- python executable
-f --flag ="--expose-gc" -- *v8 flags to include during compilation
-c --configure ="--with-dtrace" -- *pass arguments to the configure command
-m --make ="--loglevel" -- *pass arguments to the make command
-vc --vcBuild =x64 -- *pass arguments to vcbuild.bat
-c --configure ="--with-dtrace" -- *pass arguments to the configure step
-m --make ="--loglevel" -- *pass arguments to the make/build step
-s --snapshot =/path/to/snapshot -- build with warmup snapshot
-r --resource =./paths/**/* -- *embed file bytes within the binary
--bundle =./path/to/config -- pass a module path that exports nexeBundle
@@ -182,20 +157,20 @@ function extractName(options: NexeOptions) {
return name.replace(/\.exe$/, '')
}
function normalizeOptionsAsync(input: Partial<NexeOptions>) {
if (argv.help || argv._.some((x: string) => x === 'version')) {
process.stderr.write(argv.help ? help : '2.0.0-rc.1' + EOL, () => process.exit(0))
function normalizeOptionsAsync(input?: Partial<NexeOptions>): Promise<NexeOptions | never> {
if (argv.help || argv._.some((x: string) => x === 'version') || argv.version === true) {
return new Promise(() => {
process.stderr.write(argv.help ? help : nexeVersion + EOL, () => process.exit(0))
})
}
const options = Object.assign({}, defaults, input) as NexeOptions
const opts = options as any
delete opts._
options.loglevel = extractLogLevel(options)
options.name = extractName(options)
options.flags = flattenFilter(opts.flag, options.flags)
options.targets = flattenFilter(opts.target, options.targets)
options.make = flattenFilter(options.make)
options.vcBuild = flattenFilter(options.vcBuild)
options.make = flattenFilter(options.vcBuild, options.make)
options.configure = flattenFilter(options.configure)
options.resources = flattenFilter(opts.resource, options.resources)
options.rc = options.rc || extractCliMap(/^rc-.*/, options)
@@ -204,10 +179,17 @@ function normalizeOptionsAsync(input: Partial<NexeOptions>) {
options.targets = []
options.build = true
} else if (!options.targets.length) {
const defaultTarget = [process.platform, process.arch, options.version].join('-')
options.targets = [defaultTarget]
options.targets = [getTarget()]
}
options.targets = options.targets.map(getTarget)
options.patches = options.patches.map(x => {
if (typeof x === 'string') {
return require(x).default
}
return x
})
Object.keys(alias).filter(k => k !== 'rc').forEach(x => delete opts[x])
return Promise.resolve(options)
+1 -7
View File
@@ -3,11 +3,6 @@ import { readFileSync } from 'fs'
import { join } from 'path'
export default async function main(compiler: NexeCompiler, next: () => Promise<void>) {
let sourceMapSupport = ''
if (true /*compiler.options.sourceMaps*/) {
sourceMapSupport = readFileSync(join(__dirname, '../../source-map-support.js')).toString()
}
await compiler.setFileContentsAsync(
'lib/_third_party_main.js',
`
@@ -21,7 +16,7 @@ const footer = Buffer.from(Array(32))
fs.readSync(fd, footer, 0, 32, size - 32)
if (!footer.slice(0, 16).equals(Buffer.from('<nexe~~sentinel>'))) {
throw new Error('Invalid Nexe binary')
throw 'Invalid Nexe binary'
}
const contentSize = footer.readDoubleLE(16)
@@ -102,7 +97,6 @@ if (resourceSize) {
const contentBuffer = Buffer.from(Array(contentSize));
fs.readSync(fd, contentBuffer, 0, contentSize, contentStart);
fs.closeSync(fd);
${sourceMapSupport}
const Module = require('module');
process.mainModule = new Module(process.execPath, null);
process.mainModule.loaded = true;
+70
View File
@@ -0,0 +1,70 @@
import { join, dirname } from 'path'
import { readdir, unlink } from 'fs'
import { readFileAsync, writeFileAsync, isDirectoryAsync } from '../util'
import * as mkdirp from 'mkdirp'
import { NexeCompiler } from '../compiler'
import pify = require('pify')
const mkdirpAsync = pify(mkdirp)
const unlinkAsync = pify(unlink)
const readdirAsync = pify(readdir)
function readDirAsync(dir: string): Promise<string[]> {
return readdirAsync(dir).then(paths => {
return Promise.all(
paths.map((file: string) => {
const path = join(dir, file)
return isDirectoryAsync(path).then(x => (x ? readDirAsync(path) : path as any))
})
).then(result => {
return [].concat(...(result as any))
})
})
}
function maybeReadFileContentsAsync(file: string) {
return readFileAsync(file, 'utf-8').catch(e => {
if (e.code === 'ENOENT') {
return ''
}
throw e
})
}
/**
* The artifacts step is where source patches are committed, or written as "artifacts"
* Steps:
* - A temporary directory is created in the downloaded source
* - On start, any files in that directory are restored into the source tree
* - After the patch functions have run, the temporary directory is emptied
* - Original versions of sources to be patched are written to the temporary directory
* - Finally, The patched files are written into source.
*
*/
export default async function artifacts(compiler: NexeCompiler, next: () => Promise<void>) {
const { src } = compiler
const temp = join(src, 'nexe')
await mkdirpAsync(temp)
const tmpFiles = await readDirAsync(temp)
await Promise.all(
tmpFiles.map(async path => {
return compiler.writeFileAsync(path.replace(temp, ''), await readFileAsync(path, 'utf-8'))
})
)
await next()
await Promise.all(tmpFiles.map(x => unlinkAsync(x)))
return Promise.all(
compiler.files.map(async file => {
const sourceFile = join(src, file.filename)
const tempFile = join(temp, file.filename)
const fileContents = await maybeReadFileContentsAsync(sourceFile)
await mkdirpAsync(dirname(tempFile))
await writeFileAsync(tempFile, fileContents)
await compiler.writeFileAsync(file.filename, file.contents)
})
)
}
+11 -7
View File
@@ -1,12 +1,12 @@
import { normalize } from 'path'
import * as Bluebird from 'bluebird'
import { Readable } from 'stream'
import { createWriteStream, chmodSync } from 'fs'
import { readFileAsync, dequote, isWindows } from './util'
import { NexeCompiler } from './compiler'
import { readFileAsync, dequote, isWindows } from '../util'
import { NexeCompiler } from '../compiler'
import { NexeTarget } from '../target'
function readStreamAsync(stream: NodeJS.ReadableStream): PromiseLike<string> {
return new Bluebird(resolve => {
return new Promise(resolve => {
let input = ''
stream.setEncoding('utf-8')
stream.on('data', (x: string) => {
@@ -56,10 +56,14 @@ export default async function cli(compiler: NexeCompiler, next: () => Promise<vo
await next()
}
compiler.output = output || `${compiler.options.name}${isWindows ? '.exe' : ''}`
const deliverable = await compiler.compileAsync()
compiler.output = isWindows
? `${(output || compiler.options.name).replace(/\.exe$/, '')}.exe`
: `${output || compiler.options.name}`
return new Bluebird((resolve, reject) => {
const target = compiler.options.targets.shift() as NexeTarget
const deliverable = await compiler.compileAsync(target)
return new Promise((resolve, reject) => {
const step = log.step('Writing result to file')
deliverable
.pipe(createWriteStream(normalize(compiler.output!)))
+3 -3
View File
@@ -1,8 +1,8 @@
import download = require('download')
import { isDirectoryAsync } from './util'
import { LogStep } from './logger'
import { isDirectoryAsync } from '../util'
import { LogStep } from '../logger'
import { IncomingMessage } from 'http'
import { NexeCompiler } from './compiler'
import { NexeCompiler } from '../compiler'
function fetchNodeSourceAsync(cwd: string, url: string, step: LogStep, options = {}) {
const setText = (p: number) => step.modify(`Downloading Node: ${p.toFixed()}%...`)
+2 -3
View File
@@ -1,8 +1,7 @@
import { each } from 'bluebird'
import { readFileAsync, isDirectoryAsync } from './util'
import { readFileAsync, isDirectoryAsync, each } from '../util'
import { Buffer } from 'buffer'
import * as globs from 'globby'
import { NexeCompiler } from './compiler'
import { NexeCompiler } from '../compiler'
export default async function resource(compiler: NexeCompiler, next: () => Promise<void>) {
const resources = compiler.resources
+86
View File
@@ -0,0 +1,86 @@
export type NodePlatform = 'windows' | 'mac' | 'alpine' | 'linux'
export type NodeArch = 'x86' | 'x64'
const platforms: NodePlatform[] = ['windows', 'mac', 'alpine', 'linux'],
architectures: NodeArch[] = ['x86', 'x64']
export { platforms, architectures }
export interface NexeTarget {
version: string
platform: NodePlatform
arch: NodeArch
}
//TODO bsd
const prettyPlatform: { [key: string]: NodePlatform } = {
win32: 'windows',
windows: 'windows',
darwin: 'mac',
macos: 'mac',
mac: 'mac',
linux: 'linux',
static: 'alpine',
alpine: 'alpine'
}
//TODO arm
const prettyArch: { [key: string]: NodeArch } = {
x86: 'x86',
ia32: 'x86',
x32: 'x86',
x64: 'x64'
}
function isVersion(x: string) {
if (!x) {
return false
}
return /\d/.test(x.replace(/v|\./g, ''))
}
function isPlatform(x: string): x is NodePlatform {
return x in prettyPlatform
}
function isArch(x: string): x is NodeArch {
return x in prettyArch
}
class Target implements NexeTarget {
constructor(public arch: NodeArch, public platform: NodePlatform, public version: string) {}
toJSON() {
return this.toString()
}
toString() {
return `${this.platform}-${this.arch}-${this.version}`
}
}
export function targetsEqual(a: NexeTarget, b: NexeTarget) {
return a.arch === b.arch && a.platform === b.platform && a.version === b.version
}
export function getTarget(target: string | NexeTarget = ''): NexeTarget {
let arch = process.arch as NodeArch,
platform = prettyPlatform[process.platform],
version = process.version.slice(1)
if (typeof target !== 'string') {
target = `${target.platform}-${target.arch}-${target.version}`
}
target.toLowerCase().split('-').forEach(x => {
if (isVersion(x)) {
version = x
}
if (isPlatform(x)) {
platform = prettyPlatform[x]
}
if (isArch(x)) {
arch = prettyArch[x]
}
})
return new Target(arch, platform, version)
}
+3 -19
View File
@@ -1,23 +1,7 @@
declare module 'nigel' {
import { Writable } from 'stream'
interface Needle {
value: Buffer
lastPos: number
last: number
length: number
badCharShift: Buffer
}
export function compile(needle: Buffer): Needle
export function horspool(haystack: Buffer, needle: Needle, start: number): number
export function all(haystack: Buffer, needle: Needle, pos: number): number[]
export class Stream extends Writable {
constructor(needle: Buffer)
needle(needle: Buffer): void
flush(): void
}
declare module 'got' {
function got(url: string, options?: any): Promise<{ body: string }>
export = got
}
declare module 'download' {
import { Duplex } from 'stream'
interface DownloadOptions {
+30 -13
View File
@@ -1,9 +1,26 @@
import { readFile, writeFile, stat } from 'fs'
import * as Bluebird from 'bluebird'
import { execFile } from 'child_process'
import pify = require('pify')
import rimraf = require('rimraf')
const { promisify } = Bluebird
const rimrafAsync = (promisify(rimraf) as any) as (path: string) => Bluebird<void>
const rimrafAsync = pify(rimraf)
export async function each<T>(
list: T[] | Promise<T[]>,
action: (item: T, index: number, list: T[]) => Promise<any>
) {
const l = await list
for (let i = 0; i < l.length; i++) {
await action(l[i], i, l)
}
}
function falseOnEnoent(e: any) {
if (e.code === 'ENOENT') {
return false
}
throw e
}
function dequote(input: string) {
input = input.trim()
@@ -16,30 +33,30 @@ function dequote(input: string) {
}
export interface ReadFileAsync {
(path: string): Bluebird<Buffer>
(path: string, encoding: string): Bluebird<string>
(path: string): Promise<Buffer>
(path: string, encoding: string): Promise<string>
}
const readFileAsync = (promisify(readFile) as any) as ReadFileAsync
const writeFileAsync = (promisify(writeFile) as any) as (
path: string,
contents: string | Buffer
) => Promise<void>
const statAsync = promisify(stat)
const readFileAsync = pify(readFile)
const writeFileAsync = pify(writeFile)
const statAsync = pify(stat)
const execFileAsync = pify(execFile)
const isWindows = process.platform === 'win32'
function pathExistsAsync(path: string) {
return statAsync(path).then(x => true).catch({ code: 'ENOENT' }, () => false)
return statAsync(path).then(x => true).catch(falseOnEnoent)
}
function isDirectoryAsync(path: string) {
return statAsync(path).then(x => x.isDirectory()).catch({ code: 'ENOENT' }, () => false)
return statAsync(path).then(x => x.isDirectory()).catch(falseOnEnoent)
}
export {
dequote,
isWindows,
rimrafAsync,
statAsync,
execFileAsync,
readFileAsync,
pathExistsAsync,
isDirectoryAsync,