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

Commit 4835550

Browse files
committed
feat(bundle): pre and post bundle hooks
pre and post bundle hooks
1 parent d8ecb9e commit 4835550

File tree

6 files changed

+161
-20
lines changed

6 files changed

+161
-20
lines changed

README.md

+65
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ npm run build --rollup ./config/rollup.config.js
114114
| src directory | `ionic_src_dir` | `--srcDir` | `src` | The directory holding the Ionic src code |
115115
| www directory | `ionic_www_dir` | `--wwwDir` | `www` | The deployable directory containing everything needed to run the app |
116116
| build directory | `ionic_build_dir` | `--buildDir` | `build` | The build process uses this directory to store generated files, etc |
117+
| Pre-Bundle hook | `ionic_pre_bundle_hook` | `--preBundleHook` | `null` | Path to file that implements the hook |
118+
| Post-Bundle hook | `ionic_post_bundle_hook` | `--postBundleHook` | `null` | Path to file that implements the hook |
117119

118120

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

134138
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`.
135139

@@ -162,6 +166,67 @@ Example NPM Script:
162166
},
163167
```
164168

169+
## Hooks
170+
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.
171+
172+
For now, two hooks exist: the `pre-bundle` and `post-bundle` hooks.
173+
174+
To get started with a hook, add an entry to the `package.json` config section
175+
176+
```
177+
...
178+
"config": {
179+
"ionic_pre_bundle_hook": "./path/to/some/file.js"
180+
}
181+
...
182+
```
183+
184+
The hook itself has a very simple api
185+
186+
```
187+
module.exports = function(context, isUpdate, changedFiles, configFile) {
188+
return new Promise(function(resolve, reject) {
189+
// do something interesting and resolve the promise
190+
});
191+
}
192+
```
193+
194+
`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.
195+
196+
`isUpdate` is a boolean informing the user whether it is the initial full build (false), or a subsequent, incremental build (true).
197+
198+
`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.
199+
200+
`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.
201+
202+
### Example Hooks
203+
204+
Here is an example of doing string replacement. We're injecting the git commit hash into our application using the `ionic_pre_bundle_hook`.
205+
206+
```
207+
var execSync = require('child_process').execSync;
208+
209+
// the pre-bundle hook happens after the TypeScript code has been transpiled, but before it has been bundled together.
210+
// this means that if you want to modify code, you're going to want to do so on the in-memory javascript
211+
// files instead of the typescript files
212+
213+
module.exports = function(context, isUpdate, changedFiles, configFile) {
214+
return new Promise(function(resolve, reject) {
215+
// get the git hash
216+
var gitHash = execSync('git rev-parse --short HEAD').toString().trim();
217+
// get all files, and loop over them
218+
const files = context.fileCache.getAll();
219+
// find the transpiled javascript file we're looking for
220+
files.forEach(function(file) {
221+
if (file.path.indexOf('about.js') >= 0) {
222+
file.content = file.content.replace('$GIT_COMMIT_HASH', gitHash);
223+
}
224+
});
225+
resolve();
226+
});
227+
}
228+
```
229+
165230
## Tips
166231
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.
167232

src/build.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FILE_CHANGE_EVENT, FILE_DELETE_EVENT } from './util/constants';
22
import { BuildContext, BuildState, BuildUpdateMessage, ChangedFile } from './util/interfaces';
33
import { BuildError } from './util/errors';
4-
import { readFileAsync } from './util/helpers';
4+
import { readFileAsync, setContext } from './util/helpers';
55
import { bundle, bundleUpdate } from './bundle';
66
import { clean } from './clean';
77
import { copy } from './copy';
@@ -19,6 +19,8 @@ import { transpile, transpileUpdate, transpileDiagnosticsOnly } from './transpil
1919
export function build(context: BuildContext) {
2020
context = generateContext(context);
2121

22+
setContext(context);
23+
2224
const logger = new Logger(`build ${(context.isProd ? 'prod' : 'dev')}`);
2325

2426
return buildWorker(context)

src/bundle.ts

+77-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { BuildContext, ChangedFile } from './util/interfaces';
1+
import { BuildContext, ChangedFile, File } from './util/interfaces';
22
import { BuildError, IgnorableError } from './util/errors';
33
import { generateContext, BUNDLER_ROLLUP } from './util/config';
4+
import { Logger } from './logger/logger';
45
import { rollup, rollupUpdate, getRollupConfig, getOutputDest as rollupGetOutputDest } from './rollup';
56
import { webpack, webpackUpdate, getWebpackConfig, getOutputDest as webpackGetOutputDest } from './webpack';
6-
7+
import * as path from 'path';
78

89
export function bundle(context?: BuildContext, configFile?: string) {
910
context = generateContext(context);
@@ -14,26 +15,81 @@ export function bundle(context?: BuildContext, configFile?: string) {
1415
});
1516
}
1617

17-
1818
function bundleWorker(context: BuildContext, configFile: string) {
19-
if (context.bundler === BUNDLER_ROLLUP) {
20-
return rollup(context, configFile);
19+
const isRollup = context.bundler === BUNDLER_ROLLUP;
20+
const config = getConfig(context, isRollup);
21+
22+
return Promise.resolve()
23+
.then(() => {
24+
return getPreBundleHook(context, config, false, null);
25+
})
26+
.then(() => {
27+
if (isRollup) {
28+
return rollup(context, configFile);
29+
}
30+
31+
return webpack(context, configFile);
32+
}).then(() => {
33+
return getPostBundleHook(context, config, false, null);
34+
});
35+
}
36+
37+
function getPreBundleHook(context: BuildContext, config: any, isUpdate: boolean, changedFiles: ChangedFile[]) {
38+
if (process.env.IONIC_PRE_BUNDLE_HOOK) {
39+
return hookInternal(context, process.env.IONIC_PRE_BUNDLE_HOOK, 'pre-bundle hook', config, isUpdate, changedFiles);
2140
}
41+
}
2242

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

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

27-
export function bundleUpdate(changedFiles: ChangedFile[], context: BuildContext) {
28-
if (context.bundler === BUNDLER_ROLLUP) {
29-
return rollupUpdate(changedFiles, context)
30-
.catch(err => {
31-
throw new BuildError(err);
57+
const pathToModule = path.resolve(environmentVariable);
58+
const hookFunction = require(pathToModule);
59+
const logger = new Logger(loggingTitle);
60+
61+
let listOfFiles: File[] = null;
62+
if (changedFiles) {
63+
listOfFiles = changedFiles.map(changedFile => context.fileCache.get(changedFile.filePath));
64+
}
65+
66+
hookFunction(context, isUpdate, listOfFiles, config)
67+
.then(() => {
68+
logger.finish();
69+
resolve();
70+
}).catch((err: Error) => {
71+
reject(logger.fail(err));
3272
});
33-
}
73+
});
74+
}
75+
76+
export function bundleUpdate(changedFiles: ChangedFile[], context: BuildContext) {
77+
const isRollup = context.bundler === BUNDLER_ROLLUP;
78+
const config = getConfig(context, isRollup);
79+
80+
return Promise.resolve()
81+
.then(() => {
82+
return getPreBundleHook(context, config, true, changedFiles);
83+
})
84+
.then(() => {
85+
if (isRollup) {
86+
return rollupUpdate(changedFiles, context);
87+
}
3488

35-
return webpackUpdate(changedFiles, context, null)
36-
.catch(err => {
89+
return webpackUpdate(changedFiles, context, null);
90+
}).then(() => {
91+
return getPostBundleHook(context, config, true, changedFiles);
92+
}).catch(err => {
3793
if (err instanceof IgnorableError) {
3894
throw err;
3995
}
@@ -62,3 +118,10 @@ export function getJsOutputDest(context: BuildContext) {
62118
const webpackConfig = getWebpackConfig(context, null);
63119
return webpackGetOutputDest(context, webpackConfig);
64120
}
121+
122+
function getConfig(context: BuildContext, isRollup: boolean): any {
123+
if (isRollup) {
124+
return getRollupConfig(context, null);
125+
}
126+
return getWebpackConfig(context, null);
127+
}

src/serve.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BuildContext } from './util/interfaces';
22
import { generateContext, getConfigValue, hasConfigValue } from './util/config';
3+
import { setContext } from './util/helpers';
34
import { Logger } from './logger/logger';
45
import { watch } from './watch';
56
import open from './util/open';
@@ -17,6 +18,8 @@ const DEV_SERVER_DEFAULT_HOST = '0.0.0.0';
1718
export function serve(context?: BuildContext) {
1819
context = generateContext(context);
1920

21+
setContext(context);
22+
2023
const config: ServeConfig = {
2124
httpPort: getHttpServerPort(context),
2225
host: getHttpServerHost(context),

src/util/config.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ export function generateContext(context?: BuildContext): BuildContext {
4747
const sourceMapValue = getConfigValue(context, '--sourceMap', null, ENV_VAR_SOURCE_MAP, ENV_VAR_SOURCE_MAP.toLowerCase(), 'eval');
4848
setProcessEnvVar(ENV_VAR_SOURCE_MAP, sourceMapValue);
4949

50+
const preBundleHook = getConfigValue(context, '--preBundleHook', null, ENV_VAR_PRE_BUNDLE_HOOK, ENV_VAR_PRE_BUNDLE_HOOK.toLowerCase(), null);
51+
if (preBundleHook && preBundleHook.length) {
52+
setProcessEnvVar(ENV_VAR_PRE_BUNDLE_HOOK, preBundleHook);
53+
}
54+
55+
const postBundleHook = getConfigValue(context, '--postBundleHook', null, ENV_VAR_POST_BUNDLE_HOOK, ENV_VAR_POST_BUNDLE_HOOK.toLowerCase(), null);
56+
if (postBundleHook && postBundleHook.length) {
57+
setProcessEnvVar(ENV_VAR_POST_BUNDLE_HOOK, postBundleHook);
58+
}
59+
5060
if (!isValidBundler(context.bundler)) {
5161
context.bundler = bundlerStrategy(context);
5262
}
@@ -125,7 +135,6 @@ export function fillConfigDefaults(userConfigFile: string, defaultConfigFile: st
125135
}
126136

127137
const defaultConfig = require(join('..', '..', 'config', defaultConfigFile));
128-
129138
// create a fresh copy of the config each time
130139
// always assign any default values which were not already supplied by the user
131140
return objectAssign({}, defaultConfig, userConfig);
@@ -390,6 +399,8 @@ const ENV_VAR_WWW_DIR = 'IONIC_WWW_DIR';
390399
const ENV_VAR_BUILD_DIR = 'IONIC_BUILD_DIR';
391400
const ENV_VAR_APP_SCRIPTS_DIR = 'IONIC_APP_SCRIPTS_DIR';
392401
const ENV_VAR_SOURCE_MAP = 'IONIC_SOURCE_MAP';
402+
const ENV_VAR_PRE_BUNDLE_HOOK = 'IONIC_PRE_BUNDLE_HOOK';
403+
const ENV_VAR_POST_BUNDLE_HOOK = 'IONIC_POST_BUNDLE_HOOK';
393404

394405
export const BUNDLER_ROLLUP = 'rollup';
395406
export const BUNDLER_WEBPACK = 'webpack';

src/webpack.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { FileCache } from './util/file-cache';
2-
import { BuildContext, BuildState, ChangedFile, File, TaskInfo } from './util/interfaces';
1+
import { BuildContext, BuildState, ChangedFile, TaskInfo } from './util/interfaces';
32
import { BuildError, IgnorableError } from './util/errors';
4-
import { changeExtension, readFileAsync, setContext } from './util/helpers';
53
import { emit, EventType } from './util/events';
64
import { join } from 'path';
75
import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config';
@@ -30,7 +28,6 @@ export function webpack(context: BuildContext, configFile: string) {
3028
configFile = getUserConfigFile(context, taskInfo, configFile);
3129

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

3633
return webpackWorker(context, configFile)

0 commit comments

Comments
 (0)