diff --git a/package.json b/package.json index 667e5bf5..1ea7843e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nexe", "description": "Create a single executable out of your Node.js application", "license": "MIT", - "version": "2.0.0-rc.13", + "version": "2.0.0-rc.14", "contributors": [ "Craig Condon (http://crcn.io)", "Jared Allard ", @@ -10,10 +10,10 @@ ], "scripts": { "asset-compile": "ts-node tasks/asset-compile", - "prebuild": "rimraf lib && npm run lint", + "test": "mocha", + "fmt": "prettier --parser typescript --no-semi --print-width 100 --single-quote --write \"{src,plugins,tasks}/**/*.ts\"", "prepublish": "npm test && npm run build", - "test": "mocha test/**/*.spec.ts", - "lint": "prettier --parser typescript --no-semi --print-width 100 --single-quote --write \"src/**/*.ts\"", + "prebuild": "rimraf lib && npm run fmt", "build": "tsc --declaration", "postbuild": "ts-node tasks/post-build" }, diff --git a/plugins/nexe-daemon/src/index.ts b/plugins/nexe-daemon/src/index.ts index 00a3f173..12676a22 100644 --- a/plugins/nexe-daemon/src/index.ts +++ b/plugins/nexe-daemon/src/index.ts @@ -12,37 +12,36 @@ interface NexeDaemonOptions { type DaemonOptions = NexeOptions & { daemon: { windows: NexeDaemonOptions } } function renderWinswConfig(options: any) { - return '\r\n' + + return ( + '\r\n' + `${Object.keys(options).reduce((config: string, element: string) => { - return config += `<${element}>${options[element]}\r\n` + return (config += `<${element}>${options[element]}\r\n`) }, '')}\r\n` + ) } -export default function daemon (compiler: NexeCompiler, next: () => Promise) { +export default function daemon(compiler: NexeCompiler, next: () => Promise) { if (compiler.target.platform !== 'windows') { return next() } compiler.addResource( './nexe/plugin/daemon/winsw.exe', readFileSync(require.resolve('../winsw.exe')) - ) + ) const name = compiler.options.name, - options = compiler.options.daemon && compiler.options.daemon.windows || {}, + options = (compiler.options.daemon && compiler.options.daemon.windows) || {}, defaults: NexeDaemonOptions = { id: name, name, description: name, executable: '%BASE%\\' + compiler.output - } + } compiler.addResource( './nexe/plugin/daemon/winsw.xml', Buffer.from(renderWinswConfig(Object.assign(defaults, options))) ) - compiler.addResource( - './nexe/plugin/daemon/app.js', - Buffer.from(compiler.input) - ) + compiler.addResource('./nexe/plugin/daemon/app.js', Buffer.from(compiler.input)) compiler.input = '{{replace:plugins/nexe-daemon/lib/nexe-daemon.js}}' return next() diff --git a/plugins/nexe-daemon/src/nexe-daemon.ts b/plugins/nexe-daemon/src/nexe-daemon.ts index 63c30b36..782f568c 100644 --- a/plugins/nexe-daemon/src/nexe-daemon.ts +++ b/plugins/nexe-daemon/src/nexe-daemon.ts @@ -4,7 +4,7 @@ import * as path from 'path' import * as fs from 'fs' import * as cp from 'child_process' -function mkdirp (r: string, t?: any): any { +function mkdirp(r: string, t?: any): any { ;(t = t || null), (r = path.resolve(r)) try { fs.mkdirSync(r), (t = t || r) @@ -31,23 +31,20 @@ if (~installIndex) { const filename = path.join(directory, path.basename(process.execPath)) const readStream = fs.createReadStream(process.execPath) const writeStream = fs.createWriteStream(filename) - - const onError = function () { + + const onError = function() { readStream.removeAllListeners() writeStream.removeAllListeners() //TODO } - + readStream .on('error', onError) .pipe(writeStream) .on('error', onError) .on('close', () => { const winsw = filename.replace('.exe', '-service.exe') - fs.writeFileSync( - winsw, - fs.readFileSync('./nexe/plugin/daemon/winsw.exe') - ) + fs.writeFileSync(winsw, fs.readFileSync('./nexe/plugin/daemon/winsw.exe')) fs.writeFileSync( filename.replace('.exe', '-service.xml'), fs.readFileSync('./nexe/plugin/daemon/winsw.xml') @@ -56,14 +53,14 @@ if (~installIndex) { }) } } else { - console.log("DIRECTORY NOT FOUND") + console.log('DIRECTORY NOT FOUND') require('./nexe/plugin/daemon/app.js') } -function installService (filename: string) { - cp.exec(filename + ' install', (error) => { +function installService(filename: string) { + cp.exec(filename + ' install', error => { if (error && !error.message.includes('already exists')) { throw error - } + } }) } diff --git a/src/compiler.ts b/src/compiler.ts index 9d8898c1..37184001 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -45,10 +45,7 @@ export class NexeCompiler { index: {}, bundle: Buffer.from('') } - public output = isWindows - ? `${(this.options.output || this.options.name).replace(/\.exe$/, '')}.exe` - : `${this.options.output || this.options.name}` - + public output = this.options.output private nodeSrcBinPath: string constructor(public options: T) { const { python } = (this.options = options) @@ -196,18 +193,17 @@ export class NexeCompiler { throw new Error(`${assetName} not available, create it using the --build flag`) } const filename = this.getNodeExecutableLocation(target) - await download( - asset.browser_download_url, - dirname(filename), - downloadOptions - ).on('response', (res: IncomingMessage) => { - const total = +res.headers['content-length']! - let current = 0 - res.on('data', data => { - current += data.length - this.compileStep.modify(`Downloading...${(current / total * 100).toFixed()}%`) - }) - }) + await download(asset.browser_download_url, dirname(filename), downloadOptions).on( + 'response', + (res: IncomingMessage) => { + const total = +res.headers['content-length']! + let current = 0 + res.on('data', data => { + current += data.length + this.compileStep.modify(`Downloading...${(current / total * 100).toFixed()}%`) + }) + } + ) return createReadStream(filename) } diff --git a/src/nexe.ts b/src/nexe.ts index 59a43743..4bc4c1ad 100644 --- a/src/nexe.ts +++ b/src/nexe.ts @@ -1,7 +1,7 @@ import { compose, Middleware } from 'app-builder' import resource from './steps/resource' import { NexeCompiler } from './compiler' -import { argv, version, help, normalizeOptionsAsync, NexeOptions, NexePatch } from './options' +import { argv, version, help, normalizeOptions, NexeOptions, NexePatch } from './options' import cli from './steps/cli' import bundle from './steps/bundle' import download from './steps/download' @@ -15,7 +15,7 @@ async function compile( compilerOptions?: Partial, callback?: (err: Error | null) => void ) { - const options = await normalizeOptionsAsync(compilerOptions) + const options = normalizeOptions(compilerOptions) const compiler = new NexeCompiler(options) const build = compiler.options.build diff --git a/src/options.ts b/src/options.ts index 5363a304..4cb6fbd6 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,9 +1,9 @@ import * as parseArgv from 'minimist' import { NexeCompiler } from './compiler' import { isWindows, padRight } from './util' -import { basename, extname, join, isAbsolute, relative, dirname } from 'path' +import { basename, extname, join, isAbsolute, relative, dirname, resolve } from 'path' import { getTarget, NexeTarget } from './target' -import { EOL } from 'os' +import { EOL, homedir } from 'os' import * as c from 'chalk' export const version = '{{replace:0}}' @@ -113,7 +113,7 @@ ${c.bold('nexe [options]')} ${c.underline.bold('Other options:')} --bundle -- custom bundling module with 'createBundle' export - --temp -- temp file storage default './nexe' + --temp -- temp file storage default '~/.nexe' --cwd -- set the current working directory for the command --fake-argv -- fake argv[1] with entry file --clean -- force download of sources @@ -182,36 +182,38 @@ function extractName(options: NexeOptions) { return name.replace(/\.exe$/, '') } -function isEntryFile(filename: string) { - return filename && !isAbsolute(filename) && filename !== 'node' && /\.(tsx?|jsx?)$/.test(filename) +function isEntryFile(filename?: string): filename is string { + return Boolean( + filename && !isAbsolute(filename) && filename !== 'node' && /\.(tsx?|jsx?)$/.test(filename) + ) } -function findInput(input: string, cwd: string) { - const maybeInput = argv._.slice().pop() || '' +export function resolveEntry(input: string, cwd: string, maybeEntry?: string) { if (input) { - return input + return resolve(cwd, input) } - if (isEntryFile(maybeInput)) { - return maybeInput + if (isEntryFile(maybeEntry)) { + return resolve(cwd, maybeEntry) } if (!process.stdin.isTTY) { return '' } try { - const main = require.resolve(cwd) - return './' + relative(cwd, main) + return require.resolve(cwd) } catch (e) { - void e + throw new Error('No entry file found') } - return '' } -function normalizeOptionsAsync(input?: Partial): Promise { +function normalizeOptions(input?: Partial): NexeOptions { const options = Object.assign({}, defaults, input) as NexeOptions const opts = options as any - - options.temp = options.temp || process.env.NEXE_TEMP || join(options.cwd, '.nexe') - options.input = findInput(options.input, options.cwd) + const cwd = (options.cwd = resolve(options.cwd)) + options.temp = options.temp + ? resolve(cwd, options.temp) + : process.env.NEXE_TEMP || join(homedir(), '.nexe') + const maybeEntry = input === argv ? argv._[argv._.length - 1] : undefined + options.input = resolveEntry(options.input, cwd, maybeEntry) options.name = extractName(options) options.loglevel = extractLogLevel(options) options.flags = flatten(opts.flag, options.flags) @@ -220,6 +222,10 @@ function normalizeOptionsAsync(input?: Partial): Promise): Promise k !== 'rc') .forEach(x => delete opts[x]) - return Promise.resolve(options) + return options } -export { argv, normalizeOptionsAsync, help } +export { argv, normalizeOptions, help } diff --git a/src/steps/bundle.ts b/src/steps/bundle.ts index 473089c6..b1477c63 100644 --- a/src/steps/bundle.ts +++ b/src/steps/bundle.ts @@ -16,17 +16,18 @@ function createBundle(options: NexeOptions) { }) ) } + const cwd = options.cwd const fuse = FuseBox.init({ cache: false, log: Boolean(process.env.NEXE_BUNDLE_LOG) || false, - homeDir: options.cwd, + homeDir: cwd, sourceMaps: false, writeBundles: false, output: '$name.js', target: 'server', plugins }) - const input = relative(options.cwd, options.input).replace(/\\/g, '/') + const input = relative(cwd, resolve(cwd, options.input)).replace(/\\/g, '/') fuse.bundle(options.name).instructions(`> ${input}`) return fuse.run().then((x: any) => { let output = '' @@ -36,8 +37,9 @@ function createBundle(options: NexeOptions) { } export default async function bundle(compiler: NexeCompiler, next: any) { - if (!compiler.options.bundle) { - compiler.input = await readFileAsync(compiler.options.input, 'utf-8') + const { bundle, cwd, empty, input } = compiler.options + if (!bundle) { + compiler.input = await readFileAsync(resolve(cwd, input), 'utf-8') return next() } @@ -48,13 +50,13 @@ export default async function bundle(compiler: NexeCompiler, next: any) { let producer = createBundle if (typeof compiler.options.bundle === 'string') { - producer = require(resolve(compiler.options.cwd, compiler.options.bundle)).createBundle + producer = require(resolve(cwd, bundle)).createBundle } compiler.input = await producer(compiler.options) if ('string' === typeof compiler.options.debugBundle) { - await writeFileAsync(compiler.options.debugBundle, compiler.input) + await writeFileAsync(resolve(cwd, compiler.options.debugBundle), compiler.input) } return next() diff --git a/src/steps/cli.ts b/src/steps/cli.ts index b2df6c1e..ca003750 100644 --- a/src/steps/cli.ts +++ b/src/steps/cli.ts @@ -1,4 +1,4 @@ -import { normalize } from 'path' +import { normalize, relative } from 'path' import { Readable } from 'stream' import { createWriteStream, chmodSync, statSync } from 'fs' import { readFileAsync, dequote, isWindows } from '../util' @@ -56,12 +56,15 @@ export default async function cli(compiler: NexeCompiler, next: () => Promise console.log('Building...'), 300 * 1000) return () => clearInterval(keepalive) } -function assertNexeBinary (file: string) { +function assertNexeBinary(file: string) { return execFileAsync(file).catch(e => { if (e && e.stack && e.stack.includes('Invalid Nexe binary')) { return diff --git a/tasks/ci.ts b/tasks/ci.ts index add71688..e10bda52 100644 --- a/tasks/ci.ts +++ b/tasks/ci.ts @@ -5,8 +5,7 @@ const { env } = process export function triggerMacBuild(release: NexeTarget, branch: string) { assert.ok(env.CIRCLE_TOKEN) - const circle = `https://circleci.com/api/v1.1/project/github/nexe/nexe/tree/${ - branch}?circle-token=${env.CIRCLE_TOKEN}` + const circle = `https://circleci.com/api/v1.1/project/github/nexe/nexe/tree/${branch}?circle-token=${env.CIRCLE_TOKEN}` return got(circle, { json: true, body: { @@ -17,7 +16,7 @@ export function triggerMacBuild(release: NexeTarget, branch: string) { }) } -export function triggerDockerBuild (release: NexeTarget, branch: string) { +export function triggerDockerBuild(release: NexeTarget, branch: string) { assert.ok(env.TRAVIS_TOKEN) const travis = `https://api.travis-ci.org/repo/nexe%2Fnexe/requests` return got(travis, { @@ -38,15 +37,13 @@ export function triggerDockerBuild (release: NexeTarget, branch: string) { }, headers: { 'Travis-API-Version': '3', - 'Authorization': `token ${env.TRAVIS_TOKEN}` + Authorization: `token ${env.TRAVIS_TOKEN}` } }) } -export function triggerWindowsBuild (release: NexeTarget) { +export function triggerWindowsBuild(release: NexeTarget) { const hasVersion = 'NEXE_VERSION' in env - env.NEXE_VERSION = hasVersion - ? env.NEXE_VERSION!.trim() - : release.toString() + env.NEXE_VERSION = hasVersion ? env.NEXE_VERSION!.trim() : release.toString() return Promise.resolve() } diff --git a/tasks/docker.ts b/tasks/docker.ts index 87726062..06ffa15b 100644 --- a/tasks/docker.ts +++ b/tasks/docker.ts @@ -5,7 +5,7 @@ import got = require('got') import execa = require('execa') import { appendFileSync } from 'fs' -function alpine (target: NexeTarget) { +function alpine(target: NexeTarget) { return ` FROM ${target.arch === 'x64' ? '' : 'i386/'}alpine:3.4 RUN apk add --no-cache curl make gcc g++ binutils-gold python linux-headers paxctl libgcc libstdc++ git vim tar gzip wget @@ -26,29 +26,34 @@ RUN rm /nexe_temp/\${NODE_VERSION}/out/Release/node && \ `.trim() } -export async function runAlpineBuild (target: NexeTarget) { +export async function runAlpineBuild(target: NexeTarget) { await writeFileAsync('Dockerfile', alpine(target)) const outFilename = 'nexe-alpine-build-log.txt' await writeFileAsync(outFilename, '') let output: any = [] try { - output.push(await execa.shell(`docker build -t nexe-alpine .`)) - output.push(await execa.shell(`docker run -d --name nexe nexe-alpine sh`)) - output.push(await execa.shell(`docker cp nexe:/out out`)) + output.push(await execa.shell(`docker build -t nexe-alpine .`)) + output.push(await execa.shell(`docker run -d --name nexe nexe-alpine sh`)) + output.push(await execa.shell(`docker cp nexe:/out out`)) output.push(await execa.shell(`docker rm nexe`)) - } catch(e) { + } catch (e) { console.log('Error running docker', e) } finally { output.forEach((x: any) => { appendFileSync(outFilename, x.stderr) appendFileSync(outFilename, x.stdout) }) - await got(`https://transfer.sh/${Math.random().toString(36).substring(2)}.txt`, { - body: await readFileAsync(outFilename), - method: 'PUT' - }) - .then(x => console.log('Posted docker log: ', x.body)) - .catch(e => console.log('Error posting log', e)) + await got( + `https://transfer.sh/${Math.random() + .toString(36) + .substring(2)}.txt`, + { + body: await readFileAsync(outFilename), + method: 'PUT' + } + ) + .then(x => console.log('Posted docker log: ', x.body)) + .catch(e => console.log('Error posting log', e)) } } diff --git a/tasks/post-build.ts b/tasks/post-build.ts index bfe44487..d4ac2799 100644 --- a/tasks/post-build.ts +++ b/tasks/post-build.ts @@ -10,21 +10,19 @@ inject('lib/patches/third-party-main.js') inject('lib/steps/shim.js') inject('lib/options.js', JSON.stringify(require('../package.json').version)) -function inject (filename: string, ...replacements: string[]) { +function inject(filename: string, ...replacements: string[]) { let contents = readFileSync(filename, 'utf8') contents = contents.replace(/('{{(.*)}}')/g, (substring: string, ...matches: string[]) => { if (!matches || !matches[1]) { return substring - } + } const [replace, file] = matches[1].split(':') if (replace !== 'replace') { return substring } console.log('Replacing: ', substring) - return replacements[+file] - ? replacements[+file] - : JSON.stringify(readFileSync(file, 'utf8')) - }) + return replacements[+file] ? replacements[+file] : JSON.stringify(readFileSync(file, 'utf8')) + }) writeFileSync(filename, contents) console.log(`Wrote: ${filename}`) } diff --git a/test/fixture/package.json b/test/fixture/package.json new file mode 100644 index 00000000..ca419590 --- /dev/null +++ b/test/fixture/package.json @@ -0,0 +1,3 @@ +{ + "main": "entry-file.js" +} diff --git a/test/mocha.opts b/test/mocha.opts index 2bb3cb1b..431a2a90 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,4 @@ +./test/**/*.spec.ts --check-leaks --require ts-node/register --recursive diff --git a/test/options.spec.ts b/test/options.spec.ts new file mode 100644 index 00000000..ee57a929 --- /dev/null +++ b/test/options.spec.ts @@ -0,0 +1,48 @@ +import { normalizeOptions } from '../src/options' +import { expect } from 'chai' +import * as path from 'path' + +const ext = process.platform === 'win32' ? '.exe' : '' + +describe('options', () => { + describe('cwd', () => { + it('should use process.cwd() if nothing is provided', () => { + const options = normalizeOptions() + expect(options.cwd).to.equal(process.cwd()) + }) + it('should use the main module in a package directory', () => { + const options = normalizeOptions() + expect(options.input).to.equal(path.resolve('./index.js')) + }) + it('should resolve pathed options against cwd', () => { + const cwd = '/a/b/c' + const options = normalizeOptions({ + cwd, + input: '123.js', + output: 'abc', + temp: './d' + }) + expect(options.temp).to.equal(path.resolve(cwd, './d')) + expect(options.input).to.equal(path.resolve(cwd, '123.js')) + expect(options.output).to.equal(path.resolve(cwd, `abc${ext}`)) + }) + }) + describe('output', () => { + it('should work', () => { + const options = normalizeOptions({ + output: './some-output' + }) + expect(options.output).to.equal(path.resolve(`./some-output${ext}`)) + }) + it('should default to the input file name if not index', () => { + const options = normalizeOptions({ + input: 'src/folder/app.js' + }) + expect(options.output).to.equal(path.resolve(`./app${ext}`)) + }) + it('should default to the folder/project name if filename is index', () => { + const options = normalizeOptions() + expect(options.output).to.equal(path.resolve(`./nexe${ext}`)) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 1c8816a6..15e3deab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1974,8 +1974,8 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" prettier@^1.6.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.0.tgz#47481588f41f7c90f63938feb202ac82554e7150" + version "1.7.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.4.tgz#5e8624ae9363c80f95ec644584ecdf55d74f93fa" pretty-time@^0.2.0: version "0.2.0" @@ -2490,8 +2490,8 @@ type-is@~1.6.15: mime-types "~2.1.15" typescript@^2.4.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34" + version "2.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d" uglify-js@3.0.28: version "3.0.28"