diff --git a/README.md b/README.md index 91c07b41..d17aad8b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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`. @@ -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) { + 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. diff --git a/src/build.ts b/src/build.ts index 12a16b5a..6ce4c1c7 100644 --- a/src/build.ts +++ b/src/build.ts @@ -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'; @@ -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) diff --git a/src/bundle.ts b/src/bundle.ts index 6cb45c06..774c03ed 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -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); @@ -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; } @@ -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); +} diff --git a/src/serve.ts b/src/serve.ts index e79275cc..700459bc 100644 --- a/src/serve.ts +++ b/src/serve.ts @@ -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'; @@ -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), diff --git a/src/util/config.ts b/src/util/config.ts index e8e6366a..2e89ae47 100644 --- a/src/util/config.ts +++ b/src/util/config.ts @@ -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); } @@ -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); @@ -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'; diff --git a/src/webpack.ts b/src/webpack.ts index 4e6e18c2..7ffd0084 100644 --- a/src/webpack.ts +++ b/src/webpack.ts @@ -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'; @@ -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)