Skip to content
This repository has been archived by the owner on May 1, 2020. It is now read-only.

Commit

Permalink
feat(bundle): pre and post bundle hooks
Browse files Browse the repository at this point in the history
pre and post bundle hooks
  • Loading branch information
danbucholtz committed Nov 21, 2016
1 parent d8ecb9e commit 4835550
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 20 deletions.
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ npm run build --rollup ./config/rollup.config.js
| src directory | `ionic_src_dir` | `--srcDir` | `src` | The directory holding the Ionic src code |
| www directory | `ionic_www_dir` | `--wwwDir` | `www` | The deployable directory containing everything needed to run the app |
| build directory | `ionic_build_dir` | `--buildDir` | `build` | The build process uses this directory to store generated files, etc |
| Pre-Bundle hook | `ionic_pre_bundle_hook` | `--preBundleHook` | `null` | Path to file that implements the hook |
| Post-Bundle hook | `ionic_post_bundle_hook` | `--postBundleHook` | `null` | Path to file that implements the hook |


### Ionic Environment Variables
Expand All @@ -130,6 +132,8 @@ These environment variables are automatically set to [Node's `process.env`](http
| `IONIC_BUILD_DIR` | The absolute path to the app's bundled js and css files. |
| `IONIC_APP_SCRIPTS_DIR` | The absolute path to the `@ionic/app-scripts` node_module directory. |
| `IONIC_SOURCE_MAP` | The Webpack `devtool` setting. We recommend `eval` or `source-map`. |
| `IONIC_PRE_BUNDLE_HOOK` | The absolute path to the file that implements the hook. |
| `IONIC_POST_BUNDLE_HOOK`| The absolute path to the file that implements the hook. |

The `process.env.IONIC_ENV` environment variable can be used to test whether it is a `prod` or `dev` build, which automatically gets set by any command. By default the `build` task is `prod`, and the `watch` and `serve` tasks are `dev`. Additionally, using the `--dev` command line flag will force the build to use `dev`.

Expand Down Expand Up @@ -162,6 +166,67 @@ Example NPM Script:
},
```

## Hooks
Injecting dynamic data into the build is accomplished via hooks. Hooks are functions for performing actions like string replacements. Hooks *must* return a `Promise` or the build process will not work correctly.

For now, two hooks exist: the `pre-bundle` and `post-bundle` hooks.

To get started with a hook, add an entry to the `package.json` config section

```
...
"config": {
"ionic_pre_bundle_hook": "./path/to/some/file.js"
}
...
```

The hook itself has a very simple api

```
module.exports = function(context, isUpdate, changedFiles, configFile) {
return new Promise(function(resolve, reject) {
// do something interesting and resolve the promise
});
}
```

`context` is the app-scripts [BuildContext](https://github.com/driftyco/ionic-app-scripts/blob/master/src/util/interfaces.ts#L4-L24) object. It contains all of the paths and information about the application.

`isUpdate` is a boolean informing the user whether it is the initial full build (false), or a subsequent, incremental build (true).

`changedFiles` is a list of [File](https://github.com/driftyco/ionic-app-scripts/blob/master/src/util/interfaces.ts#L61-L65) objects for any files that changed as part of an update. `changedFiles` is null for full builds, and a populated list when `isUpdate` is true.

`configFile` is the config file corresponding to the hook's utility. For example, it could be the rollup config file, or the webpack config file depending on what the value of `ionic_bundler` is set to.

### Example Hooks

Here is an example of doing string replacement. We're injecting the git commit hash into our application using the `ionic_pre_bundle_hook`.

```
var execSync = require('child_process').execSync;
// the pre-bundle hook happens after the TypeScript code has been transpiled, but before it has been bundled together.
// this means that if you want to modify code, you're going to want to do so on the in-memory javascript
// files instead of the typescript files
module.exports = function(context, isUpdate, changedFiles, configFile) {
return new Promise(function(resolve, reject) {
// get the git hash
var gitHash = execSync('git rev-parse --short HEAD').toString().trim();
// get all files, and loop over them
const files = context.fileCache.getAll();
// find the transpiled javascript file we're looking for
files.forEach(function(file) {
if (file.path.indexOf('about.js') >= 0) {

This comment has been minimized.

Copy link
@biesbjerg

biesbjerg Nov 22, 2016

This also matches "other-about.js'.

Might want to do '!== -1' instead.

file.content = file.content.replace('$GIT_COMMIT_HASH', gitHash);
}
});
resolve();
});
}
```

## Tips
1. The Webpack `devtool` setting is driven by the `ionic_source_map` variable. It defaults to `eval` for fast builds, but can provide the original source map by changing the value to `source-map`. There are additional values that Webpack supports, but we only support `eval` and `source-maps` for now.

Expand Down
4 changes: 3 additions & 1 deletion src/build.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FILE_CHANGE_EVENT, FILE_DELETE_EVENT } from './util/constants';
import { BuildContext, BuildState, BuildUpdateMessage, ChangedFile } from './util/interfaces';
import { BuildError } from './util/errors';
import { readFileAsync } from './util/helpers';
import { readFileAsync, setContext } from './util/helpers';
import { bundle, bundleUpdate } from './bundle';
import { clean } from './clean';
import { copy } from './copy';
Expand All @@ -19,6 +19,8 @@ import { transpile, transpileUpdate, transpileDiagnosticsOnly } from './transpil
export function build(context: BuildContext) {
context = generateContext(context);

setContext(context);

const logger = new Logger(`build ${(context.isProd ? 'prod' : 'dev')}`);

return buildWorker(context)
Expand Down
91 changes: 77 additions & 14 deletions src/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { BuildContext, ChangedFile } from './util/interfaces';
import { BuildContext, ChangedFile, File } from './util/interfaces';
import { BuildError, IgnorableError } from './util/errors';
import { generateContext, BUNDLER_ROLLUP } from './util/config';
import { Logger } from './logger/logger';
import { rollup, rollupUpdate, getRollupConfig, getOutputDest as rollupGetOutputDest } from './rollup';
import { webpack, webpackUpdate, getWebpackConfig, getOutputDest as webpackGetOutputDest } from './webpack';

import * as path from 'path';

export function bundle(context?: BuildContext, configFile?: string) {
context = generateContext(context);
Expand All @@ -14,26 +15,81 @@ export function bundle(context?: BuildContext, configFile?: string) {
});
}


function bundleWorker(context: BuildContext, configFile: string) {
if (context.bundler === BUNDLER_ROLLUP) {
return rollup(context, configFile);
const isRollup = context.bundler === BUNDLER_ROLLUP;
const config = getConfig(context, isRollup);

return Promise.resolve()
.then(() => {
return getPreBundleHook(context, config, false, null);
})
.then(() => {
if (isRollup) {
return rollup(context, configFile);
}

return webpack(context, configFile);
}).then(() => {
return getPostBundleHook(context, config, false, null);
});
}

function getPreBundleHook(context: BuildContext, config: any, isUpdate: boolean, changedFiles: ChangedFile[]) {
if (process.env.IONIC_PRE_BUNDLE_HOOK) {
return hookInternal(context, process.env.IONIC_PRE_BUNDLE_HOOK, 'pre-bundle hook', config, isUpdate, changedFiles);
}
}

return webpack(context, configFile);
function getPostBundleHook(context: BuildContext, config: any, isUpdate: boolean, changedFiles: ChangedFile[]) {
if (process.env.IONIC_POST_BUNDLE_HOOK) {
return hookInternal(context, process.env.IONIC_POST_BUNDLE_HOOK, 'post-bundle hook', config, isUpdate, changedFiles);
}
}

function hookInternal(context: BuildContext, environmentVariable: string, loggingTitle: string, config: any, isUpdate: boolean, changedFiles: ChangedFile[]) {
return new Promise((resolve, reject) => {
if (! environmentVariable || environmentVariable.length === 0) {
// there isn't a hook, so just resolve right away
resolve();
return;
}

export function bundleUpdate(changedFiles: ChangedFile[], context: BuildContext) {
if (context.bundler === BUNDLER_ROLLUP) {
return rollupUpdate(changedFiles, context)
.catch(err => {
throw new BuildError(err);
const pathToModule = path.resolve(environmentVariable);
const hookFunction = require(pathToModule);
const logger = new Logger(loggingTitle);

let listOfFiles: File[] = null;
if (changedFiles) {
listOfFiles = changedFiles.map(changedFile => context.fileCache.get(changedFile.filePath));
}

hookFunction(context, isUpdate, listOfFiles, config)
.then(() => {
logger.finish();
resolve();
}).catch((err: Error) => {
reject(logger.fail(err));
});
}
});
}

export function bundleUpdate(changedFiles: ChangedFile[], context: BuildContext) {
const isRollup = context.bundler === BUNDLER_ROLLUP;
const config = getConfig(context, isRollup);

return Promise.resolve()
.then(() => {
return getPreBundleHook(context, config, true, changedFiles);
})
.then(() => {
if (isRollup) {
return rollupUpdate(changedFiles, context);
}

return webpackUpdate(changedFiles, context, null)
.catch(err => {
return webpackUpdate(changedFiles, context, null);
}).then(() => {
return getPostBundleHook(context, config, true, changedFiles);
}).catch(err => {
if (err instanceof IgnorableError) {
throw err;
}
Expand Down Expand Up @@ -62,3 +118,10 @@ export function getJsOutputDest(context: BuildContext) {
const webpackConfig = getWebpackConfig(context, null);
return webpackGetOutputDest(context, webpackConfig);
}

function getConfig(context: BuildContext, isRollup: boolean): any {
if (isRollup) {
return getRollupConfig(context, null);
}
return getWebpackConfig(context, null);
}
3 changes: 3 additions & 0 deletions src/serve.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BuildContext } from './util/interfaces';
import { generateContext, getConfigValue, hasConfigValue } from './util/config';
import { setContext } from './util/helpers';
import { Logger } from './logger/logger';
import { watch } from './watch';
import open from './util/open';
Expand All @@ -17,6 +18,8 @@ const DEV_SERVER_DEFAULT_HOST = '0.0.0.0';
export function serve(context?: BuildContext) {
context = generateContext(context);

setContext(context);

const config: ServeConfig = {
httpPort: getHttpServerPort(context),
host: getHttpServerHost(context),
Expand Down
13 changes: 12 additions & 1 deletion src/util/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ export function generateContext(context?: BuildContext): BuildContext {
const sourceMapValue = getConfigValue(context, '--sourceMap', null, ENV_VAR_SOURCE_MAP, ENV_VAR_SOURCE_MAP.toLowerCase(), 'eval');
setProcessEnvVar(ENV_VAR_SOURCE_MAP, sourceMapValue);

const preBundleHook = getConfigValue(context, '--preBundleHook', null, ENV_VAR_PRE_BUNDLE_HOOK, ENV_VAR_PRE_BUNDLE_HOOK.toLowerCase(), null);
if (preBundleHook && preBundleHook.length) {
setProcessEnvVar(ENV_VAR_PRE_BUNDLE_HOOK, preBundleHook);
}

const postBundleHook = getConfigValue(context, '--postBundleHook', null, ENV_VAR_POST_BUNDLE_HOOK, ENV_VAR_POST_BUNDLE_HOOK.toLowerCase(), null);
if (postBundleHook && postBundleHook.length) {
setProcessEnvVar(ENV_VAR_POST_BUNDLE_HOOK, postBundleHook);
}

if (!isValidBundler(context.bundler)) {
context.bundler = bundlerStrategy(context);
}
Expand Down Expand Up @@ -125,7 +135,6 @@ export function fillConfigDefaults(userConfigFile: string, defaultConfigFile: st
}

const defaultConfig = require(join('..', '..', 'config', defaultConfigFile));

// create a fresh copy of the config each time
// always assign any default values which were not already supplied by the user
return objectAssign({}, defaultConfig, userConfig);
Expand Down Expand Up @@ -390,6 +399,8 @@ const ENV_VAR_WWW_DIR = 'IONIC_WWW_DIR';
const ENV_VAR_BUILD_DIR = 'IONIC_BUILD_DIR';
const ENV_VAR_APP_SCRIPTS_DIR = 'IONIC_APP_SCRIPTS_DIR';
const ENV_VAR_SOURCE_MAP = 'IONIC_SOURCE_MAP';
const ENV_VAR_PRE_BUNDLE_HOOK = 'IONIC_PRE_BUNDLE_HOOK';
const ENV_VAR_POST_BUNDLE_HOOK = 'IONIC_POST_BUNDLE_HOOK';

export const BUNDLER_ROLLUP = 'rollup';
export const BUNDLER_WEBPACK = 'webpack';
5 changes: 1 addition & 4 deletions src/webpack.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { FileCache } from './util/file-cache';
import { BuildContext, BuildState, ChangedFile, File, TaskInfo } from './util/interfaces';
import { BuildContext, BuildState, ChangedFile, TaskInfo } from './util/interfaces';
import { BuildError, IgnorableError } from './util/errors';
import { changeExtension, readFileAsync, setContext } from './util/helpers';
import { emit, EventType } from './util/events';
import { join } from 'path';
import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config';
Expand Down Expand Up @@ -30,7 +28,6 @@ export function webpack(context: BuildContext, configFile: string) {
configFile = getUserConfigFile(context, taskInfo, configFile);

Logger.debug('Webpack: Setting Context on shared singleton');
setContext(context);
const logger = new Logger('webpack');

return webpackWorker(context, configFile)
Expand Down

0 comments on commit 4835550

Please sign in to comment.