Compare commits

..

42 Commits

Author SHA1 Message Date
calebboyd 52b58a107d feat: enable fake-argv by default 2017-10-21 22:48:42 -05:00
calebboyd ec734e4fb1 chore: use yarn in circle-ci 2017-10-20 17:38:20 -05:00
calebboyd 7c755e666f chore: bump fuse-box 2017-10-17 16:56:44 -05:00
calebboyd 90ae7de7ac chore: bump fuse-box 2017-10-16 13:02:08 -05:00
Alexis Tyler 96f67ecba4 chore(docs): fix typo & add missing period 2017-10-15 19:32:12 -05:00
calebboyd 6526a611a8 chore: bump deps 2017-10-10 09:56:22 -04:00
calebboyd f9bf4ca464 chore: add chat badge 2017-10-07 09:27:30 -05:00
calebboyd 4ec8a43d60 fix: consistent cwd usage 2017-10-05 20:54:37 -05:00
calebboyd 77109235c4 chore: separate bindings and .node embedding 2017-10-05 18:58:41 -05:00
Bryce Gibson c1531cbf23 Pause logger before and resume after spawn. 2017-10-04 22:22:53 -05:00
Bryce Gibson c5f3116c18 Show process output when loglevel is verbose. 2017-10-04 22:22:53 -05:00
calebboyd 03e2985c7a feat: stat and statSync 2017-10-03 01:54:06 -05:00
calebboyd a4b48611d9 feat: readdir and readdirSync 2017-10-03 00:39:33 -05:00
calebboyd 83d3ccf1c5 feat: support the cluster module 2017-10-02 22:28:04 -05:00
calebboyd 356d4c1886 fix(nix): only make output executable 2017-10-02 21:49:05 -05:00
calebboyd b74bdfc53b chore: fusebox beta 2017-10-02 18:04:32 -05:00
calebboyd 927820a5ec chore: service progress 2017-10-02 18:02:32 -05:00
calebboyd 4ef98bdc8a chore: re-order docs 2017-09-30 15:44:57 -05:00
calebboyd 90d7beb4f3 docs: add native module note 2017-09-30 15:28:03 -05:00
calebboyd 55c95b4663 docs: elaborate on target 2017-09-30 15:06:34 -05:00
calebboyd fe5aa0be09 fix: dequote 2017-09-29 23:16:09 -05:00
calebboyd 2a2526d3bc fix: disable stdin for programatic usage 2017-09-29 17:49:17 -05:00
calebboyd c8310ce3bc docs: update readme 2017-09-28 22:02:27 -05:00
calebboyd bb356a967b chore: multi-arch builds on travis 2017-09-28 16:06:55 -05:00
calebboyd 0d62d0100d chore: bump rc.9 2017-09-28 00:15:44 -05:00
calebboyd bfd07cd10a chore: move mac build to circle 2017-09-27 23:36:52 -05:00
calebboyd 4390568654 fix: ia32 target normalization 2017-09-27 01:34:07 -05:00
calebboyd 5c9cf3580c chore: update build 2017-09-27 00:47:16 -05:00
calebboyd fcb54d9085 fix: check error code on build commands 2017-09-26 23:54:18 -05:00
calebboyd 11fa087e9d refactor: move startup scripts to shims, add plugins 2017-09-26 20:16:03 -05:00
calebboyd b822984776 fix: input path for bundle 2017-09-26 00:10:09 -05:00
calebboyd 4f2b1c51d5 refactor: addResource compiler method 2017-09-26 00:10:09 -05:00
Brian McCallister dbcff4c7d2 fix: minor spelling problem 2017-09-21 18:04:07 -05:00
calebboyd 9906df8d55 chore: bump rc6 2017-09-13 19:08:44 -05:00
calebboyd d685ec21e5 fix: custom bundle resolution 2017-09-13 19:07:54 -05:00
calebboyd a2c4f3e9f1 fix: use cwd directory name for output resolution before timestring 2017-09-13 18:43:26 -05:00
calebboyd 79bcb753b4 fix: native module fuse plubin 2017-09-11 01:20:37 -05:00
calebboyd bc2a637cc4 docs: cleanup 2017-09-10 20:18:37 -05:00
calebboyd ad279e6daf docs: clarify resource and build documentation 2017-09-09 14:08:04 -05:00
calebboyd 2b9a8b1f0e feat: fake-argv option 2017-09-09 02:02:52 -05:00
calebboyd 7a7aaefa23 fix: set input before name (so name can derive from input) 2017-09-08 23:44:26 -05:00
calebboyd 04fdd165a4 feat: productversion and fileversion replacements in node-rc 2017-09-08 14:29:19 -05:00
49 changed files with 6043 additions and 5858 deletions
+2
View File
@@ -4,6 +4,8 @@ coverage
node_modules
*.log
*.exe
*.bin
!winsw.exe
.idea
.nexe
lib
+12 -9
View File
@@ -1,9 +1,12 @@
os: osx
language: node_js
node_js:
- '6'
script: npm run nexe-build
env:
- secure: LyC6djYqPfPKz5gHILES9JmSRxQ+ntKOcoaZNM5WID0+gogUoXZBhcmmtvq9RnMpFZN3pYujY6LB5iy7QS2seGjg+feyewNKI30yIK6N9ulJyZZKmrJVDdRx4wvl8YVTkttjcJFkanDGa6zRvFfFDKx/iIdcWLEpnqR/WO1ppf8=
notifications:
email: false
language: node_js
before_install:
- sudo apt-get -qq update
- sudo apt-get install -y g++-multilib
node_js:
- '6'
script: npm run asset-compile
env:
global:
- secure: LyC6djYqPfPKz5gHILES9JmSRxQ+ntKOcoaZNM5WID0+gogUoXZBhcmmtvq9RnMpFZN3pYujY6LB5iy7QS2seGjg+feyewNKI30yIK6N9ulJyZZKmrJVDdRx4wvl8YVTkttjcJFkanDGa6zRvFfFDKx/iIdcWLEpnqR/WO1ppf8=
notifications:
email: false
+88 -46
View File
@@ -5,18 +5,18 @@
<a href="https://www.npmjs.com/package/nexe"><img src="https://img.shields.io/npm/dt/nexe.svg" alt="Downloads"></a>
<a href="https://www.npmjs.com/package/nexe"><img src="https://img.shields.io/npm/v/nexe.svg" alt="Version"></a>
<a href="https://www.npmjs.com/package/nexe"><img src="https://img.shields.io/npm/l/nexe.svg" alt="License"></a>
<a href="https://gitter.im/nexe/nexe?utm_source=badge&utm_medium=badge"><img src="https://badges.gitter.im/nexe/nexe.svg"></a>
</p>
<p align="center"><code>npm i nexe@beta -g</code></p>
<p align="center"><code>npm i nexe -g</code></p>
<p align="center">Nexe is a command-line utility that compiles your Node.js application into a single executable file.</p>
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/5818726/26533446/ce19ee5a-43de-11e7-9540-caf7ebd93370.gif"/>
<img src="https://user-images.githubusercontent.com/5818726/30999006-df7e0ae0-a497-11e7-96db-9ce87ae67b34.gif"/>
</p>
## Motivation and Features
- Supports production ready builds
- Self contained applications
- Ability to run multiple applications with *different* node.js runtimes.
- Distribute binaries without needing node / npm.
@@ -28,40 +28,42 @@
## Usage
- Existing application bundle:
- Application entrypoint:
`nexe my-app-bundle.js -o my-app`
`nexe my-app.js`
- stdin interface
`rollup -c | nexe --resource ./public/**/* -o my-app.exe`
`rollup -c | nexe --resource "./public/**/*" -o my-app.exe`
For more CLI options see: `nexe --help`
## Including Additional Resources
### Examples
Additional resources can be added to the binary by passing `-r glob/pattern/**/*`. These included files can be read in the application by using `fs.readFile` or `fs.readFileSync`
- `nexe server.js -r public/**/*.html`
- `nexe my-bundle.js --no-bundle -o app.exe`
- `nexe --build`
- `nexe -t x86-8.0.0`
## Resources
Additional files or resources can be added to the binary by passing `-r "glob/pattern/**/*"`. These included files can be read in the application by using `fs.readFile` or `fs.readFileSync`.
## Compiling Node
By default `nexe` will attempt to download a pre-built executable. However, some users may want to customize the way node is built, either by changing the flags, providing a different icon, or different executable details. To build node, use the `build` option/flag
Nexe also exposes its patching pipeline to the user. This allows the application of simple patching of node sources prior to compilation.
By default `nexe` will attempt to download a pre-built executable. However, It may be unavailable ([github releases](https://github.com/nexe/nexe/releases))
or you may want to customize what is built. See `nexe --help` for a list of options available when passing the [`--build`](#build-boolean) option. You will also need to ensure your environment is setup to [build node](https://github.com/nodejs/node/blob/master/BUILDING.md). Note: the `python` binary in your path should be an acceptable version of python 2. eg. Systems that have python2 will need to create a [symlink](https://github.com/nexe/nexe/issues/354#issuecomment-319874486).
## Node.js API
Using Nexe Programatically
#### Example
```javascript
const { compile } = require('nexe')
compile({
input: './my-app.js',
output: './my-app.exe',
build: true, //builds node
build: true, //required to use patches
patches: [
async (compiler, next) => {
await compiler.setFileContentsAsync(
@@ -75,60 +77,65 @@ compile({
console.log('success')
})
```
## NexeOptions
### `options`
### `options: object`
- #### `build: boolean`
- Build node from source (required in beta)
- #### `input: string`
- Input bundle file path
- default: stdin or the current directory's main file (package.json)
- #### `output: string`
- Output executable file path
- default: same as `name` with an OS specific extension.
- #### `target: string`
- Dash seperated platform-architecture-version. e.g. `'win32-ia32-6.10.3'`
- default: `[process.platform, process.arch, process.version.slice(1)].join('-')`
- #### `target: string | object`
- An object or string describing platform-arch-version. e.g. `'windows-ia32-6.10.3'`
- each segment is optional, and will be merged with the current environment
- Examples: ([full list](https://github.com/nexe/nexe/releases))
- `'win32-x86-8.6.0`
- `{ platform: 'alpine' }`
- `darwin-8.6.0`
- `linux-x64`
- `macos-8.4.0`
See [test/target.spec.ts](test/target.spec.ts)
- If the [`build`](#build-boolean) flag is set, the platform portion of the target is ignored.
- default: `process`
- #### `bundle: string | boolean`
- If a string is provided it must be a valid relative module path
and should provide an export with the following signature:
```typescript
export function createBundle (
filename: string,
options: { name: string, minify: any, cwd: string }
): Promise<string>`
export function createBundle (options: NexeOptions): Promise<string>
```
- default: true, uses the internal fuse-box configuration
- default: true
- #### `name: string`
- Module friendly name of the application
- default: basename of the input file, or `nexe_${Date.now()}`
- #### `cwd: string`
- Directory nexe will operate on as though it is the cwd
- default: process.cwd()
- #### `version: string`
- The Node version you're building for
- default: `process.version.slice(1)`
- #### `build: boolean`
- Build node from source, passing this flag tells nexe to download and build from source. Subsequently using this flag will cause nexe to use the previously built binary. To rebuild, first add [`--clean`](#clean-boolean)
- #### `python: string`
- On Linux this is the path pointing to your python2 executable
- On Windows this is the directory where `python` can be accessed
- default: `null`
- #### `flags: Array<string>`
- #### `flags: string[]`
- Array of node runtime flags to build node with.
- Example: `['--expose-gc']`
- default: `[]`
- #### `configure: Array<string>`
- #### `configure: string[]`
- Array of arguments for the node build configure step
- Example: `['--with-dtrace', '--dest-cpu=x64']`
- default: `[]`
- #### `make: Array<string>`
- #### `make: string[]`
- Array of arguments for the node build make step, on windows this step recieves options for vcBuild.bat
- default: `[]` or `['nosign', 'release']` for non windows systems
- #### `make: Array<string>`
- #### `vcBuild: string[]`
- Alias for `make` option
- #### `snapshot: string`
- path to a file to be used as the warmup snapshot for the build
- default: `null`
- #### `resources: Array<string>`
- #### `resources: string[]`
- Array of globs with files to include in the build
- Example: `['./public/**/*']`
- default: `[]`
@@ -140,27 +147,41 @@ compile({
- Path to a user provided icon to be used (Windows only).
- #### `rc: object`
- Settings for patching the [node.rc](https://github.com/nodejs/node/blob/master/src/res/node.rc) configuration file (Windows only).
- Example: `{ CompanyName: "ACME Corp" }`
- Example:
```javascript
{
CompanyName: "ACME Corp",
PRODUCTVERSION: "17,3,0,0",
FILEVERSION: "1,2,3,4"
...
}
```
- default: `{}`
- #### `clean: boolean`
- If included, nexe will remove temporary files for accompanying configuration and exit
- If included, nexe will remove temporary files for the accompanying configuration and exit
- #### `enableNodeCli: boolean`
- Enable the original Node CLI (will prevent application cli from working)
- default: `false`
- #### `fakeArgv: boolean`
- fake the entry point file name (`process.argv[1]`). If nexe was used with stdin this will be `'[stdin]'`.
- #### `sourceUrl: string`
- Provide an alternate url for the node source. Should be a `.tar.gz`
- Provide an alternate url for the node source code
- Note: temporary files will still be created for this under the specified version
- #### `loglevel: string`
- Set the loglevel, info, silent, or verbose
- default: `'info'`
- #### `patches: Array<NexePatch>`
- #### `patches: NexePatch[]`
- Userland patches for patching or modifying node source
- default: `[]`
- #### `plugins: NexePatch[]`
- Userland plugins for modifying nexe executable behavior
- default: `[]`
### `NexePatch: (compiler: NexeCompiler, next: () => Promise<void>) => Promise<void>`
A patch is just a middleware function that takes two arguments, the `compiler`, and `next`. The compiler is described below, and `next` ensures that the pipeline continues. Its invocation should always be awaited or returned to ensure correct behavior.
Patches and Plugins are just a middleware functions that take two arguments, the `compiler`, and `next`. The compiler is described below, and `next` ensures that the pipeline continues. Its invocation should always be awaited or returned to ensure correct behavior. Patches also require that [`--build`](#build-boolean) be set, while plugins do not.
For examples, see the built in patches: [src/patches](src/patches)
For examples, see the built in patches: [src/patches](src/patches).
### `NexeCompiler`
@@ -170,7 +191,9 @@ For examples, see the built in patches: [src/patches](src/patches)
- Quickly perform a replace in a file within the downloaded Node.js source. The rest arguments are passed along to `String.prototype.replace`
- `readFileAsync(filename: string): Promise<NexeFile>`
- Access (or create) a file within the downloaded Node.js source.
- `files: Array<NexeFile>`
- `addResource(filename: string, contents: Buffer): void`
- Add a resource to the nexe bundle
- `files: NexeFile[]`
- The cache of the currently read, modified, or created files within the downloaded Node.js source.
#### `NexeFile`
@@ -180,12 +203,31 @@ For examples, see the built in patches: [src/patches](src/patches)
Any modifications made to `NexeFile#contents` will be maintained in the cache _without_ the need to explicitly write them back out, e.g. using `NexeCompiler#setFileContentsAsync`.
### Native Modules
## Native Modules
Nexe has a plugin built for use with [fuse-box](http://fuse-box.org) > 2.2.1. This plugin currently supports modules that require `.node` files and those that use the `bindings` module.
Take a look at the [example](examples/native-build/build.js)
Any `.node` binding can be used with nexe. These library files will be bundled and written alongside the resulting binary at runtime. Currently, nexe supports modules loaded directly (`.node` files) and those loaded with the `'bindings'` module. It does not yet support modules using `node-pre-gyp#find`.
- [ ] Implement support `node-pre-gyp#find`.
Its important to note: unless your native module conditionally loads each platform binary. Nexe builds with native modules will be platform specific. Eg. You will no longer be able to use cross platform builds.
Nexe builds with native modules will need to target the same version, platform and architecture as the platform hosting the module. At least until [N-API](https://github.com/nodejs/abi-stable-node) is fully propegated
TODO
- [ ] Implement support for `node-pre-gyp#find`.
## Contributing
Building
```
$ git clone git@github.com:nexe/nexe.git
$ cd nexe
$ yarn
```
Testing
```
$ npm test
```
## Maintainers
+2 -5
View File
@@ -5,16 +5,13 @@ environment:
secure: tR7HXYv3x97q90uvPrjUxKZh7R1+uqKct0HdEcqTbsg=
GITHUB_TOKEN:
secure: 3wuEGJsppSKFvdwhp3jprUA9Zkc3WINlc/9oFXjhAR6J05lYMXP7Fl7lXTKpkGZx
branches:
except:
- master
install:
- ps: Install-Product node 6
- npm i
- yarn
build: off
test: off
build_script:
- npm run nexe-build
- yarn run asset-compile
artifacts:
- path: .nexe\**\node.exe
cache:
+7 -6
View File
@@ -1,8 +1,9 @@
machine:
node:
version: 6.11.2
services:
- docker
dependencies:
override:
- export NVS_HOME="$HOME/.nvs" && git clone https://github.com/jasongin/nvs "$NVS_HOME" && . "$NVS_HOME/nvs.sh" install
- nvs add lts && nvs link lts && nvs use default
- npm i yarn -g
- yarn
test:
override:
- npm run nexe-build
- yarn run asset-compile
+12 -21
View File
@@ -1,22 +1,13 @@
const { FuseBox } = require('fuse-box')
const { NativeModulePlugin } = require('../../lib/bundling')
const fuse = FuseBox.init({
homeDir: './',
cache: false,
log: true,
debug: true,
output: '$name.js',
plugins: [
new NativeModulePlugin({
zmq: {
additionalFiles: [
'../../windows/lib/x64/libzmq-v100-mt-4_0_4.dll'
]
}
})
]
const nexe = require('../..')
nexe.compile({
output: 'native-build',
silent: true,
native: {
zmq: {
additionalFiles: [
'../../windows/lib/x64/libzmq-v100-mt-4_0_4.dll'
]
}
}
})
fuse.bundle('app')
.target('server')
.instructions(`> index.js`)
fuse.run()
-1
View File
@@ -8,7 +8,6 @@ setInterval(function(){
console.log('sending a multipart message envelope')
pub.send(['kitty cats', 'meow!'])
}, 2500)
console.log('global', global.require.main)
var sub = zmq.socket('sub')
sub.connect('tcp://127.0.0.1:3000')
sub.subscribe('kitty cats')
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -4,15 +4,13 @@
"description": "",
"main": "index.js",
"scripts": {
"bundle": "node build",
"build-windows": "nexe -i app.js -o app.exe"
"build": "node build"
},
"author": "",
"license": "ISC",
"dependencies": {
"sqlite3": "^3.1.10",
"zmq": "^2.15.3"
},
"devDependencies": {
"fuse-box": "^2.2.31"
}
"devDependencies": {}
}
+16 -7
View File
@@ -1,10 +1,19 @@
#!/usr/bin/env node
const nexe = require('./lib/nexe')
module.exports = nexe
const options = require('./lib/options')
if (require.main === module) {
nexe.compile(nexe.argv).catch((e) => {
process.stderr.write(e.stack, () => process.exit(1))
})
//fast path for help/version
const argv = options.argv
const eol = require('os').EOL
const showHelp = argv.help || argv._.some(x => x === 'help')
const showVersion = argv.version || argv._.some(x => x === 'version')
if (showHelp || showVersion) {
process.stderr.write(showHelp ? options.help : options.version + eol)
} else {
const nexe = require('./lib/nexe')
nexe.compile(argv).catch((e) => {
process.stderr.write(eol + e.stack)
})
}
} else {
module.exports = require('./lib/nexe')
}
-3001
View File
File diff suppressed because it is too large Load Diff
+13 -9
View File
@@ -2,19 +2,21 @@
"name": "nexe",
"description": "Create a single executable out of your Node.js application",
"license": "MIT",
"version": "2.0.0-rc.4",
"version": "2.0.0-rc.17",
"contributors": [
"Craig Condon <craig.j.condon@gmail.com> (http://crcn.io)",
"Jared Allard <jaredallard@outlook.com>",
"Caleb Boyd <caleb.boyd@hotmail.com>"
],
"scripts": {
"nexe-build": "ts-node tasks/build",
"prebuild": "rimraf lib && npm run lint",
"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\"",
"build": "tsc --declaration"
"asset-compile": "ts-node tasks/asset-compile",
"test": "mocha",
"fmt": "prettier --parser typescript --no-semi --print-width 100 --single-quote --write \"{src,plugins,tasks}/**/*.ts\"",
"prepare": "npm run build",
"prepublishOnly": "npm test",
"prebuild": "rimraf lib && npm run fmt",
"build": "tsc --declaration",
"postbuild": "ts-node tasks/post-build"
},
"repository": {
"type": "git",
@@ -24,6 +26,7 @@
"lib",
"source-map-support.js"
],
"typings": "lib/nexe.d.ts",
"main": "index.js",
"bin": {
"nexe": "index.js"
@@ -35,7 +38,7 @@
"app-builder": "^5.1.0",
"chalk": "^1.1.3",
"download": "^6.2.0",
"fuse-box": "2.2.31",
"fuse-box": "^2.3.4-beta.11",
"globby": "^6.1.0",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
@@ -45,9 +48,9 @@
"uglify-js": "3.0.28"
},
"devDependencies": {
"@types/bluebird": "^3.5.8",
"@types/chai": "^4.0.4",
"@types/chalk": "^0.4.31",
"@types/execa": "^0.7.0",
"@types/globby": "^0.6.0",
"@types/minimist": "^1.2.0",
"@types/mkdirp": "^0.3.29",
@@ -56,6 +59,7 @@
"@types/pify": "0.0.28",
"@types/rimraf": "0.0.28",
"chai": "^4.1.2",
"execa": "^0.8.0",
"got": "^7.1.0",
"mocha": "^3.5.0",
"prettier": "^1.6.1",
+24
View File
@@ -0,0 +1,24 @@
{
"name": "nexe-daemon",
"version": "1.0.0",
"description": "Plugin for nexe to create installable services",
"main": "lib/index.js",
"scripts": {
"clean": "rimraf *.js",
"build": "tsc",
"postpublish": "npm run clean"
},
"files": [
"lib",
"winsw.exe"
],
"author": "Caleb Boyd",
"license": "MIT",
"devDependencies": {
"rimraf": "^2.6.2",
"typescript": "^2.5.2"
},
"dependencies": {
"nexe": "^2.0.0-rc.11"
}
}
+48
View File
@@ -0,0 +1,48 @@
import { NexeCompiler, NexeOptions } from 'nexe'
import { readFileSync } from 'fs'
import { join } from 'path'
interface NexeDaemonOptions {
id: string
name: string
description: string
executable: string
}
type DaemonOptions = NexeOptions & { daemon: { windows: NexeDaemonOptions } }
function renderWinswConfig(options: any) {
return (
'<configuration>\r\n' +
`${Object.keys(options).reduce((config: string, element: string) => {
return (config += `<${element}>${options[element]}</${element}>\r\n`)
}, '')}</configuration>\r\n`
)
}
export default function daemon(compiler: NexeCompiler<DaemonOptions>, next: () => Promise<void>) {
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) || {},
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.input = '{{replace:plugins/nexe-daemon/lib/nexe-daemon.js}}'
return next()
}
+66
View File
@@ -0,0 +1,66 @@
const installCommands = ['-i', '--install']
const installIndex = process.argv.findIndex(x => installCommands.includes(x))
import * as path from 'path'
import * as fs from 'fs'
import * as cp from 'child_process'
function mkdirp(r: string, t?: any): any {
;(t = t || null), (r = path.resolve(r))
try {
fs.mkdirSync(r), (t = t || r)
} catch (c) {
if ('ENOENT' === c.code) (t = mkdirp(path.dirname(r), t)), mkdirp(r, t)
else {
var i
try {
i = fs.statSync(r)
} catch (r) {
throw c
}
if (!i.isDirectory()) throw c
}
}
return t
}
if (~installIndex) {
console.log('DIRECTORY FOUND')
const directory = process.argv[installIndex + 1]
if (directory) {
mkdirp(path.dirname(directory))
const filename = path.join(directory, path.basename(process.execPath))
const readStream = fs.createReadStream(process.execPath)
const writeStream = fs.createWriteStream(filename)
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(
filename.replace('.exe', '-service.xml'),
fs.readFileSync('./nexe/plugin/daemon/winsw.xml')
)
installService(winsw)
})
}
} else {
console.log('DIRECTORY NOT FOUND')
require('./nexe/plugin/daemon/app.js')
}
function installService(filename: string) {
cp.exec(filename + ' install', error => {
if (error && !error.message.includes('already exists')) {
throw error
}
})
}
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [ "es2016" ],
"outDir": "lib",
"strict": true
},
"include": [
"src/**/*.ts"
],
"exclude": []
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
+4 -73
View File
@@ -3,75 +3,6 @@ import * as path from 'path'
import * as child from 'child_process'
import { createHash } from 'crypto'
export interface ExtractNodeModuleOptions {
[key: string]: { additionalFiles: string[] } | true
}
function hashName(name: string | Buffer) {
return createHash('md5')
.update(name)
.digest('hex')
.toString()
.slice(0, 8)
}
export function embedDotNode(
options: ExtractNodeModuleOptions,
file: { contents: string; absPath: string }
) {
const contents = fs.readFileSync(file.absPath)
const module = Object.keys(options).find(x =>
Boolean(~file.absPath.indexOf(path.join('node_modules', x)))
)!
const bindingName = path.basename(file.absPath)
const settings = options[module]
const moduleDir = hashName(contents)
file.contents = `
var fs=require('fs');var path=require('path');var binding='${contents.toString(
'base64'
)}';function mkdirp(r,t){t=t||null,r=path.resolve(r);try{fs.mkdirSync(r),t=t||r}catch(c){if("ENOENT"===c.code)t=mkdirp(path.dirname(r),t),mkdirp(r,t);else{var i;try{i=fs.statSync(r)}catch(r){throw c}if(!i.isDirectory())throw c}}return t};`
if (settings === true) {
file.contents += `
mkdirp('${moduleDir}');
var bindingPath = path.join(process.cwd(), '${moduleDir}', '${bindingName}')
require('fs').writeFileSync(bindingPath, Buffer.from(binding, 'base64'))
process.dlopen(module, bindingPath)
`.trim()
return
}
let depth = 0
settings.additionalFiles.forEach(file => {
let ownDepth = 0
file.split('/').forEach(x => x === '..' && ownDepth++)
depth = ownDepth > depth ? ownDepth : depth
})
let segments = [moduleDir]
while (depth--) {
segments.push(hashName(moduleDir + depth))
}
segments.push(bindingName)
file.contents += `
var cwd = process.cwd()
var bindingFileParts = ${JSON.stringify(segments)};
var bindingFile = path.join.apply(path, [cwd].concat(bindingFileParts));
mkdirp(path.dirname(bindingFile));
fs.writeFileSync(bindingFile, Buffer.from(binding, 'base64'));
${settings.additionalFiles.reduce((code, filename, i) => {
const contents = fs.readFileSync(path.join(path.dirname(file.absPath), filename))
return (code += `
var file${i} = '${contents.toString('base64')}';
var filePath${i} = path.join(cwd, bindingFileParts[0], '${filename.split('../').join('')}');
mkdirp(path.dirname(filePath${i}));
fs.writeFileSync(filePath${i}, Buffer.from(file${i}, 'base64'));
`)
}, '')};
process.dlopen(module, bindingFile)
`
}
function findNativeModulePath(filePath: string, bindingsArg: string) {
const dirname = path.dirname(filePath)
const tempFile = Math.random() * 100 + '.js'
@@ -105,13 +36,13 @@ export class BindingsRewrite {
public nativeModulePaths: string[] = []
public rewrite = false
isRequireBindings(node: any) {
return node.callee.name === 'require' && node.arguments[0].value === 'bindings'
isRequire(node: any, moduleName: string) {
return node.callee.name === 'require' && node.arguments[0].value === moduleName
}
onNode(absolutePath: string, node: any, parent: any) {
if (node.type === 'CallExpression') {
if (this.isRequireBindings(node) && parent.type === 'VariableDeclarator') {
if (this.isRequire(node, 'bindings') && parent.type === 'VariableDeclarator') {
/**
* const loadBindings = require('bindings');
* -> const loadBindings = String('');
@@ -123,7 +54,7 @@ export class BindingsRewrite {
return
}
if (this.isRequireBindings(node) && parent.type === 'CallExpression') {
if (this.isRequire(node, 'bindings') && parent.type === 'CallExpression') {
/**
*const bindings = require('bindings')('native-module')....
* -> const bindings = require('./path/to/native/module.node').....
+71
View File
@@ -0,0 +1,71 @@
import { createHash } from 'crypto'
import * as path from 'path'
export interface EmbedNodeModuleOptions {
[key: string]: { additionalFiles: string[] } | true
}
function hashName(name: string | Buffer) {
return createHash('md5')
.update(name)
.digest('hex')
.toString()
.slice(0, 8)
}
export function embedDotNode(
options: EmbedNodeModuleOptions,
file: { contents: string; absPath: string }
) {
const contents = fs.readFileSync(file.absPath)
const modulePathParts = file.absPath.split(path.sep).reverse()
const module = modulePathParts[modulePathParts.findIndex(x => x === 'node_modules') - 1]
const bindingName = path.basename(file.absPath)
const settings = options[module]
const moduleDir = hashName(contents)
file.contents = `
var fs=require('fs');var path=require('path');var binding='${contents.toString(
'base64'
)}';function mkdirp(r,t){t=t||null,r=path.resolve(r);try{fs.mkdirSync(r),t=t||r}catch(c){if("ENOENT"===c.code)t=mkdirp(path.dirname(r),t),mkdirp(r,t);else{var i;try{i=fs.statSync(r)}catch(r){throw c}if(!i.isDirectory())throw c}}return t};`
if (!settings || settings === true) {
file.contents += `
mkdirp('${moduleDir}');
var bindingPath = path.join(process.cwd(), '${moduleDir}', '${bindingName}')
require('fs').writeFileSync(bindingPath, Buffer.from(binding, 'base64'))
process.dlopen(module, bindingPath)
`.trim()
return {}
}
let depth = 0
settings.additionalFiles.forEach(file => {
let ownDepth = 0
file.split('/').forEach(x => x === '..' && ownDepth++)
depth = ownDepth > depth ? ownDepth : depth
})
let segments = [moduleDir]
while (depth--) {
segments.push(hashName(moduleDir + depth))
}
segments.push(bindingName)
file.contents += `
var cwd = process.cwd()
var bindingFileParts = ${JSON.stringify(segments)};
var bindingFile = path.join.apply(path, [cwd].concat(bindingFileParts));
mkdirp(path.dirname(bindingFile));
fs.writeFileSync(bindingFile, Buffer.from(binding, 'base64'));
${settings.additionalFiles.reduce((code, filename, i) => {
const contents = fs.readFileSync(path.join(path.dirname(file.absPath), filename))
return (code += `
var file${i} = '${contents.toString('base64')}';
var filePath${i} = path.join(cwd, bindingFileParts[0], '${filename.split('../').join('')}');
mkdirp(path.dirname(filePath${i}));
fs.writeFileSync(filePath${i}, Buffer.from(file${i}, 'base64'));
`)
}, '')};
process.dlopen(module, bindingFile)
`
}
+8 -8
View File
@@ -1,4 +1,5 @@
import { BindingsRewrite, embedDotNode, ExtractNodeModuleOptions } from './bindings-rewrite'
import { BindingsRewrite } from './bindings-rewrite'
import { embedDotNode, EmbedNodeModuleOptions } from './embed-node'
export interface FuseBoxFile {
info: { absPath: string }
@@ -8,6 +9,7 @@ export interface FuseBoxFile {
dependencies: string[]
requiresRegeneration: boolean
}
consume(): void
loadContents(): void
makeAnalysis(
parsingOptions?: any,
@@ -20,19 +22,16 @@ export interface FuseBoxFile {
): void
}
export default function(options: ExtractNodeModuleOptions) {
export default function(options: EmbedNodeModuleOptions = {}) {
return new NativeModulePlugin(options)
}
export class NativeModulePlugin {
public test: RegExp
public test = /node_modules.*(\.js|\.node)$|\.node$/
public limit2Project = false
private modules: (keyof ExtractNodeModuleOptions)[]
private modules: (keyof EmbedNodeModuleOptions)[]
constructor(public options: ExtractNodeModuleOptions) {
this.options = options
this.test = new RegExp(`node_modules\/(${Object.keys(options).join('|')}).*\.js|\.node$`)
}
constructor(public options = {}) {}
init(context: any) {
context.allowExtension('.node')
@@ -47,6 +46,7 @@ export class NativeModulePlugin {
}
const bindingsRewrite = new BindingsRewrite()
file.makeAnalysis(null, {
plugins: [
{
+1
View File
@@ -0,0 +1 @@
//TODO
+105 -70
View File
@@ -4,9 +4,9 @@ import { createHash } from 'crypto'
import { createReadStream } from 'fs'
import { Readable } from 'stream'
import { spawn } from 'child_process'
import { Logger } from './logger'
import { readFileAsync, writeFileAsync, pathExistsAsync, dequote, isWindows } from './util'
import { NexeOptions, nexeVersion } from './options'
import { Logger, LogStep } from './logger'
import { readFileAsync, writeFileAsync, pathExistsAsync, dequote, isWindows, bound } from './util'
import { NexeOptions, version } from './options'
import { NexeTarget } from './target'
import download = require('download')
import { getLatestGitRelease } from './releases'
@@ -22,39 +22,40 @@ export interface NexeFile {
contents: string
}
export { NexeOptions }
interface NexeHeader {
resources: { [key: string]: number[] }
version: string
}
export class NexeCompiler {
export class NexeCompiler<T extends NexeOptions = NexeOptions> {
private start = Date.now()
private env = { ...process.env }
private compileStep: { modify: Function; log: Function }
private compileStep: LogStep
public log = new Logger(this.options.loglevel)
public src = join(this.options.temp, this.options.version)
public src: string
public files: NexeFile[] = []
public shims: string[] = []
public input: string
public bundledInput?: string
public output: string | null
public targets: NexeTarget[]
public resources: { bundle: string; index: { [key: string]: number[] } } = {
public target: NexeTarget
public resources: { bundle: Buffer; index: { [key: string]: number[] } } = {
index: {},
bundle: ''
bundle: Buffer.from('')
}
public readFileAsync: (file: string) => Promise<NexeFile>
public writeFileAsync: (file: string, contents: Buffer | string) => Promise<void>
public replaceInFileAsync: (file: string, replacer: any, replaceValue: string) => Promise<void>
public setFileContentsAsync: (file: string, contents: string | Buffer) => Promise<void>
private nodeSrcBinPath = isWindows
? join(this.src, 'Release', 'node.exe')
: join(this.src, 'out', 'Release', 'node')
constructor(public options: NexeOptions) {
public output = this.options.output
private nodeSrcBinPath: string
constructor(public options: T) {
const { python } = (this.options = options)
this.targets = options.targets as NexeTarget[]
this.log.step('nexe ' + nexeVersion, 'info')
this.target = this.targets[0]
this.src = join(this.options.temp, this.target.version)
this.nodeSrcBinPath = isWindows
? join(this.src, 'Release', 'node.exe')
: join(this.src, 'out', 'Release', 'node')
this.log.step('nexe ' + version, 'info')
if (python) {
if (isWindows) {
this.env.PATH = '"' + dequote(normalize(python)) + '";' + this.env.PATH
@@ -62,40 +63,64 @@ export class NexeCompiler {
this.env.PYTHON = python
}
}
}
this.readFileAsync = async (file: string) => {
let cachedFile = this.files.find(x => normalize(x.filename) === normalize(file))
if (!cachedFile) {
const absPath = join(this.src, file)
cachedFile = {
absPath,
filename: file,
contents: await readFileAsync(absPath, 'utf-8').catch(x => {
if (x.code === 'ENOENT') return ''
throw x
})
}
this.files.push(cachedFile)
@bound
addResource(file: string, contents: Buffer) {
const { resources } = this
resources.index[file] = [resources.bundle.byteLength, contents.byteLength]
resources.bundle = Buffer.concat([resources.bundle, contents])
}
@bound
async readFileAsync(file: string) {
this.assertBuild()
let cachedFile = this.files.find(x => normalize(x.filename) === normalize(file))
if (!cachedFile) {
const absPath = join(this.src, file)
cachedFile = {
absPath,
filename: file,
contents: await readFileAsync(absPath, 'utf-8').catch(x => {
if (x.code === 'ENOENT') return ''
throw x
})
}
return cachedFile
}
this.writeFileAsync = (file, contents) => writeFileAsync(join(this.src, file), contents)
this.replaceInFileAsync = async (file, replace: string | RegExp, value: string) => {
const entry = await this.readFileAsync(file)
entry.contents = entry.contents.replace(replace, value)
}
this.setFileContentsAsync = async (file: string, contents: string) => {
const entry = await this.readFileAsync(file)
entry.contents = contents
this.files.push(cachedFile)
}
return cachedFile
}
@bound
writeFileAsync(file: string, contents: string | Buffer) {
this.assertBuild()
return writeFileAsync(join(this.src, file), contents)
}
@bound
async replaceInFileAsync(file: string, replace: string | RegExp, value: string) {
const entry = await this.readFileAsync(file)
entry.contents = entry.contents.replace(replace, value)
}
@bound
async setFileContentsAsync(file: string, contents: string) {
const entry = await this.readFileAsync(file)
entry.contents = contents
}
quit() {
const time = Date.now() - this.start
this.log.write(`Finsihed in ${time / 1000}s`)
this.log.write(`Finished in ${time / 1000}s`)
return this.log.flush()
}
assertBuild() {
if (!this.options.build) {
throw new Error('This feature is only available with `--build`')
}
}
public getNodeExecutableLocation(target?: NexeTarget) {
if (target) {
return join(this.options.temp, target.toString())
@@ -104,14 +129,31 @@ export class NexeCompiler {
}
private _runBuildCommandAsync(command: string, args: string[]) {
if (this.log.verbose) {
this.compileStep.pause()
}
return new Promise((resolve, reject) => {
spawn(command, args, {
cwd: this.src,
env: this.env,
stdio: 'ignore'
stdio: this.log.verbose ? 'inherit' : 'ignore'
})
.once('error', reject)
.once('close', resolve)
.once('error', (e: Error) => {
if (this.log.verbose) {
this.compileStep.resume()
}
reject(e)
})
.once('close', (code: number) => {
if (this.log.verbose) {
this.compileStep.resume()
}
if (code != 0) {
const error = `${command} ${args.join(' ')} exited with code: ${code}`
reject(new Error(error))
}
resolve()
})
})
}
@@ -151,22 +193,21 @@ 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)
}
private _generateHeader() {
getHeader() {
const version =
['configure', 'vcBuild', 'make'].reduce((a, c) => {
return (a += (this.options as any)[c]
@@ -180,11 +221,6 @@ export class NexeCompiler {
.update(version)
.digest('hex')
}
const serializedHeader = this._serializeHeader(header)
return header
}
private _serializeHeader(header: NexeHeader) {
return `process.__nexe=${JSON.stringify(header)};`
}
@@ -193,7 +229,6 @@ export class NexeCompiler {
const build = this.options.build
const location = this.getNodeExecutableLocation(build ? undefined : target)
let binary = (await pathExistsAsync(location)) ? createReadStream(location) : null
const header = this._generateHeader()
if (!build && !binary) {
step.modify('Fetching prebuilt binary')
binary = await this._fetchPrebuiltBinaryAsync(target)
@@ -202,10 +237,10 @@ export class NexeCompiler {
binary = await this._buildAsync()
step.log('Node binary compiled')
}
return this._assembleDeliverable(header, binary)
return this._assembleDeliverable(binary)
}
private _assembleDeliverable(header: NexeHeader, binary: NodeJS.ReadableStream) {
private _assembleDeliverable(binary: NodeJS.ReadableStream) {
if (this.options.empty) {
return binary
}
@@ -214,12 +249,12 @@ export class NexeCompiler {
artifact.push(chunk)
})
binary.on('close', () => {
const content = this._serializeHeader(header) + this.input
const content = [this.shims.join(''), this.input].join(';')
artifact.push(content)
artifact.push(this.resources.bundle)
const lengths = Buffer.from(Array(16))
lengths.writeDoubleLE(Buffer.byteLength(content), 0)
lengths.writeDoubleLE(Buffer.byteLength(this.resources.bundle), 8)
lengths.writeDoubleLE(this.resources.bundle.byteLength, 8)
artifact.push(Buffer.concat([Buffer.from('<nexe~~sentinel>'), lengths]))
artifact.push(null)
})
+7 -3
View File
@@ -6,10 +6,12 @@ const frameLength = 120
export interface LogStep {
modify(text: string, color?: string): void
log(text: string, color?: string): void
pause(): void
resume(): void
}
export class Logger {
private verbose: boolean
public verbose: boolean
private silent: boolean
private ora: any
private modify: Function
@@ -48,7 +50,7 @@ export class Logger {
step(text: string, method: string = 'succeed'): LogStep {
if (this.silent) {
return { modify() {}, log() {} }
return { modify() {}, log() {}, pause() {}, resume() {} }
}
if (!this.ora.id) {
this.ora.start().text = text
@@ -62,7 +64,9 @@ export class Logger {
return {
modify: this.modify,
log: this.verbose ? this.write : this.modify
log: this.verbose ? this.write : this.modify,
pause: () => this.ora.stopAndPersist(),
resume: () => this.ora.start()
} as LogStep
}
}
+6 -11
View File
@@ -1,16 +1,11 @@
import { compose, PromiseConfig, Middleware } from 'app-builder'
import { compose, Middleware } from 'app-builder'
import resource from './steps/resource'
import { NexeCompiler } from './compiler'
import {
argv,
nexeVersion as version,
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'
import shim from './steps/shim'
import artifacts from './steps/artifacts'
import patches from './patches'
import { rimrafAsync } from './util'
@@ -20,7 +15,7 @@ async function compile(
compilerOptions?: Partial<NexeOptions>,
callback?: (err: Error | null) => void
) {
const options = await normalizeOptionsAsync(compilerOptions)
const options = normalizeOptions(compilerOptions)
const compiler = new NexeCompiler(options)
const build = compiler.options.build
@@ -39,7 +34,7 @@ async function compile(
const buildSteps = build
? [download, artifacts, ...patches, ...(options.patches as NexePatch[])]
: []
const nexe = compose(resource, bundle, cli, buildSteps)
const nexe = compose(resource, bundle, cli, buildSteps, shim, options.plugins as NexePatch[])
return callback
? void nexe(compiler).then(
() => callback && callback(null),
@@ -51,4 +46,4 @@ async function compile(
: nexe(compiler)
}
export { argv, compile, version }
export { argv, compile, version, NexeCompiler, NexeOptions, help }
+102 -76
View File
@@ -1,12 +1,12 @@
import * as parseArgv from 'minimist'
import { NexeCompiler } from './compiler'
import { isWindows, padRight } from './util'
import { basename, extname, join, isAbsolute, relative } from 'path'
import { basename, extname, join, isAbsolute, relative, dirname, resolve } from 'path'
import { getTarget, NexeTarget } from './target'
import { EOL } from 'os'
import * as chalk from 'chalk'
import { EOL, homedir } from 'os'
import * as c from 'chalk'
export const nexeVersion = '2.0.0-rc.4'
export const version = '{{replace:0}}'
export interface NexePatch {
(compiler: NexeCompiler, next: () => Promise<void>): Promise<void>
@@ -19,7 +19,6 @@ export interface NexeOptions {
targets: (string | NexeTarget)[]
name: string
cwd: string
version: string
flags: string[]
configure: string[]
vcBuild: string[]
@@ -31,11 +30,15 @@ export interface NexeOptions {
enableNodeCli: boolean
bundle: boolean | string
patches: (string | NexePatch)[]
plugins: (string | NexePatch)[]
native: any
empty: boolean
sourceUrl?: string
enableStdIn?: boolean
python?: string
loglevel: 'info' | 'silent' | 'verbose'
silent?: boolean
fakeArgv?: boolean
verbose?: boolean
info?: boolean
ico?: string
@@ -50,7 +53,6 @@ export interface NexeOptions {
}
const defaults = {
version: process.version.slice(1),
flags: [],
cwd: process.cwd(),
configure: [],
@@ -61,57 +63,67 @@ const defaults = {
compress: false,
build: false,
bundle: true,
patches: []
patches: [],
plugins: []
}
const alias = {
i: 'input',
o: 'output',
t: 'target',
n: 'name',
v: 'version',
t: 'target',
b: 'build',
n: 'name',
r: 'resource',
a: 'resource',
p: 'python',
f: 'flag',
c: 'configure',
m: 'make',
s: 'snapshot',
h: 'help',
l: 'loglevel'
l: 'loglevel',
'fake-argv': 'fakeArgv'
}
const argv = parseArgv(process.argv, { alias, default: defaults })
const g = chalk.gray
const help =
`
nexe --help CLI OPTIONS
const argv = parseArgv(process.argv, { alias, default: { ...defaults, enableStdIn: true } })
const g = c.gray
let help = `
${c.bold('nexe <entry-file> [options]')}
-i --input ${g('=index.js')} -- application entry point
-o --output ${g('=my-app.exe')} -- path to output file
-t --target ${g('=mac-x64-8.4.0')} -- *target a prebuilt binary
-n --name ${g('=my-app')} -- main app module name
-v --version ${g(`=${padRight(process.version.slice(1), 23)}`)}-- node version
-p --python ${g('=/path/to/python2')} -- python executable
-f --flag ${g('="--expose-gc"')} -- *v8 flags to include during compilation
-c --configure ${g('="--with-dtrace"')} -- *pass arguments to the configure step
-m --make ${g('="--loglevel"')} -- *pass arguments to the make/build step
-s --snapshot ${g('=/path/to/snapshot')} -- build with warmup snapshot
-r --resource ${g('=./paths/**/*')} -- *embed file bytes within the binary
-b --build -- build from source
--bundle ${g('=./path/to/config')} -- pass a module path that exports nexeBundle
--temp ${g('=./path/to/temp')} -- default './nexe'
--no-bundle -- set when input is already bundled
--cwd -- set the current working directory for the command
--ico -- file name for alternate icon file (windows)
--rc-* -- populate rc file options (windows)
--clean -- force download of sources
--enableNodeCli -- enable node cli enforcement (blocks app cli)
--sourceUrl -- pass an alternate source (node.tar.gz) url
--silent -- disable logging
--verbose -- set logging to verbose
${c.underline.bold('Options:')}
-* variable key name * option can be used more than once`.trim() + EOL
-i --input -- application entry point
-o --output -- path to output file
-t --target -- node version description
-n --name -- main app module name
-r --resource -- *embed files (glob) within the binary
--plugin -- extend nexe runtime behavior
function flattenFilter(...args: any[]): string[] {
${c.underline.bold('Building from source:')}
-b --build -- build from source
-p --python -- python2 (as python) executable path
-f --flag -- *v8 flags to include during compilation
-c --configure -- *arguments to the configure step
-m --make -- *arguments to the make/build step
--snapshot -- path to a warmup snapshot
--ico -- file name for alternate icon file (windows)
--rc-* -- populate rc file options (windows)
--sourceUrl -- pass an alternate source (node.tar.gz) url
--enableNodeCli -- enable node cli enforcement (blocks app cli)
${c.underline.bold('Other options:')}
--bundle -- custom bundling module with 'createBundle' export
--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
--silent -- disable logging
--verbose -- set logging to verbose
-* variable key name * option can be used more than once`.trim()
help = EOL + help + EOL
function flatten(...args: any[]): string[] {
return ([] as string[]).concat(...args).filter(x => x)
}
@@ -139,7 +151,7 @@ function tryResolveMainFileName(cwd: string) {
filename = basename(file).replace(extname(file), '')
} catch (_) {}
return !filename || filename === 'index' ? 'nexe_' + Date.now() : filename
return filename ? filename : 'nexe_' + Date.now()
}
function extractLogLevel(options: NexeOptions) {
@@ -149,61 +161,74 @@ function extractLogLevel(options: NexeOptions) {
return 'info'
}
function isName(name: string) {
return name && name !== 'index'
}
function extractName(options: NexeOptions) {
let name = options.name
if (!name && typeof options.input === 'string') {
if (!isName(name) && typeof options.input === 'string') {
name = basename(options.input).replace(extname(options.input), '')
}
name = name || tryResolveMainFileName(options.cwd)
if (!isName(name)) {
name = tryResolveMainFileName(options.cwd)
}
if (!isName(name) && basename(options.cwd)) {
name = basename(options.cwd)
}
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<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))
})
}
function normalizeOptions(input?: Partial<NexeOptions>): NexeOptions {
const options = Object.assign({}, defaults, input) as NexeOptions
const opts = options as any
options.temp = process.env.NEXE_TEMP || join(options.cwd, '.nexe')
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.input = findInput(options.input, options.cwd)
options.loglevel = extractLogLevel(options)
options.flags = flattenFilter(opts.flag, options.flags)
options.targets = flattenFilter(opts.target, options.targets).map(getTarget)
options.make = flattenFilter(options.vcBuild, options.make)
options.configure = flattenFilter(options.configure)
options.resources = flattenFilter(opts.resource, options.resources)
options.flags = flatten(opts.flag, options.flags)
options.targets = flatten(opts.target, options.targets).map(getTarget)
options.make = flatten(options.vcBuild, options.make)
options.configure = flatten(options.configure)
options.resources = flatten(opts.resource, options.resources)
options.rc = options.rc || extractCliMap(/^rc-.*/, options)
options.output = isWindows
? `${(options.output || options.name).replace(/\.exe$/, '')}.exe`
: `${options.output || options.name}`
options.output = resolve(cwd, options.output)
if (!options.targets.length) {
options.targets = [getTarget(options.version)]
options.targets.push(getTarget())
}
if (options.build) {
@@ -215,20 +240,21 @@ function normalizeOptionsAsync(input?: Partial<NexeOptions>): Promise<NexeOption
}
}
options.version = (options.targets[0] as NexeTarget).version
options.patches = options.patches.map(x => {
const requireDefault = (x: string) => {
if (typeof x === 'string') {
return require(x).default
}
return x
})
}
options.plugins = flatten(opts.plugin, options.plugins).map(requireDefault)
options.patches = flatten(opts.patch, options.patches).map(requireDefault)
Object.keys(alias)
.filter(k => k !== 'rc')
.forEach(x => delete opts[x])
return Promise.resolve(options)
return options
}
export { argv, normalizeOptionsAsync }
export { argv, normalizeOptions, help }
+53
View File
@@ -0,0 +1,53 @@
const fs = require('fs')
const fd = fs.openSync(process.execPath, 'r')
const stat = fs.statSync(process.execPath)
const footer = Buffer.from(Array(32))
fs.readSync(fd, footer, 0, 32, stat.size - 32)
if (!footer.slice(0, 16).equals(Buffer.from('<nexe~~sentinel>'))) {
throw 'Invalid Nexe binary'
}
const contentSize = footer.readDoubleLE(16)
const resourceSize = footer.readDoubleLE(24)
const contentStart = stat.size - 32 - resourceSize - contentSize
const resourceStart = contentStart + contentSize
Object.defineProperty(
process,
'__nexe',
(function() {
let nexeHeader: any = null
return {
get: function() {
return nexeHeader
},
set: function(value: any) {
if (nexeHeader) {
throw new Error('__nexe cannot be reconfigured')
}
nexeHeader = Object.assign({}, value, {
layout: {
stat,
contentSize,
contentStart,
resourceSize,
resourceStart
}
})
Object.freeze(nexeHeader)
},
enumerable: false,
configurable: false
}
})()
)
const contentBuffer = Buffer.from(Array(contentSize))
fs.readSync(fd, contentBuffer, 0, contentSize, contentStart)
fs.closeSync(fd)
const Module = require('module')
process.mainModule = new Module(process.execPath, null)
process.mainModule!.loaded = true
;(process.mainModule as any)._compile(contentBuffer.toString(), process.execPath)
+1 -1
View File
@@ -1,7 +1,7 @@
import { NexeCompiler } from '../compiler'
export default async function buildFixes(compiler: NexeCompiler, next: () => Promise<void>) {
if (!compiler.options.version.startsWith('8.2')) {
if (!compiler.target.version.startsWith('8.2')) {
return next()
}
+1 -1
View File
@@ -5,7 +5,7 @@ import cli from './disable-node-cli'
import flags from './flags'
import ico from './ico'
import rc from './node-rc'
import { NexeCompiler } from '../compiler'
import { NexeCompiler, NexeOptions } from '../compiler'
const patches = [gyp, nexePatches, buildFixes, cli, flags, ico, rc]
+9 -1
View File
@@ -12,7 +12,15 @@ export default async function nodeRc(compiler: NexeCompiler, next: () => Promise
let value = options[key]
const isVar = /^[A-Z_]+$/.test(value)
value = isVar ? value : `"${value}"`
file.contents.replace(new RegExp(`VALUE "${key}",*`), `VALUE "${key}", ${value}`)
file.contents = file.contents.replace(
new RegExp(`VALUE "${key}",*`),
`VALUE "${key}", ${value}`
)
})
;['PRODUCTVERSION', 'FILEVERSION'].forEach(x => {
if (options[x]) {
file.contents = file.contents.replace(new RegExp(x + ' .*$', 'm'), `${x} ${options[x]}`)
}
})
return next()
+1 -116
View File
@@ -5,122 +5,7 @@ import { join } from 'path'
export default async function main(compiler: NexeCompiler, next: () => Promise<void>) {
await compiler.setFileContentsAsync(
'lib/_third_party_main.js',
`
const fs = require('fs')
const path = require('path')
const Buffer = require('buffer').Buffer
const isString = x => typeof x === 'string' || x instanceof String
const fd = fs.openSync(process.execPath, 'r')
const size = fs.statSync(process.execPath).size
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 'Invalid Nexe binary'
}
const contentSize = footer.readDoubleLE(16)
const resourceSize = footer.readDoubleLE(24)
const contentStart = size - 32 - resourceSize - contentSize
const resourceStart = contentStart + contentSize
Object.defineProperty(process, '__nexe', (function () {
let nexeHeader = null
return {
get: function () {
return nexeHeader
},
set: function (value) {
if (nexeHeader) {
throw new Error('__nexe cannot be reconfigured')
}
nexeHeader = value
Object.freeze(nexeHeader)
},
enumerable: false,
configurable: false
}
})());
if (resourceSize) {
const originalReadFile = fs.readFile
const originalReadFileSync = fs.readFileSync
let setupManifest = () => {
const manifest = process.__nexe && process.__nexe.resources
if (!manifest) {
return
}
Object.keys(manifest).forEach((key) => {
const absolutePath = path.resolve(key)
if (!manifest[absolutePath]) {
manifest[absolutePath] = manifest[key]
}
const normalizedPath = path.normalize(key)
if (!manifest[normalizedPath]) {
manifest[normalizedPath] = manifest[key]
}
})
normalizeManifest = () => {}
};
fs.readFile = function readFile (file, options, callback) {
setupManifest()
const manifest = process.__nexe
const entry = manifest && manifest.resources[file]
if (!manifest || !entry || !isString(file)) {
return originalReadFile.apply(fs, arguments)
}
const [offset, length] = entry
const resourceOffset = resourceStart + offset
const encoding = isString(options) ? options : null
callback = typeof options === 'function' ? options : callback
fs.open(process.execPath, 'r', function (err, fd) {
if (err) return callback(err, null)
fs.read(fd, Buffer.alloc(length), 0, length, resourceOffset, function (error, bytesRead, buffer) {
if (error) {
return fs.close(fd, function () {
callback(error, null)
})
}
fs.close(fd, function (err) {
if (err) {
return callback(err, buffer)
}
const result = Buffer.from(buffer.toString(), 'base64')
callback(err, encoding ? result.toString(encoding) : result)
})
})
})
}
fs.readFileSync = function readFileSync (file, options) {
setupManifest()
const manifest = process.__nexe
const entry = manifest && manifest.resources[file]
if (!manifest || !entry || !isString(file)) {
return originalReadFileSync.apply(fs, arguments)
}
const [offset, length] = entry
const resourceOffset = resourceStart + offset
const encoding = isString(options) ? options : null
const fd = fs.openSync(process.execPath, 'r')
const result = Buffer.alloc(length)
fs.readSync(fd, result, 0, length, resourceOffset)
fs.closeSync(fd)
const contents = Buffer.from(result.toString(), 'base64')
return encoding ? contents.toString(encoding) : contents
}
}
const contentBuffer = Buffer.from(Array(contentSize));
fs.readSync(fd, contentBuffer, 0, contentSize, contentStart);
fs.closeSync(fd);
const Module = require('module');
process.mainModule = new Module(process.execPath, null);
process.mainModule.loaded = true;
process.mainModule._compile(contentBuffer.toString(), process.execPath);
`.trim()
'{{replace:lib/patches/boot-nexe.js}}'
)
return next()
}
-1
View File
@@ -32,7 +32,6 @@ async function getJson<T>(url: string, options?: any) {
return JSON.parse((await got(url, options)).body) as T
}
//TODO only build the latest of each major...?
function isBuildableVersion(version: string) {
const major = +version.split('.')[0]
return !~[0, 1, 2, 3, 4, 5, 7].indexOf(major) || version === '4.8.4'
+21 -20
View File
@@ -1,11 +1,13 @@
import { NexeCompiler } from '../compiler'
import { FuseBox, JSONPlugin, CSSPlugin, HTMLPlugin, QuantumPlugin } from 'fuse-box'
import { readFileAsync, writeFileAsync } from '../util'
//import NativeModulePlugin from './fuse-native-module-plugin'
import { resolve, relative } from 'path'
import NativeModulePlugin from '../bundling/fuse-native-module-plugin'
import { NexeOptions } from '../options'
function createBundle(filename: string, options: { name: string; minify: any; cwd: string }) {
const plugins: any = [JSONPlugin(), CSSPlugin(), HTMLPlugin()]
if (options.minify) {
function createBundle(options: NexeOptions) {
const { FuseBox, JSONPlugin, CSSPlugin, HTMLPlugin, QuantumPlugin } = require('fuse-box')
const plugins: any = [JSONPlugin(), CSSPlugin(), HTMLPlugin(), NativeModulePlugin(options.native)]
if (options.compress) {
plugins.push(
QuantumPlugin({
target: 'server',
@@ -14,27 +16,30 @@ function createBundle(filename: string, options: { name: string; minify: any; cw
})
)
}
const cwd = options.cwd
const fuse = FuseBox.init({
cache: false,
log: Boolean(process.env.NEXE_BUNDLE_DEBUG) || false,
homeDir: options.cwd,
log: Boolean(process.env.NEXE_BUNDLE_LOG) || false,
homeDir: cwd,
sourceMaps: false,
writeBundles: false,
output: '$name.js',
target: 'server',
plugins
})
fuse.bundle(options.name).instructions(`> ${filename}`)
return fuse.run().then(x => {
const input = relative(cwd, resolve(cwd, options.input)).replace(/\\/g, '/')
fuse.bundle(options.name).instructions(`> ${input}`)
return fuse.run().then((x: any) => {
let output = ''
x.bundles.forEach(y => (output = y.context.output.lastPrimaryOutput.content!.toString()))
x.bundles.forEach((y: any) => (output = y.context.output.lastPrimaryOutput.content.toString()))
return output
})
}
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()
}
@@ -45,17 +50,13 @@ export default async function bundle(compiler: NexeCompiler, next: any) {
let producer = createBundle
if (typeof compiler.options.bundle === 'string') {
producer = require(compiler.options.bundle).createBundle
producer = require(resolve(cwd, bundle)).createBundle
}
compiler.input = await producer(compiler.options.input, {
cwd: compiler.options.cwd,
name: compiler.options.name,
minify: compiler.options.compress
})
compiler.input = await producer(compiler.options)
if (compiler.options.debugBundle) {
await writeFileAsync(compiler.options.debugBundle, compiler.input)
if ('string' === typeof compiler.options.debugBundle) {
await writeFileAsync(resolve(cwd, compiler.options.debugBundle), compiler.input)
}
return next()
+28 -22
View File
@@ -1,19 +1,22 @@
import { normalize } from 'path'
import { normalize, relative } from 'path'
import { Readable } from 'stream'
import { createWriteStream, chmodSync } from 'fs'
import { createWriteStream, chmodSync, statSync } from 'fs'
import { readFileAsync, dequote, isWindows } from '../util'
import { NexeCompiler } from '../compiler'
import { NexeTarget } from '../target'
function readStreamAsync(stream: NodeJS.ReadableStream): PromiseLike<string> {
function getStdIn(stdin: NodeJS.ReadStream): Promise<string> {
return new Promise(resolve => {
let input = ''
stream.setEncoding('utf-8')
stream.on('data', (x: string) => {
input += x
})
stream.once('end', () => resolve(dequote(input)))
stream.resume && stream.resume()
let out = ''
stdin
.setEncoding('utf8')
.on('readable', () => {
let current
while ((current = stdin.read())) {
out += current
}
})
.on('end', () => resolve(out))
})
}
@@ -32,20 +35,15 @@ function readStreamAsync(stream: NodeJS.ReadableStream): PromiseLike<string> {
* @param {*} next
*/
export default async function cli(compiler: NexeCompiler, next: () => Promise<void>) {
const { output } = compiler.options
const { log, input: bundledInput } = compiler
if (!process.stdin.isTTY) {
log.step('Using stdin as input')
compiler.input = await readStreamAsync(process.stdin)
const { log } = compiler
let stdInUsed = false
if (!process.stdin.isTTY && compiler.options.enableStdIn) {
stdInUsed = true
compiler.input = dequote(await getStdIn(process.stdin))
}
await next()
compiler.output = isWindows
? `${(output || compiler.options.name).replace(/\.exe$/, '')}.exe`
: `${output || compiler.options.name}`
const target = compiler.options.targets.shift() as NexeTarget
const deliverable = await compiler.compileAsync(target)
@@ -58,8 +56,16 @@ export default async function cli(compiler: NexeCompiler, next: () => Promise<vo
if (e) {
reject(e)
} else if (compiler.output) {
chmodSync(compiler.output, '755')
step.log(`Executable written to: ${compiler.output}`)
const output = compiler.output
const mode = statSync(output).mode | 0o111
chmodSync(output, mode.toString(8).slice(-3))
const inputFile = relative(process.cwd(), compiler.options.input)
const outputFile = relative(process.cwd(), output)
step.log(
`Entry: '${stdInUsed
? compiler.options.empty ? '[empty]' : '[stdin]'
: inputFile}' written to: ${outputFile}`
)
resolve(compiler.quit())
}
})
+2 -9
View File
@@ -18,15 +18,8 @@ export default async function resource(compiler: NexeCompiler, next: () => Promi
count++
step.log(`Including file: ${file}`)
const contents = await readFileAsync(file)
const commentSafeContents = contents.toString('base64')
resources.index[file] = [
Buffer.byteLength(resources.bundle),
Buffer.byteLength(commentSafeContents)
]
resources.bundle += commentSafeContents
compiler.addResource(file, contents)
})
step.log(
`Included ${count} file(s). ${(Buffer.byteLength(resources.bundle) / 1e6).toFixed(3)} MB`
)
step.log(`Included ${count} file(s). ${(resources.bundle.byteLength / 1e6).toFixed(3)} MB`)
return next()
}
+188
View File
@@ -0,0 +1,188 @@
import { Stats } from 'fs'
import { ok } from 'assert'
import { dirname, resolve, normalize, basename } from 'path'
const binary = (process as any).__nexe as NexeBinary
ok(binary)
const manifest = binary.resources
const directories: { [key: string]: { [key: string]: boolean } } = {}
const isString = (x: any): x is string => typeof x === 'string' || x instanceof String
const isNotFile = () => false
const isNotDirectory = isNotFile
const isFile = () => true
const isDirectory = isFile
if (Object.keys(manifest).length) {
const fs = require('fs')
const originalReadFile = fs.readFile
const originalReadFileSync = fs.readFileSync
const originalReaddir = fs.readdir
const originalReaddirSync = fs.readdirSync
const originalStatSync = fs.statSync
const originalStat = fs.stat
const resourceStart = binary.layout.resourceStart
const statTime = function() {
const stat = binary.layout.stat
return {
dev: 0,
ino: 0,
nlink: 0,
rdev: 0,
uid: 123,
gid: 500,
blksize: 4096,
blocks: 0,
atime: new Date(stat.atime),
atimeMs: stat.atime.getTime(),
mtime: new Date(stat.mtime),
mtimeMs: stat.mtime.getTime(),
ctime: new Date(stat.ctime),
ctimMs: stat.ctime.getTime(),
birthtime: new Date(stat.birthtime),
birthtimeMs: stat.birthtime.getTime()
}
}
const createStat = function(directoryExtensions: any, fileExtensions?: any) {
if (!fileExtensions) {
return Object.assign({}, binary.layout.stat, directoryExtensions, { size: 0 }, statTime())
}
const size = directoryExtensions[1]
return Object.assign({}, binary.layout.stat, fileExtensions, { size }, statTime())
}
const ownStat = function(path: string | Buffer) {
const key = resolve(path)
if (directories[key]) {
return createStat({ isDirectory, isFile: isNotFile })
}
if (manifest[key]) {
return createStat(manifest[key], { isFile, isDirectory: isNotDirectory })
}
}
let setupManifest = () => {
Object.keys(manifest).forEach(key => {
const absolutePath = resolve(key)
const dirPath = dirname(absolutePath)
directories[dirPath] = directories[dirPath] || {}
directories[dirPath][basename(absolutePath)] = true
if (!manifest[absolutePath]) {
manifest[absolutePath] = manifest[key]
}
const normalizedPath = normalize(key)
if (!manifest[normalizedPath]) {
manifest[normalizedPath] = manifest[key]
}
})
setupManifest = () => {}
}
//naive patches intended to work for most use cases
var nfs = {
readdir: function readdir(path: string | Buffer, options: any, callback: any) {
setupManifest()
path = path.toString()
if ('function' === typeof options) {
callback = options
options = { encoding: 'utf8' }
}
const dir = directories[resolve(path)]
if (dir) {
process.nextTick(() => {
callback(null, Object.keys(dir))
})
} else {
return originalReaddir.apply(fs, arguments)
}
},
readdirSync: function readdirSync(path: string | Buffer, options: any) {
setupManifest()
path = path.toString()
const dir = directories[resolve(path)]
if (dir) {
return Object.keys(dir)
}
return originalReaddirSync.apply(fs, arguments)
},
readFile: function readFile(file: any, options: any, callback: any) {
setupManifest()
const entry = manifest[file]
if (!entry || !isString(file)) {
return originalReadFile.apply(fs, arguments)
}
const [offset, length] = entry
const resourceOffset = resourceStart + offset
const encoding = isString(options) ? options : null
callback = typeof options === 'function' ? options : callback
fs.open(process.execPath, 'r', function(err: Error, fd: number) {
if (err) return callback(err, null)
fs.read(fd, Buffer.alloc(length), 0, length, resourceOffset, function(
error: Error,
bytesRead: number,
result: Buffer
) {
if (error) {
return fs.close(fd, function() {
callback(error, null)
})
}
fs.close(fd, function(err: Error) {
if (err) {
return callback(err, result)
}
callback(err, encoding ? result.toString(encoding) : result)
})
})
})
},
readFileSync: function readFileSync(file: any, options: any) {
setupManifest()
const entry = manifest[file]
if (!entry || !isString(file)) {
return originalReadFileSync.apply(fs, arguments)
}
const [offset, length] = entry
const resourceOffset = resourceStart + offset
const encoding = isString(options) ? options : null
const fd = fs.openSync(process.execPath, 'r')
const result = Buffer.alloc(length)
fs.readSync(fd, result, 0, length, resourceOffset)
fs.closeSync(fd)
return encoding ? result.toString(encoding) : result
},
statSync: function statSync(path: string | Buffer) {
const stat = ownStat(path)
if (stat) {
return stat
}
return originalStatSync.apply(fs, arguments)
},
stat: function stat(path: string | Buffer, callback: any) {
const stat = ownStat(path)
if (stat) {
process.nextTick(() => {
callback(null, stat)
})
} else {
return originalStat.apply(fs, arguments)
}
}
}
Object.assign(fs, nfs)
}
interface NexeBinary {
resources: { [key: string]: number[] }
version: string
layout: {
stat: Stats
contentSize: number
contentStart: number
resourceSize: number
resourceStart: number
}
}
+37
View File
@@ -0,0 +1,37 @@
import { NexeCompiler } from '../compiler'
function wrap(code: string) {
return '!(function () {' + code + '})();'
}
export default function(compiler: NexeCompiler, next: () => Promise<void>) {
compiler.shims.push(wrap(compiler.getHeader()))
compiler.shims.push(
wrap(`
if (process.argv[1] && process.env.NODE_UNIQUE_ID) {
const cluster = require('cluster')
cluster._setupWorker()
delete process.env.NODE_UNIQUE_ID
}
`)
)
if (compiler.options.resources.length) {
compiler.shims.push(wrap('{{replace:lib/steps/shim-fs.js}}'))
}
//compiler.shims.push(wrap('{/{replace:lib/steps/shim-require.js}}'))
if (compiler.options.fakeArgv !== false) {
const nty = !process.stdin.isTTY
const input = nty ? '[stdin]' : compiler.options.input
compiler.shims.push(
wrap(`
var r = require('path').resolve;
process.argv.splice(1,0, ${nty ? `'${input}'` : `r("${input}")`});`)
)
}
return next()
}
+3 -1
View File
@@ -28,6 +28,7 @@ const prettyPlatform: { [key: string]: NodePlatform } = {
//TODO arm
const prettyArch: { [key: string]: NodeArch } = {
x86: 'x86',
amd64: 'x64',
ia32: 'x86',
x32: 'x86',
x64: 'x64'
@@ -63,7 +64,8 @@ export function targetsEqual(a: NexeTarget, b: NexeTarget) {
}
export function getTarget(target: string | Partial<NexeTarget> = ''): NexeTarget {
let arch = process.arch as NodeArch,
const currentArch = process.arch
let arch = currentArch in prettyArch ? prettyArch[process.arch] : process.arch as NodeArch,
platform = prettyPlatform[process.platform],
version = process.version.slice(1)
+17
View File
@@ -26,6 +26,22 @@ function padRight(str: string, l: number) {
return (str + ' '.repeat(l)).substr(0, l)
}
const bound: MethodDecorator = function bound<T>(
target: Object,
propertyKey: string,
descriptor: TypedPropertyDescriptor<T>
) {
const configurable = true
return {
configurable,
get(this: T) {
const value = (descriptor.value as any).bind(this)
Object.defineProperty(this, propertyKey, { configurable, value, writable: true })
return value
}
}
}
function dequote(input: string) {
input = input.trim()
const singleQuote = input.startsWith("'") && input.endsWith("'")
@@ -62,6 +78,7 @@ function isDirectoryAsync(path: string) {
export {
dequote,
padRight,
bound,
isWindows,
rimrafAsync,
statAsync,
+21 -25
View File
@@ -1,25 +1,23 @@
import * as nexe from '../src/nexe'
import {
getUnBuiltReleases,
getLatestGitRelease
} from '../src/releases'
import * as nexe from '../lib/nexe'
import { getUnBuiltReleases, getLatestGitRelease } from '../lib/releases'
import * as ci from './ci'
import { runAlpineBuild } from './docker'
import { getTarget } from '../src/target'
import { pathExistsAsync, statAsync, readFileAsync, execFileAsync } from '../src/util'
import { getTarget } from '../lib/target'
import { pathExistsAsync, statAsync, readFileAsync, execFileAsync } from '../lib/util'
import got = require('got')
const env = process.env,
branchName = env.CIRCLE_BRANCH || env.APPVEYOR_REPO_BRANCH || env.TRAVIS_BRANCH || '',
isScheduled = Boolean(env.APPVEYOR_SCHEDULED_BUILD || env.NEXE_TRIGGERED),
isLinux = Boolean(env.CIRCLECI),
isLinux = Boolean(env.TRAVIS),
isWindows = Boolean(env.APPVEYOR),
isMac = Boolean(env.TRAVIS),
isPullRequest = Boolean(env.CIRCLE_PR_NUMBER)
|| Boolean(env.APPVEYOR_PULL_REQUEST_NUMBER)
|| Boolean(env.TRAVIS_PULL_REQUEST_BRANCH),
headers = {
'Authorization': 'token ' + env.GITHUB_TOKEN,
isMac = Boolean(env.CIRCLECI),
isPullRequest =
Boolean(env.CIRCLE_PR_NUMBER) ||
Boolean(env.APPVEYOR_PULL_REQUEST_NUMBER) ||
Boolean(env.TRAVIS_PULL_REQUEST_BRANCH),
headers = {
Authorization: 'token ' + env.GITHUB_TOKEN,
'User-Agent': 'nexe (https://www.npmjs.com/package/nexe)'
}
@@ -32,7 +30,7 @@ if (require.main === module) {
}
}
async function build () {
async function build() {
if (isScheduled) {
const releases = await getUnBuiltReleases({ headers })
if (!releases.length) {
@@ -54,8 +52,7 @@ async function build () {
}
if (env.NEXE_VERSION) {
const
target = getTarget(env.NEXE_VERSION),
const target = getTarget(env.NEXE_VERSION),
output = isWindows ? './out.exe' : './out',
options = {
empty: true,
@@ -63,16 +60,15 @@ async function build () {
target,
output
}
const stop = keepalive()
if (target.platform === 'alpine') {
await runAlpineBuild(target, nexe.version.split('.')[0])
await runAlpineBuild(target)
} else {
await nexe.compile(options)
await nexe.compile(options)
}
stop()
if (await pathExistsAsync(output)) {
await assertNexeBinary(output)
const gitRelease = await getLatestGitRelease({ headers })
@@ -80,7 +76,7 @@ async function build () {
query: { name: target.toString() },
body: await readFileAsync(output),
headers: {
'Authorization': 'token ' + env.GITHUB_TOKEN,
Authorization: 'token ' + env.GITHUB_TOKEN,
'Content-Type': 'application/octet-stream',
'User-Agent': 'nexe (https://www.npmjs.com/package/nexe)'
}
@@ -90,12 +86,12 @@ async function build () {
}
}
function keepalive () {
function keepalive() {
const keepalive = setInterval(() => 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
+29 -28
View File
@@ -1,32 +1,9 @@
import { NexeTarget } from '../src/releases'
import { NexeTarget } from '../lib/releases'
import got = require('got')
import * as assert from 'assert'
const { env } = process
export function triggerMacBuild(release: NexeTarget, branch: string) {
assert.ok(env.TRAVIS_TOKEN)
const travis = `https://api.travis-ci.org/repo/nexe%2Fnexe/requests`
return got(travis, {
json: true,
body: {
request: {
branch: branch,
config: {
merge_mode: 'deep_merge',
env: {
NEXE_VERSION: release.toString()
}
}
}
},
headers: {
'Travis-API-Version': '3',
'Authorization': `token ${env.TRAVIS_TOKEN}`
}
})
}
export function triggerDockerBuild (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}`
return got(circle, {
@@ -39,10 +16,34 @@ export function triggerDockerBuild (release: NexeTarget, branch: string) {
})
}
export function triggerWindowsBuild (release: NexeTarget) {
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, {
json: true,
body: {
request: {
branch: branch,
config: {
merge_mode: 'deep_merge',
env: {
//use matrix so that secure global variable is merged
matrix: {
NEXE_VERSION: release.toString()
}
}
}
}
},
headers: {
'Travis-API-Version': '3',
Authorization: `token ${env.TRAVIS_TOKEN}`
}
})
}
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()
}
+35 -49
View File
@@ -1,73 +1,59 @@
import { NexeTarget } from '../src/target'
import { writeFileAsync, readFileAsync } from '../src/util'
import { NexeTarget } from '../lib/target'
import { writeFileAsync, readFileAsync } from '../lib/util'
import { spawn } from 'child_process'
import got = require('got')
import { createWriteStream, WriteStream } from 'fs'
import execa = require('execa')
import { appendFileSync } from 'fs'
function alpine (target: NexeTarget, nexeVersion: string) {
function alpine(target: NexeTarget) {
return `
FROM i386/alpine:3.4
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
ENV NODE_VERSION=${target.version}
ENV NEXE_VERSION=beta
WORKDIR /
RUN curl -sSL https://nodejs.org/dist/v\${NODE_VERSION}/node-v\${NODE_VERSION}.tar.gz | tar -xz && \
cd /node-v\${NODE_VERSION} && \
mkdir /nexe_temp && mv /node-v\${NODE_VERSION} /nexe_temp/\${NODE_VERSION} && \
cd /nexe_temp/\${NODE_VERSION} && \
./configure --prefix=/usr --fully-static && \
make -j$(getconf _NPROCESSORS_ONLN) && \
make install && \
make && make install && \
paxctl -cm /usr/bin/node
RUN mkdir /nexe_temp && mv node-v\${NODE_VERSION} /nexe_temp/\${NODE_VERSION} && \
RUN rm /nexe_temp/\${NODE_VERSION}/out/Release/node && \
npm install -g nexe@\${NEXE_VERSION} && \
nexe --build --empty --temp /nexe_temp -c="--fully-static" -o out
`.trim()
}
export async function runAsync (command: string, output: WriteStream) {
const commands = command.split(/\r?\n/)
.filter(x => x.trim())
.map(x => x.trim().split(' '))
const sequence = Promise.resolve()
for (const command of commands) {
await new Promise((resolve, reject) => {
const cp = spawn(command.shift() as string, command)
cp.stderr.pipe(output)
cp.stdout.pipe(output)
const close = function (this: any, e: Error) {
this.kill()
e ? reject(e) : resolve()
}
cp.on('error', (e) => {
output.write(e.stack)
})
.on('close', close)
.on('exit', close)
})
}
}
export async function runAlpineBuild (target: NexeTarget, nexeVersion: string) {
await writeFileAsync('Dockerfile', alpine(target, nexeVersion))
export async function runAlpineBuild(target: NexeTarget) {
await writeFileAsync('Dockerfile', alpine(target))
const outFilename = 'nexe-alpine-build-log.txt'
const output = createWriteStream(outFilename)
await writeFileAsync(outFilename, '')
let output: any = []
try {
await runAsync(`
docker build -t nexe-alpine .
docker run -d --name nexe nexe-alpine sh
docker cp nexe:/out out
docker rm nexe
`, output)
} catch(e) {
console.log(e)
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) {
console.log('Error running docker', e)
} finally {
output.close()
await got(`https://transfer.sh/${Math.random().toString(36).substring(2)}.txt`, {
body: await readFileAsync(outFilename),
method: 'PUT'
output.forEach((x: any) => {
appendFileSync(outFilename, x.stderr)
appendFileSync(outFilename, x.stdout)
})
.then(x => console.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))
}
}
+28
View File
@@ -0,0 +1,28 @@
import { writeFileSync, readFileSync } from 'fs'
/**
* post build step to insert code files into code files.
* '{{replace:path/to/file}}' => "file contents"
* And the package.json version.
*/
//inject('plugins/nexe-daemon/lib/index.js')
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[]) {
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'))
})
writeFileSync(filename, contents)
console.log(`Wrote: ${filename}`)
}
+3
View File
@@ -0,0 +1,3 @@
{
"main": "entry-file.js"
}
+1
View File
@@ -1,3 +1,4 @@
./test/**/*.spec.ts
--check-leaks
--require ts-node/register
--recursive
+48
View File
@@ -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}`))
})
})
})
+4 -2
View File
@@ -3,20 +3,22 @@ import { expect } from 'chai'
import { blue as b } from 'chalk'
import { getTarget, NexeTarget } from '../src/target'
const arch = (isWindows && process.arch === 'ia32') ? 'x86' : process.arch
const arch = process.arch === 'ia32' ? 'x86' : process.arch
describe('Targets', () => {
[
['win-ia32-6.11.2', 'windows-x86-6.11.2'],
[{ version: '6.11.2', platform: 'win', arch: 'ia32' }, 'windows-x86-6.11.2'],
['win32-x64-6.11.2', 'windows-x64-6.11.2'],
['win-amd64-6.11.2', 'windows-x64-6.11.2'],
['darwin-x64-v8.4.0', 'mac-x64-8.4.0'],
['macos-x64-v8.4.0', 'mac-x64-8.4.0'],
['static-x86-6.10.3', 'alpine-x86-6.10.3'],
['linux-x32', `linux-x86-${process.version.slice(1)}`],
['alpine-notsupported-6.10.3', `alpine-${arch}-6.10.3`],
['not-a-thing', getTarget(process).toString()]
].forEach(([input, expected]) => {
it(`${padRight(JSON.stringify(input), 53)} ${b('->')} ${expected}`, () => {
it(`should accept: ${padRight(JSON.stringify(input), 53)} ${b('->')} ${expected}`, () => {
expect(getTarget(input).toString()).to.equal(expected)
})
})
+1
View File
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target":"es5",
"experimentalDecorators": true,
"lib": ["es2017"],
"module": "commonjs",
"outDir": "./lib",
+2628
View File
File diff suppressed because it is too large Load Diff