Move packager core logic from local-cli to packager

Summary: Moves packager core logic to `packager/...` to prepare the open source split.

Reviewed By: cpojer

Differential Revision: D5116162

fbshipit-source-id: 06ee2406026686789f67acc88df41773866c3cd3
This commit is contained in:
David Aurelio 2017-05-24 07:57:23 -07:00 committed by Facebook Github Bot
parent 3e8991548b
commit f0e629b8bf
14 changed files with 777 additions and 4 deletions

View File

@ -16,7 +16,7 @@ const babelGenerate = require('babel-generator').default;
const babylon = require('babylon');
import type {AssetDescriptor} from '.';
import type {ModuleTransportLike} from '../../../local-cli/bundle/types.flow';
import type {ModuleTransportLike} from '../shared/types.flow';
type SubTree<T: ModuleTransportLike> = (
moduleTransport: T,

View File

@ -10,11 +10,11 @@
*/
'use strict';
const buildSourceMapWithMetaData = require('../../../../local-cli/bundle/output/unbundle/build-unbundle-sourcemap-with-metadata.js');
const buildSourceMapWithMetaData = require('../../shared/output/unbundle/build-unbundle-sourcemap-with-metadata.js');
const nullthrows = require('fbjs/lib/nullthrows');
const {buildTableAndContents, createModuleGroups} = require('../../../../local-cli/bundle/output/unbundle/as-indexed-file');
const {createRamBundleGroups} = require('../../Bundler/util');
const {buildTableAndContents, createModuleGroups} = require('../../shared/output/unbundle/as-indexed-file');
const {concat} = require('./util');
import type {FBIndexMap} from '../../lib/SourceMap.js';

View File

@ -10,7 +10,7 @@
*/
'use strict';
const meta = require('../../../../local-cli/bundle/output/meta');
const meta = require('../../shared/output/meta');
const {createIndexMap} = require('./source-map');
const {addModuleIdsToModuleWrapper, concat} = require('./util');

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const Server = require('../../Server');
const meta = require('./meta');
const relativizeSourceMap = require('../../lib/relativizeSourceMap');
const writeFile = require('./writeFile');
import type Bundle from '../../Bundler/Bundle';
import type {SourceMap} from '../../lib/SourceMap';
import type {OutputOptions, RequestOptions} from '../types.flow';
function buildBundle(packagerClient: Server, requestOptions: RequestOptions) {
return packagerClient.buildBundle({
...Server.DEFAULT_BUNDLE_OPTIONS,
...requestOptions,
isolateModuleIDs: true,
});
}
function createCodeWithMap(bundle: Bundle, dev: boolean, sourceMapSourcesRoot?: string): * {
const map = bundle.getSourceMap({dev});
const sourceMap = relativizeSourceMap(
typeof map === 'string' ? (JSON.parse(map): SourceMap) : map,
sourceMapSourcesRoot);
return {
code: bundle.getSource({dev}),
map: JSON.stringify(sourceMap),
};
}
function saveBundleAndMap(
bundle: Bundle,
options: OutputOptions,
log: (...args: Array<string>) => {},
): Promise<> {
const {
bundleOutput,
bundleEncoding: encoding,
dev,
sourcemapOutput,
sourcemapSourcesRoot
} = options;
log('start');
const codeWithMap = createCodeWithMap(bundle, !!dev, sourcemapSourcesRoot);
log('finish');
log('Writing bundle output to:', bundleOutput);
const {code} = codeWithMap;
const writeBundle = writeFile(bundleOutput, code, encoding);
const writeMetadata = writeFile(
bundleOutput + '.meta',
meta(code, encoding),
'binary');
Promise.all([writeBundle, writeMetadata])
.then(() => log('Done writing bundle output'));
if (sourcemapOutput) {
log('Writing sourcemap output to:', sourcemapOutput);
const writeMap = writeFile(sourcemapOutput, codeWithMap.map, null);
writeMap.then(() => log('Done writing sourcemap output'));
return Promise.all([writeBundle, writeMetadata, writeMap]);
} else {
return writeBundle;
}
}
exports.build = buildBundle;
exports.save = saveBundleAndMap;
exports.formatName = 'bundle';

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
/* global Buffer: true */
const crypto = require('crypto');
const isUTF8 = encoding => /^utf-?8$/i.test(encoding);
const constantFor = encoding =>
/^ascii$/i.test(encoding) ? 1 :
isUTF8(encoding) ? 2 :
/^(?:utf-?16(?:le)?|ucs-?2)$/.test(encoding) ? 3 : 0;
module.exports = function(
code: string,
encoding: 'ascii' | 'utf8' | 'utf16le' = 'utf8',
): Buffer {
const hash = crypto.createHash('sha1');
// remove `new Buffer` calls when RN drops support for Node 4
hash.update(Buffer.from ? Buffer.from(code, encoding) : new Buffer(code, encoding));
const digest = hash.digest();
const signature = Buffer.alloc ? Buffer.alloc(digest.length + 1) : new Buffer(digest.length + 1);
digest.copy(signature);
signature.writeUInt8(
constantFor(tryAsciiPromotion(code, encoding)),
signature.length - 1);
return signature;
};
function tryAsciiPromotion(string, encoding) {
if (!isUTF8(encoding)) { return encoding; }
for (let i = 0, n = string.length; i < n; i++) {
if (string.charCodeAt(i) > 0x7f) { return encoding; }
}
return 'ascii';
}

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const MAGIC_UNBUNDLE_NUMBER = require('./magic-number');
const buildSourceMapWithMetaData = require('./build-unbundle-sourcemap-with-metadata');
const mkdirp = require('mkdirp');
const path = require('path');
const relativizeSourceMap = require('../../../lib/relativizeSourceMap');
const writeFile = require('../writeFile');
const writeSourceMap = require('./write-sourcemap');
const {joinModules} = require('./util');
import type Bundle from '../../../Bundler/Bundle';
import type {OutputOptions} from '../../types.flow';
// must not start with a dot, as that won't go into the apk
const MAGIC_UNBUNDLE_FILENAME = 'UNBUNDLE';
const MODULES_DIR = 'js-modules';
/**
* Saves all JS modules of an app as single files
* The startup code (prelude, polyfills etc.) are written to the file
* designated by the `bundleOuput` option.
* All other modules go into a 'js-modules' folder that in the same parent
* directory as the startup file.
*/
function saveAsAssets(
bundle: Bundle,
options: OutputOptions,
log: (...args: Array<string>) => void,
): Promise<mixed> {
const {
bundleOutput,
bundleEncoding: encoding,
sourcemapOutput,
sourcemapSourcesRoot,
} = options;
log('start');
const {startupModules, lazyModules} = bundle.getUnbundle();
log('finish');
const startupCode = joinModules(startupModules);
log('Writing bundle output to:', bundleOutput);
const modulesDir = path.join(path.dirname(bundleOutput), MODULES_DIR);
const writeUnbundle =
createDir(modulesDir).then( // create the modules directory first
() => Promise.all([
writeModules(lazyModules, modulesDir, encoding),
writeFile(bundleOutput, startupCode, encoding),
writeMagicFlagFile(modulesDir),
])
);
writeUnbundle.then(() => log('Done writing unbundle output'));
const sourceMap =
relativizeSourceMap(
buildSourceMapWithMetaData({
fixWrapperOffset: true,
lazyModules: lazyModules.concat(),
moduleGroups: null,
startupModules: startupModules.concat(),
}),
sourcemapSourcesRoot
);
return Promise.all([
writeUnbundle,
sourcemapOutput && writeSourceMap(sourcemapOutput, JSON.stringify(sourceMap), log)
]);
}
function createDir(dirName) {
return new Promise((resolve, reject) =>
mkdirp(dirName, error => error ? reject(error) : resolve()));
}
function writeModuleFile(module, modulesDir, encoding) {
const {code, id} = module;
return writeFile(path.join(modulesDir, id + '.js'), code, encoding);
}
function writeModules(modules, modulesDir, encoding) {
const writeFiles =
modules.map(module => writeModuleFile(module, modulesDir, encoding));
return Promise.all(writeFiles);
}
function writeMagicFlagFile(outputDir) {
/* global Buffer: true */
const buffer = new Buffer(4);
buffer.writeUInt32LE(MAGIC_UNBUNDLE_NUMBER, 0);
return writeFile(path.join(outputDir, MAGIC_UNBUNDLE_FILENAME), buffer);
}
module.exports = saveAsAssets;

View File

@ -0,0 +1,216 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const MAGIC_UNBUNDLE_FILE_HEADER = require('./magic-number');
const buildSourceMapWithMetaData = require('./build-unbundle-sourcemap-with-metadata');
const fs = require('fs');
const relativizeSourceMap = require('../../../lib/relativizeSourceMap');
const writeSourceMap = require('./write-sourcemap');
const {joinModules} = require('./util');
import type Bundle from '../../../Bundler/Bundle';
import type {ModuleGroups, ModuleTransportLike, OutputOptions} from '../../types.flow';
const SIZEOF_UINT32 = 4;
/**
* Saves all JS modules of an app as a single file, separated with null bytes.
* The file begins with an offset table that contains module ids and their
* lengths/offsets.
* The module id for the startup code (prelude, polyfills etc.) is the
* empty string.
*/
function saveAsIndexedFile(
bundle: Bundle,
options: OutputOptions,
log: (...args: Array<string>) => void,
): Promise<> {
const {
bundleOutput,
bundleEncoding: encoding,
sourcemapOutput,
sourcemapSourcesRoot,
} = options;
log('start');
const {startupModules, lazyModules, groups} = bundle.getUnbundle();
log('finish');
const moduleGroups = createModuleGroups(groups, lazyModules);
const startupCode = joinModules(startupModules);
log('Writing unbundle output to:', bundleOutput);
const writeUnbundle = writeBuffers(
fs.createWriteStream(bundleOutput),
buildTableAndContents(startupCode, lazyModules, moduleGroups, encoding)
).then(() => log('Done writing unbundle output'));
const sourceMap =
relativizeSourceMap(
buildSourceMapWithMetaData({
startupModules: startupModules.concat(),
lazyModules: lazyModules.concat(),
moduleGroups,
fixWrapperOffset: true,
}),
sourcemapSourcesRoot
);
return Promise.all([
writeUnbundle,
sourcemapOutput && writeSourceMap(sourcemapOutput, JSON.stringify(sourceMap), log),
]);
}
/* global Buffer: true */
const fileHeader = new Buffer(4);
fileHeader.writeUInt32LE(MAGIC_UNBUNDLE_FILE_HEADER, 0);
const nullByteBuffer: Buffer = new Buffer(1).fill(0);
function writeBuffers(stream, buffers: Array<Buffer>) {
buffers.forEach(buffer => stream.write(buffer));
return new Promise((resolve, reject) => {
stream.on('error', reject);
stream.on('finish', () => resolve());
stream.end();
});
}
function nullTerminatedBuffer(contents, encoding) {
return Buffer.concat([new Buffer(contents, encoding), nullByteBuffer]);
}
function moduleToBuffer(id, code, encoding) {
return {
id,
buffer: nullTerminatedBuffer(code, encoding),
};
}
function entryOffset(n) {
// 2: num_entries + startup_code_len
// n * 2: each entry consists of two uint32s
return (2 + n * 2) * SIZEOF_UINT32;
}
function buildModuleTable(startupCode, moduleBuffers, moduleGroups) {
// table format:
// - num_entries: uint_32 number of entries
// - startup_code_len: uint_32 length of the startup section
// - entries: entry...
//
// entry:
// - module_offset: uint_32 offset into the modules blob
// - module_length: uint_32 length of the module code in bytes
const moduleIds = Array.from(moduleGroups.modulesById.keys());
const maxId = moduleIds.reduce((max, id) => Math.max(max, id));
const numEntries = maxId + 1;
const table: Buffer = new Buffer(entryOffset(numEntries)).fill(0);
// num_entries
table.writeUInt32LE(numEntries, 0);
// startup_code_len
table.writeUInt32LE(startupCode.length, SIZEOF_UINT32);
// entries
let codeOffset = startupCode.length;
moduleBuffers.forEach(({id, buffer}) => {
const group = moduleGroups.groups.get(id);
const idsInGroup = group ? [id].concat(Array.from(group)) : [id];
idsInGroup.forEach(moduleId => {
const offset = entryOffset(moduleId);
// module_offset
table.writeUInt32LE(codeOffset, offset);
// module_length
table.writeUInt32LE(buffer.length, offset + SIZEOF_UINT32);
});
codeOffset += buffer.length;
});
return table;
}
function groupCode(rootCode, moduleGroup, modulesById) {
if (!moduleGroup || !moduleGroup.size) {
return rootCode;
}
const code = [rootCode];
for (const id of moduleGroup) {
code.push((modulesById.get(id) || {}).code);
}
return code.join('\n');
}
function buildModuleBuffers(modules, moduleGroups, encoding) {
return modules
.filter(m => !moduleGroups.modulesInGroups.has(m.id))
.map(({id, code}) => moduleToBuffer(
id,
groupCode(
code,
moduleGroups.groups.get(id),
moduleGroups.modulesById,
),
encoding
));
}
function buildTableAndContents(
startupCode: string,
modules: $ReadOnlyArray<ModuleTransportLike>,
moduleGroups: ModuleGroups,
encoding?: 'utf8' | 'utf16le' | 'ascii',
) {
// file contents layout:
// - magic number char[4] 0xE5 0xD1 0x0B 0xFB (0xFB0BD1E5 uint32 LE)
// - offset table table see `buildModuleTables`
// - code blob char[] null-terminated code strings, starting with
// the startup code
const startupCodeBuffer = nullTerminatedBuffer(startupCode, encoding);
const moduleBuffers = buildModuleBuffers(modules, moduleGroups, encoding);
const table = buildModuleTable(startupCodeBuffer, moduleBuffers, moduleGroups);
return [
fileHeader,
table,
startupCodeBuffer
].concat(moduleBuffers.map(({buffer}) => buffer));
}
function createModuleGroups(
groups: Map<number, Set<number>>,
modules: $ReadOnlyArray<ModuleTransportLike>,
): ModuleGroups {
return {
groups,
modulesById: new Map(modules.map(m => [m.id, m])),
modulesInGroups: new Set(concat(groups.values())),
};
}
function * concat(iterators) {
for (const it of iterators) {
yield * it;
}
}
exports.save = saveAsIndexedFile;
exports.buildTableAndContents = buildTableAndContents;
exports.createModuleGroups = createModuleGroups;

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const {combineSourceMaps, combineSourceMapsAddingOffsets, joinModules} = require('./util');
import type {ModuleGroups, ModuleTransportLike} from '../../types.flow';
type Params = {|
fixWrapperOffset: boolean,
lazyModules: $ReadOnlyArray<ModuleTransportLike>,
moduleGroups: ?ModuleGroups,
startupModules: $ReadOnlyArray<ModuleTransportLike>,
|};
module.exports = ({fixWrapperOffset, lazyModules, moduleGroups, startupModules}: Params) => {
const options = fixWrapperOffset ? {fixWrapperOffset: true} : undefined;
const startupModule: ModuleTransportLike = {
code: joinModules(startupModules),
id: Number.MIN_SAFE_INTEGER,
map: combineSourceMaps(startupModules, undefined, options),
sourcePath: '',
};
const map = combineSourceMapsAddingOffsets(
[startupModule].concat(lazyModules),
moduleGroups,
options,
);
delete map.x_facebook_offsets[Number.MIN_SAFE_INTEGER];
return map;
};

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const Server = require('../../../Server');
const asAssets = require('./as-assets');
const asIndexedFile = require('./as-indexed-file').save;
import type Bundle from '../../../Bundler/Bundle';
import type {OutputOptions, RequestOptions} from '../../types.flow';
function buildBundle(packagerClient: Server, requestOptions: RequestOptions) {
return packagerClient.buildBundle({
...Server.DEFAULT_BUNDLE_OPTIONS,
...requestOptions,
unbundle: true,
isolateModuleIDs: true,
});
}
function saveUnbundle(
bundle: Bundle,
options: OutputOptions,
log: (x: string) => void,
): Promise<mixed> {
// we fork here depending on the platform:
// while android is pretty good at loading individual assets, ios has a large
// overhead when reading hundreds pf assets from disk
return options.platform === 'android' && !options.indexedUnbundle ?
asAssets(bundle, options, log) :
asIndexedFile(bundle, options, log);
}
exports.build = buildBundle;
exports.save = saveUnbundle;
exports.formatName = 'bundle';

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
module.exports = 0xFB0BD1E5;

View File

@ -0,0 +1,128 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const invariant = require('fbjs/lib/invariant');
import type {FBIndexMap, IndexMap, MappingsMap, SourceMap} from '../../../lib/SourceMap';
import type {ModuleGroups, ModuleTransportLike} from '../../types.flow';
const newline = /\r\n?|\n|\u2028|\u2029/g;
// fastest implementation
const countLines = (string: string) => (string.match(newline) || []).length + 1;
function lineToLineSourceMap(source: string, filename: string = ''): MappingsMap {
// The first line mapping in our package is the base64vlq code for zeros (A).
const firstLine = 'AAAA;';
// Most other lines in our mappings are all zeros (for module, column etc)
// except for the lineno mapping: curLineno - prevLineno = 1; Which is C.
const line = 'AACA;';
return {
file: filename,
mappings: firstLine + Array(countLines(source)).join(line),
sources: [filename],
names: [],
version: 3,
};
}
const wrapperEnd = wrappedCode => wrappedCode.indexOf('{') + 1;
const Section =
(line: number, column: number, map: SourceMap) =>
({map, offset: {line, column}});
type CombineOptions = {fixWrapperOffset: boolean};
function combineSourceMaps(
modules: $ReadOnlyArray<ModuleTransportLike>,
moduleGroups?: ModuleGroups,
options?: ?CombineOptions,
): IndexMap {
const sections = combineMaps(modules, null, moduleGroups, options);
return {sections, version: 3};
}
function combineSourceMapsAddingOffsets(
modules: $ReadOnlyArray<ModuleTransportLike>,
moduleGroups?: ?ModuleGroups,
options?: ?CombineOptions,
): FBIndexMap {
const x_facebook_offsets = [];
const sections = combineMaps(modules, x_facebook_offsets, moduleGroups, options);
return {sections, version: 3, x_facebook_offsets};
}
function combineMaps(modules, offsets: ?Array<number>, moduleGroups, options) {
const sections = [];
let line = 0;
modules.forEach(moduleTransport => {
const {code, id, name} = moduleTransport;
let column = 0;
let group;
let groupLines = 0;
let {map} = moduleTransport;
if (moduleGroups && moduleGroups.modulesInGroups.has(id)) {
// this is a module appended to another module
return;
}
if (offsets != null) {
group = moduleGroups && moduleGroups.groups.get(id);
if (group && moduleGroups) {
const {modulesById} = moduleGroups;
const otherModules: $ReadOnlyArray<ModuleTransportLike> =
Array.from(group || [])
.map(moduleId => modulesById.get(moduleId))
.filter(Boolean); // needed to appease flow
otherModules.forEach(m => {
groupLines += countLines(m.code);
});
map = combineSourceMaps([moduleTransport].concat(otherModules));
}
column = options && options.fixWrapperOffset ? wrapperEnd(code) : 0;
}
invariant(
!Array.isArray(map),
'Random Access Bundle source maps cannot be built from raw mappings',
);
sections.push(Section(line, column, map || lineToLineSourceMap(code, name)));
if (offsets != null && id != null) {
offsets[id] = line;
for (const moduleId of group || []) {
offsets[moduleId] = line;
}
}
line += countLines(code) + groupLines;
});
return sections;
}
const joinModules =
(modules: $ReadOnlyArray<{+code: string}>): string =>
modules.map(m => m.code).join('\n');
module.exports = {
combineSourceMaps,
combineSourceMapsAddingOffsets,
countLines,
joinModules,
lineToLineSourceMap,
};

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const writeFile = require('../writeFile');
function writeSourcemap(
fileName: string,
contents: string,
log: (...args: Array<string>) => void,
): Promise<> {
if (!fileName) {
return Promise.resolve();
}
log('Writing sourcemap output to:', fileName);
const writeMap = writeFile(fileName, contents, null);
writeMap.then(() => log('Done writing sourcemap output'));
return writeMap;
}
module.exports = writeSourcemap;

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const denodeify = require('denodeify');
const fs = require('fs');
type WriteFn =
(file: string, data: string | Buffer, encoding?: ?string) => Promise<mixed>;
const writeFile: WriteFn = denodeify(fs.writeFile);
module.exports = writeFile;

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
import type {SourceMapOrMappings} from '../lib/ModuleTransport';
export type ModuleGroups = {|
groups: Map<number, Set<number>>,
modulesById: Map<number, ModuleTransportLike>,
modulesInGroups: Set<number>,
|};
export type ModuleTransportLike = {
+code: string,
+id: number,
+map: ?SourceMapOrMappings,
+name?: string,
+sourcePath: string,
};
export type OutputOptions = {
bundleOutput: string,
bundleEncoding?: 'utf8' | 'utf16le' | 'ascii',
dev?: boolean,
platform: string,
sourcemapOutput?: string,
sourcemapSourcesRoot?: string,
sourcemapUseAbsolutePath?: boolean,
};
export type RequestOptions = {|
entryFile: string,
sourceMapUrl?: string,
dev?: boolean,
minify: boolean,
platform: string,
|};