expo-modules-test-core
Version:
Module providing native testing utilities for testing Expo modules
231 lines • 9.67 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAllExpoModulesInWorkingDirectory = getAllExpoModulesInWorkingDirectory;
// convert requires above to imports
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const glob_1 = require("glob");
const xml_js_1 = __importDefault(require("xml-js"));
const yaml_1 = __importDefault(require("yaml"));
const rootDir = process.cwd();
const pattern = `${rootDir}/**/*.swift`;
function getStructureFromFile(file) {
const command = 'sourcekitten structure --file ' + file.path;
try {
const output = (0, child_process_1.execSync)(command);
return JSON.parse(output.toString());
}
catch (error) {
console.error('An error occurred while executing the command:', error);
}
}
// find an object with "key.typename" : "ModuleDefinition" somewhere in the structure and return it
function findModuleDefinitionInStructure(structure) {
if (!structure) {
return null;
}
if (structure?.['key.typename'] === 'ModuleDefinition') {
const root = structure?.['key.substructure'];
if (!root) {
console.warn('Found ModuleDefinition but it is malformed');
}
return root;
}
const substructure = structure['key.substructure'];
if (Array.isArray(substructure) && substructure.length > 0) {
for (const child of substructure) {
let result = null;
result = findModuleDefinitionInStructure(child);
if (result) {
return result;
}
}
}
return null;
}
// Read string straight from file – needed since we can't get cursorinfo for modulename
function getIdentifierFromOffsetObject(offsetObject, file) {
// adding 1 and removing 1 to get rid of quotes
return file.content
.substring(offsetObject['key.offset'], offsetObject['key.offset'] + offsetObject['key.length'])
.replaceAll('"', '');
}
function maybeUnwrapXMLStructs(type) {
if (!type) {
return type;
}
if (typeof type === 'string') {
return type;
}
if (type['_text']) {
return type['_text'];
}
if (type['ref.struct']) {
return maybeUnwrapXMLStructs(type['ref.struct']);
}
return type;
}
function maybeWrapArray(itemOrItems) {
if (!itemOrItems) {
return null;
}
if (Array.isArray(itemOrItems)) {
return itemOrItems;
}
else {
return [itemOrItems];
}
}
function parseXMLAnnotatedDeclarations(cursorInfoOutput) {
const xml = cursorInfoOutput['key.fully_annotated_decl'];
if (!xml) {
return null;
}
const parsed = xml_js_1.default.xml2js(xml, { compact: true });
const parameters = maybeWrapArray(parsed?.['decl.function.free']?.['decl.var.parameter'])?.map((p) => ({
name: maybeUnwrapXMLStructs(p['decl.var.parameter.argument_label']),
typename: maybeUnwrapXMLStructs(p['decl.var.parameter.type']),
})) ?? [];
const returnType = maybeUnwrapXMLStructs(parsed?.['decl.function.free']?.['decl.function.returntype']);
return { parameters, returnType };
}
let cachedSDKPath = null;
function getSDKPath() {
if (cachedSDKPath) {
return cachedSDKPath;
}
const sdkPath = (0, child_process_1.execSync)('xcrun --sdk iphoneos --show-sdk-path').toString().trim();
cachedSDKPath = sdkPath;
return cachedSDKPath;
}
// Read type description with sourcekitten, works only for variables
function getTypeFromOffsetObject(offsetObject, file) {
if (!offsetObject) {
return null;
}
const request = {
'key.request': 'source.request.cursorinfo',
'key.sourcefile': file.path,
'key.offset': offsetObject['key.offset'],
'key.compilerargs': [file.path, '-target', 'arm64-apple-ios', '-sdk', getSDKPath()],
};
const yamlRequest = yaml_1.default.stringify(request, {
defaultStringType: 'QUOTE_DOUBLE',
lineWidth: 0,
defaultKeyType: 'PLAIN',
// needed since behaviour of sourcekitten is not consistent
}).replace('"source.request.cursorinfo"', 'source.request.cursorinfo');
const command = 'sourcekitten request --yaml "' + yamlRequest.replaceAll('"', '\\"') + '"';
try {
const output = (0, child_process_1.execSync)(command, { stdio: 'pipe' });
return parseXMLAnnotatedDeclarations(JSON.parse(output.toString()));
}
catch (error) {
console.error('An error occurred while executing the command:', error);
}
return null;
}
function hasSubstructure(structureObject) {
return structureObject?.['key.substructure'] && structureObject['key.substructure'].length > 0;
}
function parseClosureTypes(structureObject) {
const closure = structureObject['key.substructure']?.find((s) => s['key.kind'] === 'source.lang.swift.expr.closure');
if (!closure) {
return null;
}
const parameters = closure['key.substructure']
?.filter((s) => s['key.kind'] === 'source.lang.swift.decl.var.parameter')
.map((p) => ({ name: p['key.name'], typename: p['key.typename'] }));
const returnType = closure?.['key.typename'] ?? 'unknown';
return { parameters, returnType };
}
// Used for functions,async functions, all of shape Identifier(name, closure or function)
function findNamedDefinitionsOfType(type, moduleDefinition, file) {
const definitionsOfType = moduleDefinition.filter((md) => md['key.name'] === type);
return definitionsOfType.map((d) => {
const definitionParams = d['key.substructure'];
const name = getIdentifierFromOffsetObject(definitionParams[0], file);
let types = null;
if (hasSubstructure(definitionParams[1])) {
types = parseClosureTypes(definitionParams[1]);
}
else {
types = getTypeFromOffsetObject(definitionParams[1], file);
}
return { name, types };
});
}
// Used for events
function findGroupedDefinitionsOfType(type, moduleDefinition, file) {
const definitionsOfType = moduleDefinition.filter((md) => md['key.name'] === type);
return definitionsOfType.flatMap((d) => {
const definitionParams = d['key.substructure'];
return definitionParams.map((d) => ({ name: getIdentifierFromOffsetObject(d, file) }));
});
}
function findAndParseNestedClassesOfType(moduleDefinition, file, type) {
// we support reading definitions from closure only
const definitionsOfType = moduleDefinition.filter((md) => md['key.name'] === type);
return definitionsOfType
.map((df) => {
const nestedModuleDefinition = df['key.substructure']?.[1]?.['key.substructure']?.[0]?.['key.substructure']?.[0]?.['key.substructure'];
if (!nestedModuleDefinition) {
console.warn('Could not parse definition');
return null;
}
const name = getIdentifierFromOffsetObject(df['key.substructure']?.[0], file).replace('.self', '');
// let's drop nested view field and classes (are null anyways)
const { views: _, classes: _2, ...definition } = parseModuleDefinition(nestedModuleDefinition, file);
return { ...definition, name };
})
.flatMap((f) => (f ? [f] : []));
}
function omitParamsFromClosureArguments(definitions, paramsToOmit) {
return definitions.map((d) => ({
...d,
types: {
...d.types,
parameters: d.types?.parameters?.filter((t, idx) => !paramsToOmit.includes(t.name)) ?? [],
},
}));
}
// Some blocks have additional modifiers like runOnQueue – we may need to do additional traversing to get to the function definition
function parseBlockModifiers(structureObject) {
if (structureObject['key.name']?.includes('runOnQueue')) {
return structureObject['key.substructure'][0];
}
return structureObject;
}
function parseModuleDefinition(moduleDefinition, file) {
const preparedModuleDefinition = moduleDefinition.map(parseBlockModifiers);
const parsedDefinition = {
name: findNamedDefinitionsOfType('Name', preparedModuleDefinition, file)?.[0]?.name,
functions: findNamedDefinitionsOfType('Function', preparedModuleDefinition, file),
asyncFunctions: omitParamsFromClosureArguments(findNamedDefinitionsOfType('AsyncFunction', preparedModuleDefinition, file), ['promise']),
events: findGroupedDefinitionsOfType('Events', preparedModuleDefinition, file),
properties: findNamedDefinitionsOfType('Property', preparedModuleDefinition, file),
props: omitParamsFromClosureArguments(findNamedDefinitionsOfType('Prop', preparedModuleDefinition, file), ['view']),
views: findAndParseNestedClassesOfType(preparedModuleDefinition, file, 'View'),
classes: findAndParseNestedClassesOfType(preparedModuleDefinition, file, 'Class'),
};
return parsedDefinition;
}
function findModuleDefinitionsInFiles(files) {
const modules = [];
for (const path of files) {
const file = { path, content: fs_1.default.readFileSync(path, 'utf8') };
const definition = findModuleDefinitionInStructure(getStructureFromFile(file));
if (definition) {
modules.push(parseModuleDefinition(definition, file));
}
}
return modules;
}
function getAllExpoModulesInWorkingDirectory() {
const files = (0, glob_1.globSync)(pattern);
return findModuleDefinitionsInFiles(files);
}
//# sourceMappingURL=getStructure.js.map