diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..2bef8e59 --- /dev/null +++ b/.clang-format @@ -0,0 +1,161 @@ +--- +Language: Cpp +BasedOnStyle: Google +AccessModifierOffset: -2 +AlignAfterOpenBracket: BlockIndent +AlignArrayOfStructures: Right +AlignConsecutiveAssignments: false +AlignConsecutiveBitFields: Consecutive +AlignConsecutiveDeclarations: false +AlignConsecutiveMacros: false +AlignConsecutiveShortCaseStatements: + Enabled: true + AcrossEmptyLines: true + AcrossComments: true + AlignCaseColons: false +AlignEscapedNewlines: LeftWithLastLine +AlignOperands: AlignAfterOperator +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowBreakBeforeNoexceptSpecifier: OnlyWithParen +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: true +AllowShortCompoundRequirementOnASingleLine: true +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +#AllowShortNamespacesOnASingleLine: false +AlwaysBreakBeforeMultilineStrings: true +BinPackArguments: false +BinPackParameters: OnePerLine +BitFieldColonSpacing: Both +BraceWrapping: + AfterClass: false + AfterCaseLabel: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: true + BeforeElse: true + BeforeWhile: false + IndentBraces: false +BreakAdjacentStringLiterals: true +BreakAfterAttributes: Leave +BreakAfterReturnType: None +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom # or else brace wrapping won't be used +BreakBeforeConceptDeclarations: Allowed +#BreakBeforeTemplateCloser: true +BreakBeforeTernaryOperators: true +BreakBinaryOperations: RespectPrecedence +BreakConstructorInitializers: BeforeComma +BreakFunctionDefinitionParameters: false +BreakInheritanceList: BeforeComma +BreakStringLiterals: true +BreakTemplateDeclarations: Yes +ColumnLimit: 120 +CompactNamespaces: false # should use namespace a::b +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +FixNamespaceComments: false +ForEachMacros: [ FOR_EACH_RANGE, FOR_EACH, ] +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<[^\.]+>$' # standard library imports (without .) + Priority: 1 + - Regex: '^<.*\.h(pp)?>' + Priority: 2 + - Regex: '^<.*' + Priority: 3 + - Regex: '.*' + Priority: 4 +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: Indent +#IndentExportBlock: Indent +IndentGotoLabels: false +IndentPPDirectives: BeforeHash +IndentRequiresClause: true +IndentWidth: 4 +IndentWrappedFunctionNames: true +InsertBraces: true +InsertNewlineAtEOF: true +InsertTrailingCommas: Wrapped +IntegerLiteralSeparator: + Binary: 4 + BinaryMinDigits: 5 + Decimal: 3 + DecimalMinDigits: 5 + Hex: 4 + HexMinDigits: 5 +KeepEmptyLines: + AtEndOfFile: false + AtStartOfBlock: false + AtStartOfFile: false +LambdaBodyIndentation: Signature +LineEnding: DeriveLF +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PackConstructorInitializers: NextLine +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +#QualifierOrder: [inline, static, type, const, volatile] +ReferenceAlignment: Left +ReflowComments: true +#RemoveEmptyLinesInUnwrappedLines: true +RequiresClausePosition: OwnLine +SeparateDefinitionBlocks: Leave +SortIncludes: CaseSensitive +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 3 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: true +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParens: Custom +SpacesInParensOptions: + ExceptDoubleParentheses: true + InConditionalStatements: false + InCStyleCasts: false + InEmptyParentheses: false + Other: false +SpacesInSquareBrackets: false +Standard: c++20 +TabWidth: 4 +UseTab: Never +#WrapNamespaceBodyWithEmptyLines: true \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..6be177da --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,75 @@ +Checks: > + -*, + boost-*, + bugprone-*, + cppcoreguidelines-*, + clang-analyzer-*, + concurrency-*, + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + -misc-non-private-member-variables-in-classes, + -readability-named-parameter, + -readability-braces-around-statements, + -readability-magic-numbers + +# Turn all the warnings from the checks above into errors. +WarningsAsErrors: "*" + +HeaderFileExtensions: + - h + - hpp + - hh + - hxx + +ImplementationFileExtensions: + - c + - cpp + - cxx + - cc + +FormatStyle: file + +CheckOptions: + - { key: readability-identifier-naming.NamespaceCase, value: lower_case } + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.ConceptCase, value: CamelCase } + - { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase } + - { key: readability-identifier-naming.FunctionCase, value: camelBack } + - { key: readability-identifier-naming.VariableCase, value: camelBack } + - { key: readability-identifier-naming.MemberCase, value: camelBack } + - { key: readability-identifier-naming.ClassMemberCase, value: CamelCase } + - { key: readability-identifier-naming.PrivateMemberPrefix, value: _ } + - { key: readability-identifier-naming.ProtectedMemberPrefix, value: _ } + - { key: readability-identifier-naming.MacroDefinitionCase, value: UPPER_CASE } + - { key: readability-identifier-naming.EnumConstantCase, value: CamelCase } + - { key: readability-identifier-naming.EnumConstantPrefix, value: k } + - { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase } + - { key: readability-identifier-naming.ConstexprVariablePrefix, value: '' } + - { key: readability-identifier-naming.GlobalConstantCase, value: CamelCase } + - { key: readability-identifier-naming.GlobalConstantPrefix, value: k } + - { key: readability-identifier-naming.MemberConstantCase, value: CamelCase } + - { key: readability-identifier-naming.MemberConstantPrefix, value: k } + - { key: readability-identifier-naming.StaticConstantCase, value: CamelCase } + - { key: readability-identifier-naming.StaticConstantPrefix, value: k } + - key: readability-identifier-naming.ConstantPointerParameterSuffix + value: 'Ptr' + - key: readability-identifier-naming.GlobalConstantPointerParameterSuffix + value: 'Ptr' + - key: readability-identifier-naming.GlobalPointerParameterSuffix + value: 'Ptr' + - key: readability-identifier-naming.LocalConstantPointerParameterSuffix + value: 'Ptr' + - key: readability-identifier-naming.LocalPointerParameterSuffix + value: 'Ptr' + - key: readability-identifier-naming.ConstantPointerParameterSuffix + value: 'Ptr' + - key: readability-identifier-naming.PointerParameterSuffix + value: 'Ptr' + - { key: readability-identifier-naming.PrivateMemberHungarianPrefix, value: _ } + - { key: readability-identifier-naming.ProtectedMemberHungarianPrefix, value: _ } + - key: cppcoreguidelines-special-member-functions.AllowImplicitlyDeletedCopyOrMove + value: false \ No newline at end of file diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 77fb12f7..fff2fed2 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -1586,7 +1586,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ReactNativePolygen (0.1.0): + - ReactNativePolygen (0.2.0): - DoubleConversion - glog - hermes-engine @@ -1986,8 +1986,8 @@ SPEC CHECKSUMS: ReactCodegen: 3982de3211b80e119d9daa9cd6af090d75cb8e90 ReactCommon: 6a952e50c2a4b694731d7682aaa6c79bc156e4ad ReactNativeHost: a27bb5af1c4d73dd3e80cc7ce295407f414e0e8c - ReactNativePolygen: 275c42cfd65cc094d7f77295ae109b8a91b206ed - ReactNativeWebAssemblyHost: 4a79eb4721d78db2bff7018ef81c301b4417c742 + ReactNativePolygen: 5236e21326c11204e935ed4e44736b7178c2628c + ReactNativeWebAssemblyHost: cc689d810f1b4540a12906988768c489dcbe9ab3 ReactTestApp-DevSupport: 42abce6b0c88dfb47c86e80aa22831b2abcc3144 ReactTestApp-Resources: 7db90c026cccdf40cfa495705ad436ccc4d64154 RNGestureHandler: 0e5ae8d72ef4afb855e98dcdbe60f27d938abe13 diff --git a/biome.json b/biome.json index ef367ae9..f86c6b42 100644 --- a/biome.json +++ b/biome.json @@ -13,6 +13,7 @@ "**/.turbo", "docs/out", "**/.next", + "test-runner/", ".changeset/config.json", ".yarn" ] @@ -65,7 +66,8 @@ "useCollapsedElseIf": "off", "useConsistentBuiltinInstantiation": "warn", "useDefaultSwitchClause": "off", - "useSingleVarDeclarator": "off" + "useSingleVarDeclarator": "off", + "useImportType": "warn" }, "suspicious": { "noCatchAssign": "warn", diff --git a/docs/app/docs/layout.tsx b/docs/app/docs/layout.tsx index 5ba26c50..ed892dba 100644 --- a/docs/app/docs/layout.tsx +++ b/docs/app/docs/layout.tsx @@ -1,6 +1,6 @@ import { baseOptions } from '@/app/layout.config'; import { source } from '@/lib/source'; -import { DocsLayout, DocsLayoutProps } from 'fumadocs-ui/layouts/notebook'; +import { DocsLayout, type DocsLayoutProps } from 'fumadocs-ui/layouts/notebook'; import type { ReactNode } from 'react'; const docsOptions: DocsLayoutProps = { diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx index 071db2ba..c8c5fa1d 100644 --- a/docs/app/layout.tsx +++ b/docs/app/layout.tsx @@ -1,6 +1,6 @@ import './globals.css'; import { RootProvider } from 'fumadocs-ui/provider'; -import { Metadata } from 'next'; +import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import type { ReactNode } from 'react'; diff --git a/docs/components/DocTable.tsx b/docs/components/DocTable.tsx index 57dcafdf..b389730d 100644 --- a/docs/components/DocTable.tsx +++ b/docs/components/DocTable.tsx @@ -1,6 +1,6 @@ import fs from 'node:fs/promises'; import { - FunctionSymbolDocumentation, + type FunctionSymbolDocumentation, generateDocumentation, } from '@/lib/ts-documentation'; import { cva } from 'class-variance-authority'; diff --git a/docs/components/PrettyCard.tsx b/docs/components/PrettyCard.tsx index d60ccd90..1b4ac2f2 100644 --- a/docs/components/PrettyCard.tsx +++ b/docs/components/PrettyCard.tsx @@ -1,5 +1,5 @@ import Link from 'fumadocs-core/link'; -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; import { twMerge as cn } from 'tailwind-merge'; export type PrettyCardProps = HTMLAttributes & { diff --git a/docs/components/ProsConsOverview.tsx b/docs/components/ProsConsOverview.tsx index 88bb3ad1..03da6dc3 100644 --- a/docs/components/ProsConsOverview.tsx +++ b/docs/components/ProsConsOverview.tsx @@ -1,5 +1,5 @@ import { ThumbsDown, ThumbsUp } from 'lucide-react'; -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; import { twMerge as cn } from 'tailwind-merge'; export type ProsConsOverviewProps = HTMLAttributes & { diff --git a/packages/codegen/src/codegen-pipeline.ts b/packages/codegen/src/codegen-pipeline.ts new file mode 100644 index 00000000..1ab4dffd --- /dev/null +++ b/packages/codegen/src/codegen-pipeline.ts @@ -0,0 +1,12 @@ +import { cocoapods } from './pipeline/react-native/cocoapods.js'; +import { metroResolver } from './pipeline/react-native/metro.js'; +import { reactNativeTurboModule } from './pipeline/react-native/turbomodule.js'; +import { embedWasmRuntime } from './pipeline/wasm2c-runtime.js'; +import type { Plugin } from './plugin.js'; + +export const DEFAULT_PLUGINS: Plugin[] = [ + cocoapods(), + metroResolver(), + reactNativeTurboModule(), + embedWasmRuntime(), +]; diff --git a/packages/codegen/src/generate.ts b/packages/codegen/src/codegen.ts similarity index 57% rename from packages/codegen/src/generate.ts rename to packages/codegen/src/codegen.ts index 8c58f2c5..e98cd791 100644 --- a/packages/codegen/src/generate.ts +++ b/packages/codegen/src/codegen.ts @@ -1,18 +1,22 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { PolygenModuleConfig } from '@callstack/polygen-config'; -import { Project, resolvePathToModule } from '@callstack/polygen-project'; -import { W2CModuleContext } from './context/context.js'; -import { W2CImportedModule, W2CSharedContext } from './context/index.js'; -import { generateHostModuleBridge } from './generators/host.js'; -import { generateImportedModuleBridge } from './generators/import-bridge.js'; +import type { PolygenModuleConfig } from '@callstack/polygen-config'; +import type { Project, ResolvedModule } from '@callstack/polygen-project'; +import { DEFAULT_PLUGINS } from './codegen-pipeline.js'; +import { CodegenContext } from './codegen/context.js'; +import type { W2CExternModule, W2CGeneratedModule } from './codegen/modules.js'; import { generateModuleExportsBridge } from './generators/module-bridge.js'; -import { generateWasmJSModuleSource } from './generators/wasm-module.js'; import { OutputGenerator, - WrittenFilesMap, + type WrittenFilesMap, } from './helpers/output-generator.js'; +import type { + HostProjectGeneratedContext, + ModuleGeneratedContext, + Plugin, +} from './plugin.js'; +import * as templates from './templates/library/index.js'; export { FileExternallyChangedError, @@ -29,7 +33,7 @@ const ASSETS_DIR = path.join( /** * Options used to change W2CGenerator behavior. */ -export interface W2CGeneratorOptions { +export interface CodegenOptions { /** * Path to output directory where generated files will be created. * @@ -67,15 +71,20 @@ export interface W2CGeneratorOptions { generateMetadata?: boolean; } +export interface PluginDispatchOptions { + beforePluginDispatch?: (plugin: Plugin) => Promise | unknown; + afterPluginDispatch?: (plugin: Plugin) => Promise | unknown; +} + /** * Main generator class for WebAssembly to C code generation. * * This class is responsible for generating C code from WebAssembly modules, and * contains all state and configuration necessary for the generation process. * - * The generator is not reenterant, and should be used only once. + * The generator is not reentrant, and should be used only once. */ -export class W2CGenerator { +export class Codegen { /** * The project configuration to generate code for. */ @@ -84,19 +93,24 @@ export class W2CGenerator { /** * Configuration options for the generation process. */ - public readonly options: W2CGeneratorOptions; + public readonly options: CodegenOptions; /** - * Output generator instance used to write generated files to disk. + * Shared context for all modules. + * + * Holds codegen state and module information. */ - public readonly generator: OutputGenerator; + public readonly context: CodegenContext; /** - * List of generated modules. + * Collection of plugins to dispatch. */ - public readonly generatedModules: W2CModuleContext[] = []; + public plugins: Plugin[]; - private readonly resolvedPackages = new Map(); + /** + * Output generator instance used to write generated files to disk. + */ + private readonly generator: OutputGenerator; /** * Creates a new W2CGenerator instance for the specified project and options. @@ -105,11 +119,14 @@ export class W2CGenerator { */ private constructor( project: Project, - options: W2CGeneratorOptions = {}, - previousWrittenFiles?: WrittenFilesMap + options: CodegenOptions = {}, + previousWrittenFiles?: WrittenFilesMap, + plugins?: Plugin[] ) { this.project = project; this.options = options; + this.context = new CodegenContext(); + this.plugins = plugins ?? DEFAULT_PLUGINS; this.generator = new OutputGenerator( { outputDirectory: this.outputDirectory, @@ -128,11 +145,12 @@ export class W2CGenerator { */ public static async create( project: Project, - options: W2CGeneratorOptions = {} - ): Promise { + options: CodegenOptions = {} + ): Promise { let previouslyWrittenFiles: WrittenFilesMap | undefined; try { - const outputDir = options.outputDirectory ?? project.fullOutputDirectory; + const outputDir = + options.outputDirectory ?? project.paths.fullOutputDirectory; const previousMap = await fs.readFile( path.join(outputDir, 'polygen-output.json'), { encoding: 'utf-8' } @@ -144,14 +162,16 @@ export class W2CGenerator { } } - return new W2CGenerator(project, options, previouslyWrittenFiles); + return new Codegen(project, options, previouslyWrittenFiles); } /** * Path to output directory where generated files will be created. */ public get outputDirectory(): string { - return this.options.outputDirectory ?? this.project.fullOutputDirectory; + return ( + this.options.outputDirectory ?? this.project.paths.fullOutputDirectory + ); } /** @@ -161,70 +181,53 @@ export class W2CGenerator { * * @param module The module configuration to generate code for. */ - async generateModule(module: PolygenModuleConfig): Promise { - // Resolve path to module - const resolvedPathToModule = await resolvePathToModule( - this.project, - module - ); - - if (module.kind === 'external') { - const packagePath = resolvedPathToModule.replace(module.path, ''); - const packagePathRelativeToProject = path.relative( - this.project.projectRoot, - packagePath - ); - this.resolvedPackages.set( - module.packageName, - packagePathRelativeToProject - ); - } - - const moduleContents = await fs.readFile(resolvedPathToModule, { - encoding: null, - }); - - const moduleContext = new W2CModuleContext( - moduleContents.buffer as ArrayBuffer, - resolvedPathToModule - ); + async generateModule( + module: ResolvedModule, + options: PluginDispatchOptions = {} + ): Promise<[W2CGeneratedModule, W2CExternModule[]]> { + const [moduleContext, imports] = await this.context.addModule(module); const generator = this.generator.forPath( this.outputPathForModule(module, moduleContext.name) ); await generateModuleExportsBridge(generator, moduleContext, { - renderMetadata: this.options.generateMetadata, forceGenerate: this.options.forceGenerate, }); - this.generatedModules.push(moduleContext); - await this.generateWasmJSModule(module, resolvedPathToModule); + const pluginContext: ModuleGeneratedContext = { + codegen: this, + moduleOutput: generator, + context: moduleContext, + module, + }; + + for (const plugin of this.plugins) { + await options.beforePluginDispatch?.(plugin); + await plugin.moduleGenerated?.(pluginContext); + await options.afterPluginDispatch?.(plugin); + } - return moduleContext; + return [moduleContext, imports]; } /** - * Generates a JavaScript module file for a given WebAssembly (.wasm) module. - * The generated module will be placed under the project's output directory. + * Generates the imported module by creating necessary files and configurations in the specified output directory. * - * @param module - The WebAssembly (.wasm) module that needs to be processed. - * @param resolvedPath - The resolved path to the WebAssembly module file. - * @return A promise that resolves when the JavaScript module file is successfully created. + * @param module The imported module data that needs to be processed and generated. + * @return A promise that resolves when the module generation process is completed. */ - async generateWasmJSModule( - module: PolygenModuleConfig, - resolvedPath: string - ) { - const cleanFileName = path.basename(module.path, '.wasm'); - const dirnameInModule = path.dirname(module.path); - const generatedModulePath = path.join( - module.kind === 'external' ? `${module.packageName}` : '#local', - dirnameInModule, - `${cleanFileName}.js` - ); - const source = await generateWasmJSModuleSource(resolvedPath); + async generateImportedModule(module: W2CExternModule) { + const generator = this.hostModuleOutput; + await generator.writeAllTo({ + [`${module.name}-imports.h`]: templates.buildImportBridgeHeader(module), + [`${module.name}-imports.cpp`]: templates.buildImportBridgeSource(module), + }); + } - const generator = this.generator.forPath('modules'); - await generator.writeTo(generatedModulePath, source); + /** + * Returns the output generator for the host module. + */ + get hostModuleOutput(): OutputGenerator { + return this.generator.forPath(UMBRELLA_PROJECT_NAME); } /** @@ -233,20 +236,17 @@ export class W2CGenerator { * @param context The shared context containing module information and configurations. * @return A promise that resolves with the results of generating bridges for imported modules. Each promise result contains the status of the operation (fulfilled or rejected). */ - async generateHostModule(context: W2CSharedContext) { - const generator = this.generator.forPath(UMBRELLA_PROJECT_NAME); - await generateHostModuleBridge(generator, context.modules); - } + async generateHostModule() { + const pluginContext: HostProjectGeneratedContext = { + codegen: this, + rootOutput: this.generator, + projectOutput: this.hostModuleOutput, + generatedModules: this.context.modules, + }; - /** - * Generates the imported module by creating necessary files and configurations in the specified output directory. - * - * @param module The imported module data that needs to be processed and generated. - * @return A promise that resolves when the module generation process is completed. - */ - async generateImportedModule(module: W2CImportedModule) { - const generator = this.generator.forPath(UMBRELLA_PROJECT_NAME); - await generateImportedModuleBridge(generator.forPath(`imports`), module); + for (const plugin of this.plugins) { + await plugin.hostProjectGenerated?.(pluginContext); + } } /** @@ -256,7 +256,6 @@ export class W2CGenerator { const generatedMapPath = this.generator.outputPathTo('polygen-output.json'); const contents = { files: this.generator.writtenFiles, - externalPackages: Object.fromEntries(this.resolvedPackages.entries()), }; await fs.writeFile( diff --git a/packages/codegen/src/codegen/context.ts b/packages/codegen/src/codegen/context.ts new file mode 100644 index 00000000..b87b7d65 --- /dev/null +++ b/packages/codegen/src/codegen/context.ts @@ -0,0 +1,112 @@ +import fs from 'node:fs/promises'; +import type { ResolvedModule } from '@callstack/polygen-project'; +import { Module } from '@callstack/wasm-parser'; +import { computeChecksumBuffer } from '../helpers/checksum.js'; +import { W2CExternModule, W2CGeneratedModule } from './modules.js'; +import type { ResolvedModuleImport } from './types.js'; +import { buildGeneratedSymbol } from './utils.js'; + +/** + * Holds aggregate codegen state. + * + * Contains methods to progressively build the codegen state. + */ +export class CodegenContext { + /** + * List of generated modules + */ + public modules: W2CGeneratedModule[] = []; + + /** + * List of imported modules + * + * Generated by processing imports of generated modules + */ + public importedModules: Map = new Map(); + + /** + * Adds a module to the context. + * + * This method should be used to add new modules to the context. + * Before creating a new W2CModule, module body is scanned for imports and + * all imported modules are added to the context into `importedModules`. + * + * This way, when creating the module, all imported modules can be resolved. + */ + public async addModule( + module: ResolvedModule + ): Promise<[W2CGeneratedModule, W2CExternModule[]]> { + const moduleContents = await fs.readFile(module.resolvedPath, { + encoding: null, + }); + const checksum = computeChecksumBuffer(moduleContents.buffer); + const moduleBody = new Module(moduleContents.buffer as ArrayBuffer); + const importedModules = this.processImportedModules(moduleBody); + + const generatedModule = new W2CGeneratedModule( + this, + moduleBody, + checksum, + module.resolvedPath + ); + this.modules.push(generatedModule); + + return [generatedModule, importedModules]; + } + + /** + * Gets an imported module by name. + * + * If the module is not found, an error is thrown. + * + * @param name Name of the imported module + */ + public getImportedModule(name: string): W2CExternModule { + const externModule = this.importedModules.get(name); + if (!externModule) { + throw new Error(`Imported Module ${name} not found`); + } + return externModule; + } + + /** + * Gets or creates an imported module by name. + * + * If the module is not found, an empty one is created. + * + * @param name Name of the imported module + */ + public getOrCreatedImportedModule(name: string): W2CExternModule { + let externModule = this.importedModules.get(name); + if (!externModule) { + externModule = new W2CExternModule(name); + this.importedModules.set(name, externModule); + } + + return externModule; + } + + public resolveImport( + moduleName: string, + symbolName: string + ): ResolvedModuleImport | undefined { + return this.getImportedModule(moduleName)?.exports.get(symbolName); + } + + private processImportedModules(body: Module): W2CExternModule[] { + const importsByModuleName = Object.groupBy(body.imports, (el) => el.module); + + return Object.entries(importsByModuleName).map(([moduleName, imports]) => { + const module = this.getOrCreatedImportedModule(moduleName); + + const resolvedSymbols = (imports ?? []).map((el) => + buildGeneratedSymbol(module, el) + ); + for (const resolvedSymbol of resolvedSymbols) { + module.addSymbol(resolvedSymbol); + } + + return module; + }); + } +} diff --git a/packages/codegen/src/codegen/modules.ts b/packages/codegen/src/codegen/modules.ts new file mode 100644 index 00000000..f56a8a15 --- /dev/null +++ b/packages/codegen/src/codegen/modules.ts @@ -0,0 +1,225 @@ +import path from 'node:path'; +import type { Module, ModuleMemory, ModuleTable } from '@callstack/wasm-parser'; +import { mangleModuleName } from '../wasm2c/mangle.js'; +import type { CodegenContext } from './context.js'; +import type { + GeneratedModuleFunction, + GeneratedSymbol, + ResolvedModuleImport, +} from './types.js'; +import { buildGeneratedSymbol } from './utils.js'; + +/** + * Base class for all modules types. + * + * There are two types of modules in the codegen: + * - `W2CExternModule` - represents an imported module + * - `W2CGeneratedModule` - represents a generated module wrapping a WebAssembly module. + */ +export abstract class W2CModuleBase { + /** + * Original name of the imported module. + * + * If module name is not passed, it defaults to module file name without the extension. + * + * Unsafe to use as a symbol in source code, use `mangledName` instead. + */ + public readonly name: string; + + /** + * Mangled name of the imported module. + * + * Safe to use in source code as a symbol name. + */ + public readonly mangledName: string; + + public constructor(name: string) { + this.name = name; + this.mangledName = mangleModuleName(name); + } + + /** + * Gets the C typename that was generated by wasm2c for this imported module. + */ + get generatedContextTypeName(): string { + return `w2c_${this.mangledName}`; + } +} + +/** + * Represents an imported module in the shared context. + * + * This represents an imported module with imported symbols across all generated modules. + * + * Modules of this type have no implementation in WASM file, but the code for those modules + * must be generated to allow defining them from JS code. + */ +export class W2CExternModule extends W2CModuleBase { + /** + * All symbols required to be provided. + * + * Generated based on imports of this module from another modules. + */ + public readonly exports: Map; + + public constructor(name: string) { + super(name); + this.exports = new Map(); + } + + /** + * Gets the name of the field in the root context struct. + * + * Each generated module that imports this module contains a field that holds + * the imported module instance. + */ + public get generatedRootContextFieldName(): string { + return `import_${this.mangledName}Ctx`; + } + + public addSymbol(symbol: GeneratedSymbol) { + this.exports.set(symbol.localName, symbol); + } +} + +/** + * Represents a generated module wrapping a WebAssembly module. + * + * This class is created for each WebAssembly module that is processed by the `wasm2c` tool. + */ +export class W2CGeneratedModule extends W2CModuleBase { + /** + * Path to the file that the metadata was loaded from. + */ + public readonly sourceModulePath: string; + + /** + * SHA-256 checksum of module contents + */ + public readonly checksum: Buffer; + public readonly generatedClassName: string; + + /** + * Parsed WebAssembly module instance. + */ + public readonly body: Module; + + /** + * Map of module imports + */ + public readonly moduleImports: Record; + + /** + * Collection of generated module imports made by `wasm2c` tool. + */ + public readonly imports: ResolvedModuleImport[]; + + /** + * Collection of generated module exports made by `wasm2c` tool. + */ + public readonly exports: GeneratedSymbol[]; + + constructor( + context: CodegenContext, + body: Module, + checksum: Buffer, + sourceModulePath: string + ) { + const name = path.basename(sourceModulePath, '.wasm'); + super(name); + + this.body = body; + this.sourceModulePath = sourceModulePath; + this.generatedClassName = capitalize(mangleModuleName(name)); + this.checksum = checksum; + this.moduleImports = processImportedModulesInfo(this.body, context); + this.imports = resolveImports(context, this.body); + this.exports = processExports(this, this.body); + } + + /** + * Retrieves a reversed array of all imported modules. + * + * @return An array containing the imported modules in reverse order. + */ + public get importedModules() { + return Object.values(this.moduleImports).toSorted((a, b) => + a.name.localeCompare(b.name) + ); + } + + /** + * Retrieves an array of all exported memories. + */ + public get exportedMemories(): GeneratedSymbol[] { + return this.exports.filter( + (i) => i.target.kind === 'memory' + ) as GeneratedSymbol[]; + } + + /** + * Retrieves an array of all exported functions. + */ + public get exportedFunctions(): GeneratedSymbol[] { + return this.exports.filter( + (i) => i.target.kind === 'function' + ) as GeneratedSymbol[]; + } + + /** + * Retrieves an array of all exported tables. + */ + public get exportedTables(): GeneratedSymbol[] { + return this.exports.filter( + (i) => i.target.kind === 'table' + ) as GeneratedSymbol[]; + } + /** + * Name of the function that creates a new instance of the module. + */ + public get moduleFactoryFunctionName(): string { + return `create${this.generatedClassName}Module`; + } + + /** + * Name of the class that represents the module context. + */ + public get contextClassName(): string { + return `${this.generatedClassName}ModuleContext`; + } +} + +function processImportedModulesInfo( + module: Module, + codegenContext: CodegenContext +) { + const importedModuleNames = new Set( + module.imports.values().map((i) => i.module) + ); + + const importsInfo = importedModuleNames + .values() + .map((moduleName): [string, W2CExternModule] => [ + moduleName, + codegenContext.getImportedModule(moduleName), + ]); + + return Object.fromEntries(importsInfo); +} + +function resolveImports(codegenContext: CodegenContext, moduleBody: Module) { + return moduleBody.imports.map( + (el): ResolvedModuleImport => + codegenContext.resolveImport(el.module, el.name)! + ); +} + +function processExports(module: W2CGeneratedModule, moduleBody: Module) { + return moduleBody.exports.map( + (el): GeneratedSymbol => buildGeneratedSymbol(module, el) + ); +} + +function capitalize(name: string) { + return name.charAt(0).toUpperCase() + name.slice(1); +} diff --git a/packages/codegen/src/codegen/types.ts b/packages/codegen/src/codegen/types.ts new file mode 100644 index 00000000..f8f71f67 --- /dev/null +++ b/packages/codegen/src/codegen/types.ts @@ -0,0 +1,41 @@ +import type { + ModuleGlobal, + ModuleMemory, + ModuleTable, + ResultType, +} from '@callstack/wasm-parser'; +import type { W2CModuleBase } from './modules.js'; + +/** + * Information about an exported WASM symbol generated by wasm2c. + */ +export interface GeneratedSymbol { + module: W2CModuleBase; + localName: string; + mangledLocalName: string; + functionSymbolAccessorName: string; + target: T; +} + +/** + * Information about a generated function + */ +export interface GeneratedModuleFunction { + kind: 'function'; + parameterTypeNames: string[]; + parametersTypes: ResultType; + resultTypes: ResultType; + returnTypeName: string; +} + +export type GeneratedEntity = + | GeneratedModuleFunction + | ModuleGlobal + | ModuleMemory + | ModuleTable; + +export type ResolvedModuleImport< + TSymbol extends GeneratedEntity = GeneratedEntity, +> = GeneratedSymbol; + +export type GeneratedEntityKind = GeneratedEntity['kind']; diff --git a/packages/codegen/src/codegen/utils.ts b/packages/codegen/src/codegen/utils.ts new file mode 100644 index 00000000..376d0ead --- /dev/null +++ b/packages/codegen/src/codegen/utils.ts @@ -0,0 +1,115 @@ +import type { + ModuleEntity, + ModuleExport, + ModuleGlobal, + ModuleImport, + ModuleMemory, + ModuleTable, + ValueType, +} from '@callstack/wasm-parser'; +import { mangleName } from '../wasm2c/mangle.js'; +import type { W2CModuleBase } from './modules.js'; +import type { + GeneratedEntity, + GeneratedModuleFunction, + GeneratedSymbol, +} from './types.js'; + +export function mangleSymbolName(name: string, mangledModule: string) { + return `w2c_${mangledModule}_${mangleName(name)}`; +} + +/** + * Creates GeneratedEntity from ModuleEntity + * + * Only functions are augmented at the moment. + * + * @param entity Raw ModuleEntity + */ +export function processEntity(entity: ModuleEntity): GeneratedEntity { + switch (entity.kind) { + case 'function': + const returnType = matchW2CRType(entity.resultTypes[0]); + return { + ...entity, + parameterTypeNames: entity.parametersTypes.map(matchW2CRType), + returnTypeName: returnType, + }; + default: + return entity; + } +} + +/** + * Builds a GeneratedSymbol from ModuleImport or ModuleExport. + * + * @param module + * @param entity + */ +export function buildGeneratedSymbol( + module: W2CModuleBase, + entity: ModuleImport | ModuleExport +): GeneratedSymbol { + const accessorName = mangleSymbolName(entity.name, module.mangledName); + + return { + localName: entity.name, + mangledLocalName: mangleName(entity.name), + functionSymbolAccessorName: accessorName, + module, + target: processEntity(entity.target), + }; +} + +class SymbolMatchError extends Error { + constructor(public kind: string) { + super(`Unknown import type ${kind}`); + } +} + +export interface SymbolMatchHandler { + func: (func: GeneratedSymbol) => TResult; + global: (global: GeneratedSymbol) => TResult; + table: (table: GeneratedSymbol) => TResult; + memory: (memory: GeneratedSymbol) => TResult; +} + +/** + * Matches a GeneratedSymbol to a handler based on its kind. + * + * Throws `SymbolMatchError` if the kind is unknown. + * + * @param symbol Symbol to match + * @param matcher Handler to match the symbol based on its type + */ +export function matchSymbol( + symbol: GeneratedSymbol, + matcher: SymbolMatchHandler +): TResult { + switch (symbol.target.kind) { + case 'function': + return matcher.func(symbol as GeneratedSymbol); + case 'global': + return matcher.global(symbol as GeneratedSymbol); + case 'memory': + return matcher.memory(symbol as GeneratedSymbol); + case 'table': + return matcher.table(symbol as GeneratedSymbol); + default: + // @ts-ignore + throw new SymbolMatchError(symbol.target.kind); + } +} + +// TODO: workaround a bug(?) that all types are returned as either none, u32 or f64 +export function matchW2CRType(t?: ValueType): string { + if (!t) { + return 'void'; + } + + // TODO: figure out why wasm2c returns u32 for number types sometimes + if (t.startsWith('i')) { + return t.replace(/^i/, 'u'); + } + return t; +} diff --git a/packages/codegen/src/context/codegen-context.ts b/packages/codegen/src/context/codegen-context.ts deleted file mode 100644 index c234edaa..00000000 --- a/packages/codegen/src/context/codegen-context.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Module, type ModuleMemory, ModuleTable } from '@callstack/wasm-parser'; -import type { ModuleFunction, ValueType } from '@callstack/wasm-parser'; -import { mangleModuleName, mangleName } from '../mangle.js'; -import type { - GeneratedExport, - GeneratedFunctionExport, - GeneratedFunctionInfo, - GeneratedImport, -} from '../types.js'; - -type ImportMap = Record; - -/** - * The `W2CModuleCodegenContext` class is responsible for maintaining the state and metadata - * related to the code generation process of a WebAssembly (WASM) module using the `wasm2c` tool. - * This class stores information about module imports and exports, and provides utilities - * for handling the generated module code. - */ -export class W2CModuleCodegenContext { - /** - * Collection of imported modules and their mangled names. - */ - public readonly moduleImports: ImportMap = {}; - - /** - * Mangled module name, safe to use in source code as a symbol name. - */ - public readonly mangledName: string; - - /** - * Collection of generated module imports made by `wasm2c` tool. - */ - public readonly imports: GeneratedImport[]; - - /** - * Collection of generated module exports made by `wasm2c` tool. - */ - public readonly exports: GeneratedExport[]; - - constructor(name: string, module: Module) { - this.mangledName = mangleModuleName(name); - this.moduleImports = processImportedModulesInfo(module); - this.imports = processImports(this, module); - this.exports = processExports(this, module); - } - - /** - * Gets a C typename that was generated by wasm2c for this module. - */ - public get generatedContextTypeName(): string { - return `w2c_${this.mangledName}`; - } - - /** - * Retrieves a reversed array of all imported modules. - * - * @return An array containing the imported modules in reverse order. - */ - public get importedModules() { - return Object.values(this.moduleImports).toSorted((a, b) => - a.name.localeCompare(b.name) - ); - } - - /** - * Retrieves an array of all exported memories. - */ - public get exportedMemories(): GeneratedExport[] { - return this.exports.filter( - (i) => i.target.kind === 'memory' - ) as GeneratedExport[]; - } - - /** - * Retrieves an array of all exported functions. - */ - public get exportedFunctions(): GeneratedFunctionExport[] { - return this.exports.filter( - (i) => i.target.kind === 'function' - ) as GeneratedFunctionExport[]; - } - - /** - * Retrieves an array of all exported tables. - */ - public get exportedTables(): GeneratedExport[] { - return this.exports.filter( - (i) => i.target.kind === 'table' - ) as GeneratedExport[]; - } -} - -/** - * Represents a WebAssembly to C (W2C) imported module with details about its name and mangled name. - * - * This class provides functionalities to handle and retrieve the context type - * and root context field names for the imported module based on its mangled name. - * - * @remarks The mangled name is derived from the original module name through a specific mangle transformation. - */ -export class W2CCodegenLocalImportedModule { - /** - * Original name of the imported module. - */ - public readonly name: string; - - /** - * Mangled name of the imported module. - */ - public readonly mangledName: string; - - constructor(name: string) { - this.name = name; - this.mangledName = mangleModuleName(name); - } - - /** - * Gets the C typename that was generated by wasm2c for this imported module. - */ - get generatedContextTypeName(): string { - return `w2c_${this.mangledName}`; - } - - /** - * Gets the name of field of root context struct, that was generated by wasm2c for this imported module. - */ - get generatedRootContextFieldName(): string { - return `import_${this.mangledName}Ctx`; - } -} - -function processImportedModulesInfo(module: Module) { - const importedModuleNames = new Set( - module.imports.values().map((i) => i.module) - ); - - const importsInfo = importedModuleNames - .values() - .map((name): [string, W2CCodegenLocalImportedModule] => [ - name, - new W2CCodegenLocalImportedModule(name), - ]); - - return Object.fromEntries(importsInfo); -} - -function processImports(context: W2CModuleCodegenContext, module: Module) { - return module.imports.map((el): GeneratedImport => { - const importInfo = context.moduleImports[el.module]; - if (!importInfo) { - throw new Error( - `Assert error: could not get import info for ${el.module}` - ); - } - - const generatedFunctionName = mangleFunction( - el.name, - importInfo.mangledName - ); - - switch (el.target.kind) { - case 'function': - return { - ...el, - ...buildGeneratedFunctionInfo(el.target), - mangledName: mangleName(el.name), - moduleInfo: importInfo, - generatedFunctionName, - }; - default: - return { - ...el, - mangledName: mangleName(el.name), - moduleInfo: importInfo, - generatedFunctionName, - }; - } - }); -} - -function processExports(context: W2CModuleCodegenContext, module: Module) { - return module.exports.map((el): GeneratedExport => { - const mangledName = mangleName(el.name); - const generatedFunctionName = mangleFunction(el.name, context.mangledName); - - switch (el.target.kind) { - case 'function': - return { - ...el, - ...buildGeneratedFunctionInfo(el.target), - generatedFunctionName, - mangledName, - }; - default: - return { - ...el, - mangledName, - generatedFunctionName, - }; - } - }); -} - -function mangleFunction(name: string, mangledModule: string) { - return `w2c_${mangledModule}_${mangleName(name)}`; -} - -function buildGeneratedFunctionInfo( - func: ModuleFunction -): GeneratedFunctionInfo { - const returnType = matchW2CRType(func.resultTypes[0]); - - return { - parameterTypeNames: func.parametersTypes.map(matchW2CRType), - returnTypeName: returnType, - }; -} - -// TODO: workaround a bug(?) that all types are returned as either none, u32 or f64 -function matchW2CRType(t?: ValueType): string { - if (!t) { - return 'void'; - } - - // TODO: figure out why wasm2c returns u32 for number types sometimes - if (t.startsWith('i')) { - return t.replace(/^i/, 'u'); - } - return t; -} diff --git a/packages/codegen/src/context/context.ts b/packages/codegen/src/context/context.ts deleted file mode 100644 index d5bf296d..00000000 --- a/packages/codegen/src/context/context.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as path from 'node:path'; -import { Module } from '@callstack/wasm-parser'; -import { computeChecksumBuffer } from '../helpers/checksum.js'; -import { W2CModuleCodegenContext } from './codegen-context.js'; -import { W2CModuleTurboModuleContext } from './turbomodule-context.js'; - -/** - * Represents the context of a WebAssembly module, providing metadata such as - * its name, file path, and checksum. - */ -export class W2CModuleContext { - /** - * Name of the module, based on the filename. - * - * Unsafe to use as a symbol in source code, use `mangledName` instead. - */ - public readonly name: string; - - /** - * Path to the file that the metadata was loaded from. - */ - public readonly sourceModulePath: string; - - /** - * SHA-256 checksum of module contents - */ - public readonly checksum: Buffer; - - /** - * The parsed WebAssembly module. - */ - public readonly module: Module; - - /** - * Context for the wasm2c C code generation. - */ - public readonly codegen: W2CModuleCodegenContext; - - /** - * Context for the TurboModule code generation. - */ - public readonly turboModule: W2CModuleTurboModuleContext; - - constructor(moduleBuffer: ArrayBuffer, sourceModulePath: string) { - this.name = path.basename(sourceModulePath, '.wasm'); - this.module = new Module(moduleBuffer); - this.sourceModulePath = sourceModulePath; - this.checksum = computeChecksumBuffer(moduleBuffer); - this.codegen = new W2CModuleCodegenContext(this.name, this.module); - this.turboModule = new W2CModuleTurboModuleContext(this.name); - } -} diff --git a/packages/codegen/src/context/index.ts b/packages/codegen/src/context/index.ts deleted file mode 100644 index adcdc40d..00000000 --- a/packages/codegen/src/context/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './codegen-context.js'; -export * from './turbomodule-context.js'; -export * from './context.js'; -export * from './shared.js'; diff --git a/packages/codegen/src/context/shared.ts b/packages/codegen/src/context/shared.ts deleted file mode 100644 index 1305daa8..00000000 --- a/packages/codegen/src/context/shared.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { GeneratedImport } from '../types.js'; -import { W2CCodegenLocalImportedModule } from './codegen-context.js'; -import { W2CModuleContext } from './context.js'; - -/** - * Represents an imported module in the shared context. - * - * This represents an imported module with imported symbols across all generated modules. - */ -export class W2CImportedModule { - /** - * Information about the imported module - */ - private readonly moduleInfo: W2CCodegenLocalImportedModule; - - /** - * All imported symbols from this module, across all generated modules. - */ - public readonly imports: GeneratedImport[]; - - constructor( - moduleInfo: W2CCodegenLocalImportedModule, - imports: GeneratedImport[] - ) { - this.moduleInfo = moduleInfo; - this.imports = imports; - } - - /** - * Name of the imported module. - */ - public get name(): string { - return this.moduleInfo.name; - } - - /** - * Mangled name of the imported module. - */ - public get managedName(): string { - return this.moduleInfo.name; - } - - /** - * Gets the C typename that was generated by wasm2c for this imported module. - */ - public get generatedRootContextFieldName(): string { - return this.moduleInfo.generatedRootContextFieldName; - } - - /** - * Gets the name of field of root context struct, that was generated by wasm2c for this imported module. - */ - public get generatedContextTypeName(): string { - return this.moduleInfo.generatedContextTypeName; - } -} - -export class W2CSharedContext { - public readonly modules: W2CModuleContext[]; - public readonly importedModules: W2CImportedModule[]; - - constructor(modules: W2CModuleContext[]) { - this.modules = modules; - this.importedModules = processImportedModules(this.modules); - } -} - -function processImportedModules( - modules: W2CModuleContext[] -): W2CImportedModule[] { - const allImports = modules.flatMap((module) => module.codegen.imports); - - const importsGroupedByName = Object.groupBy( - allImports, - (el) => el.moduleInfo.name - ); - - const nameToInfoMap = Object.groupBy( - allImports.map((el) => el.moduleInfo), - (el) => el.name - ); - - return Object.entries(importsGroupedByName) - .map(([moduleName, imports]) => { - const info = nameToInfoMap[moduleName]?.[0]; - - if (!info) { - throw new Error(`Could not find info for module ${moduleName}`); - } - - return new W2CImportedModule(info, imports ?? []); - }) - .filter(Boolean); -} diff --git a/packages/codegen/src/context/turbomodule-context.ts b/packages/codegen/src/context/turbomodule-context.ts deleted file mode 100644 index 2f57951b..00000000 --- a/packages/codegen/src/context/turbomodule-context.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { mangleModuleName } from '../mangle.js'; - -/** - * W2CModuleTurboModuleContext is a utility class designed to manage the generation of - * class and function names related to React Native TurboModule bridging. - * It provides structured naming conventions for module factory functions and context classes - * based on a given module name. - */ -export class W2CModuleTurboModuleContext { - public readonly generatedClassName: string; - - constructor(name: string) { - this.generatedClassName = capitalize(mangleModuleName(name)); - } - - /** - * Name of the function that creates a new instance of the module. - */ - public get moduleFactoryFunctionName(): string { - return `create${this.generatedClassName}Module`; - } - - /** - * Name of the class that represents the module context. - */ - public get contextClassName(): string { - return `${this.generatedClassName}ModuleContext`; - } -} - -function capitalize(name: string) { - return name.charAt(0).toUpperCase() + name.slice(1); -} diff --git a/packages/codegen/src/generators/host.ts b/packages/codegen/src/generators/host.ts deleted file mode 100644 index 229156e0..00000000 --- a/packages/codegen/src/generators/host.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { W2CModuleContext } from '../context/index.js'; -import { OutputGenerator } from '../helpers/output-generator.js'; -import * as templates from '../templates/host.js'; - -/** - * Generates the host module bridge by creating necessary files - * and copying required assets for the host module. - * - * @param generator - The output generator used for file and asset operations. - * @param modules - An array of WebAssembly-to-C host module contexts to be processed. - * @return A promise that resolves once all files and assets have been processed. - */ -export async function generateHostModuleBridge( - generator: OutputGenerator, - modules: W2CModuleContext[] -) { - await Promise.all([ - generator.copyAsset('wasm-rt'), - generator.writeAllTo({ - 'loader.cpp': templates.buildHostSource(modules), - 'ReactNativeWebAssemblyHost.podspec': templates.buildPodspec(), - }), - ]); -} diff --git a/packages/codegen/src/generators/import-bridge.ts b/packages/codegen/src/generators/import-bridge.ts deleted file mode 100644 index 7e3431d3..00000000 --- a/packages/codegen/src/generators/import-bridge.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { W2CImportedModule } from '../context/index.js'; -import { OutputGenerator } from '../helpers/output-generator.js'; -import * as templates from '../templates/library/index.js'; - -export async function generateImportedModuleBridge( - generator: OutputGenerator, - module: W2CImportedModule -) { - await generator.writeAllTo({ - [`${module.name}-imports.h`]: templates.buildImportBridgeHeader(module), - [`${module.name}-imports.cpp`]: templates.buildImportBridgeSource(module), - }); -} diff --git a/packages/codegen/src/generators/module-bridge.ts b/packages/codegen/src/generators/module-bridge.ts index 5846cbf4..14be43ed 100644 --- a/packages/codegen/src/generators/module-bridge.ts +++ b/packages/codegen/src/generators/module-bridge.ts @@ -1,17 +1,16 @@ import consola from 'consola'; -import type { W2CModuleContext } from '../context/context.js'; -import { OutputGenerator } from '../helpers/output-generator.js'; +import type { W2CGeneratedModule } from '../codegen/modules.js'; +import type { OutputGenerator } from '../helpers/output-generator.js'; import * as templates from '../templates/library/index.js'; -import { generateCSources } from '../wasm2c.js'; +import { generateCSources, getOutputFilesFor } from '../wasm2c/wasm2c.js'; export interface ModuleGeneratorOptions { - renderMetadata?: boolean; forceGenerate?: boolean; } export async function generateModuleExportsBridge( generator: OutputGenerator, - module: W2CModuleContext, + module: W2CGeneratedModule, options: ModuleGeneratorOptions ) { try { @@ -19,7 +18,6 @@ export async function generateModuleExportsBridge( await Promise.allSettled([ generateCSource(generator, module, options), generateJSIBridge(generator, module), - renderMetadata(generator, module), ]); } catch (e) { consola.error(e); @@ -28,11 +26,11 @@ export async function generateModuleExportsBridge( async function generateCSource( generator: OutputGenerator, - module: W2CModuleContext, + module: W2CGeneratedModule, options: ModuleGeneratorOptions ) { const outputPath = generator.outputPathTo(module.name); - const generatedFiles = [`${outputPath}.c`, `${outputPath}.h`]; + const generatedFiles = getOutputFilesFor(module.sourceModulePath, outputPath); return generatingFromModule(generator, module, options, generatedFiles, () => generateCSources(module.sourceModulePath, outputPath) @@ -41,7 +39,7 @@ async function generateCSource( async function generateJSIBridge( generator: OutputGenerator, - module: W2CModuleContext + module: W2CGeneratedModule ) { await generator.writeAllTo({ 'jsi-exports-bridge.h': templates.buildExportBridgeHeader(module), @@ -51,24 +49,9 @@ async function generateJSIBridge( }); } -async function renderMetadata( - generator: OutputGenerator, - module: W2CModuleContext -) { - await generator.writeTo( - `${module.name}.exports.json`, - JSON.stringify(module.codegen.exports, null, 2) - ); - - await generator.writeTo( - `${module.name}.imports.json`, - JSON.stringify(module.codegen.imports, null, 2) - ); -} - async function generatingFromModule( generator: OutputGenerator, - module: W2CModuleContext, + module: W2CGeneratedModule, options: ModuleGeneratorOptions, targets: string[], cb: () => R diff --git a/packages/codegen/src/generators/wasm-module.ts b/packages/codegen/src/generators/wasm-module.ts deleted file mode 100644 index 36bcf6e6..00000000 --- a/packages/codegen/src/generators/wasm-module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import path from 'path'; -import { BinaryWriter, ByteOrder } from '@callstack/polygen-binary-utils'; -import { computeFileChecksumBuffer } from '../helpers/checksum.js'; - -const MAGIC_NUMBER = new TextEncoder().encode('CKWASM'); - -export async function generateWasmJSModuleSource( - pathToModule: string -): Promise { - const cleanName = path.basename(pathToModule, '.wasm'); - const checksumRaw = await computeFileChecksumBuffer(pathToModule); - const checksumHex = new TextEncoder().encode(checksumRaw.toString('hex')); - - const rawName = new TextEncoder().encode(cleanName); - const writer = new BinaryWriter(ByteOrder.LittleEndian); - writer.copyBytes(MAGIC_NUMBER.buffer as ArrayBuffer); - writer.writeUint8(1); - writer.copyBytes(checksumHex.buffer as ArrayBuffer); - writer.writeUint8(0); - writer.writeUint16(rawName.length); - writer.copyBytes(rawName.buffer as ArrayBuffer); - // null terminator just in case - writer.writeUint8(0); - - return ( - `const data = Uint8Array.from(${JSON.stringify([...writer.getWrittenBytes()])});\n` + - `export default data.buffer;` - ); -} diff --git a/packages/codegen/src/helpers/checksum.ts b/packages/codegen/src/helpers/checksum.ts index be00e77e..0e5fb27a 100644 --- a/packages/codegen/src/helpers/checksum.ts +++ b/packages/codegen/src/helpers/checksum.ts @@ -23,7 +23,7 @@ export function computeFileChecksumBuffer(path: string): Promise { * @param data - The data for which the checksum is to be computed, provided as an ArrayBuffer. * @return A Buffer containing the SHA-256 checksum of the input data. */ -export function computeChecksumBuffer(data: ArrayBuffer): Buffer { +export function computeChecksumBuffer(data: ArrayBufferLike): Buffer { const hash = crypto.createHash('sha256'); hash.update(Buffer.from(data)); return hash.digest(); diff --git a/packages/codegen/src/helpers/source-builder.ts b/packages/codegen/src/helpers/source-builder.ts new file mode 100644 index 00000000..0aca418a --- /dev/null +++ b/packages/codegen/src/helpers/source-builder.ts @@ -0,0 +1,55 @@ +type StringMapperFunc = (val: T) => string; +type StringMapper = T extends string + ? StringMapperFunc | undefined + : StringMapperFunc; + +/** + * Helper function to map a value to a string using a mapper function. + * + * @param value + * @param mapper + */ +function mapString(value: T, mapper: StringMapper): string { + return mapper ? mapper(value) : (value as string); +} + +export function toStringLiteral( + value: string, + mapper?: StringMapper +): string; +export function toStringLiteral(value: T, mapper: StringMapper): string; +export function toStringLiteral( + value: T, + mapper: StringMapper +): string { + const strVal = mapString(value, mapper); + return `"${strVal.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; +} + +export function toArgumentList( + values: string[], + mapper?: StringMapper +): string; +export function toArgumentList(values: T[], mapper: StringMapper): string; +export function toArgumentList( + values: T[], + mapper: StringMapper +): string { + return values + .map((el) => mapString(el, mapper)) + .join(', ') + .trimEnd() + .replace(/,$/, ''); +} + +export function toInitializerList( + values: string[], + mapper?: StringMapper +): string; +export function toInitializerList( + values: T[], + mapper: StringMapper +): string; +export function toInitializerList(values: T[], mapper: StringMapper) { + return `{ ${toArgumentList(values, mapper)} }`; +} diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index b12aeb37..b910c744 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -1,3 +1,4 @@ -export * from './types.js'; -export * from './generate.js'; -export * from './context/index.js'; +export * from './codegen/context.js'; +export * from './codegen/modules.js'; +export * from './codegen/types.js'; +export * from './codegen.js'; diff --git a/packages/codegen/src/pipeline/dump-metadata.ts b/packages/codegen/src/pipeline/dump-metadata.ts new file mode 100644 index 00000000..61fb0e96 --- /dev/null +++ b/packages/codegen/src/pipeline/dump-metadata.ts @@ -0,0 +1,25 @@ +import type { Plugin } from '../plugin.js'; + +/** + * Plugin that generates a virtual module for Metro support. + */ +export function dumpMetadata(): Plugin { + return { + name: 'core/dump-metadata', + title: 'Dump Metadata', + + async moduleGenerated({ moduleOutput, context }): Promise { + const exportsPromise = moduleOutput.writeTo( + `${context.name}.exports.json`, + JSON.stringify(context.exports, null, 2) + ); + + const importsPromise = await moduleOutput.writeTo( + `${context.name}.imports.json`, + JSON.stringify(context.imports, null, 2) + ); + + await Promise.all([exportsPromise, importsPromise]); + }, + }; +} diff --git a/packages/codegen/src/pipeline/react-native/cocoapods.ts b/packages/codegen/src/pipeline/react-native/cocoapods.ts new file mode 100644 index 00000000..8e9e2508 --- /dev/null +++ b/packages/codegen/src/pipeline/react-native/cocoapods.ts @@ -0,0 +1,82 @@ +import stripIndent from 'strip-indent'; +import type { Plugin } from '../../plugin.js'; + +/** + * Plugin that generates the ReactNativeWebAssemblyHost podspec. + */ +export function cocoapods(): Plugin { + return { + name: 'core/ios-cocoapods', + title: 'CocoaPods Integration', + + async hostProjectGenerated({ projectOutput }): Promise { + await projectOutput.forPath('@host').writeAllTo({ + 'ReactNativeWebAssemblyHost.podspec': buildPodspecSource(), + }); + }, + }; +} + +/** + * Builds the podspec source for the ReactNativeWebAssemblyHost pod. + */ +function buildPodspecSource() { + return stripIndent( + ` + require "json" + project_dir = Pathname.new(__dir__) + project_dir = project_dir.parent until + File.exist?("#{project_dir}/package.json") || + project_dir.expand_path.to_s == '/' + + package = JSON.parse(File.read(File.join(project_dir, "package.json"))) + + Pod::Spec.new do |s| + s.name = "ReactNativeWebAssemblyHost" + s.version = package["version"] || "0.0.1" + s.summary = package["description"] || "No summary" + s.homepage = package["homepage"] || "no-homepage" + s.license = package["license"] || "Unknown License" + s.authors = package["author"] || "Unknown Author" + if package["repository"] + s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" } + else + s.source = { :git => "Unknown Source", :tag => "master" } + end + + s.platforms = { :ios => min_ios_version_supported } + s.source_files = "*.{h,hpp,c,cpp}", "**/*.{h,hpp,c,cpp}" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\\"$(PODS_ROOT)/boost\\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. + # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. + if respond_to?(:install_modules_dependencies, true) + install_modules_dependencies(s) + else + s.dependency "React-Core" + + # Don't install the dependencies when we run \`pod install\` in the old architecture. + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1 -O3" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\\"$(PODS_ROOT)/boost\\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + s.dependency "React-Codegen" + s.dependency "RCT-Folly" + s.dependency "RCTRequired" + s.dependency "RCTTypeSafety" + s.dependency "ReactCommon/turbomodule/core" + end + end + + s.exclude_files = "**/FBReactNativeSpec-generated.mm", "**/RCTModulesConformingToProtocolsProvider.mm" + end + ` + ).trimStart(); +} diff --git a/packages/codegen/src/pipeline/react-native/cpp-turbomodule.ts b/packages/codegen/src/pipeline/react-native/cpp-turbomodule.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/codegen/src/pipeline/react-native/metro.ts b/packages/codegen/src/pipeline/react-native/metro.ts new file mode 100644 index 00000000..05cf2cda --- /dev/null +++ b/packages/codegen/src/pipeline/react-native/metro.ts @@ -0,0 +1,68 @@ +import path from 'path'; +import { BinaryWriter, ByteOrder } from '@callstack/polygen-binary-utils'; +import { computeFileChecksumBuffer } from '../../helpers/checksum.js'; +import type { Plugin } from '../../plugin.js'; + +const MAGIC_NUMBER = new TextEncoder().encode('CKWASM'); + +/** + * Plugin that generates a virtual module for Metro support. + */ +export function metroResolver(): Plugin { + return { + name: 'core/metro-resolver', + title: 'Metro Integration', + + /** + * Generates a JavaScript module file for a given WebAssembly (.wasm) module. + * The generated module will be placed under the project's output directory. + * + * @param module - The WebAssembly (.wasm) module that needs to be processed. + * @param resolvedPath - The resolved path to the WebAssembly module file. + * @return A promise that resolves when the JavaScript module file is successfully created. + */ + async moduleGenerated({ moduleOutput, module }): Promise { + const cleanFileName = path.basename(module.path, '.wasm'); + const dirnameInModule = path.dirname(module.path); + const generatedModulePath = path.join( + module.kind === 'external' ? `${module.packageName}` : '#local', + dirnameInModule, + `${cleanFileName}.js` + ); + const source = await buildMetroVirtualModuleSourceFor( + module.resolvedPath + ); + + const generator = moduleOutput.forPath('modules'); + await generator.writeTo(generatedModulePath, source); + }, + }; +} + +/** + * Generates source for virtual webassembly module + * @param pathToModule + */ +async function buildMetroVirtualModuleSourceFor( + pathToModule: string +): Promise { + const cleanName = path.basename(pathToModule, '.wasm'); + const checksumRaw = await computeFileChecksumBuffer(pathToModule); + const checksumHex = new TextEncoder().encode(checksumRaw.toString('hex')); + + const rawName = new TextEncoder().encode(cleanName); + const writer = new BinaryWriter(ByteOrder.LittleEndian); + writer.copyBytes(MAGIC_NUMBER.buffer as ArrayBuffer); + writer.writeUint8(1); + writer.copyBytes(checksumHex.buffer as ArrayBuffer); + writer.writeUint8(0); + writer.writeUint16(rawName.length); + writer.copyBytes(rawName.buffer as ArrayBuffer); + // null terminator just in case + writer.writeUint8(0); + + return ( + `const data = Uint8Array.from(${JSON.stringify([...writer.getWrittenBytes()])});\n` + + `export default data.buffer;` + ); +} diff --git a/packages/codegen/src/pipeline/react-native/turbomodule.ts b/packages/codegen/src/pipeline/react-native/turbomodule.ts new file mode 100644 index 00000000..4b37d2db --- /dev/null +++ b/packages/codegen/src/pipeline/react-native/turbomodule.ts @@ -0,0 +1,21 @@ +import type { Plugin } from '../../plugin.js'; +import * as templates from '../../templates/host.js'; + +/** + * Plugin that generates a virtual module for Metro support. + */ +export function reactNativeTurboModule(): Plugin { + return { + name: 'core/turbo-module', + title: 'React Native TurboModule', + + async hostProjectGenerated({ + projectOutput, + generatedModules, + }): Promise { + await projectOutput.writeAllTo({ + 'loader.cpp': templates.buildLoaderSource(generatedModules), + }); + }, + }; +} diff --git a/packages/codegen/src/pipeline/wasm2c-runtime.ts b/packages/codegen/src/pipeline/wasm2c-runtime.ts new file mode 100644 index 00000000..02443a60 --- /dev/null +++ b/packages/codegen/src/pipeline/wasm2c-runtime.ts @@ -0,0 +1,15 @@ +import type { HostProjectGeneratedContext, Plugin } from '../plugin.js'; + +/** + * Plugin that generates a virtual module for Metro support. + */ +export function embedWasmRuntime(): Plugin { + return { + name: 'core/wasm2c-runtime', + title: 'WebAssembly runtime embedding', + + async hostProjectGenerated({ projectOutput }): Promise { + await projectOutput.copyAsset('wasm-rt'); + }, + }; +} diff --git a/packages/codegen/src/plugin.ts b/packages/codegen/src/plugin.ts new file mode 100644 index 00000000..5658a968 --- /dev/null +++ b/packages/codegen/src/plugin.ts @@ -0,0 +1,31 @@ +import type { ResolvedModule } from '@callstack/polygen-project'; +import type { Codegen } from './codegen.js'; +import type { W2CGeneratedModule } from './codegen/modules.js'; +import type { OutputGenerator } from './helpers/output-generator.js'; + +export interface ModuleGeneratedContext { + codegen: Codegen; + moduleOutput: OutputGenerator; + context: W2CGeneratedModule; + module: ResolvedModule; +} + +export interface HostProjectGeneratedContext { + codegen: Codegen; + rootOutput: OutputGenerator; + projectOutput: OutputGenerator; + generatedModules: W2CGeneratedModule[]; +} + +/** + * Plugin interface that allows to hook into the code generation process. + */ +export interface Plugin { + name: string; + title: string; + + moduleGenerated?(callContext: ModuleGeneratedContext): Promise; + hostProjectGenerated?( + callContext: HostProjectGeneratedContext + ): Promise; +} diff --git a/packages/codegen/src/templates/host.ts b/packages/codegen/src/templates/host.ts index cb0553ba..1be36712 100644 --- a/packages/codegen/src/templates/host.ts +++ b/packages/codegen/src/templates/host.ts @@ -1,38 +1,41 @@ import indentString from 'indent-string'; import stripIndent from 'strip-indent'; -import { W2CModuleContext } from '../context/context.js'; +import type { W2CGeneratedModule } from '../codegen/modules.js'; +import { + toArgumentList, + toInitializerList, + toStringLiteral, +} from '../helpers/source-builder.js'; import { HEADER } from './common.js'; -export function buildHostSource(generatedModules: W2CModuleContext[]) { - const moduleNames = generatedModules - .map((module) => `"${module.name}"`) - .join(', ') - .trimEnd() - .replace(/,$/, ''); +export function buildLoaderSource(generatedModules: W2CGeneratedModule[]) { + const moduleNames = toArgumentList(generatedModules, (el) => el.name); const moduleChecksums = generatedModules - .map( - (module) => `{ "${module.name}", "${module.checksum.toString('hex')}" }` + .map(({ name, checksum }) => + toInitializerList([name, checksum.toString('hex')], toStringLiteral) ) .join(',\n ') .trimEnd() .replace(/,$/, ''); const moduleFactoriesMap = generatedModules - .map( - (module) => - `{ "${module.checksum.toString('hex')}", ${module.turboModule.moduleFactoryFunctionName} }` + .map(({ checksum, moduleFactoryFunctionName }) => + toInitializerList( + [checksum.toString('hex'), moduleFactoryFunctionName], + toStringLiteral + ) ) .join(',\n ') .trimEnd(); - function makeModuleFactoryDecl(module: W2CModuleContext) { - return `std::shared_ptr ${module.turboModule.moduleFactoryFunctionName}();`; + function makeModuleFactoryDecl(module: W2CGeneratedModule) { + return `std::shared_ptr ${module.moduleFactoryFunctionName}();`; } - function makeModuleHandler(module: W2CModuleContext) { + function makeModuleHandler(module: W2CGeneratedModule) { return stripIndent( - `if (name == "${module.name}") { return ${module.turboModule.moduleFactoryFunctionName}(); }` + `if (name == "${module.name}") { return ${module.moduleFactoryFunctionName}(); }` ); } @@ -93,64 +96,3 @@ export function buildHostSource(generatedModules: W2CModuleContext[]) { `) ); } - -export function buildPodspec() { - return stripIndent( - ` - require "json" - project_dir = Pathname.new(__dir__) - project_dir = project_dir.parent until - File.exist?("#{project_dir}/package.json") || - project_dir.expand_path.to_s == '/' - - package = JSON.parse(File.read(File.join(project_dir, "package.json"))) - - Pod::Spec.new do |s| - s.name = "ReactNativeWebAssemblyHost" - s.version = package["version"] || "0.0.1" - s.summary = package["description"] || "No summary" - s.homepage = package["homepage"] || "no-homepage" - s.license = package["license"] || "Unknown License" - s.authors = package["author"] || "Unknown Author" - if package["repository"] - s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" } - else - s.source = { :git => "Unknown Source", :tag => "master" } - end - - s.platforms = { :ios => min_ios_version_supported } - s.source_files = "*.{h,hpp,c,cpp}", "**/*.{h,hpp,c,cpp}" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\\"$(PODS_ROOT)/boost\\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - - # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. - # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. - if respond_to?(:install_modules_dependencies, true) - install_modules_dependencies(s) - else - s.dependency "React-Core" - - # Don't install the dependencies when we run \`pod install\` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then - s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1 -O3" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\\"$(PODS_ROOT)/boost\\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - s.dependency "React-Codegen" - s.dependency "RCT-Folly" - s.dependency "RCTRequired" - s.dependency "RCTTypeSafety" - s.dependency "ReactCommon/turbomodule/core" - end - end - - s.exclude_files = "**/FBReactNativeSpec-generated.mm", "**/RCTModulesConformingToProtocolsProvider.mm" - end - ` - ).trimStart(); -} diff --git a/packages/codegen/src/templates/library/imports-bridge.ts b/packages/codegen/src/templates/library/imports-bridge.ts index 62063fca..205d17f4 100644 --- a/packages/codegen/src/templates/library/imports-bridge.ts +++ b/packages/codegen/src/templates/library/imports-bridge.ts @@ -4,8 +4,17 @@ import type { ModuleTable, } from '@callstack/wasm-parser'; import stripIndent from 'strip-indent'; -import { W2CImportedModule } from '../../context/index.js'; -import type { GeneratedFunctionImport, GeneratedImport } from '../../types.js'; +import type { W2CExternModule } from '../../codegen/modules.js'; +import type { + GeneratedModuleFunction, + GeneratedSymbol, + ResolvedModuleImport, +} from '../../codegen/types.js'; +import { matchSymbol } from '../../codegen/utils.js'; +import { + toArgumentList, + toInitializerList, +} from '../../helpers/source-builder.js'; import { HEADER, STRUCT_TYPE_PREFIX, @@ -15,25 +24,24 @@ import { toJSINumber, } from '../common.js'; -export function buildImportBridgeHeader(importedModule: W2CImportedModule) { - function makeDeclaration(symbol: GeneratedImport): string { - switch (symbol.target.kind) { - case 'function': - return makeImportFunc(symbol as GeneratedFunctionImport, false); - case 'global': - return makeImportGlobal(symbol as GeneratedImport, false); - case 'memory': - return makeImportMemory(symbol as GeneratedImport, false); - case 'table': - return makeImportTable(symbol as GeneratedImport, false); - default: - // @ts-ignore - console.warn('Unknown import type', symbol.target.kind); - return ''; +export function buildImportBridgeHeader(importedModule: W2CExternModule) { + function makeDeclaration(symbol: ResolvedModuleImport): string { + try { + return matchSymbol(symbol, { + func: (f) => makeImportFunc(f, false), + global: (g) => makeImportGlobal(g, false), + table: (t) => makeImportTable(t, false), + memory: (m) => makeImportMemory(m, false), + }); + } catch (e) { + console.error('Failed to build import', symbol, e); + return ''; } } - const decls = importedModule.imports.map(makeDeclaration).join('\n'); + const decls = [...importedModule.exports.values().map(makeDeclaration)].join( + '\n' + ); return ( HEADER + @@ -61,21 +69,18 @@ export function buildImportBridgeHeader(importedModule: W2CImportedModule) { ); } -export function buildImportBridgeSource(importedModule: W2CImportedModule) { - function makeImport(imp: GeneratedImport): string { - switch (imp.target.kind) { - case 'function': - return makeImportFunc(imp as GeneratedFunctionImport, true); - case 'global': - return makeImportGlobal(imp as GeneratedImport, true); - case 'memory': - return makeImportMemory(imp as GeneratedImport, true); - case 'table': - return makeImportTable(imp as GeneratedImport, true); - default: - // @ts-ignore - console.warn('Unknown import type', imp.target.kind); - return ''; +export function buildImportBridgeSource(importedModule: W2CExternModule) { + function makeImport(imp: ResolvedModuleImport): string { + try { + return matchSymbol(imp, { + func: (f) => makeImportFunc(f, true), + global: (g) => makeImportGlobal(g, true), + table: (t) => makeImportTable(t, true), + memory: (m) => makeImportMemory(m, true), + }); + } catch (e) { + console.error('Failed to build import', imp, e); + return ''; } } @@ -93,7 +98,7 @@ export function buildImportBridgeSource(importedModule: W2CImportedModule) { extern "C" { #endif - ${importedModule.imports.map((i) => makeImport(i)).join('\n')} + ${[...importedModule.exports.values().map((i) => makeImport(i))].join('\n')} #ifdef __cplusplus } @@ -104,73 +109,74 @@ export function buildImportBridgeSource(importedModule: W2CImportedModule) { function wrapJSIReturnIntoNative( varName: string, - func: GeneratedFunctionImport + func: ResolvedModuleImport ) { - const { resultTypes } = func.target; + const { resultTypes, returnTypeName } = func.target; // Handle multiple value types (struct) if (resultTypes.length > 1) { - const elements = resultTypes - .map((t, i) => toJSINumber(`${varName}.${STRUCT_TYPE_PREFIX[t]}${i}`, t)) - .join(', '); + const elements = resultTypes.map((t, i) => + toJSINumber(`${varName}.${STRUCT_TYPE_PREFIX[t]}${i}`, t) + ); - return `return { ${elements} }`; + return `return ${toInitializerList(elements)}`; } if (resultTypes.length === 1) { - return `return ${fromJSINumber('res', resultTypes[0]!, func.returnTypeName)}`; + return `return ${fromJSINumber('res', resultTypes[0]!, returnTypeName)}`; } return 'return'; } function makeImportFunc( - func: GeneratedFunctionImport, + func: GeneratedSymbol, withBody: boolean ): string { - const { resultTypes } = func.target; + const { resultTypes, returnTypeName, parametersTypes, parameterTypeNames } = + func.target; - const declarationParams = func.parameterTypeNames + const declarationParams = parameterTypeNames .map((name, i) => `${name} arg${i}`) .map((e) => `, ${e}`) .join(''); - const args = func.target.parametersTypes + const args = parametersTypes .map((t, i) => toJSINumber(`arg${i}`, t)) .map((e) => `, ${e}`) .join(''); const hasReturn = resultTypes.length > 0; - const prototype = `${func.returnTypeName} ${func.generatedFunctionName}(${func.moduleInfo.generatedContextTypeName}* ctx${declarationParams})`; + const prototype = `${returnTypeName} ${func.functionSymbolAccessorName}(${func.module.generatedContextTypeName}* ctx${declarationParams})`; const body = `{ - auto fn = ctx->importObj.getPropertyAsFunction(ctx->rt, "${func.name}"); + auto fn = ctx->importObj.getPropertyAsFunction(ctx->rt, "${func.localName}"); ${hasReturn ? 'auto res = ' : ''}fn.call(ctx->rt${args}); ${wrapJSIReturnIntoNative('res', func)}; } `; return ` - /* import: '${func.module}' '${func.name}' */ + /* import: '${func.module}' '${func.localName}' */ ${prototype}${withBody ? body : ';'} `; } function makeImportGlobal( - global: GeneratedImport, + global: ResolvedModuleImport, withBody: boolean ): string { const cType = global.target.type.replace('i', 'u') + '*'; - const prototype = `${cType} ${global.generatedFunctionName}(${global.moduleInfo.generatedContextTypeName}* ctx)`; + const prototype = `${cType} ${global.functionSymbolAccessorName}(${global.module.generatedContextTypeName}* ctx)`; const body = `{ - auto target = ctx->importObj.getProperty(ctx->rt, "${global.name}"); + auto target = ctx->importObj.getProperty(ctx->rt, "${global.localName}"); if (target.isUndefined()) [[unlikely]] { - throw jsi::JSError(ctx->rt, "Provided imported variable '${global.name}' is not provided"); + throw jsi::JSError(ctx->rt, "Provided imported variable '${global.localName}' is not provided"); } if (!target.isObject()) [[unlikely]] { - throw jsi::JSError(ctx->rt, "Provided imported variable '${global.name}' is not an instance of WebAssembly.Global"); + throw jsi::JSError(ctx->rt, "Provided imported variable '${global.localName}' is not an instance of WebAssembly.Global"); } auto obj = target.asObject(ctx->rt); @@ -180,42 +186,42 @@ function makeImportGlobal( `; return ` - /* import: '${global.module}' '${global.name}' */ + /* import: '${global.module}' '${global.localName}' */ ${prototype}${withBody ? body : ';'} `; } function makeImportMemory( - memory: GeneratedImport, + memory: ResolvedModuleImport, withBody: boolean ): string { - const prototype = `wasm_rt_memory_t* ${memory.generatedFunctionName}(${memory.moduleInfo.generatedContextTypeName}* ctx)`; + const prototype = `wasm_rt_memory_t* ${memory.functionSymbolAccessorName}(${memory.module.generatedContextTypeName}* ctx)`; const body = `{ - auto memoryHolder = ctx->importObj.getPropertyAsObject(ctx->rt, "${memory.name}"); + auto memoryHolder = ctx->importObj.getPropertyAsObject(ctx->rt, "${memory.localName}"); auto memoryState = NativeStateHelper::tryGet(ctx->rt, memoryHolder); return memoryState->getMemory(); }`; return ` - /* import: '${memory.module}' '${memory.name}' */ + /* import: '${memory.module}' '${memory.localName}' */ ${prototype}${withBody ? body : ';'} `; } function makeImportTable( - table: GeneratedImport, + table: ResolvedModuleImport, withBody: boolean ): string { - const prototype = `${TABLE_KIND_TO_NATIVE_C_TYPE[table.target.elementType]}* ${table.generatedFunctionName}(${table.moduleInfo.generatedContextTypeName}* ctx)`; + const prototype = `${TABLE_KIND_TO_NATIVE_C_TYPE[table.target.elementType]}* ${table.functionSymbolAccessorName}(${table.module.generatedContextTypeName}* ctx)`; const body = `{ - auto tableHolder = ctx->importObj.getPropertyAsObject(ctx->rt, "${table.name}"); + auto tableHolder = ctx->importObj.getPropertyAsObject(ctx->rt, "${table.localName}"); auto table = NativeStateHelper::tryGet<${TABLE_KIND_TO_CLASS_NAME[table.target.elementType]}>(ctx->rt, tableHolder); assert(table != nullptr); return table->getTableData(); }`; return ` - /* import: '${table.module}' '${table.name}' */ + /* import: '${table.module}' '${table.localName}' */ ${prototype}${withBody ? body : ';'} `; } diff --git a/packages/codegen/src/templates/library/module-bridge.ts b/packages/codegen/src/templates/library/module-bridge.ts index c2d12c05..7851b3fc 100644 --- a/packages/codegen/src/templates/library/module-bridge.ts +++ b/packages/codegen/src/templates/library/module-bridge.ts @@ -4,8 +4,11 @@ import type { ValueType, } from '@callstack/wasm-parser'; import stripIndent from 'strip-indent'; -import { W2CModuleContext } from '../../context/context.js'; -import type { GeneratedExport, GeneratedFunctionExport } from '../../types.js'; +import type { W2CGeneratedModule } from '../../codegen/modules.js'; +import type { + GeneratedModuleFunction, + GeneratedSymbol, +} from '../../codegen/types.js'; import { HEADER, STRUCT_TYPE_PREFIX, @@ -14,8 +17,8 @@ import { toJSINumber, } from '../common.js'; -export function buildExportBridgeHeader(module: W2CModuleContext) { - const imports = module.codegen.importedModules; +export function buildExportBridgeHeader(module: W2CGeneratedModule) { + const imports = module.importedModules; const includes = imports.map((i) => `#include "${i.name}-imports.h"`); return ( @@ -27,19 +30,19 @@ export function buildExportBridgeHeader(module: W2CModuleContext) { namespace callstack::polygen::generated { - class ${module.turboModule.generatedClassName}ModuleContext: public facebook::jsi::NativeState { + class ${module.generatedClassName}ModuleContext: public facebook::jsi::NativeState { public: - ${module.turboModule.generatedClassName}ModuleContext(facebook::jsi::Runtime& rt, facebook::jsi::Object&& importObject) + ${module.generatedClassName}ModuleContext(facebook::jsi::Runtime& rt, facebook::jsi::Object&& importObject) : importObject(std::move(importObject)) ${imports.map((i) => `, INIT_IMPORT_CTX(${i.generatedRootContextFieldName}, "${i.name}")`).join('\n ')} {} facebook::jsi::Object importObject; - ${module.codegen.generatedContextTypeName} rootCtx; + ${module.generatedContextTypeName} rootCtx; ${imports.map((i) => `${i.generatedContextTypeName} ${i.generatedRootContextFieldName};`).join('\n ')} }; - void create${module.turboModule.generatedClassName}Exports(facebook::jsi::Runtime &rt, facebook::jsi::Object& target, facebook::jsi::Object&& importObject); + void create${module.generatedClassName}Exports(facebook::jsi::Runtime &rt, facebook::jsi::Object& target, facebook::jsi::Object&& importObject); } `) @@ -63,53 +66,53 @@ function wrapNativeReturnIntoJSI(varName: string, types: ValueType[]) { return 'return jsi::Value::undefined()'; } -export function buildExportBridgeSource(module: W2CModuleContext) { - function makeExportFunc(func: GeneratedFunctionExport) { - const { resultTypes } = func.target; +export function buildExportBridgeSource(module: W2CGeneratedModule) { + function makeExportFunc(func: GeneratedSymbol) { + const { resultTypes, parameterTypeNames, parametersTypes } = func.target; - const args = func.target.parametersTypes + const args = parametersTypes .map((type, i) => - fromJSINumber(`args[${i}]`, type, func.parameterTypeNames[i]!) + fromJSINumber(`args[${i}]`, type, parameterTypeNames[i]!) ) .map((e) => `, ${e}`) .join(''); const res = resultTypes.length > 0 ? 'auto res = ' : ''; return ` - /* export: '${func.name}' */ - exports.setProperty(rt, "${func.name}", HOSTFN("${func.name}", ${func.parameterTypeNames.length}) { - ${res}${func.generatedFunctionName}(&inst->rootCtx${args}); + /* export: '${func.localName}' */ + exports.setProperty(rt, "${func.localName}", HOSTFN("${func.localName}", ${parameterTypeNames.length}) { + ${res}${func.functionSymbolAccessorName}(&inst->rootCtx${args}); ${wrapNativeReturnIntoJSI('res', resultTypes)}; })); `; } - function makeExportMemory(mem: GeneratedExport) { + function makeExportMemory(mem: GeneratedSymbol) { return ` - /* exported memory: '${mem.name}' */ + /* exported memory: '${mem.localName}' */ { jsi::Object holder {rt}; - auto memory = std::make_shared(${mem.generatedFunctionName}(&inst->rootCtx)); + auto memory = std::make_shared(${mem.functionSymbolAccessorName}(&inst->rootCtx)); holder.setNativeState(rt, std::move(memory)); - memories.setProperty(rt, "${mem.name}", std::move(holder)); + memories.setProperty(rt, "${mem.localName}", std::move(holder)); } `; } - function makeExportTable(table: GeneratedExport) { + function makeExportTable(table: GeneratedSymbol) { const className = TABLE_KIND_TO_CLASS_NAME[table.target.elementType]; return ` - /* exported table: '${table.name}' */ + /* exported table: '${table.localName}' */ { jsi::Object holder {rt}; - auto table = std::make_shared<${className}>(${table.generatedFunctionName}(&inst->rootCtx)); + auto table = std::make_shared<${className}>(${table.functionSymbolAccessorName}(&inst->rootCtx)); holder.setNativeState(rt, std::move(table)); - tables.setProperty(rt, "${table.name}", std::move(holder)); + tables.setProperty(rt, "${table.localName}", std::move(holder)); } `; } - const initArgs = module.codegen.importedModules + const initArgs = module.importedModules .map((mod) => `, &inst->${mod.generatedRootContextFieldName}`) .join(''); @@ -126,29 +129,29 @@ export function buildExportBridgeSource(module: W2CModuleContext) { using namespace callstack::polygen; namespace callstack::polygen::generated { - void create${module.turboModule.generatedClassName}Exports(jsi::Runtime &rt, jsi::Object& target, jsi::Object&& importObject) { + void create${module.generatedClassName}Exports(jsi::Runtime &rt, jsi::Object& target, jsi::Object&& importObject) { if (!wasm_rt_is_initialized()) { wasm_rt_init(); } - auto inst = std::make_shared<${module.turboModule.contextClassName}>(rt, std::move(importObject)); - wasm2c_${module.codegen.mangledName}_instantiate(&inst->rootCtx${initArgs}); + auto inst = std::make_shared<${module.contextClassName}>(rt, std::move(importObject)); + wasm2c_${module.mangledName}_instantiate(&inst->rootCtx${initArgs}); target.setNativeState(rt, inst); // Memories jsi::Object memories {rt}; - ${module.codegen.exportedMemories.map(makeExportMemory).join('\n ')} + ${module.exportedMemories.map(makeExportMemory).join('\n ')} target.setProperty(rt, "memories", std::move(memories)); // Tables jsi::Object tables {rt}; - ${module.codegen.exportedTables.map(makeExportTable).join('\n ')} + ${module.exportedTables.map(makeExportTable).join('\n ')} target.setProperty(rt, "tables", std::move(tables)); // Exported functions jsi::Object exports {rt}; - ${module.codegen.exportedFunctions.map(makeExportFunc).join('\n ')} + ${module.exportedFunctions.map(makeExportFunc).join('\n ')} exports.setNativeState(rt, inst); target.setProperty(rt, "exports", std::move(exports)); } diff --git a/packages/codegen/src/templates/library/static-lib.ts b/packages/codegen/src/templates/library/static-lib.ts index 4271e968..87afcc27 100644 --- a/packages/codegen/src/templates/library/static-lib.ts +++ b/packages/codegen/src/templates/library/static-lib.ts @@ -1,10 +1,10 @@ -import type { ModuleSymbol } from '@callstack/wasm-parser'; +import type { ModuleEntityKind } from '@callstack/wasm-parser'; import stripIndent from 'strip-indent'; -import { W2CModuleContext } from '../../context/context.js'; +import type { W2CGeneratedModule } from '../../codegen/modules.js'; import { HEADER } from '../common.js'; -export function buildStaticLibraryHeader(module: W2CModuleContext) { - const className = `WASM${module.turboModule.generatedClassName}Module`; +export function buildStaticLibraryHeader(module: W2CGeneratedModule) { + const className = `WASM${module.generatedClassName}Module`; return ( HEADER + @@ -14,7 +14,7 @@ export function buildStaticLibraryHeader(module: W2CModuleContext) { namespace callstack::polygen::generated { - std::shared_ptr ${module.turboModule.moduleFactoryFunctionName}(); + std::shared_ptr ${module.moduleFactoryFunctionName}(); class ${className}: public callstack::polygen::StaticLibraryModule { public: @@ -29,24 +29,25 @@ export function buildStaticLibraryHeader(module: W2CModuleContext) { ); } -const symbolTypeMapping: Record = { +const symbolTypeMapping: Record = { function: 'Module::SymbolKind::Function', memory: 'Module::SymbolKind::Memory', global: 'Module::SymbolKind::Global', table: 'Module::SymbolKind::Table', }; -export function buildStaticLibrarySource(module: W2CModuleContext) { - const className = `WASM${module.turboModule.generatedClassName}Module`; - const moduleImportsIter = module.codegen.imports +export function buildStaticLibrarySource(module: W2CGeneratedModule) { + const className = `WASM${module.generatedClassName}Module`; + const moduleImportsIter = module.imports .values() .map( - (i) => `{"${i.module}", "${i.name}", ${symbolTypeMapping[i.target.kind]}}` + (i) => + `{"${i.module}", "${i.localName}", ${symbolTypeMapping[i.target.kind]}}` ); const moduleImports = [...moduleImportsIter].join(', '); - const moduleExportsIter = module.codegen.exports + const moduleExportsIter = module.exports .values() - .map((e) => `{"${e.name}", ${symbolTypeMapping[e.target.kind]}}`); + .map((e) => `{"${e.localName}", ${symbolTypeMapping[e.target.kind]}}`); const moduleExports = [...moduleExportsIter].join(', '); return ( @@ -62,7 +63,7 @@ export function buildStaticLibrarySource(module: W2CModuleContext) { const std::vector imports { ${moduleImports} }; const std::vector exports { ${moduleExports} }; - std::shared_ptr ${module.turboModule.moduleFactoryFunctionName}() { + std::shared_ptr ${module.moduleFactoryFunctionName}() { return std::make_shared<${className}>("${module.name}"); } @@ -75,7 +76,7 @@ export function buildStaticLibrarySource(module: W2CModuleContext) { } void ${className}::createInstance(jsi::Runtime& rt, facebook::jsi::Object& target, jsi::Object&& importObject) const { - return create${module.turboModule.generatedClassName}Exports(rt, target, std::move(importObject)); + return create${module.generatedClassName}Exports(rt, target, std::move(importObject)); } } diff --git a/packages/codegen/src/types.ts b/packages/codegen/src/types.ts deleted file mode 100644 index d87acb85..00000000 --- a/packages/codegen/src/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { - ModuleExport, - ModuleFunction, - ModuleImport, - ModuleSymbol, -} from '@callstack/wasm-parser'; -import type { W2CCodegenLocalImportedModule } from './context/index.js'; - -export interface GeneratedFunctionInfo { - parameterTypeNames: string[]; - returnTypeName: string; -} - -export interface GeneratedImport extends ModuleImport { - mangledName: string; - moduleInfo: W2CCodegenLocalImportedModule; - generatedFunctionName: string; -} - -export type GeneratedFunctionImport = GeneratedImport & - GeneratedFunctionInfo; - -export interface GeneratedExport extends ModuleExport { - mangledName: string; - generatedFunctionName: string; -} - -export type GeneratedFunctionExport = GeneratedExport & - GeneratedFunctionInfo; diff --git a/packages/codegen/src/mangle.ts b/packages/codegen/src/wasm2c/mangle.ts similarity index 100% rename from packages/codegen/src/mangle.ts rename to packages/codegen/src/wasm2c/mangle.ts diff --git a/packages/codegen/src/wasm2c.ts b/packages/codegen/src/wasm2c/wasm2c.ts similarity index 54% rename from packages/codegen/src/wasm2c.ts rename to packages/codegen/src/wasm2c/wasm2c.ts index d5e1c4e6..2361fdb1 100644 --- a/packages/codegen/src/wasm2c.ts +++ b/packages/codegen/src/wasm2c/wasm2c.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs/promises'; import path from 'node:path'; import { execa } from 'execa'; @@ -7,7 +8,7 @@ let finalWasm2cPath: string | undefined; /** * Error class for wasm2c validation errors. */ -class Wasm2cError extends Error { +export class Wasm2cError extends Error { constructor(message: string) { super(message); this.name = 'Wasm2cError'; @@ -41,13 +42,15 @@ async function findBinary(): Promise { } /** - * Validates the presence of the `WABT_PATH` environment variable. - * If the `WABT_PATH` environment variable is not set, an error message is logged indicating the missing configuration, + * Validates the presence of the `wasm2c` binary. + * + * Binary is searched in `PATH` and in `WABT_PATH` environment variable. + * If not found, an error message is logged indicating the missing configuration, * and the process exits with a failure code. * * @return No value is returned from this function. */ -export async function validate() { +export async function ensureBinaryAvailable() { if (finalWasm2cPath) { return; } @@ -71,27 +74,64 @@ export interface Wasm2cGenerateOptions { moduleName?: string; } +/** + * Returns the output files for a given configuration. + * + * @param inputFile Path to the input WASM module + * @param outputPath Path to the output C source file + * @param options Optional configuration for the generation process + */ +export function getOutputFilesFor( + inputFile: string, + outputPath: string, + options?: Wasm2cGenerateOptions +): string[] { + const outputDirectory = path.dirname(outputPath); + const moduleBasename = path.basename(inputFile, '.wasm'); + return [ + `${outputDirectory}/${moduleBasename}.c`, + `${outputDirectory}/${moduleBasename}.h`, + ]; +} + /** * Generates C source files from a given WebAssembly input file. * - * @param inputFile The path to the WebAssembly (.wasm) input file. - * @param outputSourceFile The path where the generated C source file will be saved. + * @param inputPath The path to the WebAssembly (.wasm) input file. + * @param outputSourcePath The path where the generated C source file will be saved. * @param options Optional configuration for the generation process, including the module name. * @return A promise that resolves once the C source file generation is complete. */ export async function generateCSources( - inputFile: string, - outputSourceFile: string, + inputPath: string, + outputSourcePath: string, options?: Wasm2cGenerateOptions -) { - await validate(); +): Promise { + await ensureBinaryAvailable(); + const generatedFiles = getOutputFilesFor( + inputPath, + outputSourcePath, + options + ); - const args = [inputFile, '-o', `${outputSourceFile}.c`]; - const defaultModuleName = path.basename(inputFile, '.wasm'); + const args = [inputPath, '-o', outputSourcePath]; + const defaultModuleName = path.basename(inputPath, '.wasm'); const moduleName = options?.moduleName ?? defaultModuleName; args.push('--module-name'); args.push(moduleName); await execa(finalWasm2cPath!, args); + + // Check generated files exist + const statPromises = generatedFiles.map((file) => fs.stat(file)); + const fileStats = await Promise.all(statPromises); + + for (const stat of fileStats) { + if (!stat.isFile()) { + throw new Wasm2cError(`Failed to generate file ${stat}`); + } + } + + return generatedFiles; } diff --git a/packages/metro-config/src/index.ts b/packages/metro-config/src/index.ts index 7d19d7b1..3d15769a 100644 --- a/packages/metro-config/src/index.ts +++ b/packages/metro-config/src/index.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { Project } from '@callstack/polygen-project'; -import { type ConfigT } from 'metro-config'; +import type { ConfigT } from 'metro-config'; import type { CustomResolver } from 'metro-resolver'; interface PolygenConfig { @@ -34,12 +34,12 @@ export function withPolygenConfig( // Load polygen-output.json to get the mapping of resolved external packages const polygenModuleMapping = JSON.parse( - fs.readFileSync(project.pathToOutput(OUTPUT_INFO), 'utf-8') + fs.readFileSync(project.paths.pathToOutput(OUTPUT_INFO), 'utf-8') ).externalPackages as Record; let packageName = ''; // Path to directory where the mock modules are located - let mockModuleSubtreePath = project.pathToOutput('modules'); + let mockModuleSubtreePath = project.paths.pathToOutput('modules'); // Path in target module let pathInModule; // Resolved absolute path to WASM file @@ -54,15 +54,18 @@ export function withPolygenConfig( `Attempting to import unknown external package '${packageName}'` ); } - mockModuleSubtreePath = project.pathToOutput('modules', packageName); - absoluteWasmPath = project.pathTo(resolvedPath, pathInModule); + mockModuleSubtreePath = project.paths.pathToOutput( + 'modules', + packageName + ); + absoluteWasmPath = project.paths.pathTo(resolvedPath, pathInModule); } // local dependency else { - mockModuleSubtreePath = project.pathToOutput('modules/#local'); + mockModuleSubtreePath = project.paths.pathToOutput('modules/#local'); const requestingDirectory = path.dirname(context.originModulePath); - pathInModule = project.globalPathToLocal( + pathInModule = project.paths.globalPathToLocal( path.join(requestingDirectory, moduleName) ); absoluteWasmPath = path.join(project.projectRoot, pathInModule); diff --git a/packages/polygen-cli/src/actions/clean.ts b/packages/polygen-cli/src/actions/clean.ts new file mode 100644 index 00000000..1cacd3bf --- /dev/null +++ b/packages/polygen-cli/src/actions/clean.ts @@ -0,0 +1,17 @@ +import fs from 'node:fs/promises'; +// TODO: clean only generated files (not whole directory) +import type { Project } from '@callstack/polygen-project'; +import chalk from 'chalk'; +import { oraPromise } from 'ora'; + +export async function cleanGeneratedFiles(project: Project): Promise { + const { + fullOutputDirectory: generatedPath, + localOutputDirectory: displayPath, + } = project.paths; + + await oraPromise( + fs.rm(generatedPath, { recursive: true, force: true }), + `Removing ${chalk.bold(displayPath)}` + ); +} diff --git a/packages/polygen-cli/src/actions/codegen.ts b/packages/polygen-cli/src/actions/codegen.ts new file mode 100644 index 00000000..cc755a09 --- /dev/null +++ b/packages/polygen-cli/src/actions/codegen.ts @@ -0,0 +1,120 @@ +import { + Codegen, + type CodegenOptions, + type GeneratedSymbol, + type PluginDispatchOptions, + type W2CGeneratedModule, +} from '@callstack/polygen-codegen'; +import type { GeneratedEntityKind } from '@callstack/polygen-codegen'; +import { Project, type ResolvedModule } from '@callstack/polygen-project'; +import chalk from 'chalk'; +import consola from 'consola'; +import { oraPromise } from 'ora'; + +export interface GenerateOptions { + outputDir?: string; + force?: boolean; +} + +function printCodegenStats(generatedModule: W2CGeneratedModule) { + const hglt = chalk.dim; + const { imports, exports } = generatedModule; + + function statsOf(set: GeneratedSymbol[]): string { + const highlight = chalk.dim; + const grouped = Object.groupBy( + set, + (s) => s.target.kind as GeneratedEntityKind + ); + const countOf = (type: GeneratedEntityKind) => grouped[type]?.length ?? 0; + return [ + `${highlight(countOf('function'))} functions`, + `${highlight(countOf('memory'))} memories`, + `${highlight(countOf('global'))} globals`, + `${highlight(countOf('table'))} tables`, + ].join(', '); + } + + consola.info(` Found ${hglt(imports.length)} imports (${statsOf(imports)})`); + consola.info(` Found ${hglt(exports.length)} exports (${statsOf(exports)})`); +} + +/** + * Generates code for a single module. + * + * @param generator + * @param moduleMeta + */ +export async function generateModule( + generator: Codegen, + moduleMeta: ResolvedModule +): Promise { + const pluginOptions: PluginDispatchOptions = { + beforePluginDispatch: (plugin) => { + consola.debug(`Executing plugin ${chalk.bold(plugin.title)}`); + }, + afterPluginDispatch: (plugin) => {}, + }; + const [generatedModule, imports] = await oraPromise( + async () => generator.generateModule(moduleMeta, pluginOptions), + `Processing ${moduleMeta.kind} module ${chalk.bold(moduleMeta.path)}` + ); + + printCodegenStats(generatedModule); + + return generatedModule; +} + +/** + * Generate command logic. + * + * Generates code for all defined modules for a project. + * If project is not passed, closest project is found. + * + * @note Extracted as a function so that it can be called from other commands. + * + * @param options Generate options + * @param project Optionally loaded project + */ +export async function generate(options: GenerateOptions, project?: Project) { + project ??= await Project.findClosest(); + + const generatorOptions: CodegenOptions = { + outputDirectory: project.paths.fullOutputDirectory, + singleProject: true, + generateMetadata: true, + forceGenerate: options.force ?? false, + }; + const codegen = await Codegen.create(project, generatorOptions); + const allModules = project.modules.webAssemblyModules; + + consola.info( + 'Generating code for', + chalk.bold(allModules.length), + 'WebAssembly module(s)' + ); + + for (const mod of allModules) { + const resolvedModule = await project.modules.resolvePolygenModule(mod); + await generateModule(codegen, resolvedModule); + } + + await oraPromise( + () => codegen.generateHostModule(), + 'Generating host module' + ); + + const generateImportsPromises = codegen.context.importedModules + .values() + .map(async (mod) => { + await codegen.generateImportedModule(mod); + consola.success(`Generated import ${chalk.magenta(mod.name)} bridge`); + }); + + await Promise.allSettled(generateImportsPromises); + await codegen.finalize(); + + consola.info( + `Run ${chalk.bold('pod install')} to regenerate XCode project and make sure new source files are included` + ); +} diff --git a/packages/polygen-cli/src/commands/clean.ts b/packages/polygen-cli/src/commands/clean.ts index 8b31f172..c7187247 100644 --- a/packages/polygen-cli/src/commands/clean.ts +++ b/packages/polygen-cli/src/commands/clean.ts @@ -1,24 +1,19 @@ -import fs from 'node:fs/promises'; -import { Project } from '@callstack/polygen-project'; import chalk from 'chalk'; -import { Command } from 'commander'; import consola from 'consola'; -import { oraPromise } from 'ora'; - -const command = new Command('clean') - .description('Cleans all WASM generated output files') - .option('-y, --yes', 'Remove files without confirmation'); +import { cleanGeneratedFiles } from '../actions/clean.js'; +import { ProjectCommand } from '../helpers/with-project-options.js'; interface Options { yes: boolean; } -command.action(async (options: Options) => { - const project = await Project.findClosest(); - const generatedPath = project.fullOutputDirectory; - const displayPath = project.localOutputDirectory; +const command = new ProjectCommand('clean') + .description('Cleans all WASM generated output files') + .option('-y, --yes', 'Remove files without confirmation'); +command.action(async (project, options) => { let confirmed = options.yes; + const displayPath = project.paths.localOutputDirectory; if (!confirmed) { confirmed = await consola.prompt( @@ -34,10 +29,7 @@ command.action(async (options: Options) => { return; } - await oraPromise( - fs.rm(generatedPath, { recursive: true, force: true }), - `Removing ${chalk.bold(displayPath)}` - ); + await cleanGeneratedFiles(project); consola.success('Generated files removed!'); }); diff --git a/packages/polygen-cli/src/commands/generate.ts b/packages/polygen-cli/src/commands/generate.ts index 1c06bc68..988c9e25 100644 --- a/packages/polygen-cli/src/commands/generate.ts +++ b/packages/polygen-cli/src/commands/generate.ts @@ -1,31 +1,28 @@ +import fs from 'node:fs/promises'; import { FileExternallyChangedError, FileOverwriteError, - W2CGenerator, - type W2CGeneratorOptions, - W2CSharedContext, } from '@callstack/polygen-codegen'; -import { Project } from '@callstack/polygen-project'; -import type { ModuleSymbol } from '@callstack/wasm-parser'; import chalk from 'chalk'; -import { Command, Option } from 'commander'; +import { Option } from 'commander'; import consola from 'consola'; -import { oraPromise } from 'ora'; +import { cleanGeneratedFiles } from '../actions/clean.js'; +import { type GenerateOptions, generate } from '../actions/codegen.js'; +import { ProjectCommand } from '../helpers/with-project-options.js'; -const command = new Command('generate') +interface CommandOptions extends GenerateOptions { + clean?: boolean; +} + +const command = new ProjectCommand('generate') .description('Generates React Native Modules from Wasm') + .addOption(new Option('--clean', 'Clean output before generating')) .addOption( new Option('-o, --output-dir ', 'Path to output directory') ) .addOption(new Option('-f, --force', 'Generate code even if not outdated')); -interface Options { - outputDir?: string; - force?: boolean; -} - -command.action(async (options: Options) => { - const project = await Project.findClosest(); +command.action(async (project, options) => { if (options.outputDir) { consola.info(`Using ${chalk.dim(options.outputDir)} as output directory`); project.updateOptionsInMemory({ output: { directory: options.outputDir } }); @@ -35,70 +32,25 @@ command.action(async (options: Options) => { consola.warn('Using force overwrite flag, all files will be overwritten'); } - const generatorOptions: W2CGeneratorOptions = { - outputDirectory: project.fullOutputDirectory, - singleProject: true, - generateMetadata: true, - forceGenerate: options.force ?? false, - }; - const generator = await W2CGenerator.create(project, generatorOptions); - const allModules = project.webAssemblyModules; - - consola.info( - 'Generating code for', - chalk.bold(allModules.length), - 'WebAssembly module(s)' - ); - - try { - for (const mod of allModules) { - const generatedModule = await oraPromise( - async () => generator.generateModule(mod), - `Processing ${mod.kind} module ${chalk.bold(mod.path)}` - ); - - const imports = generatedModule.codegen.imports; - const exports = generatedModule.codegen.exports; - const hglt = chalk.dim; - - function statsOf(set: ModuleSymbol[]): string { - const highlight = chalk.dim; - const grouped = Object.groupBy(set, (s) => s.kind); - const countOf = (type: ModuleSymbol['kind']) => - grouped[type]?.length ?? 0; - return [ - `${highlight(countOf('function'))} functions`, - `${highlight(countOf('memory'))} memories`, - `${highlight(countOf('global'))} globals`, - `${highlight(countOf('table'))} tables`, - ].join(', '); - } - - consola.info( - ` Found ${hglt(imports.length)} imports (${statsOf(imports.map((i) => i.target))})` - ); - consola.info( - ` Found ${hglt(exports.length)} exports (${statsOf(exports.map((i) => i.target))})` - ); - } - - const sharedContext = new W2CSharedContext(generator.generatedModules); - await generator.generateHostModule(sharedContext); - consola.success('Generated host module'); - - const generateImportsPromises = sharedContext.importedModules.map( - async (mod) => { - await generator.generateImportedModule(mod); - consola.success(`Generated import ${chalk.magenta(mod.name)} bridge`); - } + const outputDirStat = await fs.stat(project.paths.fullOutputDirectory); + if (outputDirStat.isFile()) { + consola.error( + `Output directory ${chalk.magenta( + project.paths.fullOutputDirectory + )} is a file` ); + return; + } - await Promise.allSettled(generateImportsPromises); - await generator.finalize(); + await fs.mkdir(project.paths.fullOutputDirectory, { recursive: true }); - consola.info( - `Run ${chalk.bold('pod install')} to regenerate XCode project and make sure new source files are included` - ); + if (options.clean) { + await cleanGeneratedFiles(project); + consola.info('Cleaned generated files'); + } + + try { + await generate(options, project); } catch (error: unknown) { if (error instanceof FileExternallyChangedError) { consola.error( diff --git a/packages/polygen-cli/src/commands/init.ts b/packages/polygen-cli/src/commands/init.ts index f19ba996..782dcbef 100644 --- a/packages/polygen-cli/src/commands/init.ts +++ b/packages/polygen-cli/src/commands/init.ts @@ -8,7 +8,7 @@ import chalk from 'chalk'; import { Command, Option } from 'commander'; import consola from 'consola'; import { - PackageManager, + type PackageManager, addDependency, detectPackageManager, installDependencies, diff --git a/packages/polygen-cli/src/commands/scan.ts b/packages/polygen-cli/src/commands/scan.ts index 4e4702da..fdd1ef02 100644 --- a/packages/polygen-cli/src/commands/scan.ts +++ b/packages/polygen-cli/src/commands/scan.ts @@ -1,18 +1,22 @@ -import fs from 'node:fs/promises'; import { DependencyNotFoundError, PackageNotADependencyError, - Project, + type Project, resolveProjectDependency, } from '@callstack/polygen-project'; import chalk from 'chalk'; -import { Argument, Command, Option } from 'commander'; +import { Argument, Option } from 'commander'; import consola from 'consola'; import { globby } from 'globby'; import { oraPromise } from 'ora'; import 'core-js/proposals/set-methods-v2.js'; +import { ProjectCommand } from '../helpers/with-project-options.js'; -const command = new Command('scan') +interface Options { + update: boolean; +} + +const command = new ProjectCommand('scan') .description('Searches for WebAssembly modules in the project') .addArgument( new Argument('[package-name]', 'Optional name of external package') @@ -21,10 +25,6 @@ const command = new Command('scan') new Option('-u, --update', 'Automatically update polygen config file') ); -interface Options { - update: boolean; -} - async function scanLocal(project: Project): Promise { const { paths: pathsToScan } = project.options.scan; @@ -33,7 +33,9 @@ async function scanLocal(project: Project): Promise { 'Scanning for WebAssembly modules' ); - const currentModules = new Set(project.localModules.map((m) => m.path)); + const currentModules = new Set( + project.modules.declaredLocalModules.map((m) => m.path) + ); const foundModules = new Set(files); const added = foundModules.difference(currentModules); const removed = currentModules.difference(foundModules); @@ -50,7 +52,7 @@ async function scanLocal(project: Project): Promise { if (added.size > 0) { consola.info( - `To add them to the project, add following lines to your ${chalk.bold(project.configFileName)}:` + `To add them to the project, add following lines to your ${chalk.bold(project.paths.configFileName)}:` ); consola.log(``); @@ -99,7 +101,9 @@ async function scanExternal( ); const currentModules = new Set( - project.getModulesOfExternalDependency(packageName).map((m) => m.path) + project.modules + .getModulesOfExternalDependency(packageName) + .map((m) => m.path) ); const foundModules = new Set(files); const added = foundModules.difference(currentModules); @@ -117,7 +121,7 @@ async function scanExternal( if (added.size > 0) { consola.info( - `To add them to the project, add following lines to your ${chalk.bold(project.configFileName)}:` + `To add them to the project, add following lines to your ${chalk.bold(project.paths.configFileName)}:` ); consola.log(``); @@ -136,9 +140,7 @@ async function scanExternal( } } -command.action(async (packageName, options: Options) => { - const project = await Project.findClosest(); - +command.action(async (project, options, packageName) => { if (packageName) { await scanExternal(project, packageName); } else { diff --git a/packages/polygen-cli/src/helpers/with-project-options.ts b/packages/polygen-cli/src/helpers/with-project-options.ts new file mode 100644 index 00000000..0e522824 --- /dev/null +++ b/packages/polygen-cli/src/helpers/with-project-options.ts @@ -0,0 +1,34 @@ +import { Project } from '@callstack/polygen-project'; +import { Command } from 'commander'; +import type { GlobalOptions } from '../types.js'; + +function withProjectOptions( + func: ( + project: Project, + options: TOptions & GlobalOptions, + ...args: string[] + ) => void | Promise +) { + return async (...allArgs: any[]) => { + const args = allArgs.slice(0, -1) as string[]; + const options = allArgs[allArgs.length - 1] as TOptions & GlobalOptions; + const project = options.project + ? await Project.fromPath(options.project) + : await Project.findClosest(); + + await func(project, options, ...args); + }; +} + +export class ProjectCommand extends Command { + action( + func: ( + project: Project, + options: TOptions & GlobalOptions, + ...args: string[] + ) => void | Promise + ) { + super.action(withProjectOptions(func) as any); + return this; + } +} diff --git a/packages/polygen-cli/src/index.ts b/packages/polygen-cli/src/index.ts index aa87f2f5..dabf1b7d 100644 --- a/packages/polygen-cli/src/index.ts +++ b/packages/polygen-cli/src/index.ts @@ -8,6 +8,7 @@ import { import chalk from 'chalk'; import { Command } from 'commander'; import consola from 'consola'; +import pkgJson from '../package.json' with { type: 'json' }; import cleanCommand from './commands/clean.js'; import generateCommand from './commands/generate.js'; import initCommand from './commands/init.js'; @@ -15,23 +16,32 @@ import scanCommand from './commands/scan.js'; const program = new Command(); -program.name('polygen').description('Generates React Native Modules from Wasm'); +program + .name('polygen') + .version(pkgJson.version) + .description('Generates React Native Modules from Wasm'); program .configureHelp({ showGlobalOptions: true }) + // must match ./types.ts .option('-p, --project', 'Path to JS project') .option('-c, --config', 'Path to configuration file') - .option('-v, --verbose', 'Output verbose'); + .option('-v, --verbose', 'Output verbose') + .hook('preAction', (thisCommand) => { + if (thisCommand.opts().verbose) { + consola.level = 4; + } + }); + +program.action(() => { + program.help(); +}); program.addCommand(initCommand); program.addCommand(scanCommand); program.addCommand(generateCommand); program.addCommand(cleanCommand); -program.action(() => { - program.help(); -}); - async function run() { try { await program.parseAsync(); diff --git a/packages/polygen-cli/src/plugin.ts b/packages/polygen-cli/src/plugin.ts new file mode 100644 index 00000000..808d837a --- /dev/null +++ b/packages/polygen-cli/src/plugin.ts @@ -0,0 +1,30 @@ +import type { Codegen, W2CGeneratedModule } from '@callstack/polygen-codegen'; +import type { PolygenModuleConfig, Project } from '@callstack/polygen-project'; + +export interface CodegenPlugin { + name: string; + definition: PluginDefinition; +} + +export interface PluginCodegenHooks { + beforeModulesGenerated?(codegen: Codegen): Promise | void; + moduleGenerated( + codegen: Codegen, + moduleConfig: PolygenModuleConfig, + module: W2CGeneratedModule + ): Promise | void; +} + +export interface PluginDefinition extends PluginCodegenHooks { + projectDidLoad?(project: Project): void; +} + +export function definePlugin( + name: string, + definition: PluginDefinition +): CodegenPlugin { + return { + name, + definition, + }; +} diff --git a/packages/polygen-cli/src/types.ts b/packages/polygen-cli/src/types.ts new file mode 100644 index 00000000..8fae35f3 --- /dev/null +++ b/packages/polygen-cli/src/types.ts @@ -0,0 +1,8 @@ +export interface GlobalProjectOptions { + project?: string; + config?: string; +} + +export interface GlobalOptions extends GlobalProjectOptions { + verbose?: boolean; +} diff --git a/packages/polygen-cli/tsconfig.json b/packages/polygen-cli/tsconfig.json index aa1cb9e9..6daf1a1a 100644 --- a/packages/polygen-cli/tsconfig.json +++ b/packages/polygen-cli/tsconfig.json @@ -5,6 +5,7 @@ "compilerOptions": { "rootDir": "./src", "module": "nodenext", + "resolveJsonModule": true, "outDir": "dist" } } diff --git a/packages/polygen-config/src/types/global-config.ts b/packages/polygen-config/src/types/global-config.ts index 8799bb9b..6a443bfd 100644 --- a/packages/polygen-config/src/types/global-config.ts +++ b/packages/polygen-config/src/types/global-config.ts @@ -1,4 +1,4 @@ -import { PolygenModuleConfig } from './module-config'; +import type { PolygenModuleConfig } from './module-config'; export interface PolygenOutputConfig { /** diff --git a/packages/polygen-project/src/deps.ts b/packages/polygen-project/src/deps.ts index 863fd80b..32a03c97 100644 --- a/packages/polygen-project/src/deps.ts +++ b/packages/polygen-project/src/deps.ts @@ -1,8 +1,6 @@ import fs from 'node:fs/promises'; -import path from 'node:path'; -import { PolygenModuleConfig } from '@callstack/polygen-config'; import findUp from 'find-up'; -import { Project } from './project.js'; +import type { Project } from './project.js'; /** * Error thrown when specified package is not a dependency of the project. @@ -35,7 +33,9 @@ export class DependencyNotFoundError extends Error { export async function getPackageJson( project: Project ): Promise> { - return JSON.parse(await fs.readFile(project.pathTo('package.json'), 'utf-8')); + return JSON.parse( + await fs.readFile(project.paths.pathTo('package.json'), 'utf-8') + ); } /** @@ -70,26 +70,3 @@ export async function resolveProjectDependency( return resolved; } - -/** - * Resolves path to the specified module. - * - * @param project Project to resolve module for - * @param module Module to resolve path for - */ -export async function resolvePathToModule( - project: Project, - module: PolygenModuleConfig -): Promise { - switch (module.kind) { - case 'local': - return project.pathTo(module.path); - case 'external': - const modulePath = module.path; - const packagePath = await resolveProjectDependency( - project, - module.packageName - ); - return path.join(packagePath, modulePath); - } -} diff --git a/packages/polygen-project/src/index.ts b/packages/polygen-project/src/index.ts index 4b791537..9ab5f6d7 100644 --- a/packages/polygen-project/src/index.ts +++ b/packages/polygen-project/src/index.ts @@ -1,3 +1,4 @@ export * from '@callstack/polygen-config'; export * from './project.js'; +export * from './types.js'; export * from './deps.js'; diff --git a/packages/polygen-project/src/project.modules.ts b/packages/polygen-project/src/project.modules.ts new file mode 100644 index 00000000..5b1c3fa0 --- /dev/null +++ b/packages/polygen-project/src/project.modules.ts @@ -0,0 +1,126 @@ +import path from 'node:path'; +import type { + PolygenExternalModuleConfig, + PolygenLocalModuleConfig, + PolygenModuleConfig, +} from '@callstack/polygen-config'; +import { resolveProjectDependency } from './deps'; +import type { Project } from './project'; +import type { ResolvedExternalModule, ResolvedLocalModule } from './types'; + +export type Resolved = + TModule extends PolygenLocalModuleConfig + ? ResolvedLocalModule + : ResolvedExternalModule; + +/** + * Helper class for managing project modules. + */ +export class ProjectModules { + private readonly project: Project; + + private resolvedModuleCache: WeakMap< + PolygenModuleConfig, + Resolved + > = new WeakMap(); + + public constructor(project: Project) { + this.project = project; + } + + /** + * Get all WebAssembly modules in the project + */ + public get webAssemblyModules(): PolygenModuleConfig[] { + return this.project.options.modules; + } + + /** + * Declared Local modules + */ + public get declaredLocalModules(): PolygenLocalModuleConfig[] { + return this.project.options.modules.filter((m) => m.kind === 'local'); + } + + /** + * Local modules + */ + public async getLocalModules(): Promise { + const promises = this.declaredLocalModules.map(this.resolvePolygenModule); + return Promise.all(promises); + } + + /** + * Declared external modules + */ + public get declaredExternalModules(): PolygenExternalModuleConfig[] { + return this.project.options.modules.filter((m) => m.kind === 'external'); + } + + /** + * External modules + */ + public async getExternalModules(): Promise { + const promises = this.declaredExternalModules.map( + this.resolvePolygenModule + ); + return Promise.all(promises); + } + + /** + * Gets external webassembly modules from specified package + * + * @param packageName Name of the package + */ + public getModulesOfExternalDependency( + packageName: string + ): PolygenExternalModuleConfig[] { + return this.declaredExternalModules.filter( + (m) => m.packageName === packageName + ); + } + + /** + * Returns specified module with resolved path. + * + * @param project + * @param module + */ + public async resolvePolygenModule( + module: TModule + ): Promise> { + const cached = this.resolvedModuleCache.get(module); + if (cached) { + return cached as Resolved; + } + + const resolvedPath = await this.resolvePathToModule(module); + const resolvedModule: Resolved = { + ...module, + resolvedPath, + } as unknown as Resolved; + this.resolvedModuleCache.set(module, resolvedModule); + return resolvedModule; + } + + /** + * Resolves path to the specified module. + * + * @param module Module to resolve path for + */ + private async resolvePathToModule( + module: PolygenModuleConfig + ): Promise { + switch (module.kind) { + case 'local': + return this.project.paths.pathTo(module.path); + case 'external': + const modulePath = module.path; + const packagePath = await resolveProjectDependency( + this.project, + module.packageName + ); + return path.join(packagePath, modulePath); + } + } +} diff --git a/packages/polygen-project/src/project.paths.ts b/packages/polygen-project/src/project.paths.ts new file mode 100644 index 00000000..f3210156 --- /dev/null +++ b/packages/polygen-project/src/project.paths.ts @@ -0,0 +1,93 @@ +import path from 'path'; +import type { Project } from './project'; + +/** + * Helper type for accessing project paths + */ +export class ProjectPaths { + private readonly project: Project; + + constructor(project: Project) { + this.project = project; + } + + public get configFileName(): string { + return path.basename(this.project.configPath); + } + + /** + * Get full path to a file in the project + * + * @param components Path components + */ + public pathTo(...components: string[]): string { + return path.join(this.project.projectRoot, ...components); + } + + /** + * Get full path to a file in the source directory + * + * @param components Path components + */ + public pathToSource(...components: string[]): string { + return this.pathTo(this.localSourceDir, ...components); + } + + /** + * Get full path to a file in the output directory + * + * @param components Path components + */ + public pathToOutput(...components: string[]): string { + return this.pathTo(this.localOutputDirectory, ...components); + } + + /** + * Convert a global path to a local path + * + * @param targetPath Global path + * @param directoryInProject Directory in the project to consider as root + */ + public globalPathToLocal( + targetPath: string, + directoryInProject: string = '' + ): string { + const fullProjectPath = path.join( + this.project.projectRoot, + directoryInProject + ); + if (targetPath.startsWith(fullProjectPath)) { + return targetPath.slice(fullProjectPath.length).replace(/^\/+/, ''); + } + + return targetPath; + } + + /** + * Local output directory + */ + public get localOutputDirectory() { + return this.project.options.output.directory; + } + + /** + * Full path to the output directory + */ + public get fullOutputDirectory() { + return this.pathTo(this.localOutputDirectory); + } + + /** + * Local source directory + */ + public get localSourceDir(): string { + return 'src'; + } + + /** + * Full path to the source directory + */ + public get fullSourceDir(): string { + return this.pathToSource(); + } +} diff --git a/packages/polygen-project/src/project.ts b/packages/polygen-project/src/project.ts index ea46a414..9933bf6f 100644 --- a/packages/polygen-project/src/project.ts +++ b/packages/polygen-project/src/project.ts @@ -1,10 +1,4 @@ -import path from 'path'; -import type { - PolygenExternalModuleConfig, - PolygenLocalModuleConfig, - PolygenModuleConfig, - ResolvedPolygenConfig, -} from '@callstack/polygen-config'; +import type { ResolvedPolygenConfig } from '@callstack/polygen-config'; import { InvalidProjectConfigurationError, ProjectConfigurationNotFound, @@ -14,6 +8,8 @@ import { findProjectRootSync, } from '@callstack/polygen-config/find'; import deepmerge from 'deepmerge'; +import { ProjectModules } from './project.modules'; +import { ProjectPaths } from './project.paths'; type DeepPartial = T extends object ? { @@ -40,7 +36,14 @@ export class Project { */ public options: ResolvedPolygenConfig; - constructor( + /** + * Helper object for accessing project paths + */ + public readonly paths: ProjectPaths = new ProjectPaths(this); + + public readonly modules: ProjectModules = new ProjectModules(this); + + private constructor( projectRoot: string, configPath: string, options: ResolvedPolygenConfig @@ -51,7 +54,7 @@ export class Project { } /** - * Creates a project from closes package.json + * Creates a project from nearest package.json. * * @param from Directory to start looking from, defaults to current directory. * @returns Promise with Project instance @@ -60,6 +63,31 @@ export class Project { */ static async findClosest(from?: string): Promise { const projectRoot = await findProjectRoot(from); + return Project.fromPath(projectRoot); + } + + /** + * Creates a project from nearest package.json, synchronously. + * + * @param from Directory to start looking from, defaults to current directory. + * @returns Promise with Project instance + * + * @see findClosest Asynchronous version + */ + static findClosestSync(from?: string): Project { + const projectRoot = findProjectRootSync(from); + return Project.fromPathSync(projectRoot); + } + + /** + * Creates a project from specified project root path. + * + * @param projectRoot Path to the project + * @returns Promise with Project instance + * + * @see fromPathSync Synchronous version + */ + static async fromPath(projectRoot: string): Promise { const configPath = await findConfigFile(projectRoot); if (!configPath) { throw new ProjectConfigurationNotFound(); @@ -77,15 +105,14 @@ export class Project { } /** - * Creates a project from closes package.json, synchronously + * Creates a project from specified project root path, synchronously. * - * @param from Directory to start looking from, defaults to current directory. - * @returns Promise with Project instance + * @param projectRoot Path to the project + * @returns Project instance * - * @see findClosest Asynchronous version + * @see fromPath Asynchronous version */ - static findClosestSync(from?: string): Project { - const projectRoot = findProjectRootSync(from); + static fromPathSync(projectRoot: string): Project { const configPath = findConfigFileSync(projectRoot); if (!configPath) { throw new ProjectConfigurationNotFound(); @@ -102,116 +129,7 @@ export class Project { } } - public get configFileName(): string { - return path.basename(this.configPath); - } - public updateOptionsInMemory(options: DeepPartial) { this.options = deepmerge(this.options, options) as ResolvedPolygenConfig; } - - /** - * Get full path to a file in the project - * - * @param components Path components - */ - public pathTo(...components: string[]): string { - return path.join(this.projectRoot, ...components); - } - - /** - * Get full path to a file in the source directory - * - * @param components Path components - */ - public pathToSource(...components: string[]): string { - return this.pathTo(this.localSourceDir, ...components); - } - - /** - * Get full path to a file in the output directory - * - * @param components Path components - */ - public pathToOutput(...components: string[]): string { - return this.pathTo(this.localOutputDirectory, ...components); - } - - /** - * Convert a global path to a local path - * - * @param targetPath Global path - * @param directoryInProject Directory in the project to consider as root - */ - public globalPathToLocal( - targetPath: string, - directoryInProject: string = '' - ): string { - const fullProjectPath = path.join(this.projectRoot, directoryInProject); - if (targetPath.startsWith(fullProjectPath)) { - return targetPath.slice(fullProjectPath.length).replace(/^\/+/, ''); - } - - return targetPath; - } - - /** - * Get all WebAssembly modules in the project - */ - public get webAssemblyModules(): PolygenModuleConfig[] { - return this.options.modules; - } - - /** - * Local output directory - */ - public get localOutputDirectory() { - return this.options.output.directory; - } - - /** - * Full path to the output directory - */ - public get fullOutputDirectory() { - return this.pathTo(this.localOutputDirectory); - } - - /** - * Local source directory - */ - public get localSourceDir(): string { - return 'src'; - } - - /** - * Full path to the source directory - */ - public get fullSourceDir(): string { - return this.pathToSource(); - } - - /** - * Local modules - */ - public get localModules(): PolygenLocalModuleConfig[] { - return this.options.modules.filter((m) => m.kind === 'local'); - } - - /** - * External modules - */ - public get externalModules(): PolygenExternalModuleConfig[] { - return this.options.modules.filter((m) => m.kind === 'external'); - } - - /** - * Gets external webassembly modules from specified package - * - * @param packageName Name of the package - */ - public getModulesOfExternalDependency( - packageName: string - ): PolygenExternalModuleConfig[] { - return this.externalModules.filter((m) => m.packageName === packageName); - } } diff --git a/packages/polygen-project/src/types.ts b/packages/polygen-project/src/types.ts new file mode 100644 index 00000000..1bea3107 --- /dev/null +++ b/packages/polygen-project/src/types.ts @@ -0,0 +1,15 @@ +import type { + PolygenExternalModuleConfig, + PolygenLocalModuleConfig, + PolygenModuleConfig, +} from '@callstack/polygen-config'; + +export interface ModuleResolutionInfo { + resolvedPath: string; +} + +export type ResolvedLocalModule = PolygenLocalModuleConfig & + ModuleResolutionInfo; +export type ResolvedExternalModule = PolygenExternalModuleConfig & + ModuleResolutionInfo; +export type ResolvedModule = ResolvedLocalModule | ResolvedExternalModule; diff --git a/packages/polygen/cpp/CMakeLists.txt b/packages/polygen/cpp/CMakeLists.txt new file mode 100644 index 00000000..9a6b5d86 --- /dev/null +++ b/packages/polygen/cpp/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(wasm-rt) +add_subdirectory(ReactNativePolygen) \ No newline at end of file diff --git a/packages/polygen/cpp/ReactNativePolygen.cpp b/packages/polygen/cpp/ReactNativePolygen.cpp deleted file mode 100644 index 6648b184..00000000 --- a/packages/polygen/cpp/ReactNativePolygen.cpp +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (c) callstack.io. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -#include -#include -#include "ReactNativePolygen.h" -#include "bridge.h" -#include "WebAssembly.h" -#include "NativeStateHelper.h" - -using namespace callstack::polygen; - -namespace facebook::react { - -ReactNativePolygen::ReactNativePolygen(std::shared_ptr jsInvoker) -: NativePolygenCxxSpecJSI(std::move(jsInvoker)) { - wasm_rt_init(); -} - -ReactNativePolygen::~ReactNativePolygen() { - wasm_rt_free(); -} - -bool ReactNativePolygen::copyNativeHandle(jsi::Runtime &rt, jsi::Object holder, jsi::Object source) { - auto hasNativeState = source.hasNativeState(rt); - - if (hasNativeState) { - holder.setNativeState(rt, source.getNativeState(rt)); - } - - return hasNativeState; -} - - -jsi::Object ReactNativePolygen::loadModule(jsi::Runtime &rt, jsi::Object holder, jsi::Object moduleData) { - auto buffer = moduleData.getArrayBuffer(rt); - std::span bufferView { buffer.data(rt), buffer.size(rt) }; - try { - auto mod = generated::loadWebAssemblyModule(bufferView); - NativeStateHelper::attach(rt, holder, mod); - return buildModuleMetadata(rt, mod); - } catch (const LoaderError& loaderError) { - throw jsi::JSError(rt, loaderError.what()); - } -} - -void ReactNativePolygen::unloadModule(jsi::Runtime &rt, jsi::Object module) { - module.setNativeState(rt, nullptr); -} - -jsi::Object ReactNativePolygen::getModuleMetadata(jsi::Runtime &rt, jsi::Object moduleHolder) { - auto mod = NativeStateHelper::tryGet(rt, moduleHolder); - return buildModuleMetadata(rt, mod); -} - -void ReactNativePolygen::createModuleInstance(jsi::Runtime &rt, jsi::Object instanceHolder, jsi::Object moduleHolder, jsi::Object importObject) { - auto mod = NativeStateHelper::tryGet(rt, moduleHolder); - mod->createInstance(rt, instanceHolder, std::move(importObject)); -} - -void ReactNativePolygen::destroyModuleInstance(jsi::Runtime &rt, jsi::Object instance) { - instance.setNativeState(rt, nullptr); -} - - -// Memories -void ReactNativePolygen::createMemory(jsi::Runtime &rt, jsi::Object holder, double initial, std::optional maximum) { - auto maxPages = (uint64_t)maximum.value_or(100); - auto memory = std::make_shared((uint64_t)initial, maxPages, false); - NativeStateHelper::attach(rt, holder, memory); -} - -jsi::Object ReactNativePolygen::getMemoryBuffer(jsi::Runtime &rt, jsi::Object instance) { - auto memoryState = NativeStateHelper::tryGet(rt, instance); - - jsi::ArrayBuffer buffer {rt, memoryState}; - return buffer; -} - -void ReactNativePolygen::growMemory(jsi::Runtime &rt, jsi::Object instance, double delta) { - auto memory = NativeStateHelper::tryGet(rt, instance); - memory->grow((uint64_t)delta); -} - - -// Globals -void ReactNativePolygen::createGlobal(jsi::Runtime &rt, jsi::Object holder, jsi::Object globalDescriptor, double initialValue) { - auto descriptor = Bridging::fromJs(rt, globalDescriptor, jsInvoker_); - auto waType = static_cast(descriptor.type); - jsi::Value initial { initialValue }; - - auto globalVar = std::make_shared(waType, std::move(initial), descriptor.isMutable); - NativeStateHelper::attach(rt, holder, globalVar); -} - -double ReactNativePolygen::getGlobalValue(jsi::Runtime &rt, jsi::Object instance) { - auto globalVar = NativeStateHelper::tryGet(rt, instance); - return globalVar->getValue().asNumber(); -} - -void ReactNativePolygen::setGlobalValue(jsi::Runtime &rt, jsi::Object instance, double newValue) { - auto globalVar = NativeStateHelper::tryGet(rt, instance); - globalVar->setValue(rt, {newValue}); -} - - -// Tables -void ReactNativePolygen::createTable(jsi::Runtime &rt, jsi::Object holder, jsi::Object tableDescriptor, std::optional initial) { - auto descriptor = NativeTableDescriptorBridging::fromJs(rt, tableDescriptor, this->jsInvoker_); - std::shared_ptr table; - auto maxSizeNumber = descriptor.maxSize.has_value() - ? std::make_optional((double)descriptor.maxSize.value()) - : std::nullopt; - - switch (descriptor.element) { - case NativeTableElementType::AnyFunc: - table = std::make_shared((size_t)descriptor.initialSize, descriptor.maxSize); - break; - case NativeTableElementType::ExternRef: - table = std::make_shared((size_t)descriptor.initialSize, descriptor.maxSize); - break; - default: - assert(0); - } - - NativeStateHelper::attach(rt, holder, table); -} - -void ReactNativePolygen::growTable(jsi::Runtime &rt, jsi::Object instance, double delta) { - auto table = NativeStateHelper::tryGet
(rt, instance); - table->grow(delta); -} - -jsi::Object ReactNativePolygen::getTableElement(jsi::Runtime &rt, jsi::Object instance, double index) { - auto table = NativeStateHelper::tryGet
(rt, instance); - - auto element = table->getElement(index); - return NativeStateHelper::wrap(rt, element); -} - -void ReactNativePolygen::setTableElement(jsi::Runtime &rt, jsi::Object instance, double index, jsi::Object value) { - auto table = NativeStateHelper::tryGet
(rt, instance); - auto element = NativeStateHelper::tryGet(rt, value); - table->setElement(index, element); -} - -double ReactNativePolygen::getTableSize(jsi::Runtime &rt, jsi::Object instance) { - auto table = NativeStateHelper::tryGet
(rt, instance); - return table->getSize(); -} - -jsi::Object ReactNativePolygen::buildModuleMetadata(jsi::Runtime& rt, const std::shared_ptr& mod) { - auto imports = mod->getImports(); - auto exports = mod->getExports(); - - std::vector importsMapped; - std::vector exportsMapped; - - importsMapped.reserve(imports.size()); - exportsMapped.reserve(exports.size()); - - for (auto& import_ : imports) { - importsMapped.push_back({ import_.module, import_.name, static_cast(import_.kind) }); - } - - for (auto& export_ : exports) { - exportsMapped.push_back({ export_.name, static_cast(export_.kind) }); - } - - NativeModuleMetadata result { importsMapped, exportsMapped }; - return bridging::toJs(rt, result, this->jsInvoker_); -} - -} diff --git a/packages/polygen/cpp/ReactNativePolygen/CMakeLists.txt b/packages/polygen/cpp/ReactNativePolygen/CMakeLists.txt new file mode 100644 index 00000000..9c4b0d2b --- /dev/null +++ b/packages/polygen/cpp/ReactNativePolygen/CMakeLists.txt @@ -0,0 +1,25 @@ +file(GLOB_RECURSE polygen_headers CONFIGURE_DEPENDS *.h *.hpp) +file(GLOB_RECURSE polygen_sources CONFIGURE_DEPENDS *.cpp *.c *.cxx) + +add_library(polygen STATIC ${polygen_sources} ${polygen_headers}) +target_sources(polygen + PRIVATE + ${polygen_sources} + ${polygen_headers} + + PUBLIC + FILE_SET HEADERS + BASE_DIRS .. + FILES ${polygen_headers} +) +#target_include_directories(polygen PUBLIC SYSTEM .) + +# Check for test-runner +#if(DEFINED fakern) +target_link_libraries(polygen INTERFACE JSI) +target_link_libraries(polygen PUBLIC wasm-rt fakern) + +get_target_property(polygen_SOURCE_DIR polygen SOURCE_DIR) +set(project_dir "${polygen_SOURCE_DIR}/../..") +target_add_react_native_codegen(polygen PATH "${project_dir}") +#endif() \ No newline at end of file diff --git a/packages/polygen/cpp/NativeStateHelper.h b/packages/polygen/cpp/ReactNativePolygen/NativeStateHelper.h similarity index 100% rename from packages/polygen/cpp/NativeStateHelper.h rename to packages/polygen/cpp/ReactNativePolygen/NativeStateHelper.h diff --git a/packages/polygen/cpp/ReactNativePolygen/ReactNativePolygen.cpp b/packages/polygen/cpp/ReactNativePolygen/ReactNativePolygen.cpp new file mode 100644 index 00000000..a710a6b0 --- /dev/null +++ b/packages/polygen/cpp/ReactNativePolygen/ReactNativePolygen.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) callstack.io. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +#include +#include + +#include "ReactNativePolygen.h" +#include "bridge.h" +#include "NativeStateHelper.h" + +using namespace callstack::polygen; + +namespace facebook::react { + ReactNativePolygen::ReactNativePolygen(std::shared_ptr jsInvoker) + : NativePolygenCxxSpecJSI(std::move(jsInvoker)) { + wasm_rt_init(); + } + + ReactNativePolygen::~ReactNativePolygen() { + wasm_rt_free(); + } + + bool ReactNativePolygen::copyNativeHandle(jsi::Runtime &rt, jsi::Object holder, jsi::Object source) { + auto hasNativeState = source.hasNativeState(rt); + + if (hasNativeState) { + holder.setNativeState(rt, source.getNativeState(rt)); + } + + return hasNativeState; + } + + + jsi::Object ReactNativePolygen::loadModule(jsi::Runtime &rt, jsi::Object holder, jsi::Object moduleData) { + auto buffer = moduleData.getArrayBuffer(rt); + std::span bufferView{buffer.data(rt), buffer.size(rt)}; + try { + auto mod = generated::loadWebAssemblyModule(bufferView); + NativeStateHelper::attach(rt, holder, mod); + return buildModuleMetadata(rt, mod); + } catch (const LoaderError &loaderError) { + throw jsi::JSError(rt, loaderError.what()); + } + } + + void ReactNativePolygen::unloadModule(jsi::Runtime &rt, jsi::Object module) { + module.setNativeState(rt, nullptr); + } + + jsi::Object ReactNativePolygen::getModuleMetadata(jsi::Runtime &rt, jsi::Object moduleHolder) { + auto mod = NativeStateHelper::tryGet(rt, moduleHolder); + return buildModuleMetadata(rt, mod); + } + + void ReactNativePolygen::createModuleInstance(jsi::Runtime &rt, jsi::Object instanceHolder, + jsi::Object moduleHolder, jsi::Object importObject) { + auto mod = NativeStateHelper::tryGet(rt, moduleHolder); + mod->createInstance(rt, instanceHolder, std::move(importObject)); + } + + void ReactNativePolygen::destroyModuleInstance(jsi::Runtime &rt, jsi::Object instance) { + instance.setNativeState(rt, nullptr); + } + + + // Memories + void ReactNativePolygen::createMemory(jsi::Runtime &rt, jsi::Object holder, double initial, + std::optional maximum) { + auto maxPages = (uint64_t) maximum.value_or(100); + auto memory = std::make_shared((uint64_t) initial, maxPages, false); + NativeStateHelper::attach(rt, holder, memory); + } + + jsi::Object ReactNativePolygen::getMemoryBuffer(jsi::Runtime &rt, jsi::Object instance) { + auto memoryState = NativeStateHelper::tryGet(rt, instance); + + jsi::ArrayBuffer buffer{rt, memoryState}; + return buffer; + } + + void ReactNativePolygen::growMemory(jsi::Runtime &rt, jsi::Object instance, double delta) { + auto memory = NativeStateHelper::tryGet(rt, instance); + memory->grow((uint64_t) delta); + } + + + // Globals + void ReactNativePolygen::createGlobal(jsi::Runtime &rt, jsi::Object holder, jsi::Object globalDescriptor, + double initialValue) { + auto descriptor = Bridging::fromJs(rt, globalDescriptor, jsInvoker_); + auto waType = static_cast(descriptor.type); + jsi::Value initial{initialValue}; + + auto globalVar = std::make_shared(waType, std::move(initial), descriptor.isMutable); + NativeStateHelper::attach(rt, holder, globalVar); + } + + double ReactNativePolygen::getGlobalValue(jsi::Runtime &rt, jsi::Object instance) { + auto globalVar = NativeStateHelper::tryGet(rt, instance); + return globalVar->getValue().asNumber(); + } + + void ReactNativePolygen::setGlobalValue(jsi::Runtime &rt, jsi::Object instance, double newValue) { + auto globalVar = NativeStateHelper::tryGet(rt, instance); + globalVar->setValue(rt, {newValue}); + } + + + // Tables + void ReactNativePolygen::createTable(jsi::Runtime &rt, jsi::Object holder, jsi::Object tableDescriptor, + std::optional initial) { + auto descriptor = NativeTableDescriptorBridging::fromJs(rt, tableDescriptor, this->jsInvoker_); + std::shared_ptr
table; + auto maxSizeNumber = descriptor.maxSize.has_value() + ? std::make_optional((double) descriptor.maxSize.value()) + : std::nullopt; + + switch (descriptor.element) { + case NativeTableElementType::AnyFunc: + table = std::make_shared((size_t) descriptor.initialSize, descriptor.maxSize); + break; + case NativeTableElementType::ExternRef: + table = std::make_shared((size_t) descriptor.initialSize, descriptor.maxSize); + break; + default: + assert(0); + } + + NativeStateHelper::attach(rt, holder, table); + } + + void ReactNativePolygen::growTable(jsi::Runtime &rt, jsi::Object instance, double delta) { + auto table = NativeStateHelper::tryGet
(rt, instance); + table->grow(delta); + } + + jsi::Object ReactNativePolygen::getTableElement(jsi::Runtime &rt, jsi::Object instance, double index) { + auto table = NativeStateHelper::tryGet
(rt, instance); + + auto element = table->getElement(index); + return NativeStateHelper::wrap(rt, element); + } + + void ReactNativePolygen::setTableElement(jsi::Runtime &rt, jsi::Object instance, double index, jsi::Object value) { + auto table = NativeStateHelper::tryGet
(rt, instance); + auto element = NativeStateHelper::tryGet(rt, value); + table->setElement(index, element); + } + + double ReactNativePolygen::getTableSize(jsi::Runtime &rt, jsi::Object instance) { + auto table = NativeStateHelper::tryGet
(rt, instance); + return table->getSize(); + } + + jsi::Object ReactNativePolygen::buildModuleMetadata(jsi::Runtime &rt, const std::shared_ptr &mod) { + auto imports = mod->getImports(); + auto exports = mod->getExports(); + + std::vector importsMapped; + std::vector exportsMapped; + + importsMapped.reserve(imports.size()); + exportsMapped.reserve(exports.size()); + + for (auto &import_: imports) { + importsMapped.push_back({import_.module, import_.name, static_cast(import_.kind)}); + } + + for (auto &export_: exports) { + exportsMapped.push_back({export_.name, static_cast(export_.kind)}); + } + + NativeModuleMetadata result{importsMapped, exportsMapped}; + return bridging::toJs(rt, result, this->jsInvoker_); + } +} diff --git a/packages/polygen/cpp/ReactNativePolygen.h b/packages/polygen/cpp/ReactNativePolygen/ReactNativePolygen.h similarity index 100% rename from packages/polygen/cpp/ReactNativePolygen.h rename to packages/polygen/cpp/ReactNativePolygen/ReactNativePolygen.h diff --git a/packages/polygen/cpp/SharedLibraryModule.h b/packages/polygen/cpp/ReactNativePolygen/SharedLibraryModule.h similarity index 100% rename from packages/polygen/cpp/SharedLibraryModule.h rename to packages/polygen/cpp/ReactNativePolygen/SharedLibraryModule.h diff --git a/packages/polygen/cpp/StaticLibraryModule.h b/packages/polygen/cpp/ReactNativePolygen/StaticLibraryModule.h similarity index 100% rename from packages/polygen/cpp/StaticLibraryModule.h rename to packages/polygen/cpp/ReactNativePolygen/StaticLibraryModule.h diff --git a/packages/polygen/cpp/ReactNativePolygen/WebAssembly.h b/packages/polygen/cpp/ReactNativePolygen/WebAssembly.h new file mode 100644 index 00000000..4be340ec --- /dev/null +++ b/packages/polygen/cpp/ReactNativePolygen/WebAssembly.h @@ -0,0 +1,14 @@ +/* + * Copyright (c) callstack.io. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +#pragma once + +#include +#include +#include +#include +#include +#include diff --git a/packages/polygen/cpp/webassembly/ExternRefTable.h b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/ExternRefTable.h similarity index 98% rename from packages/polygen/cpp/webassembly/ExternRefTable.h rename to packages/polygen/cpp/ReactNativePolygen/WebAssembly/ExternRefTable.h index a0ea3416..d6452e8c 100644 --- a/packages/polygen/cpp/webassembly/ExternRefTable.h +++ b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/ExternRefTable.h @@ -7,7 +7,7 @@ #pragma once #include -#include +#include #include "Table.h" namespace callstack::polygen { diff --git a/packages/polygen/cpp/webassembly/FuncRefTable.h b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/FuncRefTable.h similarity index 96% rename from packages/polygen/cpp/webassembly/FuncRefTable.h rename to packages/polygen/cpp/ReactNativePolygen/WebAssembly/FuncRefTable.h index c5c0f32e..a08a0e91 100644 --- a/packages/polygen/cpp/webassembly/FuncRefTable.h +++ b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/FuncRefTable.h @@ -8,7 +8,7 @@ #include #include -#include +#include #include "Table.h" namespace callstack::polygen { @@ -50,7 +50,7 @@ class FuncRefTable: public Table { return this->table_->max_size; } - void grow(ptrdiff_t delta) { + void grow(ptrdiff_t delta) override { wasm_rt_grow_funcref_table(this->table_, delta, { nullptr, nullptr, nullptr, nullptr }); } diff --git a/packages/polygen/cpp/webassembly/Global.h b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/Global.h similarity index 98% rename from packages/polygen/cpp/webassembly/Global.h rename to packages/polygen/cpp/ReactNativePolygen/WebAssembly/Global.h index c80dcfb5..8c7959ef 100644 --- a/packages/polygen/cpp/webassembly/Global.h +++ b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/Global.h @@ -7,7 +7,7 @@ #pragma once #include -#include +#include namespace callstack::polygen { diff --git a/packages/polygen/cpp/webassembly/Memory.h b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/Memory.h similarity index 96% rename from packages/polygen/cpp/webassembly/Memory.h rename to packages/polygen/cpp/ReactNativePolygen/WebAssembly/Memory.h index 35977b7b..3d324d32 100644 --- a/packages/polygen/cpp/webassembly/Memory.h +++ b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/Memory.h @@ -7,7 +7,7 @@ #pragma once #include -#include +#include namespace callstack::polygen { diff --git a/packages/polygen/cpp/webassembly/Module.h b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/Module.h similarity index 100% rename from packages/polygen/cpp/webassembly/Module.h rename to packages/polygen/cpp/ReactNativePolygen/WebAssembly/Module.h diff --git a/packages/polygen/cpp/webassembly/Table.h b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/Table.h similarity index 96% rename from packages/polygen/cpp/webassembly/Table.h rename to packages/polygen/cpp/ReactNativePolygen/WebAssembly/Table.h index 26b50f6d..49d5908f 100644 --- a/packages/polygen/cpp/webassembly/Table.h +++ b/packages/polygen/cpp/ReactNativePolygen/WebAssembly/Table.h @@ -8,7 +8,7 @@ #include #include -#include +#include namespace callstack::polygen { diff --git a/packages/polygen/cpp/bridge.cpp b/packages/polygen/cpp/ReactNativePolygen/bridge.cpp similarity index 100% rename from packages/polygen/cpp/bridge.cpp rename to packages/polygen/cpp/ReactNativePolygen/bridge.cpp diff --git a/packages/polygen/cpp/bridge.h b/packages/polygen/cpp/ReactNativePolygen/bridge.h similarity index 94% rename from packages/polygen/cpp/bridge.h rename to packages/polygen/cpp/ReactNativePolygen/bridge.h index 66c0f3b3..d946cb7c 100644 --- a/packages/polygen/cpp/bridge.h +++ b/packages/polygen/cpp/ReactNativePolygen/bridge.h @@ -7,11 +7,10 @@ #pragma once #include -#include #include #include -#include -#include "Module.h" +#include +#include namespace callstack::polygen { diff --git a/packages/polygen/cpp/gen-utils.h b/packages/polygen/cpp/ReactNativePolygen/gen-utils.h similarity index 100% rename from packages/polygen/cpp/gen-utils.h rename to packages/polygen/cpp/ReactNativePolygen/gen-utils.h diff --git a/packages/polygen/cpp/utils/checksum.cpp b/packages/polygen/cpp/ReactNativePolygen/utils/checksum.cpp similarity index 100% rename from packages/polygen/cpp/utils/checksum.cpp rename to packages/polygen/cpp/ReactNativePolygen/utils/checksum.cpp diff --git a/packages/polygen/cpp/utils/checksum.h b/packages/polygen/cpp/ReactNativePolygen/utils/checksum.h similarity index 100% rename from packages/polygen/cpp/utils/checksum.h rename to packages/polygen/cpp/ReactNativePolygen/utils/checksum.h diff --git a/packages/polygen/cpp/utils/hashpp.h b/packages/polygen/cpp/ReactNativePolygen/utils/hashpp.h similarity index 100% rename from packages/polygen/cpp/utils/hashpp.h rename to packages/polygen/cpp/ReactNativePolygen/utils/hashpp.h diff --git a/packages/polygen/cpp/w2c.cpp b/packages/polygen/cpp/ReactNativePolygen/w2c.cpp similarity index 100% rename from packages/polygen/cpp/w2c.cpp rename to packages/polygen/cpp/ReactNativePolygen/w2c.cpp diff --git a/packages/polygen/cpp/w2c.h b/packages/polygen/cpp/ReactNativePolygen/w2c.h similarity index 100% rename from packages/polygen/cpp/w2c.h rename to packages/polygen/cpp/ReactNativePolygen/w2c.h diff --git a/packages/polygen/cpp/WebAssembly.h b/packages/polygen/cpp/WebAssembly.h deleted file mode 100644 index 15451808..00000000 --- a/packages/polygen/cpp/WebAssembly.h +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) callstack.io. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -#pragma once - -#include -#include -#include -#include -#include -#include diff --git a/packages/polygen/cpp/wasm-rt/CMakeLists.txt b/packages/polygen/cpp/wasm-rt/CMakeLists.txt new file mode 100644 index 00000000..46a553ed --- /dev/null +++ b/packages/polygen/cpp/wasm-rt/CMakeLists.txt @@ -0,0 +1,14 @@ +file(GLOB_RECURSE wasmrt_headers CONFIGURE_DEPENDS *.h *.hpp) +file(GLOB_RECURSE wasmrt_sources CONFIGURE_DEPENDS *.cpp *.c *.cxx) + +add_library(wasm-rt STATIC) +target_sources(wasm-rt + PRIVATE + ${wasmrt_sources} + ${wasmrt_headers} + + PUBLIC + FILE_SET HEADERS + BASE_DIRS .. + FILES ${wasmrt_headers} +) \ No newline at end of file diff --git a/packages/polygen/cpp/wasm-rt/wasm-rt.h b/packages/polygen/cpp/wasm-rt/wasm-rt.h index 82fa35c7..d15aeac7 100644 --- a/packages/polygen/cpp/wasm-rt/wasm-rt.h +++ b/packages/polygen/cpp/wasm-rt/wasm-rt.h @@ -32,8 +32,13 @@ extern "C" { #endif #if __has_builtin(__builtin_expect) +#ifndef UNLIKELY #define UNLIKELY(x) __builtin_expect(!!(x), 0) +#endif + +#ifndef LIKELY #define LIKELY(x) __builtin_expect(!!(x), 1) +#endif #else #define UNLIKELY(x) (x) #define LIKELY(x) (x) diff --git a/packages/polygen/package.json b/packages/polygen/package.json index 731dee90..6e309988 100644 --- a/packages/polygen/package.json +++ b/packages/polygen/package.json @@ -69,6 +69,7 @@ "devDependencies": { "@callstack/polygen-cli": "0.2.0", "@callstack/polygen-typescript-config": "workspace:^", + "@react-native-community/cli": "^15.1.3", "@types/react": "^18.2.44", "del-cli": "^5.1.0", "react": "17.0.2", diff --git a/packages/polygen/src/api/Instance.ts b/packages/polygen/src/api/Instance.ts index f780686f..8c4ef5d1 100644 --- a/packages/polygen/src/api/Instance.ts +++ b/packages/polygen/src/api/Instance.ts @@ -1,4 +1,4 @@ -import NativeWASM, { InternalModuleMetadata } from '../NativePolygen'; +import NativeWASM, { type InternalModuleMetadata } from '../NativePolygen'; import { Global } from './Global'; import { Memory } from './Memory'; import { Module } from './Module'; diff --git a/packages/wasm-parser/src/module.ts b/packages/wasm-parser/src/module.ts index 446e2b54..ecf12364 100644 --- a/packages/wasm-parser/src/module.ts +++ b/packages/wasm-parser/src/module.ts @@ -19,12 +19,12 @@ import type { TypeSection, } from './reader/types.js'; import type { + ModuleEntity, ModuleExport, ModuleFunction, ModuleGlobal, ModuleImport, ModuleMemory, - ModuleSymbol, ModuleTable, } from './types.js'; @@ -32,7 +32,7 @@ import type { * Class representing a WebAssembly Module. */ export class Module { - private importsByType: Record = { + private importsByType: Record = { function: [], global: [], memory: [], @@ -167,7 +167,7 @@ export class Module { */ resolveExportDescriptor( descriptor: ExportDescriptor - ): ModuleSymbol | undefined { + ): ModuleEntity | undefined { const importsOfType = this.importsByType[descriptor.type]; if (descriptor.index < importsOfType.length) { return importsOfType[descriptor.index]; @@ -233,7 +233,7 @@ function mapTable(table: TableType): ModuleTable { function resolveImportDescriptor( descriptor: ImportDescriptor, types: FunctionType[] -): ModuleSymbol | undefined { +): ModuleEntity | undefined { switch (descriptor.type) { case 'function': const targetType = types[descriptor.index]; diff --git a/packages/wasm-parser/src/reader/section-reader.ts b/packages/wasm-parser/src/reader/section-reader.ts index 33760ecd..00e38cd3 100644 --- a/packages/wasm-parser/src/reader/section-reader.ts +++ b/packages/wasm-parser/src/reader/section-reader.ts @@ -1,4 +1,4 @@ -import { BinaryReader } from '@callstack/polygen-binary-utils'; +import type { BinaryReader } from '@callstack/polygen-binary-utils'; import { WebAssemblyDecodeError } from './errors.js'; import { readFunctionType, diff --git a/packages/wasm-parser/src/reader/type-reader.ts b/packages/wasm-parser/src/reader/type-reader.ts index 07313f1e..22363a1b 100644 --- a/packages/wasm-parser/src/reader/type-reader.ts +++ b/packages/wasm-parser/src/reader/type-reader.ts @@ -1,4 +1,4 @@ -import { BinaryReader } from '@callstack/polygen-binary-utils'; +import type { BinaryReader } from '@callstack/polygen-binary-utils'; import { WebAssemblyDecodeError } from './errors.js'; import type { FunctionType, diff --git a/packages/wasm-parser/src/reader/utils.ts b/packages/wasm-parser/src/reader/utils.ts index 8501d433..f7ba8ca8 100644 --- a/packages/wasm-parser/src/reader/utils.ts +++ b/packages/wasm-parser/src/reader/utils.ts @@ -1,4 +1,4 @@ -import { BinaryReader } from '@callstack/polygen-binary-utils'; +import type { BinaryReader } from '@callstack/polygen-binary-utils'; import { WebAssemblyDecodeError } from './errors.js'; const decoder = new TextDecoder(); diff --git a/packages/wasm-parser/src/types.ts b/packages/wasm-parser/src/types.ts index 09e44b53..33875afc 100644 --- a/packages/wasm-parser/src/types.ts +++ b/packages/wasm-parser/src/types.ts @@ -38,18 +38,20 @@ export interface ModuleTable { } /** - * Union type representing any symbol in a WebAssembly module. + * Union type representing any entity in a WebAssembly module. */ -export type ModuleSymbol = +export type ModuleEntity = | ModuleFunction | ModuleGlobal | ModuleMemory | ModuleTable; +export type ModuleEntityKind = ModuleEntity['kind']; + /** * Represents an import in a WebAssembly module. */ -export interface ModuleImport { +export interface ModuleImport { kind: 'import'; module: string; name: string; @@ -59,7 +61,7 @@ export interface ModuleImport { /** * Represents an export in a WebAssembly module. */ -export interface ModuleExport { +export interface ModuleExport { kind: 'export'; name: string; target: T; diff --git a/test-runner/CMakeLists.txt b/test-runner/CMakeLists.txt new file mode 100644 index 00000000..8658654f --- /dev/null +++ b/test-runner/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.30) +project(polygen_test_runner C CXX) + +include(cmake/RNCodeGen.cmake) +include(cmake/Polygen.cmake) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +#find_package(GTest CONFIG REQUIRED) +find_package(Hermes CONFIG REQUIRED) +find_package(folly REQUIRED) + +add_subdirectory(../packages/polygen/cpp polygen) +add_subdirectory(fakern) +add_subdirectory(shell) + +add_subdirectory(tests) + diff --git a/test-runner/cmake/Polygen.cmake b/test-runner/cmake/Polygen.cmake new file mode 100644 index 00000000..32717a10 --- /dev/null +++ b/test-runner/cmake/Polygen.cmake @@ -0,0 +1,30 @@ +include(CMakeParseArguments) + +function(polygen_module TARGET_NAME) + cmake_parse_arguments(ARGS + "" # Flags + "PATH" # Single value arguments + "" # Multi value arguments + ${ARGV} + ) + + if(NOT ARGS_PATH) + message(FATAL_ERROR "You must provide a PATH") + endif() + + set(OUTPUT_DIR "${CMAKE_BINARY_DIR}/polygen-codegen/${TARGET_NAME}") + set(GENERATED_SOURCE_PATH "${OUTPUT_DIR}") + + add_custom_command( + OUTPUT "${OUTPUT_DIR}" + WORKING_DIRECTORY "${ARGS_PATH}" + COMMAND npx ARGS polygen generate --output-dir "${OUTPUT_DIR}" + ) + + file(GLOB_RECURSE GENERATED_SOURCE_FILES "${GENERATED_SOURCE_PATH}/*.h" "${GENERATED_SOURCE_PATH}/*.cpp") + message(GENERATED_SOURCE_PATH=${GENERATED_SOURCE_PATH}) + message(GENERATED_SOURCE_FILES=${GENERATED_SOURCE_FILES}) + add_library(${TARGET_NAME} STATIC ${GENERATED_SOURCE_FILES}) + target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${GENERATED_SOURCE_PATH}) + target_link_libraries(${TARGET_NAME} PRIVATE JSI polygen) +endfunction() \ No newline at end of file diff --git a/test-runner/cmake/RNCodeGen.cmake b/test-runner/cmake/RNCodeGen.cmake new file mode 100644 index 00000000..22a145e1 --- /dev/null +++ b/test-runner/cmake/RNCodeGen.cmake @@ -0,0 +1,49 @@ +include(CMakeParseArguments) + +function(react_native_codegen TARGET_NAME) + cmake_parse_arguments(ARGS + "" # Flags + "PATH" # Single value arguments + "" # Multi value arguments + ${ARGV} + ) + + if(NOT ARGS_PATH) + message(FATAL_ERROR "You must provide a PATH") + endif() + + file(GLOB_RECURSE project_native_files + CONFIGURE_DEPENDS + ${ARGS_PATH}/*.h + ${ARGS_PATH}/*.hxx + ${ARGS_PATH}/*.hpp + ${ARGS_PATH}/*.cpp + ${ARGS_PATH}/*.cxx + ${ARGS_PATH}/*.cxx + ) + + set(OUTPUT_DIR "${CMAKE_BINARY_DIR}/rn-codegen") + set(GENERATED_SOURCE_PATH "${OUTPUT_DIR}/build/generated/ios") + + add_custom_command( + DEPENDS ${TARGET_NAME} + OUTPUT "${OUTPUT_DIR}" + WORKING_DIRECTORY "${ARGS_PATH}" + COMMAND yarn ARGS run react-native codegen --outputPath "${OUTPUT_DIR}" + ) + + file(GLOB_RECURSE GENERATED_SOURCE_FILES "${GENERATED_SOURCE_PATH}/*.h" "${GENERATED_SOURCE_PATH}/*.cpp") + target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${GENERATED_SOURCE_PATH}) + target_sources(${TARGET_NAME} PRIVATE ${GENERATED_SOURCE_FILES}) +endfunction() + +function(target_add_react_native_codegen TARGET_NAME) + cmake_parse_arguments(ARGS + "" # Flags + "PATH" # Single value arguments + "" # Multi value arguments + ${ARGV} + ) + + react_native_codegen(${TARGET_NAME} PATH "${ARGS_PATH}") +endfunction() \ No newline at end of file diff --git a/test-runner/fakern/CMakeLists.txt b/test-runner/fakern/CMakeLists.txt new file mode 100644 index 00000000..5dd9ad13 --- /dev/null +++ b/test-runner/fakern/CMakeLists.txt @@ -0,0 +1,14 @@ +file(GLOB_RECURSE fakern_headers CONFIGURE_DEPENDS *.hpp *.h) +file(GLOB_RECURSE fakern_sources CONFIGURE_DEPENDS *.c *.cpp *.cxx) + +add_library(fakern STATIC ${fakern_headers} ${fakern_sources} + DummyRuntime.cpp) +target_sources(fakern PUBLIC FILE_SET HEADERS + TYPE HEADERS + BASE_DIRS "${fakern_SOURCE_BASE_DIR}" + FILES ${fakern_headers} +) + +target_include_directories(fakern PUBLIC SYSTEM .) +target_include_directories(fakern PUBLIC .) +target_link_libraries(fakern PUBLIC Hermes::Hermes Folly::folly) \ No newline at end of file diff --git a/test-runner/fakern/DummyCallInvoker.cpp b/test-runner/fakern/DummyCallInvoker.cpp new file mode 100644 index 00000000..b584fd25 --- /dev/null +++ b/test-runner/fakern/DummyCallInvoker.cpp @@ -0,0 +1,45 @@ +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. +// + +#include + +#include "DummyCallInvoker.hpp" + +namespace fakern { + +void DummyCallInvoker::invokeAsync(facebook::react::CallFunc&& func) noexcept +{ + _runtime.runLater(std::move(func)); +} + +void DummyCallInvoker::invokeAsync( + facebook::react::SchedulerPriority priority, + facebook::react::CallFunc&& func +) noexcept +{ + // folly prio is reversed + _runtime.runWithPriority(std::move(func), 5 - static_cast(priority)); +} + +void DummyCallInvoker::invokeSync(facebook::react::CallFunc&& func) +{ + _runtime.runNowBlocking(std::move(func)); +} + +void DummyCallInvoker::invokeAsync(std::function&& function) noexcept +{ + _runtime.runLater([func = std::move(function)](auto& rt) mutable { + func(); + }); +} + +void DummyCallInvoker::invokeSync(std::function&& function) +{ + _runtime.runNowBlocking([func = std::move(function)](auto& rt) mutable { + func(); + }); +} + +} diff --git a/test-runner/fakern/DummyCallInvoker.hpp b/test-runner/fakern/DummyCallInvoker.hpp new file mode 100644 index 00000000..2da009bd --- /dev/null +++ b/test-runner/fakern/DummyCallInvoker.hpp @@ -0,0 +1,34 @@ +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. +// + +#pragma once +#include +#include + +namespace fakern { + +class DummyCallInvoker final: public facebook::react::CallInvoker { +public: + DummyCallInvoker(DummyRuntime& runtime) noexcept: _runtime(runtime) {} + ~DummyCallInvoker() noexcept override = default; + + DummyCallInvoker(const DummyCallInvoker&) = delete; + DummyCallInvoker& operator=(const DummyCallInvoker&) = delete; + + DummyCallInvoker(DummyCallInvoker&&) = delete; + DummyCallInvoker& operator=(DummyCallInvoker&&) = delete; + + void invokeAsync(facebook::react::CallFunc&& func) noexcept override; + + void invokeAsync(facebook::react::SchedulerPriority priority, facebook::react::CallFunc&& func) noexcept override; + void invokeSync(facebook::react::CallFunc&& func) override; + void invokeAsync(std::function&& func) noexcept override; + void invokeSync(std::function&& func) override; + + private: + DummyRuntime& _runtime; +}; + +} \ No newline at end of file diff --git a/test-runner/fakern/DummyRuntime.cpp b/test-runner/fakern/DummyRuntime.cpp new file mode 100644 index 00000000..08050375 --- /dev/null +++ b/test-runner/fakern/DummyRuntime.cpp @@ -0,0 +1,78 @@ +// +// Created by Robert Pasiński on 25/02/2025. +// +#include +#include + +#include "DummyRuntime.h" +#include "DummyCallInvoker.hpp" + +namespace fakern { + +DummyRuntime::DummyRuntime(std::unique_ptr&& rt) noexcept : _jsiRuntime(std::move(rt)) +{ + _invoker = std::make_unique(*this); +} + +void DummyRuntime::runLater(Func function) +{ + _eventBaseThread.add([func = std::move(function), this]() mutable { + func(*this); + }); +} + +void DummyRuntime::runWithPriority(Func function, int32_t priority) +{ + _eventBaseThread.addWithPriority([func = std::move(function), this]() mutable { + func(*this); + }, priority); +} + +void DummyRuntime::runAfter(Func function, std::chrono::milliseconds delay) +{ + _eventBaseThread.getEventBase()->runAfterDelay([func = std::move(function), this]() mutable { + func(*this); + }, delay.count()); +} + +void DummyRuntime::runNowBlocking(Func func) +{ + folly::Baton<> ready; + if (_eventBaseThread.getEventBase()->isInEventBaseThread()) { + func(*this); + } + else { + _eventBaseThread.getEventBase()->runInEventBaseThread([&ready, func = std::move(func), this]() mutable { + SCOPE_EXIT + { + ready.post(); + }; + folly::copy(std::move(func))(*this); + }); + } + + ready.wait(); +} + +void DummyRuntime::runInBackground(Func function) +{ + _eventBaseThread.getEventBase()->add([func = std::move(function), this]() mutable { + func(*this); + }); +} + +facebook::jsi::Value DummyRuntime::executeScript(const std::string& script, const std::string& name) +{ + return _jsiRuntime->evaluateJavaScript(std::make_unique(script), name); +} + +bool DummyRuntime::isRunning() const noexcept +{ + return _eventBaseThread.getEventBase()->isRunning(); +} + +void DummyRuntime::stop() +{ + return _eventBaseThread.getEventBase()->terminateLoopSoon(); +} +} diff --git a/test-runner/fakern/DummyRuntime.h b/test-runner/fakern/DummyRuntime.h new file mode 100644 index 00000000..48512239 --- /dev/null +++ b/test-runner/fakern/DummyRuntime.h @@ -0,0 +1,46 @@ +// +// Created by Robert Pasiński on 25/02/2025. +// +#pragma once +#include +#include +#include +#include + +namespace fakern { + +class DummyRuntime { + public: + using Func = std::function; + + explicit DummyRuntime(std::unique_ptr&& rt) noexcept; + ~DummyRuntime() noexcept = default; + + void runLater(Func function); + void runWithPriority(Func function, int32_t priority); + void runAfter(Func function, std::chrono::milliseconds delay); + void runNowBlocking(Func func); + void runInBackground(Func func); + + facebook::jsi::Value executeScript(const std::string& script, const std::string& name); + + bool isRunning() const noexcept; + void stop(); + + operator facebook::jsi::Runtime&() const noexcept + { + return *_jsiRuntime; + } + + auto callInvoker() const & -> facebook::react::CallInvoker& + { + return *_invoker; + } + + private: + folly::ScopedEventBaseThread _eventBaseThread; + std::unique_ptr _invoker; + std::unique_ptr _jsiRuntime; +}; + +} diff --git a/test-runner/fakern/ReactCommon/CallInvoker.h b/test-runner/fakern/ReactCommon/CallInvoker.h new file mode 100644 index 00000000..947d3fb4 --- /dev/null +++ b/test-runner/fakern/ReactCommon/CallInvoker.h @@ -0,0 +1,64 @@ +/* +* Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include "SchedulerPriority.h" + +namespace facebook::jsi { +class Runtime; +} + +namespace facebook::react { + +using CallFunc = std::function; + +/** + * An interface for a generic native-to-JS call invoker. See BridgeJSCallInvoker + * for an implementation. + */ +class CallInvoker { +public: + virtual void invokeAsync(CallFunc&& func) noexcept = 0; + virtual void invokeAsync( + SchedulerPriority /*priority*/, + CallFunc&& func) noexcept { + // When call with priority is not implemented, fall back to a regular async + // execution + invokeAsync(std::move(func)); + } + virtual void invokeSync(CallFunc&& func) = 0; + + // Backward compatibility only, prefer the CallFunc methods instead + virtual void invokeAsync(std::function&& func) noexcept { + invokeAsync([func](jsi::Runtime&) { func(); }); + } + + virtual void invokeSync(std::function&& func) { + invokeSync([func](jsi::Runtime&) { func(); }); + } + + virtual ~CallInvoker() {} +}; + +using NativeMethodCallFunc = std::function; + +class NativeMethodCallInvoker { +public: + virtual void invokeAsync( + const std::string& methodName, + NativeMethodCallFunc&& func) noexcept = 0; + virtual void invokeSync( + const std::string& methodName, + NativeMethodCallFunc&& func) = 0; + virtual ~NativeMethodCallInvoker() {} +}; + +} // namespace facebook::react \ No newline at end of file diff --git a/test-runner/fakern/ReactCommon/SchedulerPriority.h b/test-runner/fakern/ReactCommon/SchedulerPriority.h new file mode 100644 index 00000000..15309673 --- /dev/null +++ b/test-runner/fakern/ReactCommon/SchedulerPriority.h @@ -0,0 +1,22 @@ +/* +* Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +enum class SchedulerPriority : int { + ImmediatePriority = 1, + UserBlockingPriority = 2, + NormalPriority = 3, + LowPriority = 4, + IdlePriority = 5, + }; + +} // namespace facebook::react \ No newline at end of file diff --git a/test-runner/fakern/ReactCommon/TurboModule.cpp b/test-runner/fakern/ReactCommon/TurboModule.cpp new file mode 100644 index 00000000..b4fcaf6d --- /dev/null +++ b/test-runner/fakern/ReactCommon/TurboModule.cpp @@ -0,0 +1,57 @@ +/* +* Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TurboModule.h" +//#include + +namespace facebook::react { + +TurboModuleMethodValueKind getTurboModuleMethodValueKind( + jsi::Runtime& rt, + const jsi::Value* value) { + if (!value || value->isUndefined() || value->isNull()) { + return VoidKind; + } else if (value->isBool()) { + return BooleanKind; + } else if (value->isNumber()) { + return NumberKind; + } else if (value->isString()) { + return StringKind; + } else if (value->isObject()) { + auto object = value->asObject(rt); + if (object.isArray(rt)) { + return ArrayKind; + } else if (object.isFunction(rt)) { + return FunctionKind; + } + return ObjectKind; + } + return VoidKind; +} + +void TurboModule::emitDeviceEvent( + const std::string& eventName, + ArgFactory argFactory) { + jsInvoker_->invokeAsync([eventName, argFactory](jsi::Runtime& rt) { + jsi::Value emitter = rt.global().getProperty(rt, "__rctDeviceEventEmitter"); + if (!emitter.isUndefined()) { + jsi::Object emitterObject = emitter.asObject(rt); + // TODO: consider caching these + jsi::Function emitFunction = + emitterObject.getPropertyAsFunction(rt, "emit"); + std::vector args; + args.emplace_back(jsi::String::createFromAscii(rt, eventName.c_str())); + if (argFactory) { + argFactory(rt, args); + } + emitFunction.callWithThis( + rt, emitterObject, (const jsi::Value*)args.data(), args.size()); + } + }); +} + +} // namespace facebook::react \ No newline at end of file diff --git a/test-runner/fakern/ReactCommon/TurboModule.h b/test-runner/fakern/ReactCommon/TurboModule.h new file mode 100644 index 00000000..027b83a9 --- /dev/null +++ b/test-runner/fakern/ReactCommon/TurboModule.h @@ -0,0 +1,163 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +#include + +#include +#include + +namespace facebook::react { + +/** + * For now, support the same set of return types as existing impl. + * This can be improved to support richer typed objects. + */ +enum TurboModuleMethodValueKind { + VoidKind, + BooleanKind, + NumberKind, + StringKind, + ObjectKind, + ArrayKind, + FunctionKind, + PromiseKind, +}; + +/** + * Determines TurboModuleMethodValueKind based on the jsi::Value type. + */ +TurboModuleMethodValueKind getTurboModuleMethodValueKind( + jsi::Runtime& rt, + const jsi::Value* value); + +class TurboCxxModule; +class TurboModuleBinding; + +/** + * Base HostObject class for every module to be exposed to JS + */ +class JSI_EXPORT TurboModule : public jsi::HostObject { + public: + TurboModule(std::string name, std::shared_ptr jsInvoker) + : name_(std::move(name)), jsInvoker_(std::move(jsInvoker)) {} + + // DO NOT OVERRIDE - it will become final in a future release. + // This method provides automatic caching of properties on the TurboModule's + // JS representation. To customize lookup of properties, override `create`. + // Note: keep this method declared inline to avoid conflicts + // between RTTI and non-RTTI compilation units + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& propName) + override { + auto prop = create(runtime, propName); + // If we have a JS wrapper, cache the result of this lookup + // We don't cache misses, to allow for methodMap_ to dynamically be + // extended + if (jsRepresentation_ && !prop.isUndefined()) { + jsRepresentation_->lock(runtime).asObject(runtime).setProperty( + runtime, propName, prop); + } + return prop; + } + + std::vector getPropertyNames( + jsi::Runtime& runtime) override { + std::vector result; + result.reserve(methodMap_.size()); + for (auto it = methodMap_.cbegin(); it != methodMap_.cend(); ++it) { + result.push_back(jsi::PropNameID::forUtf8(runtime, it->first)); + } + return result; + } + + protected: + const std::string name_; + std::shared_ptr jsInvoker_; + + struct MethodMetadata { + size_t argCount; + jsi::Value (*invoker)( + jsi::Runtime& rt, + TurboModule& turboModule, + const jsi::Value* args, + size_t count); + }; + std::unordered_map methodMap_; + std::unordered_map> + eventEmitterMap_; + + using ArgFactory = + std::function& args)>; + + /** + * Calls RCTDeviceEventEmitter.emit to JavaScript, with given event name and + * an optional list of arguments. + * If present, argFactory is a callback used to construct extra arguments, + * e.g. + * + * emitDeviceEvent(rt, "myCustomEvent", + * [](jsi::Runtime& rt, std::vector& args) { + * args.emplace_back(jsi::Value(true)); + * args.emplace_back(jsi::Value(42)); + * }); + */ + void emitDeviceEvent( + const std::string& eventName, + ArgFactory argFactory = nullptr); + + // Backwards compatibility version + void emitDeviceEvent( + jsi::Runtime& /*runtime*/, + + const std::string& eventName, + ArgFactory argFactory = nullptr) { + emitDeviceEvent(eventName, std::move(argFactory)); + } + + virtual jsi::Value create( + jsi::Runtime& runtime, + const jsi::PropNameID& propName) { + std::string propNameUtf8 = propName.utf8(runtime); + if (auto methodIter = methodMap_.find(propNameUtf8); + methodIter != methodMap_.end()) { + const MethodMetadata& meta = methodIter->second; + return jsi::Function::createFromHostFunction( + runtime, + propName, + static_cast(meta.argCount), + [this, meta]( + jsi::Runtime& rt, + [[maybe_unused]] const jsi::Value& thisVal, + const jsi::Value* args, + size_t count) { return meta.invoker(rt, *this, args, count); }); + } else if (auto eventEmitterIter = eventEmitterMap_.find(propNameUtf8); + eventEmitterIter != eventEmitterMap_.end()) { + return eventEmitterIter->second->get(runtime, jsInvoker_); + } else { + // Neither Method nor EventEmitter were found, let JS decide what to do + return jsi::Value::undefined(); + } + } + + private: + friend class TurboModuleBinding; + std::unique_ptr jsRepresentation_; +}; + +/** + * An app/platform-specific provider function to get an instance of a module + * given a name. + */ +using TurboModuleProviderFunctionType = + std::function(const std::string& name)>; + +} // namespace facebook::react \ No newline at end of file diff --git a/test-runner/fakern/react/bridging/AString.h b/test-runner/fakern/react/bridging/AString.h new file mode 100644 index 00000000..05929553 --- /dev/null +++ b/test-runner/fakern/react/bridging/AString.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static std::string fromJs(jsi::Runtime& rt, const jsi::String& value) { + return value.utf8(rt); + } + + static jsi::String toJs(jsi::Runtime& rt, const std::string& value) { + return jsi::String::createFromUtf8(rt, value); + } +}; + +template <> +struct Bridging { + static jsi::String toJs(jsi::Runtime& rt, std::string_view value) { + return jsi::String::createFromUtf8( + rt, reinterpret_cast(value.data()), value.length()); + } +}; + +template <> +struct Bridging : Bridging {}; + +template +struct Bridging : Bridging {}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Array.h b/test-runner/fakern/react/bridging/Array.h new file mode 100644 index 00000000..58455aa2 --- /dev/null +++ b/test-runner/fakern/react/bridging/Array.h @@ -0,0 +1,151 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace array_detail { + +template +struct BridgingStatic { + static jsi::Array toJs( + jsi::Runtime& rt, + const T& array, + const std::shared_ptr& jsInvoker) { + return toJs(rt, array, jsInvoker, std::make_index_sequence{}); + } + + private: + template + static jsi::Array toJs( + facebook::jsi::Runtime& rt, + const T& array, + const std::shared_ptr& jsInvoker, + std::index_sequence) { + return jsi::Array::createWithElements( + rt, bridging::toJs(rt, std::get(array), jsInvoker)...); + } +}; + +template +struct BridgingDynamic { + static jsi::Array toJs( + jsi::Runtime& rt, + const T& list, + const std::shared_ptr& jsInvoker) { + jsi::Array result(rt, list.size()); + size_t index = 0; + + for (const auto& item : list) { + result.setValueAtIndex(rt, index++, bridging::toJs(rt, item, jsInvoker)); + } + + return result; + } +}; + +} // namespace array_detail + +template +struct Bridging> + : array_detail::BridgingStatic, N> { + static std::array fromJs( + facebook::jsi::Runtime& rt, + const jsi::Array& array, + const std::shared_ptr& jsInvoker) { + size_t length = array.length(rt); + + std::array result; + for (size_t i = 0; i < length; i++) { + result[i] = + bridging::fromJs(rt, array.getValueAtIndex(rt, i), jsInvoker); + } + + return result; + } +}; + +template +struct Bridging> + : array_detail::BridgingStatic, 2> { + static std::pair fromJs( + facebook::jsi::Runtime& rt, + const jsi::Array& array, + const std::shared_ptr& jsInvoker) { + return std::make_pair( + bridging::fromJs(rt, array.getValueAtIndex(rt, 0), jsInvoker), + bridging::fromJs(rt, array.getValueAtIndex(rt, 1), jsInvoker)); + } +}; + +template +struct Bridging> + : array_detail::BridgingStatic, sizeof...(Types)> {}; + +template +struct Bridging> : array_detail::BridgingDynamic> { +}; + +template +struct Bridging> + : array_detail::BridgingDynamic> {}; + +template +struct Bridging> : array_detail::BridgingDynamic> {}; + +template +struct Bridging> + : array_detail::BridgingDynamic> { + static std::vector fromJs( + facebook::jsi::Runtime& rt, + const jsi::Array& array, + const std::shared_ptr& jsInvoker) { + size_t length = array.length(rt); + + std::vector vector; + vector.reserve(length); + + for (size_t i = 0; i < length; i++) { + vector.push_back( + bridging::fromJs(rt, array.getValueAtIndex(rt, i), jsInvoker)); + } + + return vector; + } +}; + +template +struct Bridging> : array_detail::BridgingDynamic> { + static std::set fromJs( + facebook::jsi::Runtime& rt, + const jsi::Array& array, + const std::shared_ptr& jsInvoker) { + size_t length = array.length(rt); + + std::set set; + for (size_t i = 0; i < length; i++) { + set.insert( + bridging::fromJs(rt, array.getValueAtIndex(rt, i), jsInvoker)); + } + + return set; + } +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Base.h b/test-runner/fakern/react/bridging/Base.h new file mode 100644 index 00000000..214d9ddd --- /dev/null +++ b/test-runner/fakern/react/bridging/Base.h @@ -0,0 +1,177 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include +#include +#include + +namespace facebook::react { + +class CallInvoker; + +template +struct Bridging; + +template <> +struct Bridging { + // Highly generic code may result in "casting" to void. + static void fromJs(jsi::Runtime&, const jsi::Value&) {} +}; + +namespace bridging { +namespace detail { + +template +struct function_wrapper; + +template +struct function_wrapper { + using type = std::function; +}; + +template +struct function_wrapper { + using type = std::function; +}; + +template +struct bridging_wrapper { + using type = remove_cvref_t; +}; + +// Convert lambda types to move-only function types since we can't specialize +// Bridging templates for arbitrary lambdas. +template +struct bridging_wrapper< + T, + std::void_t::operator())>> + : function_wrapper::operator())> {}; + +} // namespace detail + +template +using bridging_t = typename detail::bridging_wrapper::type; + +template , int> = 0> +auto fromJs(jsi::Runtime& rt, T&& value, const std::shared_ptr&) + -> decltype(static_cast( + std::move(convert(rt, std::forward(value))))) { + return static_cast(std::move(convert(rt, std::forward(value)))); +} + +template +auto fromJs(jsi::Runtime& rt, T&& value, const std::shared_ptr&) + -> decltype(Bridging>::fromJs( + rt, + convert(rt, std::forward(value)))) { + return Bridging>::fromJs( + rt, convert(rt, std::forward(value))); +} + +template +auto fromJs( + jsi::Runtime& rt, + T&& value, + const std::shared_ptr& jsInvoker) + -> decltype(Bridging>::fromJs( + rt, + convert(rt, std::forward(value)), + jsInvoker)) { + return Bridging>::fromJs( + rt, convert(rt, std::forward(value)), jsInvoker); +} + +template , int> = 0> +auto toJs( + jsi::Runtime& rt, + T&& value, + const std::shared_ptr& = nullptr) -> remove_cvref_t { + return convert(rt, std::forward(value)); +} + +template +auto toJs( + jsi::Runtime& rt, + T&& value, + const std::shared_ptr& = nullptr) + -> decltype(Bridging>::toJs(rt, std::forward(value))) { + return Bridging>::toJs(rt, std::forward(value)); +} + +template +auto toJs( + jsi::Runtime& rt, + T&& value, + const std::shared_ptr& jsInvoker) + -> decltype(Bridging>::toJs( + rt, + std::forward(value), + jsInvoker)) { + return Bridging>::toJs(rt, std::forward(value), jsInvoker); +} + +template +inline constexpr bool supportsFromJs = false; + +template +inline constexpr bool supportsFromJs< + T, + Arg, + std::void_t( + std::declval(), + std::declval(), + nullptr))>> = true; + +template +inline constexpr bool supportsFromJs< + T, + jsi::Value, + std::void_t( + std::declval(), + std::declval(), + nullptr))>> = true; + +template +inline constexpr bool supportsToJs = false; + +template +inline constexpr bool supportsToJs< + T, + Ret, + std::void_t(), + std::declval(), + nullptr))>> = + std::is_convertible_v< + decltype(toJs( + std::declval(), + std::declval(), + nullptr)), + Ret>; + +template +inline constexpr bool supportsToJs< + T, + jsi::Value, + std::void_t(), + std::declval(), + nullptr))>> = + std::is_convertible_v< + decltype(toJs( + std::declval(), + std::declval(), + nullptr)), + jsi::Value>; + +} // namespace bridging +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Bool.h b/test-runner/fakern/react/bridging/Bool.h new file mode 100644 index 00000000..86143113 --- /dev/null +++ b/test-runner/fakern/react/bridging/Bool.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +template <> +struct Bridging { + static bool fromJs(jsi::Runtime&, const jsi::Value& value) { + return value.asBool(); + } + + static jsi::Value toJs(jsi::Runtime&, bool value) { + return value; + } +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Bridging.h b/test-runner/fakern/react/bridging/Bridging.h new file mode 100644 index 00000000..2ea53d58 --- /dev/null +++ b/test-runner/fakern/react/bridging/Bridging.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/test-runner/fakern/react/bridging/CMakeLists.txt b/test-runner/fakern/react/bridging/CMakeLists.txt new file mode 100644 index 00000000..09dad427 --- /dev/null +++ b/test-runner/fakern/react/bridging/CMakeLists.txt @@ -0,0 +1,23 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"ReactNative\") + +file(GLOB react_bridging_SRC CONFIGURE_DEPENDS *.cpp) + +add_library(react_bridging OBJECT ${react_bridging_SRC}) + +target_include_directories(react_bridging PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_bridging jsi callinvoker) diff --git a/test-runner/fakern/react/bridging/CallbackWrapper.h b/test-runner/fakern/react/bridging/CallbackWrapper.h new file mode 100644 index 00000000..3f0d4c81 --- /dev/null +++ b/test-runner/fakern/react/bridging/CallbackWrapper.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include + +#include + +namespace facebook::react { + +class CallInvoker; + +// Helper for passing jsi::Function arg to other methods. +class CallbackWrapper : public LongLivedObject { + private: + CallbackWrapper( + jsi::Function&& callback, + jsi::Runtime& runtime, + std::shared_ptr jsInvoker) + : LongLivedObject(runtime), + callback_(std::move(callback)), + jsInvoker_(std::move(jsInvoker)) {} + + jsi::Function callback_; + std::shared_ptr jsInvoker_; + + public: + static std::weak_ptr createWeak( + jsi::Function&& callback, + jsi::Runtime& runtime, + std::shared_ptr jsInvoker) { + auto wrapper = std::shared_ptr(new CallbackWrapper( + std::move(callback), runtime, std::move(jsInvoker))); + LongLivedObjectCollection::get(runtime).add(wrapper); + return wrapper; + } + + // Delete the enclosed jsi::Function + void destroy() { + allowRelease(); + } + + jsi::Function& callback() noexcept { + return callback_; + } + + jsi::Runtime& runtime() noexcept { + return runtime_; + } + + CallInvoker& jsInvoker() noexcept { + return *(jsInvoker_); + } + + std::shared_ptr jsInvokerPtr() noexcept { + return jsInvoker_; + } +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Class.h b/test-runner/fakern/react/bridging/Class.h new file mode 100644 index 00000000..61fa82e3 --- /dev/null +++ b/test-runner/fakern/react/bridging/Class.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react::bridging { + +template < + typename T, + typename C, + typename R, + typename... Args, + typename... JSArgs> +T callFromJs( + jsi::Runtime& rt, + R (C::*method)(jsi::Runtime&, Args...), + const std::shared_ptr& jsInvoker, + C* instance, + JSArgs&&... args) { + static_assert( + sizeof...(Args) == sizeof...(JSArgs), "Incorrect arguments length"); + static_assert( + (supportsFromJs && ...), "Incompatible arguments"); + + if constexpr (std::is_void_v) { + (instance->*method)( + rt, fromJs(rt, std::forward(args), jsInvoker)...); + + } else if constexpr (std::is_void_v) { + static_assert( + std::is_same_v, + "Void functions may only return undefined"); + + (instance->*method)( + rt, fromJs(rt, std::forward(args), jsInvoker)...); + return jsi::Value(); + + } else if constexpr (is_jsi_v) { + static_assert(supportsToJs, "Incompatible return type"); + + return toJs( + rt, + (instance->*method)( + rt, fromJs(rt, std::forward(args), jsInvoker)...), + jsInvoker); + + } else if constexpr (is_optional_jsi_v) { + static_assert( + is_optional_v + ? supportsToJs + : supportsToJs, + "Incompatible return type"); + + auto result = toJs( + rt, + (instance->*method)( + rt, fromJs(rt, std::forward(args), jsInvoker)...), + jsInvoker); + + if constexpr (std::is_same_v) { + if (result.isNull() || result.isUndefined()) { + return std::nullopt; + } + } + + return convert(rt, std::move(result)); + } else { + static_assert(std::is_convertible_v, "Incompatible return type"); + return (instance->*method)( + rt, fromJs(rt, std::forward(args), jsInvoker)...); + } +} + +template +constexpr size_t getParameterCount(R (*)(Args...)) { + return sizeof...(Args); +} + +template +constexpr size_t getParameterCount(R (C::*)(Args...)) { + return sizeof...(Args); +} + +} // namespace facebook::react::bridging diff --git a/test-runner/fakern/react/bridging/Convert.h b/test-runner/fakern/react/bridging/Convert.h new file mode 100644 index 00000000..7659cb06 --- /dev/null +++ b/test-runner/fakern/react/bridging/Convert.h @@ -0,0 +1,170 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include + +namespace facebook::react::bridging { + +// std::remove_cvref_t is not available until C++20. +template +using remove_cvref_t = std::remove_cv_t>; + +template +inline constexpr bool is_jsi_v = + std::is_same_v> || + std::is_same_v> || + std::is_base_of_v>; + +template +struct is_optional : std::false_type {}; + +template +struct is_optional> : std::true_type {}; + +template +inline constexpr bool is_optional_v = is_optional::value; + +template +inline constexpr bool is_optional_jsi_v = false; + +template +inline constexpr bool + is_optional_jsi_v>> = + is_jsi_v; + +template +struct Converter; + +template +struct ConverterBase { + using BaseT = remove_cvref_t; + + ConverterBase(jsi::Runtime& rt, T&& value) + : rt_(rt), value_(std::forward(value)) {} + + operator BaseT() && { + if constexpr (std::is_lvalue_reference_v) { + // Copy the reference into a Value that then can be moved from. + auto value = jsi::Value(rt_, value_); + + if constexpr (std::is_same_v) { + return std::move(value); + } else if constexpr (std::is_same_v) { + return std::move(value).getString(rt_); + } else if constexpr (std::is_same_v) { + return std::move(value).getObject(rt_); + } else if constexpr (std::is_same_v) { + return std::move(value).getObject(rt_).getArray(rt_); + } else if constexpr (std::is_same_v) { + return std::move(value).getObject(rt_).getFunction(rt_); + } + } else { + return std::move(value_); + } + } + + template < + typename U, + std::enable_if_t< + std::is_lvalue_reference_v && + // Ensure non-reference type can be converted to the desired type. + std::is_convertible_v, U>, + int> = 0> + operator U() && { + return Converter(rt_, std::move(*this).operator BaseT()); + } + + template < + typename U, + std::enable_if_t && std::is_same_v, int> = 0> + operator U() && = delete; // Prevent unwanted upcasting of JSI values. + + protected: + jsi::Runtime& rt_; + T value_; +}; + +template +struct Converter : public ConverterBase { + using ConverterBase::ConverterBase; +}; + +template <> +struct Converter : public ConverterBase { + using ConverterBase::ConverterBase; + + operator jsi::String() && { + return std::move(value_).asString(rt_); + } + + operator jsi::Object() && { + return std::move(value_).asObject(rt_); + } + + operator jsi::Array() && { + return std::move(value_).asObject(rt_).asArray(rt_); + } + + operator jsi::Function() && { + return std::move(value_).asObject(rt_).asFunction(rt_); + } +}; + +template <> +struct Converter : public ConverterBase { + using ConverterBase::ConverterBase; + + operator jsi::Array() && { + return std::move(value_).asArray(rt_); + } + + operator jsi::Function() && { + return std::move(value_).asFunction(rt_); + } +}; + +template +struct Converter> : public ConverterBase { + Converter(jsi::Runtime& rt, std::optional value) + : ConverterBase(rt, value ? std::move(*value) : jsi::Value::null()) {} + + operator std::optional() && { + if (value_.isNull() || value_.isUndefined()) { + return {}; + } + return std::move(value_); + } +}; + +template , int> = 0> +auto convert(jsi::Runtime& rt, T&& value) { + return Converter(rt, std::forward(value)); +} + +template < + typename T, + std::enable_if_t || std::is_scalar_v, int> = 0> +auto convert(jsi::Runtime& rt, std::optional value) { + return Converter>(rt, std::move(value)); +} + +template , int> = 0> +auto convert(jsi::Runtime& /*rt*/, T&& value) { + return value; +} + +template +auto convert(jsi::Runtime& /*rt*/, Converter&& converter) { + return std::move(converter); +} + +} // namespace facebook::react::bridging diff --git a/test-runner/fakern/react/bridging/Dynamic.h b/test-runner/fakern/react/bridging/Dynamic.h new file mode 100644 index 00000000..90d19543 --- /dev/null +++ b/test-runner/fakern/react/bridging/Dynamic.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static folly::dynamic fromJs(jsi::Runtime& rt, const jsi::Value& value) { + return jsi::dynamicFromValue(rt, value); + } + + static jsi::Value toJs(jsi::Runtime& rt, const folly::dynamic& value) { + return jsi::valueFromDynamic(rt, value); + } +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Error.h b/test-runner/fakern/react/bridging/Error.h new file mode 100644 index 00000000..e8b45973 --- /dev/null +++ b/test-runner/fakern/react/bridging/Error.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +class Error { + public: + // TODO (T114055466): Retain stack trace (at least caller location) + Error(std::string message) : message_(std::move(message)) {} + + Error(const char* message) : Error(std::string(message)) {} + + const std::string& message() const { + return message_; + } + + private: + std::string message_; +}; + +template <> +struct Bridging { + static jsi::JSError fromJs(jsi::Runtime& rt, const jsi::Value& value) { + return jsi::JSError(rt, jsi::Value(rt, value)); + } + + static jsi::JSError fromJs(jsi::Runtime& rt, jsi::Value&& value) { + return jsi::JSError(rt, std::move(value)); + } + + static jsi::Value toJs(jsi::Runtime& rt, std::string message) { + return jsi::Value(rt, jsi::JSError(rt, std::move(message)).value()); + } +}; + +template <> +struct Bridging { + static jsi::Value toJs(jsi::Runtime& rt, const Error& error) { + return jsi::Value(rt, jsi::JSError(rt, error.message()).value()); + } +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/EventEmitter.h b/test-runner/fakern/react/bridging/EventEmitter.h new file mode 100644 index 00000000..acfb6a31 --- /dev/null +++ b/test-runner/fakern/react/bridging/EventEmitter.h @@ -0,0 +1,134 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#define FRIEND_TEST(test_case_name, test_name) \ + friend class test_case_name##_##test_name##_Test + +namespace facebook::react { + +class EventSubscription { + public: + explicit EventSubscription(std::function remove) + : remove_(std::move(remove)) {} + ~EventSubscription() = default; + EventSubscription(EventSubscription&&) noexcept = default; + EventSubscription& operator=(EventSubscription&&) noexcept = default; + EventSubscription(const EventSubscription&) = delete; + EventSubscription& operator=(const EventSubscription&) = delete; + + private: + friend Bridging; + + std::function remove_; +}; + +template <> +struct Bridging { + static jsi::Object toJs( + jsi::Runtime& rt, + const EventSubscription& eventSubscription, + const std::shared_ptr& jsInvoker) { + auto result = jsi::Object(rt); + result.setProperty( + rt, "remove", bridging::toJs(rt, eventSubscription.remove_, jsInvoker)); + return result; + } +}; + +class IAsyncEventEmitter { + public: + IAsyncEventEmitter() noexcept = default; + virtual ~IAsyncEventEmitter() noexcept = default; + IAsyncEventEmitter(IAsyncEventEmitter&&) noexcept = default; + IAsyncEventEmitter& operator=(IAsyncEventEmitter&&) noexcept = default; + IAsyncEventEmitter(const IAsyncEventEmitter&) = delete; + IAsyncEventEmitter& operator=(const IAsyncEventEmitter&) = delete; + + virtual jsi::Object get( + jsi::Runtime& rt, + const std::shared_ptr& jsInvoker) const = 0; +}; + +template +class AsyncEventEmitter : public IAsyncEventEmitter { + static_assert( + sizeof...(Args) <= 1, + "AsyncEventEmitter must have at most one argument"); + + public: + AsyncEventEmitter() : state_(std::make_shared()) { + listen_ = [state = state_](AsyncCallback listener) { + std::lock_guard lock(state->mutex); + auto listenerId = state->listenerId++; + state->listeners.emplace(listenerId, std::move(listener)); + return EventSubscription([state, listenerId]() { + std::lock_guard innerLock(state->mutex); + state->listeners.erase(listenerId); + }); + }; + } + ~AsyncEventEmitter() override = default; + AsyncEventEmitter(AsyncEventEmitter&&) noexcept = default; + AsyncEventEmitter& operator=(AsyncEventEmitter&&) noexcept = default; + AsyncEventEmitter(const AsyncEventEmitter&) = delete; + AsyncEventEmitter& operator=(const AsyncEventEmitter&) = delete; + + void emit(std::function&& converter) { + std::lock_guard lock(state_->mutex); + for (auto& [_, listener] : state_->listeners) { + listener.call([converter](jsi::Runtime& rt, jsi::Function& jsFunction) { + jsFunction.call(rt, converter(rt)); + }); + } + } + + void emit(Args... value) { + std::lock_guard lock(state_->mutex); + for (const auto& [_, listener] : state_->listeners) { + listener.call(static_cast(value)...); + } + } + + jsi::Object get( + jsi::Runtime& rt, + const std::shared_ptr& jsInvoker) const override { + return bridging::toJs(rt, listen_, jsInvoker); + } + + private: + friend Bridging; + FRIEND_TEST(BridgingTest, eventEmitterTest); + + struct SharedState { + std::mutex mutex; + std::unordered_map> listeners; + size_t listenerId{}; + }; + + std::function)> listen_; + std::shared_ptr state_; +}; + +template +struct Bridging> { + static jsi::Object toJs( + jsi::Runtime& rt, + const AsyncEventEmitter& eventEmitter, + const std::shared_ptr& jsInvoker) { + return eventEmitter.get(rt, jsInvoker); + } +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Function.h b/test-runner/fakern/react/bridging/Function.h new file mode 100644 index 00000000..bd1725f1 --- /dev/null +++ b/test-runner/fakern/react/bridging/Function.h @@ -0,0 +1,283 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace facebook::react { + +template +class SyncCallback; + +template +class AsyncCallback { + public: + AsyncCallback( + jsi::Runtime& runtime, + jsi::Function function, + std::shared_ptr jsInvoker) + : callback_(std::make_shared>( + runtime, + std::move(function), + std::move(jsInvoker))) {} + + void operator()(Args... args) const noexcept { + call(std::forward(args)...); + } + + void call(Args... args) const noexcept { + callWithArgs(std::nullopt, std::forward(args)...); + } + + void callWithPriority(SchedulerPriority priority, Args... args) + const noexcept { + callWithArgs(priority, std::forward(args)...); + } + + void call(std::function&& callImpl) + const noexcept { + callWithFunction(std::nullopt, std::move(callImpl)); + } + + void callWithPriority( + SchedulerPriority priority, + std::function&& callImpl) + const noexcept { + callWithFunction(priority, std::move(callImpl)); + } + + private: + friend Bridging; + + std::shared_ptr> callback_; + + void callWithArgs(std::optional priority, Args... args) + const noexcept { + if (auto wrapper = callback_->wrapper_.lock()) { + auto fn = [callback = callback_, + argsPtr = std::make_shared>( + std::make_tuple(std::forward(args)...))]( + jsi::Runtime&) { callback->apply(std::move(*argsPtr)); }; + + auto& jsInvoker = wrapper->jsInvoker(); + if (priority) { + jsInvoker.invokeAsync(*priority, std::move(fn)); + } else { + jsInvoker.invokeAsync(std::move(fn)); + } + } + } + + void callWithFunction( + std::optional priority, + std::function&& callImpl) + const noexcept { + if (auto wrapper = callback_->wrapper_.lock()) { + // Capture callback_ and not wrapper_. If callback_ is deallocated or the + // JSVM is shutdown before the async task is scheduled, the underlying + // function will have been deallocated. + auto fn = [callback = callback_, + callImpl = std::move(callImpl)](jsi::Runtime& rt) { + if (auto wrapper2 = callback->wrapper_.lock()) { + callImpl(rt, wrapper2->callback()); + } + }; + + auto& jsInvoker = wrapper->jsInvoker(); + if (priority) { + jsInvoker.invokeAsync(*priority, std::move(fn)); + } else { + jsInvoker.invokeAsync(std::move(fn)); + } + } + } +}; + +// You must ensure that when invoking this you're located on the JS thread, or +// have exclusive control of the JS VM context. If you cannot ensure this, use +// AsyncCallback instead. +template +class SyncCallback { + public: + SyncCallback( + jsi::Runtime& rt, + jsi::Function function, + std::shared_ptr jsInvoker) + : wrapper_(CallbackWrapper::createWeak( + std::move(function), + rt, + std::move(jsInvoker))) {} + + // Disallow copying, as we can no longer safely destroy the callback + // from the destructor if there's multiple copies + SyncCallback(const SyncCallback&) = delete; + SyncCallback& operator=(const SyncCallback&) = delete; + + // Allow move + SyncCallback(SyncCallback&& other) noexcept + : wrapper_(std::move(other.wrapper_)) {} + + SyncCallback& operator=(SyncCallback&& other) noexcept { + wrapper_ = std::move(other.wrapper_); + return *this; + } + + ~SyncCallback() { + if (auto wrapper = wrapper_.lock()) { + wrapper->destroy(); + } + } + + R operator()(Args... args) const { + return call(std::forward(args)...); + } + + R call(Args... args) const { + auto wrapper = wrapper_.lock(); + + // If the wrapper has been deallocated, we can no longer provide a return + // value consistently, so our only option is to throw + if (!wrapper) { + if constexpr (std::is_void_v) { + return; + } else { + throw std::runtime_error("Failed to call invalidated sync callback"); + } + } + + auto& callback = wrapper->callback(); + auto& rt = wrapper->runtime(); + auto jsInvoker = wrapper->jsInvokerPtr(); + + if constexpr (std::is_void_v) { + callback.call( + rt, bridging::toJs(rt, std::forward(args), jsInvoker)...); + } else { + return bridging::fromJs( + rt, + callback.call( + rt, bridging::toJs(rt, std::forward(args), jsInvoker)...), + jsInvoker); + } + } + + private: + friend AsyncCallback; + friend Bridging; + + R apply(std::tuple&& args) const { + return apply(std::move(args), std::index_sequence_for{}); + } + + template + R apply(std::tuple&& args, std::index_sequence) const { + return call(std::move(std::get(args))...); + } + + // Held weakly so lifetime is managed by LongLivedObjectCollection. + std::weak_ptr wrapper_; +}; + +template +struct Bridging> { + static AsyncCallback fromJs( + jsi::Runtime& rt, + jsi::Function&& value, + const std::shared_ptr& jsInvoker) { + return AsyncCallback(rt, std::move(value), jsInvoker); + } + + static jsi::Function toJs( + jsi::Runtime& rt, + const AsyncCallback& value) { + return value.callback_->function_.getFunction(rt); + } +}; + +template +struct Bridging> { + static SyncCallback fromJs( + jsi::Runtime& rt, + jsi::Function&& value, + const std::shared_ptr& jsInvoker) { + return SyncCallback(rt, std::move(value), jsInvoker); + } + + static jsi::Function toJs( + jsi::Runtime& rt, + const SyncCallback& value) { + return value.function_.getFunction(rt); + } +}; + +template +struct Bridging> { + using Func = std::function; + using IndexSequence = std::index_sequence_for; + + static constexpr size_t kArgumentCount = sizeof...(Args); + + static jsi::Function toJs( + jsi::Runtime& rt, + Func fn, + const std::shared_ptr& jsInvoker) { + return jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "BridgedFunction"), + kArgumentCount, + [fn = std::make_shared(std::move(fn)), jsInvoker]( + jsi::Runtime& rt, + const jsi::Value&, + const jsi::Value* args, + size_t count) -> jsi::Value { + if (count < kArgumentCount) { + throw jsi::JSError(rt, "Incorrect number of arguments"); + } + + if constexpr (std::is_void_v) { + callFromJs(*fn, rt, args, jsInvoker, IndexSequence{}); + return jsi::Value(); + } else { + return bridging::toJs( + rt, + callFromJs(*fn, rt, args, jsInvoker, IndexSequence{}), + jsInvoker); + } + }); + } + + private: + template + static R callFromJs( + Func& fn, + jsi::Runtime& rt, + const jsi::Value* args, + const std::shared_ptr& jsInvoker, + std::index_sequence) { + return fn(bridging::fromJs(rt, args[Index], jsInvoker)...); + } +}; + +template +struct Bridging< + std::function, + std::enable_if_t< + !std::is_same_v, std::function>>> + : Bridging> {}; + +template +struct Bridging : Bridging> {}; + +template +struct Bridging : Bridging> {}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/LongLivedObject.cpp b/test-runner/fakern/react/bridging/LongLivedObject.cpp new file mode 100644 index 00000000..9c6d7c38 --- /dev/null +++ b/test-runner/fakern/react/bridging/LongLivedObject.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "LongLivedObject.h" +#include + +namespace facebook::react { + +// LongLivedObjectCollection + +LongLivedObjectCollection& LongLivedObjectCollection::get( + jsi::Runtime& runtime) { + static std::unordered_map> + instances; + static std::mutex instancesMutex; + + std::scoped_lock lock(instancesMutex); + void* key = static_cast(&runtime); + auto entry = instances.find(key); + if (entry == instances.end()) { + entry = + instances.emplace(key, std::make_shared()) + .first; + } + return *(entry->second); +} + +void LongLivedObjectCollection::add(std::shared_ptr so) { + std::scoped_lock lock(collectionMutex_); + collection_.insert(std::move(so)); +} + +void LongLivedObjectCollection::remove(const LongLivedObject* o) { + std::scoped_lock lock(collectionMutex_); + for (auto p = collection_.begin(); p != collection_.end(); p++) { + if (p->get() == o) { + collection_.erase(p); + break; + } + } +} + +void LongLivedObjectCollection::clear() { + std::scoped_lock lock(collectionMutex_); + collection_.clear(); +} + +size_t LongLivedObjectCollection::size() const { + std::scoped_lock lock(collectionMutex_); + return collection_.size(); +} + +// LongLivedObject + +void LongLivedObject::allowRelease() { + LongLivedObjectCollection::get(runtime_).remove(this); +} + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/LongLivedObject.h b/test-runner/fakern/react/bridging/LongLivedObject.h new file mode 100644 index 00000000..d043a0a9 --- /dev/null +++ b/test-runner/fakern/react/bridging/LongLivedObject.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +/** + * A simple wrapper class that can be registered to a collection that keep it + * alive for extended period of time. This object can be removed from the + * collection when needed. + * + * The subclass of this class must be created using std::make_shared(). + * After creation, add it to the `LongLivedObjectCollection`. When done with the + * object, call `allowRelease()` to reclaim its memory. + * + * When using LongLivedObject to keep JS values alive, ensure you only hold weak + * references to the object outside the JS thread to avoid accessing deallocated + * values when the JS VM is shutdown. + */ +class LongLivedObject { + public: + virtual void allowRelease(); + + protected: + explicit LongLivedObject(jsi::Runtime& runtime) : runtime_(runtime) {} + virtual ~LongLivedObject() = default; + jsi::Runtime& runtime_; +}; + +/** + * A singleton, thread-safe, write-only collection for the `LongLivedObject`s. + */ +class LongLivedObjectCollection { + public: + static LongLivedObjectCollection& get(jsi::Runtime& runtime); + + LongLivedObjectCollection() = default; + LongLivedObjectCollection(const LongLivedObjectCollection&) = delete; + void operator=(const LongLivedObjectCollection&) = delete; + + void add(std::shared_ptr o); + void remove(const LongLivedObject* o); + void clear(); + size_t size() const; + + private: + std::unordered_set> collection_; + mutable std::mutex collectionMutex_; +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Number.h b/test-runner/fakern/react/bridging/Number.h new file mode 100644 index 00000000..6d480ae3 --- /dev/null +++ b/test-runner/fakern/react/bridging/Number.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +template <> +struct Bridging { + static double fromJs(jsi::Runtime&, const jsi::Value& value) { + return value.asNumber(); + } + + static jsi::Value toJs(jsi::Runtime&, double value) { + return value; + } +}; + +template <> +struct Bridging { + static float fromJs(jsi::Runtime&, const jsi::Value& value) { + return (float)value.asNumber(); + } + + static jsi::Value toJs(jsi::Runtime&, float value) { + return (double)value; + } +}; + +template <> +struct Bridging { + static int32_t fromJs(jsi::Runtime&, const jsi::Value& value) { + return (int32_t)value.asNumber(); + } + + static jsi::Value toJs(jsi::Runtime&, int32_t value) { + return value; + } +}; + +template <> +struct Bridging { + static uint32_t fromJs(jsi::Runtime&, const jsi::Value& value) { + return (uint32_t)value.asNumber(); + } + + static jsi::Value toJs(jsi::Runtime&, uint32_t value) { + return (double)value; + } +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Object.h b/test-runner/fakern/react/bridging/Object.h new file mode 100644 index 00000000..89e5714b --- /dev/null +++ b/test-runner/fakern/react/bridging/Object.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static jsi::WeakObject fromJs(jsi::Runtime& rt, const jsi::Object& value) { + return jsi::WeakObject(rt, value); + } + + static jsi::Value toJs(jsi::Runtime& rt, jsi::WeakObject& value) { + return value.lock(rt); + } +}; + +template +struct Bridging< + std::shared_ptr, + std::enable_if_t>> { + static std::shared_ptr fromJs(jsi::Runtime& rt, const jsi::Object& value) { + return value.getHostObject(rt); + } + + static jsi::Object toJs(jsi::Runtime& rt, std::shared_ptr value) { + return jsi::Object::createFromHostObject(rt, std::move(value)); + } +}; + +namespace map_detail { + +template +struct Bridging { + static T fromJs( + jsi::Runtime& rt, + const jsi::Object& value, + const std::shared_ptr& jsInvoker) { + T result; + auto propertyNames = value.getPropertyNames(rt); + auto length = propertyNames.length(rt); + + for (size_t i = 0; i < length; i++) { + auto propertyName = propertyNames.getValueAtIndex(rt, i); + + result.emplace( + bridging::fromJs(rt, propertyName, jsInvoker), + bridging::fromJs( + rt, value.getProperty(rt, propertyName.asString(rt)), jsInvoker)); + } + + return result; + } + + static jsi::Object toJs( + jsi::Runtime& rt, + const T& map, + const std::shared_ptr& jsInvoker) { + auto resultObject = jsi::Object(rt); + + for (const auto& [key, value] : map) { + resultObject.setProperty( + rt, + jsi::PropNameID::forUtf8(rt, key), + bridging::toJs(rt, value, jsInvoker)); + } + + return resultObject; + } +}; + +} // namespace map_detail + +template +struct Bridging> + : map_detail::Bridging> {}; + +template +struct Bridging> + : map_detail::Bridging> {}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Promise.h b/test-runner/fakern/react/bridging/Promise.h new file mode 100644 index 00000000..5a049c85 --- /dev/null +++ b/test-runner/fakern/react/bridging/Promise.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace facebook::react { + +template +class AsyncPromise { + public: + AsyncPromise(jsi::Runtime& rt, const std::shared_ptr& jsInvoker) + : state_(std::make_shared()) { + auto constructor = rt.global().getPropertyAsFunction(rt, "Promise"); + + auto promise = constructor.callAsConstructor( + rt, + bridging::toJs( + rt, + // Safe to capture this since this is called synchronously. + [this](AsyncCallback resolve, AsyncCallback reject) { + state_->resolve = std::move(resolve); + state_->reject = std::move(reject); + }, + jsInvoker)); + + auto promiseHolder = + std::make_shared(rt, promise.asObject(rt)); + LongLivedObjectCollection::get(rt).add(promiseHolder); + + // The shared state can retain the promise holder weakly now. + state_->promiseHolder = promiseHolder; + } + + void resolve(T value) { + std::lock_guard lock(state_->mutex); + + if (state_->resolve) { + state_->resolve->call(std::move(value)); + state_->resolve.reset(); + state_->reject.reset(); + } + } + + void reject(Error error) { + std::lock_guard lock(state_->mutex); + + if (state_->reject) { + state_->reject->call(std::move(error)); + state_->reject.reset(); + state_->resolve.reset(); + } + } + + jsi::Object get(jsi::Runtime& rt) const { + if (auto holder = state_->promiseHolder.lock()) { + return jsi::Value(rt, holder->promise).asObject(rt); + } else { + throw jsi::JSError(rt, "Failed to get invalidated promise"); + } + } + + private: + struct PromiseHolder : LongLivedObject { + PromiseHolder(jsi::Runtime& runtime, jsi::Object p) + : LongLivedObject(runtime), promise(std::move(p)) {} + + jsi::Object promise; + }; + + struct SharedState { + ~SharedState() { + if (auto holder = promiseHolder.lock()) { + holder->allowRelease(); + } + } + + std::mutex mutex; + std::weak_ptr promiseHolder; + std::optional> resolve; + std::optional> reject; + }; + + std::shared_ptr state_; +}; + +template +struct Bridging> { + static jsi::Object toJs(jsi::Runtime& rt, const AsyncPromise& promise) { + return promise.get(rt); + } +}; + +} // namespace facebook::react diff --git a/test-runner/fakern/react/bridging/Value.h b/test-runner/fakern/react/bridging/Value.h new file mode 100644 index 00000000..c2f37f4c --- /dev/null +++ b/test-runner/fakern/react/bridging/Value.h @@ -0,0 +1,107 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static std::nullptr_t fromJs(jsi::Runtime& rt, const jsi::Value& value) { + if (value.isNull() || value.isUndefined()) { + return nullptr; + } else { + throw jsi::JSError(rt, "Cannot convert value to nullptr"); + } + } + + static std::nullptr_t toJs(jsi::Runtime&, std::nullptr_t) { + return nullptr; + } +}; + +template +struct Bridging> { + static std::optional fromJs( + jsi::Runtime& rt, + const jsi::Value& value, + const std::shared_ptr& jsInvoker) { + if (value.isNull() || value.isUndefined()) { + return {}; + } + return bridging::fromJs(rt, value, jsInvoker); + } + + template + static std::optional fromJs( + jsi::Runtime& rt, + const std::optional& value, + const std::shared_ptr& jsInvoker) { + if (value) { + return bridging::fromJs(rt, *value, jsInvoker); + } + return {}; + } + + static jsi::Value toJs( + jsi::Runtime& rt, + const std::optional& value, + const std::shared_ptr& jsInvoker) { + if (value) { + return bridging::toJs(rt, *value, jsInvoker); + } + return jsi::Value::null(); + } +}; + +template +struct Bridging< + std::shared_ptr, + std::enable_if_t>> { + static jsi::Value toJs( + jsi::Runtime& rt, + const std::shared_ptr& ptr, + const std::shared_ptr& jsInvoker) { + if (ptr) { + return bridging::toJs(rt, *ptr, jsInvoker); + } + return jsi::Value::null(); + } +}; + +template +struct Bridging> { + static jsi::Value toJs( + jsi::Runtime& rt, + const std::unique_ptr& ptr, + const std::shared_ptr& jsInvoker) { + if (ptr) { + return bridging::toJs(rt, *ptr, jsInvoker); + } + return jsi::Value::null(); + } +}; + +template +struct Bridging> { + static jsi::Value toJs( + jsi::Runtime& rt, + const std::weak_ptr& weakPtr, + const std::shared_ptr& jsInvoker) { + if (auto ptr = weakPtr.lock()) { + return bridging::toJs(rt, *ptr, jsInvoker); + } + return jsi::Value::null(); + } +}; + +} // namespace facebook::react diff --git a/test-runner/shell/CMakeLists.txt b/test-runner/shell/CMakeLists.txt new file mode 100644 index 00000000..28fcb2af --- /dev/null +++ b/test-runner/shell/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(polygen_test_runner_shell main.cpp) +target_link_libraries(polygen_test_runner_shell PUBLIC Hermes::Hermes fakern) +target_link_libraries(polygen_test_runner_shell PUBLIC polygen) \ No newline at end of file diff --git a/test-runner/shell/main.cpp b/test-runner/shell/main.cpp new file mode 100644 index 00000000..efbe4b82 --- /dev/null +++ b/test-runner/shell/main.cpp @@ -0,0 +1,48 @@ +#include +#include + +#include +#include +#include +#include + +using namespace fakern; +using namespace facebook; + +void run(DummyRuntime& rt, react::CallInvoker& invoker) { + // auto polygenTurboModule = std::make_shared(invoker); + // + // // Expose TurboModule as `globalThis.Polygen` (instead of TurboModule.get) + // auto polygenObject = jsi::Object::createFromHostObject(env, polygenTurboModule); + // env.globalObject().setProperty(env, "Polygen", std::move(polygenObject)); + // + // env.evaluateScript("print(typeof Polygen);"); + // env.evaluateScript("print(Object.keys(Polygen));"); +} + +int main() { + auto hermesConfig = ::hermes::vm::RuntimeConfig::Builder() + .withMicrotaskQueue(true) + .withIntl(false) + .build(); + + auto runtime = std::make_unique(facebook::hermes::makeHermesRuntime(hermesConfig)); + + // Execute some JS. + int status = 0; + try { + runtime->executeScript("print('hello!')", ""); + // run(rt, env); + } catch (jsi::JSError &e) { + // Handle JS exceptions here. + std::cerr << "JS Exception: " << e.getStack() << std::endl; + status = 1; + } + catch (jsi::JSIException &e) { + // Handle JSI exceptions here. + std::cerr << "JSI Exception: " << e.what() << std::endl; + status = 1; + } + + return status; +} diff --git a/test-runner/tests/CMakeLists.txt b/test-runner/tests/CMakeLists.txt new file mode 100644 index 00000000..2a7aa275 --- /dev/null +++ b/test-runner/tests/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(fib_with_host_fn) \ No newline at end of file diff --git a/test-runner/tests/fib_with_host_fn/CMakeLists.txt b/test-runner/tests/fib_with_host_fn/CMakeLists.txt new file mode 100644 index 00000000..5eb1eff7 --- /dev/null +++ b/test-runner/tests/fib_with_host_fn/CMakeLists.txt @@ -0,0 +1,15 @@ +enable_testing() + +add_executable( + test_fib_with_host_fn + Test.cpp +) +target_link_libraries( + test_fib_with_host_fn + GTest::gtest_main +) + +polygen_module(test_fib_with_host_wasm PATH ${CMAKE_SOURCE_DIR}) + +include(GoogleTest) +gtest_discover_tests(test_fib_with_host_fn) \ No newline at end of file diff --git a/test-runner/tests/fib_with_host_fn/Test.cpp b/test-runner/tests/fib_with_host_fn/Test.cpp new file mode 100644 index 00000000..994dbd89 --- /dev/null +++ b/test-runner/tests/fib_with_host_fn/Test.cpp @@ -0,0 +1,9 @@ +#include + +// Demonstrate some basic assertions. +TEST(HelloTest, BasicAssertions) { + // Expect two strings not to be equal. + EXPECT_STRNE("hello", "world"); + // Expect equality. + EXPECT_EQ(7 * 6, 42); +} \ No newline at end of file diff --git a/test-runner/tests/fib_with_host_fn/module.wasm b/test-runner/tests/fib_with_host_fn/module.wasm new file mode 100644 index 00000000..6e48ed9f Binary files /dev/null and b/test-runner/tests/fib_with_host_fn/module.wasm differ diff --git a/test-runner/tests/fib_with_host_fn/polygen.config.mjs b/test-runner/tests/fib_with_host_fn/polygen.config.mjs new file mode 100644 index 00000000..a7ff61f6 --- /dev/null +++ b/test-runner/tests/fib_with_host_fn/polygen.config.mjs @@ -0,0 +1,13 @@ +import { + localModule, + polygenConfig, +} from '@callstack/polygen-config'; + +/** + * @type {import('@callstack/polygen/config').PolygenConfig} + */ +export default polygenConfig({ + modules: [ + localModule('./module.wasm'), + ], +}); diff --git a/test-runner/vcpkg-configuration.json b/test-runner/vcpkg-configuration.json new file mode 100644 index 00000000..127aad24 --- /dev/null +++ b/test-runner/vcpkg-configuration.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json", + "default-registry": { + "kind": "git", + "baseline": "0ca64b4e1c70fa6d9f53b369b8f3f0843797c20c", + "repository": "https://github.com/microsoft/vcpkg" + }, + "overlay-ports": ["./vcpkg-ports"] +} diff --git a/test-runner/vcpkg-ports/hermes/HermesConfig.cmake b/test-runner/vcpkg-ports/hermes/HermesConfig.cmake new file mode 100644 index 00000000..c1bfe4d7 --- /dev/null +++ b/test-runner/vcpkg-ports/hermes/HermesConfig.cmake @@ -0,0 +1,25 @@ +get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../" ABSOLUTE) + +set(Hermes_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include") +set(HermesPrivate_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include/hermes-private/include") +set(JSI_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include/jsi") +set(Hermes_LIBRARY "${PACKAGE_PREFIX_DIR}/lib/libhermesvm${CMAKE_SHARED_LIBRARY_SUFFIX}") + +if (NOT TARGET Hermes::Hermes) + add_library(Hermes::Hermes UNKNOWN IMPORTED) + set_property(TARGET Hermes::Hermes PROPERTY IMPORTED_LOCATION ${Hermes_LIBRARY}) + target_include_directories(Hermes::Hermes INTERFACE "${Hermes_INCLUDE_DIR}") + target_link_libraries(Hermes::Hermes INTERFACE "${Hermes_LIBRARY}") +endif() + +if (NOT TARGET JSI) + add_library(JSI INTERFACE IMPORTED) + target_include_directories(JSI INTERFACE "${JSI_INCLUDE_DIR}") + target_link_libraries(JSI INTERFACE "${Hermes_LIBRARY}") +endif() + +if (NOT TARGET Hermes::HermesPrivate) + add_library(Hermes::HermesPrivate INTERFACE IMPORTED) +# target_link_libraries(Hermes::HermesPrivate Hermes::Hermes) + target_include_directories(Hermes::HermesPrivate INTERFACE "${HermesPrivate_INCLUDE_DIR}") +endif() \ No newline at end of file diff --git a/test-runner/vcpkg-ports/hermes/portfile.cmake b/test-runner/vcpkg-ports/hermes/portfile.cmake new file mode 100644 index 00000000..ef96d7c7 --- /dev/null +++ b/test-runner/vcpkg-ports/hermes/portfile.cmake @@ -0,0 +1,67 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO facebook/hermes + REF 1d7433bf196d57d2e50ca02e7e48095140e8bab9 + SHA512 9b755182ef540be48b9cb6c54257a3e3477d288e758fc21b91516daf5444f3647b79587e0b3a41da712e1be4caf93f212abeaf8639d9506d4dadd99ac3547458 + HEAD_REF static_h +) + +# Copy HermesConfig.cmake into packages dir. +# +# This allows us to use `find_package(Hermes CONFIG)` +# +# We do this because Hermes does not provide one after `install` +# +file( + INSTALL "${CMAKE_CURRENT_LIST_DIR}/HermesConfig.cmake" + DESTINATION "${CURRENT_PACKAGES_DIR}/share/hermes" +) + +vcpkg_cmake_configure( + SOURCE_PATH ${SOURCE_PATH} + OPTIONS + -DHERMES_ENABLE_TEST_SUITE=OFF + # Tools seem to be required, otherwise library is not built + # -DHERMES_ENABLE_TOOLS=ON +) + +vcpkg_cmake_build() + +# Copy headers to package directory. +# +# We are not running `cmake --install`, so we need to do this manually. +# Even if we did, the output only contains tools. +file(COPY + "${SOURCE_PATH}/public/hermes" + "${SOURCE_PATH}/API/hermes" + "${SOURCE_PATH}/API/jsi/jsi" + DESTINATION "${CURRENT_PACKAGES_DIR}/include" + FILES_MATCHING + PATTERN "*.h" +) + +# Private API +file(COPY "${SOURCE_PATH}/include" + DESTINATION "${CURRENT_PACKAGES_DIR}/include/hermes-private" + FILES_MATCHING + PATTERN "*.h" +) + +# +# Copy built libraries into package directory. +# +set(BUILD_DIR "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}") + +file( + COPY "${BUILD_DIR}-rel/lib/libhermesvm${CMAKE_SHARED_LIBRARY_SUFFIX}" + DESTINATION "${CURRENT_PACKAGES_DIR}/lib" +) +file( + COPY "${BUILD_DIR}-dbg/lib/libhermesvm${CMAKE_SHARED_LIBRARY_SUFFIX}" + DESTINATION "${CURRENT_PACKAGES_DIR}/debug/lib" +) + +# Copy license file +# +# vcpkg is noisy if we don't. +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") \ No newline at end of file diff --git a/test-runner/vcpkg-ports/hermes/vcpkg.json b/test-runner/vcpkg-ports/hermes/vcpkg.json new file mode 100644 index 00000000..f344ca7c --- /dev/null +++ b/test-runner/vcpkg-ports/hermes/vcpkg.json @@ -0,0 +1,16 @@ +{ + "name": "hermes", + "version-string": "0.13.0", + "description": "A JavaScript engine optimized for running React Native.", + "homepage": "https://github.com/facebook/hermes", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} diff --git a/test-runner/vcpkg.json b/test-runner/vcpkg.json new file mode 100644 index 00000000..b5b6c0ae --- /dev/null +++ b/test-runner/vcpkg.json @@ -0,0 +1,20 @@ +{ + "name": "polygen-test-runner", + "version": "0.0.1", + "license": "MIT", + "dependencies": [ + { + "name": "fmt", + "version>=": "11.0.2#1" + }, + { + "name": "folly", + "version>=": "2025.01.27.00" + }, + { + "name": "gtest", + "version>=": "1.15.2" + }, + "hermes" + ] +} diff --git a/yarn.lock b/yarn.lock index ab8af6ae..9c52972b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1663,6 +1663,7 @@ __metadata: dependencies: "@callstack/polygen-cli": "npm:0.2.0" "@callstack/polygen-typescript-config": "workspace:^" + "@react-native-community/cli": "npm:^15.1.3" "@types/react": "npm:^18.2.44" del-cli: "npm:^5.1.0" react: "npm:17.0.2" @@ -2807,6 +2808,42 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-clean@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-clean@npm:15.1.3" + dependencies: + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + checksum: 10c0/95fd0ccf2485eb157ab93bfc5015c4f1242c950e50e00d613f2b9f434253b5236655766fa090386043e1ec64503f4e1e6a455a1f2bd90683c4cfd0b9cf30ea8f + languageName: node + linkType: hard + +"@react-native-community/cli-config-android@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-config-android@npm:15.1.3" + dependencies: + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + fast-glob: "npm:^3.3.2" + fast-xml-parser: "npm:^4.4.1" + checksum: 10c0/ac0903c70b6e30592a69b23a2080bf6cd9d32c30ef465310a164d7254227ec35749484d6306a6c547a129f8efdc0a56e15d1adeafbcffa96587a767b1e450bc5 + languageName: node + linkType: hard + +"@react-native-community/cli-config-apple@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-config-apple@npm:15.1.3" + dependencies: + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + checksum: 10c0/57526305ef3767a8f89aee2804e6d4fd80843c3b67db21b0bec288f80bf76147dea334e5bcf55867d9c7b3f87f80ccceb1278fb6e97f3ff11888a31a56c82492 + languageName: node + linkType: hard + "@react-native-community/cli-config@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-config@npm:14.1.0" @@ -2821,6 +2858,20 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-config@npm:15.1.3" + dependencies: + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + cosmiconfig: "npm:^9.0.0" + deepmerge: "npm:^4.3.0" + fast-glob: "npm:^3.3.2" + joi: "npm:^17.2.1" + checksum: 10c0/5cf211aa747df1cfce6fe1f82aa4b1552445d2d0b10d123c6e88ff94432288f5c9fcb2c6b1d903aeff95cbc6e7e67942afb18f088281fce8aec139a2e1e53e03 + languageName: node + linkType: hard + "@react-native-community/cli-debugger-ui@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-debugger-ui@npm:14.1.0" @@ -2830,6 +2881,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-debugger-ui@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-debugger-ui@npm:15.1.3" + dependencies: + serve-static: "npm:^1.13.1" + checksum: 10c0/676420dd3a87fbdaa21967606dd4661d9667b9b3689f9b7c380cc6f99055248e1121ced988c74eae639fa3c2d3b1b1336c11c3b776ddab03ad1ee69d72b738ab + languageName: node + linkType: hard + "@react-native-community/cli-doctor@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-doctor@npm:14.1.0" @@ -2854,6 +2914,30 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-doctor@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-doctor@npm:15.1.3" + dependencies: + "@react-native-community/cli-config": "npm:15.1.3" + "@react-native-community/cli-platform-android": "npm:15.1.3" + "@react-native-community/cli-platform-apple": "npm:15.1.3" + "@react-native-community/cli-platform-ios": "npm:15.1.3" + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + command-exists: "npm:^1.2.8" + deepmerge: "npm:^4.3.0" + envinfo: "npm:^7.13.0" + execa: "npm:^5.0.0" + node-stream-zip: "npm:^1.9.1" + ora: "npm:^5.4.1" + semver: "npm:^7.5.2" + strip-ansi: "npm:^5.2.0" + wcwidth: "npm:^1.0.1" + yaml: "npm:^2.2.1" + checksum: 10c0/f66693558c583eb9b3aacae779185b93d5e7984f33436bf3b716176c955468ea286e989416e7c1a85cc68a16a4e3b8038c995210098b7a22a338e86ade08709c + languageName: node + linkType: hard + "@react-native-community/cli-platform-android@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-platform-android@npm:14.1.0" @@ -2868,6 +2952,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-android@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-platform-android@npm:15.1.3" + dependencies: + "@react-native-community/cli-config-android": "npm:15.1.3" + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + logkitty: "npm:^0.7.1" + checksum: 10c0/31bdb49b6687fc182f3149cd4a041c106e779dbf3d8ad374a8c7c413d4fc8de99324508350e34b9fbe150c4b0c53dcc839793d96dab376a3e50de648e989cf48 + languageName: node + linkType: hard + "@react-native-community/cli-platform-apple@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-platform-apple@npm:14.1.0" @@ -2882,6 +2979,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-apple@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-platform-apple@npm:15.1.3" + dependencies: + "@react-native-community/cli-config-apple": "npm:15.1.3" + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-xml-parser: "npm:^4.4.1" + checksum: 10c0/a0903761a038cb95406226b13e356b62e31efc7bb3d8961a11578f1f2df012b475bec290cbedcecb4e0f61d67a6778cc274242e35c87d39ee160279b4cbd1813 + languageName: node + linkType: hard + "@react-native-community/cli-platform-ios@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-platform-ios@npm:14.1.0" @@ -2891,6 +3001,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-ios@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-platform-ios@npm:15.1.3" + dependencies: + "@react-native-community/cli-platform-apple": "npm:15.1.3" + checksum: 10c0/47b02d73054d63f75c4a813b605701e765d69372267ba3220753159eb2e3430b8d224a092a4ef453652620ee84644d618994a837ce4c5955e8b423d679e7dbd1 + languageName: node + linkType: hard + "@react-native-community/cli-server-api@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-server-api@npm:14.1.0" @@ -2908,6 +3027,23 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-server-api@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-server-api@npm:15.1.3" + dependencies: + "@react-native-community/cli-debugger-ui": "npm:15.1.3" + "@react-native-community/cli-tools": "npm:15.1.3" + compression: "npm:^1.7.1" + connect: "npm:^3.6.5" + errorhandler: "npm:^1.5.1" + nocache: "npm:^3.0.1" + pretty-format: "npm:^26.6.2" + serve-static: "npm:^1.13.1" + ws: "npm:^6.2.3" + checksum: 10c0/2aa49781264e210e79ae464a084e258e0a62aee238fb68dd209eb649553814d9ebea5a0e76306cd1cda48049120267f4205bb4f9ac2191989948299b751d6fa9 + languageName: node + linkType: hard + "@react-native-community/cli-tools@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-tools@npm:14.1.0" @@ -2926,6 +3062,25 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-tools@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-tools@npm:15.1.3" + dependencies: + appdirsjs: "npm:^1.2.4" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + mime: "npm:^2.4.1" + open: "npm:^6.2.0" + ora: "npm:^5.4.1" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + shell-quote: "npm:^1.7.3" + sudo-prompt: "npm:^9.0.0" + checksum: 10c0/e458f3a5e97456b6fa8741cd8c04ca384b7657df9f53111daaf132911b00b6b5bf08fad2206c8461d0974b71548296b9da669af76dddf7f3261ac5d527df6bcc + languageName: node + linkType: hard + "@react-native-community/cli-types@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli-types@npm:14.1.0" @@ -2935,6 +3090,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-types@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-types@npm:15.1.3" + dependencies: + joi: "npm:^17.2.1" + checksum: 10c0/0e11be10184d531485734773391c80e9ce0f9a430841da328932ccbbb2b97f321fc0ced5f55550e58c41f23ec4ea7b7e0bfedce9b60fc00f842bcad5a4d57c35 + languageName: node + linkType: hard + "@react-native-community/cli@npm:14.1.0": version: 14.1.0 resolution: "@react-native-community/cli@npm:14.1.0" @@ -2961,6 +3125,32 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli@npm:^15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli@npm:15.1.3" + dependencies: + "@react-native-community/cli-clean": "npm:15.1.3" + "@react-native-community/cli-config": "npm:15.1.3" + "@react-native-community/cli-debugger-ui": "npm:15.1.3" + "@react-native-community/cli-doctor": "npm:15.1.3" + "@react-native-community/cli-server-api": "npm:15.1.3" + "@react-native-community/cli-tools": "npm:15.1.3" + "@react-native-community/cli-types": "npm:15.1.3" + chalk: "npm:^4.1.2" + commander: "npm:^9.4.1" + deepmerge: "npm:^4.3.0" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + fs-extra: "npm:^8.1.0" + graceful-fs: "npm:^4.1.3" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + bin: + rnc-cli: build/bin.js + checksum: 10c0/db5589e85851b917cbaaa95b3a0ee7c8c78535980a4bc5d07249e64c3e735bba1b510c7434c4d32592a062f8e60475cfd989ae240143828653319cfea75b5d9e + languageName: node + linkType: hard + "@react-native/assets-registry@npm:0.75.4": version: 0.75.4 resolution: "@react-native/assets-registry@npm:0.75.4"