@interaktiv/mibuilder-core
Version:
Core libraries to interact with MiBuilder projects.
456 lines (387 loc) • 16.7 kB
JavaScript
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.MiBuilderProject = exports.MiBuilderProjectJson = exports.MIBUILDER_PROJECT_TYPE = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _path = _interopRequireDefault(require("path"));
var _types = require("@interaktiv/types");
var _dxl = require("@interaktiv/dxl");
var _uuid = require("uuid");
var _configAggregator = require("./config/config-aggregator");
var _constants = require("./config/constants");
var _configFile = require("./config/config-file");
var _resolveConfigPath = require("./config/resolve-config-path");
var deliver = _interopRequireWildcard(require("./fastlane/deliver"));
var _constants2 = require("./util/constants");
var _mibuilderError = require("./mibuilder-error");
var _tiapp = require("./tiapp");
/**
* Returns the first key within the object that has an upper case first letter.
*
* @param {Object} data The object in which to check key casing.
* @param {Object} sectionBlacklist properties in the object to exclude from the search. e.g. a blacklist of `["a"]` and data of `{ "a": { "B" : "b"}}` would ignore `B` because it is in the object value under `a`.
* @return {String} The first upper case key
*/
function findUpperCaseKeys(data, sectionBlacklist = []) {
let key;
(0, _dxl.findKey)(data, (val, k) => {
if (k[0] === k[0].toUpperCase()) {
key = k;
} else if ((0, _types.isJsonMap)(val)) {
if (sectionBlacklist.includes(k)) {
return key;
}
key = findUpperCaseKeys((0, _types.asJsonMap)(val));
}
return key;
});
return key;
}
const MIBUILDER_PROJECT_TYPE = {
RA: 'Rechtsanwalt',
STB: 'Steuerberater'
};
/**
* The mibuilder-project.json config object. This file determines if a folder
* is a valid mibuilder project.
*
* *Note:* Any non-standard properties stored in
* mibuilder-project.json should be in a top level property that represents your
* project or plugin.
*
* ```
* const project = await MiBuilderProjectJson.retrieve();
* const myPluginProperties = project.get('myplugin') || {};
*
* myPluginProperties.myprop = 'someValue';
*
* project.set('myplugin', myPluginProperties);
*
* await project.write();
* ```
*/
exports.MIBUILDER_PROJECT_TYPE = MIBUILDER_PROJECT_TYPE;
class MiBuilderProjectJson extends _configFile.ConfigFile {
static getFileName() {
return _constants.MIBUILDER_PROJECT_JSON;
}
static getDefaultOptions(isGlobal = false) {
const options = _configFile.ConfigFile.getDefaultOptions(isGlobal, MiBuilderProjectJson.getFileName());
options.isState = false;
return options;
}
static getDefaultContents() {
throw new _mibuilderError.MiBuilderError('No default contents for project config file');
}
static async resolveRootDir(isGlobal) {
if ((0, _types.isBoolean)(isGlobal) === false) {
throw new _mibuilderError.MiBuilderError('isGlobal must be a boolean', 'InvalidTypeForIsGlobal');
}
const configPath = await (0, _resolveConfigPath.resolveConfigPath)(isGlobal ? _constants.MIBUILDER_PROJECT_JSON_DIR : '', process.cwd());
return _path.default.dirname(configPath);
}
async read() {
const contents = await super.read(); // Verify that the configObject does not have upper case keys; throw if it
// does. Must be heads down camel case.
const upperCaseKey = findUpperCaseKeys(this.toObject(), MiBuilderProjectJson.BLACKLIST);
if ((0, _types.isString)(upperCaseKey)) {
throw new _mibuilderError.MiBuilderError(`Die ${_constants.MIBUILDER_PROJECT_JSON} Datei enhält nicht das korrekte Format`, 'InvalidJsonCasing', ['Hast du `mibuilder migrate` bereits ausgeführt?', `Oder die ${_constants.MIBUILDER_PROJECT_JSON_LEGACY} Datei manuell in ${_constants.MIBUILDER_PROJECT_JSON} umbenannt?`, `Dann benenne die ${_constants.MIBUILDER_PROJECT_JSON} Datei wieder zurück zu ${_constants.MIBUILDER_PROJECT_JSON_LEGACY} um und führe den Befehl \`mibuilder migrate\` aus`]);
}
return contents;
}
write(newContents = this.getContents()) {
// Verify that the configObject does not have upper case keys; throw if it does. Must be heads down camel case.
const upperCaseKey = findUpperCaseKeys(newContents, MiBuilderProjectJson.BLACKLIST);
if (upperCaseKey) throw _mibuilderError.MiBuilderError.wrap('InvalidJsonCasing'); // For now only write default properties omitting all other ones
const defaultProperties = MiBuilderProjectJson.getDefaultProperties();
const defaultPropertiesPaths = defaultProperties.map(def => `${def.group}.${def.key}`);
const reducedContents = defaultPropertiesPaths // Get entries with [path,value]
.map(p => [p, (0, _types.get)(newContents, p)]) // Filter out ones that are there
.filter(([, value]) => Boolean(value)) // Reduced to result obj
.reduce((result, [p, value]) => {
(0, _dxl.set)(result, p, value);
return result;
}, {});
return super.write(reducedContents);
}
getDefaultOptions(options = {}) {
return (0, _extends2.default)({
isState: false
}, options);
}
async init() {
await super.init();
if (!MiBuilderProjectJson.defaultProperties) {
MiBuilderProjectJson.defaultProperties = [// App
{
group: 'app',
key: 'name'
}, {
group: 'app',
key: 'bundleIdentifier'
}, {
group: 'app',
key: 'packageName'
}, {
group: 'app',
key: 'companyUrl'
}, {
group: 'app',
key: 'copyright'
}, {
group: 'app',
key: 'description'
}, {
group: 'app',
key: 'guid'
}, {
group: 'app',
key: 'projectName'
}, {
group: 'app',
key: 'publisher'
}, {
group: 'app',
key: 'servicecode'
}, // Appc
{
group: 'appc',
key: 'appcAppId'
}, // Push
{
group: 'push',
key: 'appKeyDevelopment'
}, {
group: 'push',
key: 'appSecretDevelopment'
}, {
group: 'push',
key: 'appKeyProduction'
}, {
group: 'push',
key: 'appSecretProduction'
}, {
group: 'push',
key: 'gcmSenderId'
}, // Launch screens
{
group: 'launchScreen',
key: 'color'
}, // - iOS
{
group: 'ios',
key: 'appleId'
}, {
group: 'ios',
key: 'teamId'
}, {
group: 'ios',
key: 'ascTeamId'
}];
}
}
static getDefaultProperties() {
if (!MiBuilderProjectJson.defaultProperties) {
throw new _mibuilderError.MiBuilderError('MiBuilderProjectJson meta information has not been initialized.', null, ['Use MiBuilderProjectJson.create()']);
}
return MiBuilderProjectJson.defaultProperties;
}
}
exports.MiBuilderProjectJson = MiBuilderProjectJson;
MiBuilderProjectJson.BLACKLIST = [''];
/**
* Represents an MiBuilder project directory. This directory contains a
* {@link MiBuilderProjectJson} config file as well as a hidden .mibuilder
* folder that contains all the other local project config files.
*
* ```
* const project = await MiBuilderProject.resolve();
* const projectJson = await project.resolveProjectConfig();
*
* console.log(projectJson.sfdcLoginUrl);
* ```
*/
class MiBuilderProject {
/**
* Do not directly construct instances of this class
* -- use {@link MiBuilderProject.resolve} instead.
*
* @ignore
* @param {String} config The path to the project config
*/
constructor(config) {
this.configPath = config;
}
/**
* Get a Project from the given config path or the working directory.
*
* **Throws** *{@link MiBuilderError}{ name: 'InvalidProjectFileWorkspace' }* If the current folder is not located in a workspace.
*
* @param {String} [rootOrConfig] Either a direct path to the config or a path to the directory containing the config.
* @return {Object<MiBuilderProject>} The MiBuilderProject instance
*/
static async resolve(rootOrConfig = _constants.MIBUILDER_PROJECT_JSON_DIR) {
return new MiBuilderProject(await this.resolveConfigPath(rootOrConfig));
}
static resolveConfigPath(rootOrConfig) {
return (0, _resolveConfigPath.resolveConfigPath)(rootOrConfig, process.cwd());
}
/**
* Performs an upward directory search for an mibuilder project file. Returns
* the absolute path to the project.
*
* **Throws** *{@link MiBuilderError}{ name: 'InvalidProjectFileWorkspace' }* If the current folder is not located in a workspace.
*
* **See** {@link traverseForFile}
*
* **See** [process.cwd()](https://nodejs.org/api/process.html#process_process_cwd)
*
* @param {String} [dir=localConfig.DIR] The directory path to start traversing from.
* @return {Promise} A promise that resolves with the project path
*/
/**
* Returns the project config path.
*
* @return {String} The project config path
*/
getProjectConfigPath() {
return this.configPath;
}
/**
* Returns the project path.
*
* @return {String} The project path
*/
getPath() {
return this.path;
}
/**
* Get the mibuilder-project.json config.
*
* *Note:* When reading values from {@link MiBuilderProjectJson}, it is
* recommended to use {@link MiBuilderProject.resolveProjectConfig} instead.
*
* @return {Object<MiBuilderProjectJson>} The mibuilder project json instance
*/
async retrieveMiBuilderProjectJson() {
const options = MiBuilderProjectJson.getDefaultOptions(false);
if (this.mibuilderProjectJson == null) {
const configFilePath = this.getProjectConfigPath();
options.rootDir = _path.default.dirname(configFilePath);
options.filename = _path.default.basename(configFilePath);
this.mibuilderProjectJson = await MiBuilderProjectJson.create(options);
}
return this.mibuilderProjectJson;
}
/**
* The project config is resolved from local and global
* {@link MiBuilderProjectJson}, {@link ConfigAggregator}, and a set of
* defaults. It is recommended to use this when reading values from
* MiBuilderProjectJson.
*
* @param {Boolean} [force=false] Forcing a fresh fetch of the cached config
* @return {Promise<Object>} A resolved config object that contains a bunch of different properties, including some 3rd party custom properties.
*/
async resolveProjectConfig(force = false) {
if (this.projectConfig && force !== true) return this.projectConfig;
const global = await this.retrieveMiBuilderProjectJson();
await global.read();
const defaultValues = {
app: {
guid: (0, _uuid.v4)()
}
};
this.projectConfig = (0, _dxl.defaults)(global ? global.toObject() : {}, defaultValues); // Add fields in mibuilder-config.json
this.projectConfig = (0, _dxl.merge)(this.projectConfig, (await _configAggregator.ConfigAggregator.create()).getConfig()); // Set `packageName` to `bundleIdentifier` if not given. Allows handling of
// default tax consultant app with different Android Package Name / iOS
// Bundle Identifier
const bundleIdentifier = (0, _types.get)(this.projectConfig, 'app.bundleIdentifier');
const packageName = (0, _types.get)(this.projectConfig, 'app.packageName', bundleIdentifier);
(0, _dxl.set)(this.projectConfig, 'app.packageName', packageName); // Sanitize app name and take it as project name
const appName = (0, _types.get)(this.projectConfig, 'app.name', '');
let projectName = (0, _types.get)(this.projectConfig, 'app.projectName', null);
if (projectName == null) projectName = (0, _tiapp.sanitizeAppName)(appName);
(0, _dxl.set)(this.projectConfig, 'app.projectName', projectName);
const sanitizedAppName = (0, _tiapp.sanitizeAppName)(appName);
(0, _dxl.set)(this.projectConfig, 'app.sanitizedAppName', sanitizedAppName); // Create Activity Name for Android
const activityName = (0, _tiapp.generateActivityName)(sanitizedAppName);
(0, _dxl.set)(this.projectConfig, 'app.activityName', activityName); // Determine app type
let appType = MIBUILDER_PROJECT_TYPE.STB;
const servicecode = (0, _types.get)(this.projectConfig, 'app.servicecode');
if (servicecode.match(/(1)[0-9]{6}/)) {
appType = MIBUILDER_PROJECT_TYPE.RA;
}
(0, _dxl.set)(this.projectConfig, 'app.type', appType);
(0, _dxl.set)(this.projectConfig, 'app.isDefault', ['000000', '1000000'].includes(servicecode));
this.projectConfig.configDir = _path.default.dirname(this.getProjectConfigPath());
this.projectConfig.rootDir = _path.default.resolve((0, _types.get)(this.projectConfig, 'app.workspace'), (0, _types.get)(this.projectConfig, 'app.projectName'));
this.path = this.projectConfig.rootDir;
await this.resolveProjectPaths();
await (0, _dxl.mkdirp)(this.path);
return this.projectConfig;
}
/**
* Get a Project from the `app.workspace` set in the config.
*
* @return {Promise} A promise that resolves with the project path
*/
async resolveProjectPaths() {
if (this.paths) return this.paths;
if (this.projectConfig == null) {
await this.resolveProjectConfig();
}
const paths = {};
paths.rootDir = this.getPath();
paths.configDir = this.projectConfig.configDir; // Assign paths objects
paths.assets = _path.default.resolve(paths.configDir, _constants2.ASSETS_DIR);
paths.debug = _path.default.join(paths.configDir, _constants2.DEBUG_REPLACED_FILES_DIR);
paths.projectPlatform = _path.default.resolve(paths.rootDir, _constants2.PLATFORM_DIR);
paths.projectPlatformRes = _path.default.resolve(paths.rootDir, _constants2.PLATFORM_RES_DIR);
paths.projectI18n = _path.default.resolve(paths.rootDir, _constants2.I18N_DIR);
paths.projectResources = _path.default.resolve(paths.rootDir, _constants2.RESOURCES_DIR);
paths.projectiOsSettings = _path.default.resolve(paths.rootDir, _constants2.SETTINGS_IOS_DIR);
paths.settingsAboutPlistFile = _path.default.resolve(paths.projectiOsSettings, 'About.plist');
paths.webAppDir = _path.default.resolve(paths.rootDir, _constants2.WEB_APP_DIR);
paths.remoteConfigFile = _path.default.resolve(paths.rootDir, _constants2.WEB_APP_DIR, this.projectConfig.app.servicecode, _constants.DATA_CONFIG_JSON);
paths.splashes = {
input: _path.default.resolve(paths.assets, 'splash.png'),
inputIOS: _path.default.resolve(paths.assets, 'splash_ios.png'),
inputAndroid: _path.default.resolve(paths.assets, 'splash_android.png'),
inputDefault: _path.default.resolve(paths.assets, 'splash.png'),
outputDir: paths.rootDir,
launchLogo: {
inputs: [// Maybe we should reuse an existent one?
// path.join(paths.assets, 'LaunchLogo.png'),
_path.default.resolve(paths.assets, 'appicon_ios.png'), _path.default.resolve(paths.assets, 'appicon.png')],
temp: _path.default.resolve(paths.assets, 'LaunchLogo.png'),
outputDir: _path.default.resolve(paths.rootDir)
}
};
paths.icons = {
input: _path.default.resolve(paths.assets, 'appicon.png'),
inputIOS: _path.default.resolve(paths.assets, 'appicon_ios.png'),
inputAndroid: _path.default.resolve(paths.assets, 'appicon_android.png'),
outputDir: paths.rootDir,
outputIOS: _path.default.resolve(paths.rootDir, 'DefaultIcon-ios.png'),
pushicon: {
inputPath: _path.default.join(paths.projectPlatformRes, 'drawable-hdpi/appicon.png'),
outputPath: _path.default.join(paths.projectPlatformRes, 'drawable/pushicon.png')
},
'pushicon-v21': {
inputPath: _path.default.join(paths.assets, 'pushicon-v21.png'),
outputPath: _path.default.join(paths.projectPlatformRes, 'drawable-v21/pushicon.png')
}
};
paths.fastlaneDir = _path.default.join(paths.rootDir, 'fastlane');
paths.fastlaneMetaDataDiriOS = _path.default.join(paths.fastlaneDir, 'ios', 'metadata');
paths.fastlaneScreenshotsDiriOS = _path.default.join(paths.fastlaneDir, 'ios', deliver.SCREENSHOTS_FOLDER_NAME);
paths.fastlaneMetaDataDirAndroid = _path.default.join(paths.fastlaneDir, 'android', 'metadata');
paths.googleServicesConfig = _path.default.resolve(paths.configDir, 'google-services.json');
this.paths = paths;
return paths;
}
}
exports.MiBuilderProject = MiBuilderProject;