diff --git a/.editorconfig b/.editorconfig index 28efd54..d1d9749 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,11 +5,7 @@ root = true [*] end_of_line = lf indent_style = space -indent_size = 4 +indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true - -[package.json] -indent_style = space -indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..aa8c8f8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,27 @@ +{ + "root": true, + "parser": "babel-eslint", + "extends": "standard", + "env": { + "browser": true + }, + "rules": { + "object-curly-spacing": [ + 2, + "always" + ], + "space-before-function-paren": [ + 2, + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + } + ], + "no-var": 2, + "semi": [ + 2, + "never" + ] + } +} diff --git a/README.md b/README.md index 9bbf947..0936b7f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # hero-cli +[中文文档](https://github.com/hero-mobile/hero-cli/blob/master/README.zh.md) + Create Hero apps with no build configuration. * [Getting Started](#getting-started) – How to create a new app. @@ -61,14 +63,17 @@ It will create a directory called `my-app` inside the current folder.
Inside that directory, it will generate the initial project structure and then you can run command `npm install` to install the dependencies manually: ``` +├── environments +│   ├── environment-dev.js +│   └── environment-prod.js +├── platforms +│   ├── android +│   └── iOS ├── public │   ├── ... │   └── favicon.ico ├── src │   ├── ... -│   ├── environments -│   │   ├── environment-dev.js -│   │   └── environment-prod.js │   ├── index.html │   └── index.js ├── .babelrc @@ -82,15 +87,16 @@ Inside that directory, it will generate the initial project structure and then y ``` For the project to build, **these files must exist with exact filenames**: +* `platforms` folder contains Android/iOS codes. * `src/index.html` is the entry page; * `src/index.js` is the JavaScript entry point. -* `.hero-cli.json` is the configuration file for hero-cli build, it tell hero loads which configuration when you run command `hero start -e dev` or `hero build -e prod`(which is invoked by `npm start` or `npm build`) according to the value of `-e` parameter. For more build options please refer to [Build Scripts](#build-scripts). +* `.hero-cli.json` is the configuration file for hero-cli build, it tell hero loads which configuration when you run command `hero start -e dev` or `hero build -e prod`(which is invoked by `npm start` or `npm run build`) according to the value of `-e` parameter. For more build options please refer to [Build Scripts](#build-scripts). You can delete or rename the other files. * `public` assets like images inside this folder will **copied into the build folder untouched**. It will not be processed by Webpack. * `src` For faster rebuilds, only files inside this folder are processed by Webpack. You need to **put any JS and CSS files inside this folder**, or Webpack won’t see them. -* `src/environments` where your configurations exists(these file path configured in file `.hero-cli.json`, you can change it later) and you can access the configuration values in JavaScript or HTML code. See [Adding Custom Environment Variables](#adding-custom-environment-variables). +* `environments` where your configurations exists(these file path configured in file `.hero-cli.json`, you can change it later) and you can access the configuration values in JavaScript or HTML code. See [Adding Custom Environment Variables](#adding-custom-environment-variables). You may curious about where is the `pages/start.html`. Yes, it's generated by hero-cli. See [Generate HTML](#generate-html) @@ -107,7 +113,7 @@ You may curious about where is the `pages/start.html`. Yes, it's generated by he * [Android](#android) * [iOS](#ios) * [Build Scripts](#build-scripts) - * [`hero start`](#hero-start) + * [`hero dev`](#hero-dev) * [`hero build`](#hero-build) * [`hero serve`](#hero-serve) * [`hero init`](#hero-init) @@ -122,8 +128,8 @@ Any JS file meet the following 2 conditions will treat as JavaScript entry point Which would cause a HTML file generated using Webpack plugin [html-webpack-plugin](https://www.npmjs.com/package/html-webpack-plugin): -* Options specified in `@Entry(options)` will passed to `html-webpack-plugin` transparently. -* Destination of generated HTML file will keep the file path structure of the Javascript entry, or you can override it using the option `filename` provided by `html-webpack-plugin`. +* You can specify options during generate HTML via `@Entry(options)`. +* Destination of generated HTML file will keep the file path structure of the Javascript entry, or you can override it using the option `path`. * Generated HTML file can access the [Custom Environment Variables](#adding-custom-environment-variables). Example:
@@ -131,17 +137,7 @@ Example:
Below JavaScript file `src/pages/start.js` will generate a HTML file access by URL `/pages/start.html`, that's why we can visit [http://localhost:3000/pages/start.html](http://localhost:3000/pages/start.html). ```javascript -// content of file: src/pages/start.js -import { Entry } from 'hero-cli/decorator'; - -// class marked by @Entry -// will generate HTML accessed by URL /pages/start.html -// Equal to -// @Entry({ -// filename: 'pages/start.html' -// }) -// -@Entry() + export class DecoratePage { sayHello(data){ @@ -151,6 +147,14 @@ export class DecoratePage { } ``` +If you want to rename filename, you can add a key/val to `router.js`: +````javascript +// router.js +module.exports = { + rename_home: require.resolve('./src/pages/start.js') +} +```` +than build will emit `rename_home.html` ### Adding Custom Environment Variables Your project can consume variables declared in your environment as if they were declared locally in your JS files. By default you will have any environment variables starting with `HERO_APP_`. These environment variables can be useful for consuming sensitive data that lives outside of version control.
@@ -243,11 +247,10 @@ module.exports = environment; When you run command `hero start -e dev` or `hero build -e dev`, all variables from `src/environments/environment-dev.js` can be accessed via `process.env`. ### Proxying API Requests in Development -People often serve the front-end React app from the same host and port as their backend implementation. +People often serve the front-end appplication from the same host and port as their backend implementation. For example, a production setup might look like this after the app is deployed: ``` -/ - static server returns index.html with React app -/todos - static server returns index.html with React app +/ - static server returns index.html with application /api/todos - server handles any /api/* requests using the backend implementation ``` @@ -270,7 +273,7 @@ To tell the development server to proxy any unknown requests to your API server ``` -This way, when you `fetch('/api/v2/todos')` in development, the development server will proxy your request to `http://localhost:4000/api/v2/todos`, and when you `fetch('/feapi/todos')`, the request will proxy to `https://localhost:4001`. +This way, when you `fetch('/api/v2/todos')` in development, the development server will proxy your request to `https://localhost:4000/api/v2/todos`, and when you `fetch('/feapi/todos')`, the request will proxy to `https://localhost:4001/feapi/todos`. ### Build Native App #### Android @@ -286,12 +289,13 @@ This way, when you `fetch('/api/v2/todos')` in development, the development serv * `ANDROID_HOME` * `GRADLE_HOME` -Currently, generated android apk will loads resources hosted by remote server. In order to make the appliation available in the your mobile. +Currently, generated native app will loads resources hosted by remote server. In order to make the appliation available in the your mobile. -Firstly, you have to deploy the codes generate by command [`hero build`](#hero-build) into remote server.
-Secondly, before you generate the apk, you should using parameter `-e` to tell the apk loads which url when it startup. +* Firstly, you can using command [`hero build`](#hero-build) to generate the package which contains the application codes for deploy.
+* Secondly, specify the entry url in file `.hero-cli.json` when native app startup. +* Finally, using command [`hero platform build`](#hero- platform-build) build the native app. before you generate the apk, you should using parameter `-e` to tell the apk loads which url when it startup. -For example, you can config the url in file `.hero-cli.json` like this: +For example: ```json { @@ -316,6 +320,7 @@ For more options, see command of Build Scripts: [`Build Android App`](#build-and ### Build Scripts +Below scripts support option `--verbose` to output more details. #### `hero start` @@ -342,7 +347,7 @@ You will see the build errors and lint warnings in the console. * `-e`
Environment name of the configuration when start the application * `-s`
Build the boundle as standalone version, which should run in Native App environment. That's to say, build version without libarary like [webcomponent polyfills](http://webcomponents.org/polyfills/) or [hero-js](https://github.com/hero-mobile/hero-js)(These libarary is necessary for Hero App run in web browser, not Native App). -* `-i`
Inline JavaScript code into HTML. Default value is [false]. +* `-i`
Inline JavaScript/CSS code into HTML. Default value is [false]. * `-b`
Build pakcage only contain dependecies like hero-js or webcomponents, withou code in /src folder. Default value is [false] * `-m`
Build without sourcemap. Default value is [false], will generate sourcemap. * `-f`
Generate AppCache file, default file name is "app.appcache". Default value is [false], will not generate this file. @@ -385,7 +390,7 @@ This will let Hero App correctly infer the root path to use in the generated HTM After `hero build` process completedly, `build` folder will generated. You can serve a static server using `hero serve`. #### `hero init` -You can run `hero build -h` for help. It will generate the initial project structure of Hero App. See [Creating an App](#creating-an-app). +You can run `hero build -h` for help. It will generate the initial project structure of Hero App. See [Quick Overview](#quick-overview). #### `hero platform build` This command used for build native app. And you can run `hero platform build -h` for help.
@@ -418,7 +423,7 @@ Once command `hero platform build android -e prod` execute successfully, a andro ###### How to specify the android build tool version in SDK Hero will dynamic using the lastest available one from your local install versions by default. -You might have multiple available versions in the Android SDK. You can specify the `ANDROID_HOME/build-toos` version and compile `com.android.support:appcompat-v7` version following the keyword `android` and seperated by colon. +You might have multiple available versions in the Android SDK. You can specify the `ANDROID_HOME/build-toos` version and compile `com.android.support:appcompat-v7` version following the command `hero platform build android` and seperated by colon. For example, you can using the below command specify the `ANDROID_HOME/build-toos` version `23.0.2` and compile `com.android.support:appcompat-v7` version `24.0.0-alpha1`: diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..49493d8 --- /dev/null +++ b/README.zh.md @@ -0,0 +1,432 @@ +# hero-cli + +[Documetation for English](https://github.com/hero-mobile/hero-cli) + +用来快速构建Hero App的命令行工具 + +* [快速开始](#快速开始) – 如何使用hero-cli工具快速创建一个Hero App +* [使用文档](#使用文档) – 如何使用hero-cli工具帮助开发一个Hero App + +Hero App可以运行在Android, iOS和高级浏览器等环境中。如果发现任何issue,可以[点击这里](https://github.com/hero-mobile/hero-cli/issues/new)告诉我们。 + +## 如何构建Hero App + +运行以下命令 + +```sh +npm install -g hero-mobile/hero-cli + +hero init my-app +cd my-app/ + +npm install + +``` + +当相关依赖安装完成后,在项目更目录下,可以运行以下命令: + +* `npm start` 启动Hero App服务 +* `npm run build` 将项目代码打包,生成一份优化后的代码,用于部署 +* `npm run android` 生成一个Android APK格式的安装文件 + +成功运行`npm start`后,打开链接[http://localhost:3000/index.html](http://localhost:3000/index.html)可看到如下界面: + +npm start + +hero-cli使用[Webpack](http://webpack.github.io/)来进行项目的打包,相应的配置隐藏在工具内部,让开放者更关注与业务逻辑的开发。 + +## 快速开始 + +### 安装 + +运行以下命令进行全局安装,安装后可以使用`hero`命令 + +```sh +npm install -g hero-mobile/hero-cli +``` + +**Node版本需要高于4.0,建议使用Node >= 6 和 npm >= 3** +你可以使用[nvm](https://github.com/creationix/nvm#usage)来安装,管理多个Node版本,并可以方便地在各个版本间进行切换。 + +### 创建Hero App + +使用`hero init`命令来初始化一个Hero App + +```sh +hero init my-app +cd my-app +``` + +该命令会在当前目录下创建一个`my-app`的目录并生成相应的项目代码,目录结构如下: + +``` +├── environments +│   ├── environment-dev.js +│   └── environment-prod.js +├── platforms +│   ├── android +│   └── iOS +├── public +│   ├── ... +│   └── favicon.ico +├── src +│   ├── ... +│   ├── index.html +│   └── index.js +├── .babelrc +├── .editorconfig +├── .eslintrc +├── .gitattributes +├── .gitignore +├── .hero-cli.json +├── package.json +└── README.md +``` +在初始化的项目代码中, **以下文件或目录必须存在**: + +* `platforms` 存放Android/iOS原生代码 +* `src/index.html` 项目的HTML入口文件; +* `src/index.js` 项目的JavaScript入口文件. +* `.hero-cli.json` Hero App的配置文件。比如当用户运行`npm start`或`npm run build`(实际上在`package.json`中配置的调用的命令为`hero start -e dev` 和`hero build -e prod`)时,该加载哪些配置项。了解更多部署构建选项,点击查看[构建命令](#构建命令). + +其他: +* `public` 该目录可以用来存放一个通用的资源,这些资源不会经过Webpack处理,而是直接复制到打包后的文件中 +* `src` 该目录存放的文件会被Webpack处理,可以把JS,CSS等文件放在这个目录 +* `environments` 存放不同环境的配置文件(该目录的路径可以在文件`.hero-cli.json`中配置),该配置文件中的值可以通过全局变量访问到。具体方法见[添加环境变量](#添加环境变量). + +你也许会发现HTML文件`pages/start.html`不存在,没错,该文件是生成出来的。详情见[动态生成HTML](#动态生成HTML) + +## 使用文档 + +* [动态生成HTML](#动态生成HTML) +* [添加环境变量](#添加环境变量) + * [JavaScript文件中引用环境变量](#JavaScript文件中引用环境变量) + * [HTML文件中引用环境变量](#HTML文件中引用环境变量) + * [Shell命令中添加环境变量](#Shell命令中添加环境变量) + * [`.hero-cli.json`文件中添加环境变量](#hero-clijson文件中添加环境变量) +* [请求代理服务](#请求代理服务) +* [构建原生App安装包](#构建原生App安装包) + * [Android](#android) + * [iOS](#ios) +* [构建命令](#构建命令) + * [`hero start`](#hero-start) + * [`hero build`](#hero-build) + * [`hero serve`](#hero-serve) + * [`hero init`](#hero-init) + * [`hero platform build android`](`#hero-platform-build-android) + +### 动态生成HTML + +在`src`目录中的JS文件,如果满足以下2个条件 + +* JS文件中存在`class`的类声明 +* 声明的`class`类被来自[hero-cli/decorator](https://github.com/hero-mobile/hero-cli/blob/master/decorator.js)的[Decorator](https://github.com/wycats/javascript-decorators/blob/master/README.md) `@Entry`所修饰 + +则会经Webpack插件[html-webpack-plugin](https://www.npmjs.com/package/html-webpack-plugin)生成相应的HTML文件 + +* 可以通过`@Entry(options)`指定HTML文件生成的选项 +* 默认情况下,该HTML文件生成后,访问的路径与当前JS的路径结构一致,你可以通过属性`path`来自定HTML访问的路径 +* 生成的HTML文件也可以访问环境变量。如何添加环境变量?点击查看[添加环境变量](#添加环境变量)。 + +示例:
+ +文件`src/pages/start.js`将会生成一个对应的HTML文件,该HTML文件的访问路径为`/pages/start.html`, 同时该HTML文件包含了对`src/pages/start.js`的引用。 + +这就是为什么我们可以访问HTML[http://localhost:3000/pages/start.html](http://localhost:3000/pages/start.html). + +```javascript +// 文件: +import { Entry } from 'hero-cli/decorator'; + +// 被@Entry所修饰的class,会生成一个对应的HTML文件 +// 当前JS路径为: src/pages/start.js +// 所以生成的HTML访问路径为 /pages/start.html +// +// 代码@Entry() +// 效果同 +// @Entry({ +// path: '/pages/start.html' +// }) +// +@Entry() +export class DecoratePage { + + sayHello(data){ + console.log('Hello Hero!') + } + +} + +``` + +### 添加环境变量 + +项目代码可以访问机器上的环境变量。默认情况下,可以访问所有以`HERO_APP_`开头的变量,这样做的好处是,这些变量可以脱离版本管理工具如Git, SVN的控制。 + +**环境变量是在构建过程中添加进去的**.
+ +#### JavaScript文件中引用环境变量 + +在JavaScript文件中,可以通过全局变量`process.env`来访问环境变量,例如,当存在一个名为`HERO_APP_SECRET_CODE`的环境变量,则可以在JS中使用`process.env.HERO_APP_SECRET_CODE`来访问。 + +```javascript + +console.log('Send Request with Token: '+ process.env.HERO_APP_SECRET_CODE); + +``` + +同时,hero-cli会内置额外的2个环境变量`NODE_ENV`和`HOME_PAGE`到`process.env`中。 + +当运行`hero start`时,`NODE_ENV`的值为`'development'`;当运行`hero build`时,`NODE_ENV`的值为`'production'`;你不可以改变它的值,这是为了防止开发着误操作,将开发环境的包部署至生产。 + +使用`NODE_ENV`可以方便的执行某些操作: + +例如:根据当前环境确定是否需要进行数据统计 +```javascript + +if (process.env.NODE_ENV !== 'production') { + analytics.disable(); +} + +``` + +你可以访问`HOME_PAGE`,该变量的至为用户在`.hero-cli.json`中配置的`homepage`属性的值,它表示当前页面部署的URL路径。参考[构建路径配置](#构建路径配置). + +#### HTML文件中引用环境变量 + +在HTML文件中,可以是用如下格式访问: + +实例:当存在值为`Welcome Hero`的环境变量`HERO_APP_WEBSITE_NAME`, + +```html +%HERO_APP_WEBSITE_NAME% + +``` +当用户访问该HTML文件时,标题``的值为`Welcome Hero`: + +```html +<title>Welcome Hero + +``` +#### Shell命令中添加环境变量 + +可以在Shell命令中添加环境变量,不同操作系统的声明方式不同。 + +##### Windows (cmd.exe) +``` +set HERO_APP_SECRET_CODE=abcdef&&npm start + +``` + +##### Linux, macOS (Bash) +``` +HERO_APP_SECRET_CODE=abcdef npm start + +``` + +#### `.hero-cli.json`文件中添加环境变量 +部署阶段,环境变量会随着部署环境的不同而不同,如开发环境, 测试环境和生产环境等。你可以通过在文件`.hero-cli.json`中指定相应环境及环境变量的对应关系。 + +示例: + +以下是文件`.hero-cli.json`的内容,指定了`dev`和`prod`环境及环境变量的对应关系 +```json +{ + "environments": { + "dev": "src/environments/environment-dev.js", + "prod": "src/environments/environment-prod.js" + } +} + +``` +以下是文件`src/environments/environment-prod.js`的内容 + +```javascript +var environment = { + backendURL: 'http://www.my-website.com/api' +}; + +module.exports = environment; + +``` +当运行命令`hero start -e dev`或`hero build -e dev`时,文件`src/environments/environment-dev.js`中定义的变量在JavaScript中可以通过`process.env`访问。 + +### 请求代理服务 +项目部署阶段,运维人员经常会把前后端部署在同一域名和端口下,从而避免跨域问题。 + +例如,项目部署后,前后的的访问情况如下: + +``` +/ - 可以访问前端的静态资源,如index.html +/api/todos - URL匹配/api/*的请求,能访问后端的服务API接口 + +``` +这样的线上配置并不是必须的,不过这样配置后,可以使用类似`fetch('/api/v2/todos')`的代码来发起后端服务请求。 + +然而在开发过程中,前端服务与后端服务通常在不同的端口,会出现跨域的问题。为了解决该问题,可以透明地将请求转发至相应的后端服务,从而解决跨域问题。 + +可以通过配置文件`.hero-cli.json`,实现请求代理服务。 + +实例:以下是文件`.hero-cli.json`内容 +```json +{ + "proxy": { + "/api/v2": "https://localhost:4000", + "/feapi": "https://localhost:4001", + }, + "environments": { + "dev": "src/environments/environment-dev.js", + "prod": "src/environments/environment-prod.js" + } +} + +``` +这样配置之后,当代码`fetch('/api/v2/todos')`发送请求时,该请求会被转发至`https://localhost:4000/api/v2/todos`;当代码`fetch('/feapi/todos')`发送请求时,该请求会被转发至`https://localhost:4001/feapi/todos`. + +### 构建原生App安装包 +#### Android +##### Prerequisites + +* [Java](http://www.oracle.com/technetwork/java/javase/overview/index.html) +* [Android SDK](https://developer.android.com/) +* [Gradle](https://gradle.org/) + +##### 配置环境变量 + +* `JAVA_HOME` +* `ANDROID_HOME` +* `GRADLE_HOME` + +当安装使用该工具生成后的App,在启动时会加载一个入口文件,该入口文件包含相应的业务逻辑代码。 + +因此,在生成该App的安装包时,需要进行以下步骤: + +* 使用[`hero build`](#hero-build)命令将业务逻辑代码打包部署 +* 在文件`.hero-cli.json`中指定一个HTML入口地址 +* 使用[`hero platform build`](#hero- platform-build)命令生成App安装包,同时需使用`-e`参数指定使用哪个配置文件 + +示例:  + +以下是文件`.hero-cli.json`的内容 +```json +{ + "android": { + "prod": { + "host": "http://www.my-site.com:3000/mkt/pages/start.html" + } + } +} +``` +接着运行以下命令便可生成Android APK安装文件: + +```sh +hero platform build android -e prod +``` + +该APK文件生成的路径为: `platforms/android/app/build/outputs/apk`. + +查看更多信息,点击查看[构建Android APK安装包](#构建android-apk安装包) + +#### iOS + + +### 构建命令 +以下构建命令可以添加`--verbose`参数输出详细日志。 + +#### `hero start` +该命令会启动开发环境的模式,你可以运行`hero start -h`查看帮助。 +该命令需要一个`-e`参数,指定启动时的配置文件。用法为: `hero start -e `。 + +`-e`参数的值是根据文件`.hero-cli.json`中属性`environments`来确定的。用法在[这里](#hero-clijson文件中添加环境变量)有说明 + +同时,可以使用`-p`参数指定服务启动的端口。 + +```sh +hero start -e dev -p 3000 +``` +启动成功后,队友`src`目录中的改动,代码都会重新编译并且浏览器会重新刷新页面,对有JavaScript代码的ESLint结果会显示在浏览器的控制台中。 + +syntax error terminal + +##### 更多选项 + +* `-e`
指定在启动时加载的配置环境的名称 +* `-s`
指定打包后的文件中只包含业务逻辑代码,不包括[webcomponent polyfills](http://webcomponents.org/polyfills/) 和[hero-js](https://github.com/hero-mobile/hero-js)等组件库。 +* `-i`
指定将JavaScript和CSS内联至HTML文件中。 +* `-b`
指定打包后的文件中只包含[webcomponent polyfills](http://webcomponents.org/polyfills/) 和[hero-js](https://github.com/hero-mobile/hero-js)等组件库。 +* `-m`
不生成sourcemap文件。默认生成sourcemap文件。 +* `-f`
指定生成AppCache文件,默认的文件名称为"app.appcache"。默认情况下不生成该文件。 +* `-n`
指定文件名保留原始名称,不添加hash值。默认是添加hash值。 + +#### `hero build` +使用该命令可以对项目进行打包构建,该命令也需要指定一个必需的参数`-e`,相关的参数同`hero start`。 +你可以运行`hero build -h`查看帮助。 + +##### 构建路径配置 +默认情况下,hero-cli会认为该项目打包构建后生成的文件,在这些文件中,互相引用的路径为绝对路径,并且部署在域名的根路径之下。 +当需要修改该情况,可以通过修改文件`.hero-cli.json`中属性 **homepage** 的值。 + +示例: + +以下是文件`.hero-cli.json`的内容 +```json +{ + "environments": { + "dev": "src/environments/environment-dev.js", + "prod": "src/environments/environment-prod.js" + }, + "homepage": "/mkt/" +} + +``` +这样配置之后,资源在访问是需要加上前缀`/mkt`,比如原先的`/start.html`路径变为`/mkt/pages/start.html`。 + +#### `hero serve` +打使用`hero build`命令打包完成后,在部署之前,可以使用该命令预览构建后的代码运行后的效果。 + +#### `hero init` +使用该命令可以初始化一个Hero App项目工程。示例用法见[如何构建Hero App](#如何构建hero-app). + +#### `hero platform build` +This command used for build native app. And you can run `hero platform build -h` for help.
+##### 构建Android APK安装包 + +`hero platform build android` + +该命令需要指定一个必需的参数`-e `,其中``的取值根据文件`.hero-cli.json`中属性`android`来确定。 + +示例: +以下是文件`.hero-cli.json`的内容 +```json +{ + "android": { + "dev": { + "host": "http://10.0.2.2:3000/mkt/pages/start.html" + }, + "prod": { + "host": "http://www.my-site.com:3000/mkt/pages/start.html" + } + } +} + +``` +那么可选的`env`参数则有`dev`和`prod`。 + +当安装并打开该Native App时,启动时则会加载页面http://www.my-site.com:3000/mkt/pages/start.html。 + +###### How to specify the android build tool version in SDK +hero-cli会自动检测当前开发者环境已安装的Android build tool的所有可用版本,并会默认使用最新的版本。 +当然,你也可以手动的指定在打包时需要使用的各个版本。方法是在命令`hero platform build android`中指定各个版本,不同工具的版本使用分号分割。 + +示例: + +指定`ANDROID_HOME/build-toos`的版本为`23.0.2`,同时指定`com.android.support:appcompat-v7`的版本为`24.0.0-alpha1`,其命令格式如下 + +```sh +hero platform build android:23.0.2:24.0.0-alpha1 -e prod +``` +或者只指定`ANDROID_HOME/build-toos`为`23.0.2`,则其命令格式如下: + +```sh +hero platform build android:23.0.2 -e prod +``` diff --git a/bin/index.js b/bin/index.js index c9906c0..675b649 100755 --- a/bin/index.js +++ b/bin/index.js @@ -1,62 +1,58 @@ #!/usr/bin/env node - -'use strict'; - -var spawn = require('cross-spawn'); -var script = process.argv[2]; -var args = process.argv.slice(3); -var yargs = require('yargs'); -var result = null; +'use strict' +let spawn = require('cross-spawn') +let script = process.argv[2] +let args = process.argv.slice(3) +let yargs = require('yargs') +let result = null function showUsage() { - // var argv = yargs - // .usage('Usage: $0 [options]') - // .usage(['init [project-directory]'], 'Initialize workspace, copy project and other basic configuration') - // // .command(['start [options]'], 'Initialize workspace, copy project and other basic configuration') - // // .command(['build [options]'], 'Initialize workspace, copy project and other basic configuration') - // .example('$0 init my-hero-app') - // .demandOption([]) - // // .epilog('copyright 2017') - // .argv; - - console.log(); - console.log('Usage: ' + yargs.argv.$0 + ' '); - console.log(); - console.log('where is one of:'); - console.log(' init, start, build'); - console.log(); - console.log(yargs.argv.$0 + ' -h\tquick help on '); - console.log(yargs.argv.$0 + ' init\tInitialize workspace, copy project and other basic configuration'); - console.log(yargs.argv.$0 + ' platform\tBuild native package.'); - console.log(yargs.argv.$0 + ' start\tStart the http server'); - console.log(yargs.argv.$0 + ' build\tCreate a build package'); - console.log(); - console.log('See: https://github.com/hero-mobile/hero-cli'); - console.log(); + console.log() + console.log('Usage: ' + yargs.argv.$0 + ' ') + console.log() + console.log('where is one of:') + console.log(' init, start, build') + console.log() + console.log(yargs.argv.$0 + ' -h\tquick help on ') + console.log(yargs.argv.$0 + ' init \t Initialize workspace, copy project and other basic configuration') + console.log(yargs.argv.$0 + ' platform \t Build native package.') + console.log(yargs.argv.$0 + ' dev \t Start the http server') + console.log(yargs.argv.$0 + ' build \t Create a build package') + console.log(yargs.argv.$0 + ' publish \t publish build to IPFS') + console.log() + console.log('See: https://github.com/hero-mobile/hero-cli') + console.log() } + switch (script) { - case 'build': - case 'init': - case 'platform': - case 'serve': - case 'start': - result = spawn.sync('node', [require.resolve('../scripts/' + script)].concat(args), { stdio: 'inherit' }); + case 'dev': + require('../builder/dev-server')() + break + case 'build': + require('../builder/build')() + break + case 'init': + case 'platform': + case 'serve': + case 'start': + case 'publish': + result = spawn.sync('node', [require.resolve('../scripts/' + script)].concat(args), { stdio: 'inherit' }) + + if (result.signal) { + if (result.signal === 'SIGKILL') { + console.log('The build failed because the process exited too early. ' + + 'This probably means the system ran out of memory or someone called ' + + '`kill -9` on the process.') + } else if (result.signal === 'SIGTERM') { + console.log('The build failed because the process exited too early. ' + + 'Someone might have called `kill` or `killall`, or the system could ' + + 'be shutting down.') + } + process.exit(1) + } + process.exit(result.status) - if (result.signal) { - if (result.signal === 'SIGKILL') { - console.log('The build failed because the process exited too early. ' + - 'This probably means the system ran out of memory or someone called ' + - '`kill -9` on the process.'); - } else if (result.signal === 'SIGTERM') { - console.log('The build failed because the process exited too early. ' + - 'Someone might have called `kill` or `killall`, or the system could ' + - 'be shutting down.'); - } - process.exit(1); - } - process.exit(result.status); - break; - default: - showUsage(); - break; + default: + showUsage() + break } diff --git a/builder/build.js b/builder/build.js new file mode 100644 index 0000000..3e18428 --- /dev/null +++ b/builder/build.js @@ -0,0 +1,31 @@ +const webpack = require('webpack') +const rm = require('rimraf') +const ora = require('ora') +const chalk = require('chalk') + +const config = require('./webpack.prod.conf') + +module.exports = function build() { + const spinner = ora('building for production...') + spinner.start() + + rm(config.output.path, err => { + if (err) throw err + + webpack(config, (err, stats) => { + spinner.stop() + if (err) throw err + + process.stdout.write(stats.toString({ + colors: true, + process: true, + modules: false, + children: false, + chunks: false, + chunkModules: false + }) + '\n\n') + + console.log(chalk.cyan(' Build complete.\n')) + }) + }) +} diff --git a/builder/dev-client.js b/builder/dev-client.js new file mode 100644 index 0000000..18aa1e2 --- /dev/null +++ b/builder/dev-client.js @@ -0,0 +1,9 @@ +/* eslint-disable */ +require('eventsource-polyfill') +var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') + +hotClient.subscribe(function (event) { + if (event.action === 'reload') { + window.location.reload() + } +}) diff --git a/builder/dev-server.js b/builder/dev-server.js new file mode 100755 index 0000000..c0c22bc --- /dev/null +++ b/builder/dev-server.js @@ -0,0 +1,63 @@ +process.env.NODE_ENV = 'dev' + +const webpack = require('webpack') +const express = require('express') +const http = require('http') +const proxyMiddleware = require('http-proxy-middleware') +const chalk = require('chalk') + +const devConfig = require('./webpack.dev.conf') +const { getUserConfig, resolve } = require('./utils') + +const { dev } = getUserConfig() +const proxyTable = dev.proxyTable +const publicPath = devConfig.output.publicPath + +function serve() { + const app = express() + const server = http.createServer(app) + + const compiler = webpack(devConfig, (err) => { + if (err) throw err + }) + + const devMiddleware = require('webpack-dev-middleware')(compiler, { + publicPath, + quiet: true + }) + + const hotMiddleware = require('webpack-hot-middleware')(compiler, { + log: false + }) + + Object.keys(proxyTable).forEach((context) => { + let options = proxyTable[context] + if (typeof options === 'string') { + options = { target: options, changeOrigin: true } + } + app.use(proxyMiddleware(options.filter || context, options)) + }) + + compiler.plugin('compilation', (compilation) => { + compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => { + hotMiddleware.publish({ action: 'reload' }) + cb() + }) + }) + + app.use(devMiddleware) + app.use(hotMiddleware) + app.use(publicPath, express.static(resolve('public'))) + + const uri = `http://localhost:${dev.port}${publicPath}` + console.log(chalk.green('> Starting dev server...')) + server.listen(dev.port) + devMiddleware.waitUntilValid( + // force compile to fix '__webpack_require__ ... is not a function' + () => compiler.run( + () => console.log('> Listening at ' + uri + '\n') + ) + ) +} + +module.exports = serve diff --git a/builder/utils.js b/builder/utils.js new file mode 100644 index 0000000..da1fdcd --- /dev/null +++ b/builder/utils.js @@ -0,0 +1,55 @@ +/* eslint-disable standard/computed-property-even-spacing */ +const path = require('path') +const glob = require('glob') + +const { build } = getUserConfig() + +function resolve(dir) { + return path.resolve(process.cwd(), dir) +} + +function getEntries(dir = 'src/pages') { + const entries = {} + const root = resolve(dir) + '/' + const files = glob.sync(root + '**/*.js', { + ignore: [ + '**/view.js', + '**/_*/**', '_*.js' + ].concat(build.ignore || []) + }) + + files.forEach((file) => { + entries[ + file + .replace(root, '') + .replace('/index.js', '') + .replace(/\//g, '_') + .replace('.js', '') + ] = file + }) + + return entries +} + +function mergeEntries(entry, router) { + const reducer = (prev, [key, val]) => { + prev[val] = key + return prev + } + const reverse = source => Object + .entries(source) + .reduce(reducer, {}) + + return reverse(reverse(Object.assign({}, entry, router))) +} + +function getUserConfig() { + return require(resolve('hero.config.js')) +} + +module.exports = { + resolve, + getEntries, + mergeEntries, + getUserConfig +} diff --git a/builder/webpack.base.conf.js b/builder/webpack.base.conf.js new file mode 100644 index 0000000..ee1912c --- /dev/null +++ b/builder/webpack.base.conf.js @@ -0,0 +1,85 @@ +const path = require('path') +const webpack = require('webpack') +const merge = require('webpack-merge') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const HappyPack = require('happypack') +const os = require('os') + +const { getEntries, mergeEntries, getUserConfig, resolve } = require('./utils') +const userConfig = getUserConfig() +const entry = mergeEntries(getEntries('src/pages'), userConfig.router) + +const config = { + context: path.resolve(__dirname, '../'), + entry, + output: { + path: resolve('dist'), + filename: '[name].js' + }, + resolve: { + extensions: ['.js', '.json'], + alias: { + '@': resolve('src') + } + }, + module: { + rules: [ + { + test: /\.js$/, + loader: 'happypack/loader?id=js', + include: [resolve('src'), resolve('test')] + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'file-loader', + options: { + name: 'img/[name].[hash:7].[ext]' + } + } + ] + }, + plugins: [ + new HappyPack({ + id: 'js', + threadPool: HappyPack.ThreadPool({ size: os.cpus().length }), + loaders: [{ + path: 'babel-loader', + query: { + cacheDirectory: 'node_modules/.cache/babel' + } + }] + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'commons', + minChunks: 3 + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks(module) { + return (module.context && module.context.indexOf('node_modules') !== -1) + } + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest' + }) + ] +} + +module.exports = merge(config, { + plugins: Object.keys(config.entry).map((file) => { + const chunks = ['manifest', 'vendor', 'commons', file] + return new HtmlWebpackPlugin({ + chunks, + filename: file + '.html', + template: resolve('index.html'), + inject: true, + NODE_ENV: process.env.NODE_ENV, + minify: { + removeComments: true, + collapseWhitespace: true, + removeAttributeQuotes: true + }, + chunksSortMode: (a, b) => chunks.indexOf(a.names[0]) - chunks.indexOf(b.names[0]) + }) + }) +}) diff --git a/builder/webpack.dev.conf.js b/builder/webpack.dev.conf.js new file mode 100644 index 0000000..47575b2 --- /dev/null +++ b/builder/webpack.dev.conf.js @@ -0,0 +1,45 @@ +const merge = require('webpack-merge') +const webpack = require('webpack') +const path = require('path') +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') + +const { getUserConfig, resolve } = require('./utils') +const baseConfig = require('./webpack.base.conf') +const { dev } = getUserConfig() + +const publicPath = dev.publicPath + +const client = path.resolve(__dirname, './dev-client') +Object.keys(baseConfig.entry).forEach((name) => { + baseConfig.entry[name] = [client].concat(baseConfig.entry[name]) +}) + +module.exports = merge(baseConfig, { + output: { + publicPath + }, + module: { + rules: [ + { + test: /\.(js)$/, + loader: 'eslint-loader', + enforce: 'pre', + query: { + eslintPath: resolve('node_modules/eslint') + }, + include: [resolve('src'), resolve('test')] + } + ] + }, + devtool: '#cheap-module-source-map', + plugins: [ + new webpack.NoEmitOnErrorsPlugin(), + new webpack.HotModuleReplacementPlugin(), + new FriendlyErrorsPlugin(), + new webpack.DefinePlugin({ + 'process.env': JSON.stringify(Object.assign({ + HOME_PAGE: publicPath + }, dev.env)) + }) + ] +}) diff --git a/builder/webpack.prod.conf.js b/builder/webpack.prod.conf.js new file mode 100644 index 0000000..8ce8b89 --- /dev/null +++ b/builder/webpack.prod.conf.js @@ -0,0 +1,58 @@ +const webpack = require('webpack') +const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin') +const merge = require('webpack-merge') +const AppCachePlugin = require('appcache-webpack-plugin') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const CompressionWebpackPlugin = require('compression-webpack-plugin') + +const baseConfig = require('./webpack.base.conf') +const { resolve, getUserConfig } = require('./utils') +const { build } = getUserConfig() +const publicPath = build.publicPath + +module.exports = merge(baseConfig, { + output: { + filename: 'js/[name].[chunkhash:8].js', + chunkFilename: 'js/[name].[chunkhash:8].js' + }, + devtool: '#source-map', + plugins: [ + new webpack.DefinePlugin({ + 'process.env': JSON.stringify(Object.assign({ + HOME_PAGE: publicPath + }, build.env)) + }), + new ParallelUglifyPlugin({ + cacheDir: 'node_modules/.cache/uglify', + sourceMap: build.sourceMap, + uglifyJS: { + output: { + comments: false + }, + compress: { + warnings: false + } + } + }), + new CompressionWebpackPlugin({ + asset: '[path].gz[query]', + algorithm: 'gzip', + test: new RegExp( + '\\.(js)$' + ), + threshold: 10240, + minRatio: 0.8 + }), + new AppCachePlugin({ + output: 'app.appcache', + exclude: build.appCacheExclude + }), + new CopyWebpackPlugin([ + { + from: resolve('public'), + to: resolve('dist'), + ignore: ['.*'] + } + ]) + ] +}) diff --git a/config/entryTemplate.html b/config/entryTemplate.html index d2b0a4c..09ee3f8 100644 --- a/config/entryTemplate.html +++ b/config/entryTemplate.html @@ -2,8 +2,62 @@ - + <%= htmlWebpackPlugin.options.title %> + <% if (htmlWebpackPlugin.files.favicon) { %> + + <% } %> + <% if (htmlWebpackPlugin.options.mobile) { %> + + <% } %> + <% for (var chunk in htmlWebpackPlugin.files.chunks) { %> + + <% } %> + <% for (var css in htmlWebpackPlugin.files.css) { %> + + <% } %> + - + <% if (htmlWebpackPlugin.options.unsupportedBrowser) { %> + +
+ Sorry, your browser is not supported. Please upgrade to + the latest version or switch your browser to use this site. + See outdatedbrowser.com + for options. +
+ <% } %> + <% if (htmlWebpackPlugin.options.appMountId) { %> +
+ <% } %> + <% if (htmlWebpackPlugin.options.appMountIds && htmlWebpackPlugin.options.appMountIds.length > 0) { %> + <% for (var index in htmlWebpackPlugin.options.appMountIds) { %> +
+ <% } %> + <% } %> + <% if (htmlWebpackPlugin.options.window) { %> + + <% } %> + <% if (htmlWebpackPlugin.options.devServer) { %> + + <% } %> + <% if (htmlWebpackPlugin.options.googleAnalytics) { %> + + <% } %> + diff --git a/config/env.js b/config/env.js index ee96cb6..4da2d06 100644 --- a/config/env.js +++ b/config/env.js @@ -1,62 +1,61 @@ -'use strict'; - -var path = require('path'); -var checkRequiredFiles = require('../lib/checkRequiredFiles'); -var chalk = require('chalk'); -var HERO_APP = /^HERO_APP_/i; - -// Warn and crash if required files are missing +'use strict' +let path = require('path') +let checkRequiredFiles = require('../lib/checkRequiredFiles') +let chalk = require('chalk') +let HERO_APP = /^HERO_APP_/i function getClientEnvironment(env, heroFileConfig, homePageConfig) { - var paths = global.paths; - - if (!checkRequiredFiles([global.paths.heroCliConfig])) { - process.exit(1); - } - var config = global.defaultCliConfig; - var environments = heroFileConfig[config.environmentKey]; - - if (!environments || !environments[env]) { - console.log(chalk.red('Unknown Environment "' + env + '".')); - console.log('You have to add attribute ' + chalk.cyan(env) + ' under key ' + chalk.cyan(config.environmentKey) + ' in file ' + chalk.cyan(paths.heroCliConfig)); - console.log(); - console.log('For example:'); - console.log(); - console.log(' ' + chalk.dim('// ...')); - console.log(' ' + chalk.yellow('"environments"') + ': {'); - console.log(' ' + chalk.dim('// ...')); - console.log(' ' + chalk.yellow('"' + env + '"') + ': ' + chalk.yellow('"src/environments/environment-' + env + '.js"')); - console.log(' }'); - console.log(); - process.exit(1); - } - - var configPath = path.resolve(paths.heroCliConfig, '../', heroFileConfig[config.environmentKey][env]); - - // delete require.cache[configPath]; - var heroCustomConfig = require(configPath); - - var raw = Object - .keys(process.env) - .filter(key => HERO_APP.test(key)) - .reduce((rawConfig, key) => { - rawConfig[key] = process.env[key]; - return rawConfig; - }, heroCustomConfig); - - raw.NODE_ENV = process.env.NODE_ENV || 'development'; - // `publicUrl` is just like `publicPath`, but we will provide it to our app - // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. - // Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz. - raw.HOME_PAGE = homePageConfig.publicURL; + let paths = global.paths + + if (!checkRequiredFiles([global.paths.heroCliConfig])) { + process.exit(1) + } + let config = global.defaultCliConfig + let environments = heroFileConfig[config.environmentKey] + + if (!environments || !environments[env]) { + console.log(chalk.red('Unknown Environment "' + env + '".')) + console.log('You have to add attribute ' + chalk.cyan(env) + ' under key ' + chalk.cyan(config.environmentKey) + ' in file ' + chalk.cyan(paths.heroCliConfig)) + console.log() + console.log('For example:') + console.log() + console.log(' ' + chalk.dim('// ...')) + console.log(' ' + chalk.yellow('"environments"') + ': {') + console.log(' ' + chalk.dim('// ...')) + console.log(' ' + chalk.yellow('"' + env + '"') + ': ' + chalk.yellow('"src/environments/environment-' + env + '.js"')) + console.log(' }') + console.log() + process.exit(1) + } else { + global.logger.debug('├── Using Envrionment Config: ' + chalk.yellow(environments[env])) + } + + let configPath = path.resolve(paths.heroCliConfig, '../', heroFileConfig[config.environmentKey][env]) + + // delete require.cache[configPath]; + let heroCustomConfig = require(configPath) + + let raw = Object + .keys(process.env) + .filter(key => HERO_APP.test(key)) + .reduce((rawConfig, key) => { + rawConfig[key] = process.env[key] + return rawConfig + }, heroCustomConfig) + + raw.NODE_ENV = process.env.NODE_ENV || 'development' + // `publicUrl` is just like `publicPath`, but we will provide it to our app + // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. + // Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz. + raw.HOME_PAGE = homePageConfig.publicURL // Stringify all values so we can feed into Webpack DefinePlugin - var stringified = { - 'process.env': JSON.stringify(raw) - }; + let stringified = { + 'process.env': JSON.stringify(raw) + } - return { raw, stringified, configPath }; + return { raw, stringified, configPath } } -module.exports = getClientEnvironment; +module.exports = getClientEnvironment diff --git a/config/getDynamicEntries.js b/config/getDynamicEntries.js index 39c5c9d..b2de906 100644 --- a/config/getDynamicEntries.js +++ b/config/getDynamicEntries.js @@ -1,130 +1,224 @@ -'use strict'; +'use strict' -var path = require('path'); -var HtmlWebpackPlugin = require('html-webpack-plugin'); -var ensureSlash = require('../lib/ensureSlash'); -var getEntries = require('../lib/getEntries'); -var webpackHotDevClientKey = 'web-hot-reload'; -var appIndexKey = 'appIndex'; -var getComponentsData = require('../lib/getComponentsData').getComponentsData; +let path = require('path') +let yargs = require('yargs') +let chalk = require('chalk') +let HtmlWebpackPlugin = require('html-webpack-plugin') +let ensureSlash = require('../lib/ensureSlash') +let getEntries = require('../lib/getEntries') +let webpackHotDevClientKey = 'web-hot-reload' +let appIndexKey = 'appIndex' +let getComponentsData = require('../lib/getComponentsData').getComponentsData +let hasVerbose = yargs.argv.verbose +let firstError = true +let isRelativePath = global.homePageConfigs.isRelativePath function getEntryAndPlugins(isDevelopmentEnv) { - global.entryTemplates = []; - var inlineSourceRegex = global.defaultCliConfig.inlineSourceRegex; - var isStandAlone = global.options.isStandAlone; - var isInlineSource = global.options.isInlineSource; - var isHeroBasic = global.options.isHeroBasic; - var paths = global.paths; - var buildEntries = {}; - - // We ship a few polyfills by default: - var indexOptions, buildPlugins = []; - - if (isHeroBasic) { - // buildEntries[webComponentsKey] = require.resolve('../lib/webcomponents-lite'); - buildEntries[appIndexKey] = paths.appIndexJs; - - indexOptions = { - inject: 'head', - // webconfig html file loader using Polymer HTML - template: '!!html!' + paths.appHtml, - minify: { - removeComments: true, - // collapseWhitespace: true, - // removeRedundantAttributes: true, - useShortDoctype: true - // removeEmptyAttributes: true, - // removeStyleLinkTypeAttributes: true, - // keepClosingSlash: true, - // minifyJS: true, - // minifyCSS: true, - // minifyURLs: true - }, - // chunks: isDevelopmentEnv ? [webComponentsKey, appIndexKey, webpackHotDevClientKey] : [webComponentsKey, appIndexKey] - chunks: isDevelopmentEnv ? [appIndexKey, webpackHotDevClientKey] : [appIndexKey] - }; - - if (isInlineSource) { - indexOptions.inlineSource = inlineSourceRegex; - } - // Generates an `index.html` file with the