@react-native-windows/codegen
Version:
Generators for react-native-codegen targeting react-native-windows
416 lines (368 loc) • 11.2 kB
text/typescript
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*
* @format
*/
import path from 'path';
import fs from '@react-native-windows/fs';
import globby from 'globby';
import type {CppStringTypes} from './generators/GenerateNM2';
import {createNM2Generator} from './generators/GenerateNM2';
import {createComponentGenerator} from './generators/GenerateComponentWindows';
import {
generateTypeScript,
setOptionalTurboModule,
} from './generators/GenerateTypeScript';
import type {SchemaType} from '@react-native/codegen/lib/CodegenSchema';
import type {Parser} from '@react-native/codegen/lib/parsers/parser';
export type {CppStringTypes} from './generators/GenerateNM2';
// Load @react-native/codegen from react-native
const rnPath = path.dirname(require.resolve('react-native/package.json'));
const rncodegenPath = path.dirname(
require.resolve('@react-native/codegen/package.json', {paths: [rnPath]}),
);
function getParser(isTypeScript: boolean): Parser {
if (isTypeScript) {
const fp = require(path.resolve(
rncodegenPath,
'lib/parsers/typescript/parser',
));
return new fp.TypeScriptParser();
} else {
const fp = require(path.resolve(rncodegenPath, 'lib/parsers/flow/parser'));
return new fp.FlowParser();
}
}
const schemaValidator = require(path.resolve(
rncodegenPath,
'lib/SchemaValidator',
));
export interface SharedOptions {
libraryName: string;
methodOnly: boolean;
modulesCxx: boolean;
modulesTypeScriptTypes: boolean;
modulesWindows: boolean;
componentsWindows: boolean;
internalComponents: boolean;
namespace: string;
outputDirectory: string;
cppStringType: CppStringTypes;
separateDataTypes: boolean;
}
interface Options extends SharedOptions {
moduleSpecName: string;
schema: SchemaType;
}
interface Config {
generators: any[] /*Generators[]*/;
test?: boolean;
}
function normalizeFileMap(
map: Map<string, string>,
outputDir: string,
outMap: Map<string, string>,
): void {
for (const [fileName, contents] of map) {
const location = path.join(outputDir, fileName);
outMap.set(path.normalize(location), contents);
}
}
function checkFilesForChanges(
map: Map<string, string>,
outputDir: string,
): boolean {
let hasChanges = false;
outputDir = path.resolve(outputDir);
const globbyDir = outputDir.replace(/\\/g, '/');
const allExistingFiles = globby
.sync([`${globbyDir}/**`, `${globbyDir}/**/.*`], {absolute: true})
.map(_ => path.normalize(_));
const allGeneratedFiles = [...map.keys()].map(_ => path.normalize(_)).sort();
if (
allExistingFiles.length !== allGeneratedFiles.length ||
!allGeneratedFiles.every(filepath =>
allExistingFiles.includes(
path.normalize(path.resolve(process.cwd(), filepath)),
),
)
)
return true;
for (const [fileName, contents] of map) {
if (!fs.existsSync(fileName)) {
hasChanges = true;
continue;
}
const currentContents = fs.readFileSync(fileName, 'utf8');
if (currentContents !== contents) {
console.log(`- ${fileName} has changed`);
hasChanges = true;
continue;
}
}
return hasChanges;
}
function writeMapToFiles(map: Map<string, string>, outputDir: string) {
let success = true;
outputDir = path.resolve(outputDir);
const globbyDir = outputDir.replace(/\\/g, '/');
// This ensures that we delete any generated files from modules that have been deleted
const allExistingFiles = globby.sync(
[`${globbyDir}/**`, `${globbyDir}/**/.*`],
{absolute: true},
);
const allGeneratedFiles = [...map.keys()].map(_ => path.normalize(_)).sort();
allExistingFiles.forEach(existingFile => {
if (!allGeneratedFiles.includes(path.normalize(existingFile))) {
console.log('Deleting ', existingFile);
fs.unlinkSync(existingFile);
}
});
for (const [fileName, contents] of map) {
try {
fs.mkdirSync(path.dirname(fileName), {recursive: true});
if (fs.existsSync(fileName)) {
const currentContents = fs.readFileSync(fileName, 'utf8');
// Don't update the files if there are no changes as this breaks incremental builds
if (currentContents === contents) {
continue;
}
}
console.log('Writing ', fileName);
fs.writeFileSync(fileName, contents);
} catch (error) {
success = false;
console.error(`Failed to write ${fileName} to ${fileName}`, error);
}
}
return success;
}
export function parseFile(filename: string): SchemaType {
try {
const isTypeScript =
path.extname(filename) === '.ts' || path.extname(filename) === '.tsx';
const contents = fs.readFileSync(filename, 'utf8');
const schema = getParser(isTypeScript).parseString(contents, filename);
// there will be at most one turbo module per file
const moduleName = Object.keys(schema.modules)[0];
if (moduleName) {
const spec = schema.modules[moduleName];
if (spec.type === 'NativeModule') {
if (contents) {
// This is a temporary implementation until such information is added to the schema in facebook/react-native
if (contents.includes('TurboModuleRegistry.get<')) {
setOptionalTurboModule(spec, true);
} else if (contents.includes('TurboModuleRegistry.getEnforcing<')) {
setOptionalTurboModule(spec, false);
}
}
}
}
return schema;
} catch (e) {
if (e instanceof Error) {
e.message = `(${filename}): ${e.message}`;
}
throw e;
}
}
export function combineSchemas(files: string[]): SchemaType {
return files.reduce(
(merged, filename) => {
const contents = fs.readFileSync(filename, 'utf8');
if (
contents &&
(/export\s+default\s+\(?codegenNativeComponent</.test(contents) ||
contents.includes('extends TurboModule'))
) {
const schema = parseFile(filename);
merged.modules = {...merged.modules, ...schema.modules};
}
return merged;
},
{modules: {}},
);
}
export function generate(
{
libraryName,
methodOnly,
modulesCxx,
modulesTypeScriptTypes,
modulesWindows,
internalComponents,
componentsWindows,
namespace,
outputDirectory,
cppStringType,
separateDataTypes,
moduleSpecName,
schema,
}: Options,
{/*generators,*/ test}: Config,
): boolean {
schemaValidator.validate(schema);
const componentOutputdir = path.join(
outputDirectory,
'react/components',
libraryName,
);
const generatedFiles = new Map<string, string>();
generatedFiles.set(
path.join(outputDirectory, '.clang-format'),
'DisableFormat: true\nSortIncludes: false',
);
const generateNM2 = createNM2Generator({
methodOnly,
namespace,
cppStringType,
separateDataTypes,
});
const generateJsiModuleH = require(path.resolve(
rncodegenPath,
'lib/generators/modules/GenerateModuleH',
)).generate;
const generateJsiModuleCpp = require(path.resolve(
rncodegenPath,
'lib/generators/modules/GenerateModuleCpp',
)).generate;
const generatorPropsH = require(path.resolve(
rncodegenPath,
'lib/generators/components/GeneratePropsH',
)).generate;
const generatorPropsCPP = require(path.resolve(
rncodegenPath,
'lib/generators/components/GeneratePropsCpp',
)).generate;
const generatorShadowNodeH = require(path.resolve(
rncodegenPath,
'lib/generators/components/GenerateShadowNodeH',
)).generate;
const generatorShadowNodeCPP = require(path.resolve(
rncodegenPath,
'lib/generators/components/GenerateShadowNodeCpp',
)).generate;
const generatorComponentDescriptorH = require(path.resolve(
rncodegenPath,
'lib/generators/components/GenerateComponentDescriptorH',
)).generate;
const generatorEventEmitterH = require(path.resolve(
rncodegenPath,
'lib/generators/components/GenerateEventEmitterH',
)).generate;
const generatorEventEmitterCPP = require(path.resolve(
rncodegenPath,
'lib/generators/components/GenerateEventEmitterCpp',
)).generate;
const generatorStateCPP = require(path.resolve(
rncodegenPath,
'lib/generators/components/GenerateStateCpp',
)).generate;
const generatorStateH = require(path.resolve(
rncodegenPath,
'lib/generators/components/GenerateStateH',
)).generate;
const moduleGenerators = [];
if (modulesWindows) {
moduleGenerators.push(generateNM2);
}
if (modulesCxx) {
moduleGenerators.push(generateJsiModuleH);
moduleGenerators.push(generateJsiModuleCpp);
}
if (modulesTypeScriptTypes) {
moduleGenerators.push(generateTypeScript);
}
moduleGenerators.forEach(generator => {
const generated: Map<string, string> = generator(
libraryName,
schema,
moduleSpecName,
);
normalizeFileMap(generated, outputDirectory, generatedFiles);
});
if (
Object.keys(schema.modules).some(
moduleName => schema.modules[moduleName].type === 'Component',
)
) {
const componentGenerators = [];
if (internalComponents) {
componentGenerators.push(
generatorComponentDescriptorH,
generatorEventEmitterCPP,
generatorEventEmitterH,
generatorPropsCPP,
generatorPropsH,
generatorShadowNodeCPP,
generatorShadowNodeH,
generatorStateCPP,
generatorStateH,
);
}
if (componentsWindows) {
const generateComponentWindows = createComponentGenerator({
namespace,
cppStringType,
});
componentGenerators.push(generateComponentWindows);
}
componentGenerators.forEach(generator => {
const generated: Map<string, string> = generator(
libraryName,
schema,
moduleSpecName,
);
normalizeFileMap(generated, componentOutputdir, generatedFiles);
});
}
if (test === true) {
return checkFilesForChanges(generatedFiles, outputDirectory);
}
return writeMapToFiles(generatedFiles, outputDirectory);
}
export interface CodeGenOptions extends SharedOptions {
file?: string;
files?: string[];
test: boolean;
}
export function runCodeGen(options: CodeGenOptions): boolean {
if (!options.file && !options.files)
throw new Error('Must specify file or files option');
const schema = options.file
? parseFile(options.file)
: combineSchemas(globby.sync(options.files!));
const libraryName = options.libraryName;
const moduleSpecName = 'moduleSpecName';
const {
methodOnly,
modulesCxx,
modulesTypeScriptTypes,
modulesWindows,
componentsWindows,
internalComponents,
namespace,
outputDirectory,
cppStringType,
separateDataTypes,
} = options;
return generate(
{
libraryName,
methodOnly,
modulesCxx,
modulesTypeScriptTypes,
modulesWindows,
componentsWindows,
internalComponents,
namespace,
outputDirectory,
cppStringType,
separateDataTypes,
moduleSpecName,
schema,
},
{generators: [], test: options.test},
);
}