src/plugin.js
/**
* It updates targets Babel configuration in order to add support for AngularJS annotations.
*/
class ProjextAngularJSPlugin {
/**
* Class constructor.
*/
constructor() {
/**
* The name of the reducer event the service will listen for in order to exclude AngularJS
* packages from the bundle when the target is a library.
* @type {string}
* @access protected
* @ignore
*/
this._externalSettingsEventName = 'webpack-externals-configuration-for-browser';
/**
* The list of AngularJS packages that should never end up on the bundle if the target is a
* library.
* @type {Array}
* @access protected
* @ignore
*/
this._externalModules = ['angular'];
/**
* The name of the reducer event the service will listen for in order to add support for
* AngularJS annotations.
* @type {string}
* @access protected
* @ignore
*/
this._babelConfigurationEvent = 'babel-configuration';
/**
* The list of Babel plugins that need to be added in order to add support for AngularJS
* annotations.
* @type {Array}
* @access protected
* @ignore
*/
this._babelPlugin = ['angularjs-annotate', { explicitOnly: true }];
/**
* The list of transformations the AngularJS annotations plugin needs in order to work. The
* plugin only works with standard `function` statements, so it's necessary for Babel to
* transpile the following things into `function`s so the annotations can be injected.
* @type {Array}
* @access protected
* @ignore
*/
this._babelRequiredEnvFeatures = [
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-classes',
'@babel/plugin-transform-parameters',
];
/**
* The name of the reducer event the service uses to intercept a browser target default HTML
* file settings.
* @type {string}
* @access protected
* @ignore
*/
this._htmlSettingsEventName = 'target-default-html-settings';
/**
* The required value a target `framework` setting needs to have in order for the service to
* take action.
* @type {string}
* @access protected
* @ignore
*/
this._frameworkProperty = 'angularjs';
/**
* The default values for the options a target can use to customize the default HTML projext
* generates.
* @type {Object}
* @property {?string} title A custom value for the `<title />` tag. If the target
* doesn't define it, the plugin will use the one projext
* sets by default (The name of the target).
* @property {?string} appName The value of the `ng-app` attribute. If the target
* doesn't define it, the plugin will convert the name of
* the target to `lowerCamelCase` and use that instead.
* @property {boolean} strict Whether the app tag should include the `ng-strict-di`
* directive or not.
* @property {boolean} cloak Whether the app tag should include the `ng-cloak`
* directive or not.
* @property {boolean} useBody Whether or not the `body` should be used as the app tag
* (`ng-app`).
* @property {?string} mainComponent The name of a component that should be added inside the
* app tag.
* @access protected
* @ignore
*/
this._frameworkOptions = {
title: null,
appName: null,
strict: true,
cloak: true,
useBody: true,
mainComponent: 'main',
};
}
/**
* This is the method called when the plugin is loaded by projext. It setups all the listeners
* for the events the plugin needs to intercept in order to:
* 1. Add support for AngularJS annotations.
* 2. Exclude AngularJS packages from the bundle when the target is a library.
* 3. Generate the settings for a target default HTML.
* @param {Projext} app The projext main container.
*/
register(app) {
// Get the `events` service to listen for the events.
const events = app.get('events');
// Get the `babelHelper` to send to the method that adds support for AngularJS annotations.
const babelHelper = app.get('babelHelper');
// Add the listener for the target Babel configuration.
events.on(this._babelConfigurationEvent, (configuration, target) => (
this._updateBabelConfiguration(configuration, target, babelHelper)
));
// Add the listener for the default HTML settings.
events.on(this._htmlSettingsEventName, (settings, target) => (
this._updateHTMLSettings(settings, target)
));
// Add the listener for the event that updates the external dependencies.
events.on(this._externalSettingsEventName, (externals, params) => (
this._updateExternals(externals, params.target)
));
}
/**
* This method gets called when projext reduces a target Babel configuration. The method will
* validate the target settings and add the Babel plugin needed for AngularJS annotations.
* @param {Object} currentConfiguration The current Babel configuration for the target.
* @param {Target} target The target information.
* @param {BabelHelper} babelHelper To update the target configuration and add the
* required preset and plugin.
* @return {Object} The updated configuration.
* @access protected
* @ignore
*/
_updateBabelConfiguration(currentConfiguration, target, babelHelper) {
let updatedConfiguration;
if (target.framework === this._frameworkProperty) {
updatedConfiguration = babelHelper.addEnvPresetFeature(
currentConfiguration,
this._babelRequiredEnvFeatures
);
updatedConfiguration = babelHelper.addPlugin(
updatedConfiguration,
this._babelPlugin
);
} else {
updatedConfiguration = currentConfiguration;
}
return updatedConfiguration;
}
/**
* Read the settings projext is using to build browser target default HTML file and update them
* based on the framework options defined by the target in order to run an AngularJS app.
* @param {TargetDefaultHTMLSettings} currentSettings The settings projext uses to build a target
* default HTML file.
* @param {Target} target The target information.
* @return {TargetDefaultHTMLSettings}
* @access protected
* @ignore
*/
_updateHTMLSettings(currentSettings, target) {
let updatedSettings;
// If the target has a valid type and the right `framework`...
if (target.is.browser && target.framework === this._frameworkProperty) {
// ...copy the list of rules.
updatedSettings = Object.assign({}, currentSettings);
// Get a lowerCamelCase name for the AngularJS app by parsing the target name.
const appName = target.name.replace(/-(\w)/ig, (match, letter) => letter.toUpperCase());
// Merge the default options with any overwrite the target may have.
const options = Object.assign(
{},
this._frameworkOptions,
{ appName },
(target.frameworkOptions || {})
);
// If there's a custom title on the options, set it.
if (options.title) {
updatedSettings.title = options.title;
}
// Define the attributes list of the app tag.
const attributesList = [`ng-app="${options.appName}"`];
// - Check if the app will run with strict mode.
if (options.strict) {
attributesList.push('ng-strict-di');
}
// - Check if the app should hide the template while rendering.
if (options.cloak) {
attributesList.push('ng-cloak');
}
// Format the attributes list into a string.
const attributes = attributesList.join(' ');
/**
* If a main component was defined, generate an opening and closing tag for it, otherwise just
* keep it as an empty string.
*/
const mainComponent = options.mainComponent ?
`<${options.mainComponent}></${options.mainComponent}>` :
'';
// If the app tag should be the `body`...
if (options.useBody) {
// ...set the app tag attributes to the `body`.
updatedSettings.bodyAttributes = attributes;
// Set the main component as the contents of the `body`.
updatedSettings.bodyContents = mainComponent;
} else {
/**
* ...otherwise, create `div` with the app tag attributes, with the main component inside it
* and set it as the content of the `body`.
*/
updatedSettings.bodyContents = `<div id="app" ${attributes}>${mainComponent}</div>`;
}
} else {
// ...otherwise, just set to return the received settings.
updatedSettings = currentSettings;
}
// Return the updated settings.
return updatedSettings;
}
/**
* This method gets called when the webpack plugin reduces the list of modules that should be
* handled as external dependencies. The method validates the target settings and if it's a
* Node target or a browser library, it pushes the AngularJS packages to the list.
* @param {Object} currentExternals A dictionary of external dependencies with the format
* webpack uses: `{ 'module': 'commonjs module'}`.
* @param {Target} target The target information.
* @return {Object} The updated externals dictionary.
* @access protected
* @ignore
*/
_updateExternals(currentExternals, target) {
let updatedExternals;
if (target.framework === this._frameworkProperty && target.library) {
updatedExternals = Object.assign({}, currentExternals);
this._externalModules.forEach((name) => {
updatedExternals[name] = `commonjs ${name}`;
});
} else {
updatedExternals = currentExternals;
}
return updatedExternals;
}
}
module.exports = ProjextAngularJSPlugin;