diff --git a/arkui-plugins/common/abstract-visitor.ts b/arkui-plugins/common/abstract-visitor.ts index 2c7a307da160e41671fa8d0aca5586a38d30ef8e..6b70c35739aa54bb4bb1b016bfb806bc616a2cb5 100644 --- a/arkui-plugins/common/abstract-visitor.ts +++ b/arkui-plugins/common/abstract-visitor.ts @@ -43,6 +43,8 @@ export abstract class AbstractVisitor implements VisitorOptions { abstract visitor(node: arkts.AstNode): arkts.AstNode; + init(): void {} + reset(): void { this.indentation = 0; } diff --git a/arkui-plugins/common/metadata-collector.ts b/arkui-plugins/common/metadata-collector.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed732acd216368eb92ea4355daee7f1c4404131b --- /dev/null +++ b/arkui-plugins/common/metadata-collector.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RouterInfo } from '../ui-plugins/struct-translators/utils'; +import { ProjectConfig } from './plugin-context'; + +export class MetaDataCollector { + public projectConfig: ProjectConfig | undefined; + public fileAbsName: string | undefined; + public externalSourceName: string | undefined; + public routerInfo: Map = new Map(); + private static instance: MetaDataCollector | null = null; + + static getInstance(): MetaDataCollector { + if (!this.instance) { + this.instance = new MetaDataCollector(); + } + return this.instance; + } + + setProjectConfig(config: ProjectConfig | undefined): this { + this.projectConfig = config; + return this; + } + + setAbsName(fileName: string | undefined): this { + this.fileAbsName = fileName; + return this; + } + + setExternalSourceName(externalSourceName: string | undefined): this { + this.externalSourceName = externalSourceName; + return this; + } + + setRouterInfo(routerInfo: Map): this { + this.routerInfo = routerInfo; + return this; + } + + reset(): void { + this.projectConfig = undefined; + this.fileAbsName = undefined; + this.externalSourceName = undefined; + this.routerInfo.clear(); + } +} diff --git a/arkui-plugins/common/predefines.ts b/arkui-plugins/common/predefines.ts index 68abfc5bb60b8d99b9b43ccf2be35f7fa7c9890c..ab5b75aaf1890da7cde4d606458735b0dea9cae1 100644 --- a/arkui-plugins/common/predefines.ts +++ b/arkui-plugins/common/predefines.ts @@ -36,6 +36,8 @@ export const ENTRY_POINT_IMPORT_SOURCE_NAME: string = 'arkui.UserView'; export const ARKUI_COMPONENT_COMMON_SOURCE_NAME: string = 'arkui.component.common'; export const ARKUI_FOREACH_SOURCE_NAME: string = 'arkui.component.forEach'; export const ARKUI_BUILDER_SOURCE_NAME: string = 'arkui.component.builder'; +export const ARKUI_NAVIGATION_SOURCE_NAME: string = 'arkui.component.navigation'; +export const ARKUI_NAV_DESTINATION_SOURCE_NAME: string = 'arkui.component.navDestination'; export enum ModuleType { HAR = 'har', @@ -83,6 +85,7 @@ export enum EntryWrapperNames { WRAPPER_CLASS_NAME = '__EntryWrapper', ENTRY_STORAGE_LOCAL_STORAGE_PROPERTY_NAME = '_entry_local_storage_', ENTRY_POINT_CLASS_NAME = 'EntryPoint', + NAVIGATION_BUILDER_REGISTER = 'NavigationBuilderRegister', REGISTER_NAMED_ROUTER = 'RegisterNamedRouter', ROUTER_NAME = 'routerName', INSTANCE = 'instance', @@ -119,6 +122,14 @@ export enum EntryParamNames { export enum InnerComponentNames { FOR_EACH = 'ForEach', + NAVIGATION = 'Navigation', + NAV_DESTINATION = 'NavDestination', +} + +export enum BuilderNames { + BUILDER = 'builder', + WRAP_BUILDER = 'wrapBuilder', + WRAPPED_BUILDER = 'WrappedBuilder', } export enum InnerComponentAttributes { @@ -168,6 +179,7 @@ export enum TypeNames { ARRAY = 'Array', MAP = 'Map', STRING = 'string', + TYPE_T = 'T', } export enum DecoratorIntrinsicNames { @@ -248,6 +260,10 @@ export enum NavigationNames { PAGE_PATH = 'pagePath', PAGE_FULL_PATH = 'pageFullPath', INTEGRATED_HSP = 'integratedHsp', + MODULE_INFO = 'ModuleInfo', + NAVIGATION_MODULE_INFO = 'NavigationModuleInfo', + IS_USER_CREATE_STACK = 'isUserCreateStack', + NAME = 'name', } export enum ConditionNames { diff --git a/arkui-plugins/common/program-visitor.ts b/arkui-plugins/common/program-visitor.ts index 20850fd5e538b80ad9de0006d7469de5508d9223..9327b9075aadf6c5ecde84d8f41361d1f7ae06de 100644 --- a/arkui-plugins/common/program-visitor.ts +++ b/arkui-plugins/common/program-visitor.ts @@ -270,7 +270,6 @@ export class ProgramVisitor extends AbstractVisitor { transformer.registerMap(this.legacyStructMap); } this.visitTransformer(transformer, script, externalSourceName, program); - transformer.reset(); arkts.setAllParents(script); if (!transformer.isExternal) { debugDumpAstNode( @@ -308,7 +307,9 @@ export class ProgramVisitor extends AbstractVisitor { transformer.isExternal = !!externalSourceName; transformer.externalSourceName = externalSourceName; transformer.program = program; + transformer.init(); const newScript = transformer.visitor(script) as arkts.EtsScript; + transformer.reset(); return newScript; } } diff --git a/arkui-plugins/test/demo/mock/component/basic-nav-destination.ets b/arkui-plugins/test/demo/mock/component/basic-nav-destination.ets new file mode 100644 index 0000000000000000000000000000000000000000..e080d67648f687c153047dc4e7f06d774fd46471 --- /dev/null +++ b/arkui-plugins/test/demo/mock/component/basic-nav-destination.ets @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Column, Button, NavDestination } from "@ohos.arkui.component" + +@Component +struct NavDestinationStruct { + build() { + NavDestination(){ + Column(){ + Button('abc') + } + }.width(80) + } +} \ No newline at end of file diff --git a/arkui-plugins/test/demo/mock/component/basic-navigation.ets b/arkui-plugins/test/demo/mock/component/basic-navigation.ets new file mode 100644 index 0000000000000000000000000000000000000000..99b8afdae1d1140cbcc0d9b2be8b34e415798cb4 --- /dev/null +++ b/arkui-plugins/test/demo/mock/component/basic-navigation.ets @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Navigation, NavPathStack, Column, Button } from "@ohos.arkui.component" + +@Component +struct MyStateSample1 { + pathStack: NavPathStack = new NavPathStack() + + build() { + Navigation(this.pathStack){ + Column(){ + Button('abc').width(100).height(300) + } + }.width(80) + } +} + +@Component +struct MyStateSample2 { + pathStack: NavPathStack = new NavPathStack() + + build() { + Navigation(){ + Column(){ + Button('abc') + } + } + } +} \ No newline at end of file diff --git a/arkui-plugins/test/demo/mock/component/mix-navigation-nav-destination.ets b/arkui-plugins/test/demo/mock/component/mix-navigation-nav-destination.ets new file mode 100644 index 0000000000000000000000000000000000000000..b795b25ba097fe69d73e432230b797a14b206d7b --- /dev/null +++ b/arkui-plugins/test/demo/mock/component/mix-navigation-nav-destination.ets @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Text, BuilderParam, Builder, Component, NavPathStack, Navigation, NavPathInfo, NavDestination, NavigationMode } from '@ohos.arkui.component'; +import { State, Observed, ObjectLink, Link } from '@ohos.arkui.stateManagement'; + +@Component +export struct SubNavigation { + @Link isPortrait: boolean; + @State displayMode: number = 0; + @BuilderParam navDestination: ((name: String, param: Object|undefined) => void) | undefined; + @State primaryWidth: number | string = '50%'; + onNavigationModeChange?: OnNavigationModeChangeCallback = (mode: NavigationMode) => {}; + @State primaryStack: MyNavPathStack = new MyNavPathStack(); + @State secondaryStack: MyNavPathStack = new MyNavPathStack(); + + @Builder + SubNavDestination(name: string, param?: object) { + this.navDestination!(name, param as Object); + } + + build() { + NavDestination() { + Navigation(this.secondaryStack) { + Navigation(this.primaryStack) { + } + .hideNavBar(true) + .mode(NavigationMode.Stack) + .navDestination(this.SubNavDestination) + .hideTitleBar(true, true) + .hideToolBar(true, true) + .hideBackButton(true) + } + .onNavigationModeChange(this?.onNavigationModeChange) + .hideBackButton(true) + .hideTitleBar(true, true) + .navDestination(this.SubNavDestination) + .navBarWidth(this.primaryWidth) + } + } +} + +export enum SplitPolicy { + HOME_PAGE = 0, + DETAIL_PAGE = 1, + FULL_PAGE = 2, + PlACE_HOLDER_PAGE = 3, +} + +class MultiNavPolicyInfo { + public policy: SplitPolicy = SplitPolicy.DETAIL_PAGE; + public navInfo: NavPathInfo | undefined = undefined; + public isFullScreen: boolean | undefined = undefined; + + constructor(policy: SplitPolicy, navInfo: NavPathInfo) { + this.policy = policy; + this.navInfo = navInfo; + } +} + +export class MyNavPathStack extends NavPathStack { + public operates:NavPathStackOperate[] = []; + public type:string = 'NavPathStack'; + public policyInfoList: MultiNavPolicyInfo[] = []; + + public registerStackOperateCallback(operate: NavPathStackOperate) { + let index = this.operates.findIndex((item) => { return item === operate}); + if (index === -1) { + this.operates.push(operate); + } + } + + public unregisterStackOperateCallback(operate: NavPathStackOperate) { + let index = this.operates.findIndex((item) => { return item === operate}); + if (index !== -1) { + this.operates.splice(index, 1); + } + } + + public popInner(animated?: boolean): NavPathInfo | undefined { + console.log('MyNavPathStack pop from inner:'); + return super.pop({}, animated); + } + + public pop(animated?: boolean): NavPathInfo | undefined { + console.log('MyNavPathStack pop from system:'); + animated = typeof animated === 'undefined' ? true : animated + let ret: NavPathInfo | undefined = undefined; + ret = super.pop({}, animated); + this.policyInfoList.pop(); + this.operates.forEach((item) => { + item.onSystemPop(); + }) + return ret; + } +} + +interface NavPathStackOperate { + onSystemPop: ()=>void; +} + +interface MultiNavPathStackOperate { + onPrimaryPop: ()=>void; + onSecondaryPop: ()=>void; +} + +declare type OnNavigationModeChangeCallback = (mode: NavigationMode) => void; + +declare type OnHomeShowOnTopCallback = (name: string) => void; \ No newline at end of file diff --git a/arkui-plugins/test/ut/ui-plugins/component/basic-nav-destination.test.ts b/arkui-plugins/test/ut/ui-plugins/component/basic-nav-destination.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..65d25b33537da148bdcbb0f969562bb376845760 --- /dev/null +++ b/arkui-plugins/test/ut/ui-plugins/component/basic-nav-destination.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import { PluginTester } from '../../../utils/plugin-tester'; +import { mockBuildConfig } from '../../../utils/artkts-config'; +import { getRootPath, MOCK_ENTRY_DIR_PATH } from '../../../utils/path-config'; +import { parseDumpSrc } from '../../../utils/parse-string'; +import { recheck, uiNoRecheck } from '../../../utils/plugins'; +import { BuildConfig, PluginTestContext } from '../../../utils/shared-types'; +import { uiTransform } from '../../../../ui-plugins'; +import { Plugins } from '../../../../common/plugin-context'; + +const COMPONENT_DIR_PATH: string = 'component'; + +const buildConfig: BuildConfig = mockBuildConfig(); +buildConfig.compileFiles = [ + path.resolve(getRootPath(), MOCK_ENTRY_DIR_PATH, COMPONENT_DIR_PATH, 'basic-nav-destination.ets'), +]; + +const pluginTester = new PluginTester('test basic navDestination transformation', buildConfig); + +const parsedTransform: Plugins = { + name: 'parsedTrans', + parsed: uiTransform().parsed +}; + +const expectedCheckedScript: string = ` +import { NavDestinationAttribute as NavDestinationAttribute } from "arkui.component.navDestination"; + +import { ColumnAttribute as ColumnAttribute } from "arkui.component.column"; + +import { memo as memo } from "arkui.stateManagement.runtime"; + +import { ButtonAttribute as ButtonAttribute } from "arkui.component.button"; + +import { ButtonImpl as ButtonImpl } from "arkui.component.button"; + +import { ColumnImpl as ColumnImpl } from "arkui.component.column"; + +import { NavDestinationImpl as NavDestinationImpl } from "arkui.component.navDestination"; + +import { CustomComponent as CustomComponent } from "arkui.component.customComponent"; + +import { Component as Component, Column as Column, Button as Button, NavDestination as NavDestination } from "@ohos.arkui.component"; + +function main() {} + +@Component() final struct NavDestinationStruct extends CustomComponent { + public __initializeStruct(initializers: (__Options_NavDestinationStruct | undefined), @memo() content: ((()=> void) | undefined)): void {} + + public __updateStruct(initializers: (__Options_NavDestinationStruct | undefined)): void {} + + @memo() public build() { + NavDestinationImpl(@memo() ((instance: NavDestinationAttribute): void => { + instance.setNavDestinationOptions({ + moduleName: "entry", + pagePath: "mock/component/basic-nav-destination", + }).width(80).applyAttributesFinish(); + return; + }), @memo() (() => { + ColumnImpl(@memo() ((instance: ColumnAttribute): void => { + instance.setColumnOptions(undefined).applyAttributesFinish(); + return; + }), @memo() (() => { + ButtonImpl(@memo() ((instance: ButtonAttribute): void => { + instance.setButtonOptions("abc", undefined).applyAttributesFinish(); + return; + }), undefined); + })); + })); + } + + public constructor() {} + +} + +@Component() export interface __Options_NavDestinationStruct { + +} +`; + +function testCheckedTransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedCheckedScript)); +} + +pluginTester.run( + 'test basic navDestination transformation', + [parsedTransform, uiNoRecheck, recheck], + { + 'checked:ui-no-recheck': [testCheckedTransformer], + }, + { + stopAfter: 'checked', + } +); diff --git a/arkui-plugins/test/ut/ui-plugins/component/basic-navigation.test.ts b/arkui-plugins/test/ut/ui-plugins/component/basic-navigation.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b5fde3a06f3457910b9283eb11214fbccf5e5a8 --- /dev/null +++ b/arkui-plugins/test/ut/ui-plugins/component/basic-navigation.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import { PluginTester } from '../../../utils/plugin-tester'; +import { mockBuildConfig } from '../../../utils/artkts-config'; +import { getRootPath, MOCK_ENTRY_DIR_PATH } from '../../../utils/path-config'; +import { parseDumpSrc } from '../../../utils/parse-string'; +import { recheck, uiNoRecheck } from '../../../utils/plugins'; +import { BuildConfig, PluginTestContext } from '../../../utils/shared-types'; +import { uiTransform } from '../../../../ui-plugins'; +import { Plugins } from '../../../../common/plugin-context'; + +const COMPONENT_DIR_PATH: string = 'component'; + +const buildConfig: BuildConfig = mockBuildConfig(); +buildConfig.compileFiles = [ + path.resolve(getRootPath(), MOCK_ENTRY_DIR_PATH, COMPONENT_DIR_PATH, 'basic-navigation.ets'), +]; + +const pluginTester = new PluginTester('test basic navigation transformation', buildConfig); + +const parsedTransform: Plugins = { + name: 'parsedTrans', + parsed: uiTransform().parsed +}; + +const expectedCheckedScript: string = ` +import { NavigationAttribute as NavigationAttribute } from "arkui.component.navigation"; + +import { ColumnAttribute as ColumnAttribute } from "arkui.component.column"; + +import { memo as memo } from "arkui.stateManagement.runtime"; + +import { ButtonAttribute as ButtonAttribute } from "arkui.component.button"; + +import { ButtonImpl as ButtonImpl } from "arkui.component.button"; + +import { ColumnImpl as ColumnImpl } from "arkui.component.column"; + +import { NavigationImpl as NavigationImpl } from "arkui.component.navigation"; + +import { CustomComponent as CustomComponent } from "arkui.component.customComponent"; + +import { Component as Component, Navigation as Navigation, NavPathStack as NavPathStack, Column as Column, Button as Button } from "@ohos.arkui.component"; + +function main() {} + +@Component() final struct MyStateSample1 extends CustomComponent { + public __initializeStruct(initializers: (__Options_MyStateSample1 | undefined), @memo() content: ((()=> void) | undefined)): void { + this.__backing_pathStack = ((({let gensym___1107384 = initializers; + (((gensym___1107384) == (null)) ? undefined : gensym___1107384.pathStack)})) ?? (new NavPathStack())); + } + + public __updateStruct(initializers: (__Options_MyStateSample1 | undefined)): void {} + + private __backing_pathStack?: NavPathStack; + + public get pathStack(): NavPathStack { + return (this.__backing_pathStack as NavPathStack); + } + + public set pathStack(value: NavPathStack) { + this.__backing_pathStack = value; + } + + @memo() public build() { + NavigationImpl(@memo() ((instance: NavigationAttribute): void => { + instance.setNavigationOptions(this.pathStack, { + moduleName: "entry", + pagePath: "mock/component/basic-navigation", + isUserCreateStack: true, + }).width(80).applyAttributesFinish(); + return; + }), @memo() (() => { + ColumnImpl(@memo() ((instance: ColumnAttribute): void => { + instance.setColumnOptions(undefined).applyAttributesFinish(); + return; + }), @memo() (() => { + ButtonImpl(@memo() ((instance: ButtonAttribute): void => { + instance.setButtonOptions("abc", undefined).width(100).height(300).applyAttributesFinish(); + return; + }), undefined); + })); + })); + } + + public constructor() {} + +} + +@Component() final struct MyStateSample2 extends CustomComponent { + public __initializeStruct(initializers: (__Options_MyStateSample2 | undefined), @memo() content: ((()=> void) | undefined)): void { + this.__backing_pathStack = ((({let gensym___199081302 = initializers; + (((gensym___199081302) == (null)) ? undefined : gensym___199081302.pathStack)})) ?? (new NavPathStack())); + } + + public __updateStruct(initializers: (__Options_MyStateSample2 | undefined)): void {} + + private __backing_pathStack?: NavPathStack; + + public get pathStack(): NavPathStack { + return (this.__backing_pathStack as NavPathStack); + } + + public set pathStack(value: NavPathStack) { + this.__backing_pathStack = value; + } + + @memo() public build() { + NavigationImpl(@memo() ((instance: NavigationAttribute): void => { + instance.setNavigationOptions(undefined, { + moduleName: "entry", + pagePath: "mock/component/basic-navigation", + isUserCreateStack: false, + }).applyAttributesFinish(); + return; + }), @memo() (() => { + ColumnImpl(@memo() ((instance: ColumnAttribute): void => { + instance.setColumnOptions(undefined).applyAttributesFinish(); + return; + }), @memo() (() => { + ButtonImpl(@memo() ((instance: ButtonAttribute): void => { + instance.setButtonOptions("abc", undefined).applyAttributesFinish(); + return; + }), undefined); + })); + })); + } + + public constructor() {} + +} + +@Component() export interface __Options_MyStateSample1 { + set pathStack(pathStack: (NavPathStack | undefined)) + + get pathStack(): (NavPathStack | undefined) + +} + +@Component() export interface __Options_MyStateSample2 { + set pathStack(pathStack: (NavPathStack | undefined)) + + get pathStack(): (NavPathStack | undefined) + +} +`; + +function testCheckedTransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedCheckedScript)); +} + +pluginTester.run( + 'test basic navigation transformation', + [parsedTransform, uiNoRecheck, recheck], + { + 'checked:ui-no-recheck': [testCheckedTransformer], + }, + { + stopAfter: 'checked', + } +); diff --git a/arkui-plugins/test/ut/ui-plugins/component/mix-navigation-nav-destination.test.ts b/arkui-plugins/test/ut/ui-plugins/component/mix-navigation-nav-destination.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..a64f1208ea9d5cc260197573a59db45b084c731d --- /dev/null +++ b/arkui-plugins/test/ut/ui-plugins/component/mix-navigation-nav-destination.test.ts @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import { PluginTester } from '../../../utils/plugin-tester'; +import { mockBuildConfig } from '../../../utils/artkts-config'; +import { getRootPath, MOCK_ENTRY_DIR_PATH } from '../../../utils/path-config'; +import { parseDumpSrc } from '../../../utils/parse-string'; +import { recheck, uiNoRecheck } from '../../../utils/plugins'; +import { BuildConfig, PluginTestContext } from '../../../utils/shared-types'; +import { uiTransform } from '../../../../ui-plugins'; +import { Plugins } from '../../../../common/plugin-context'; + +const COMPONENT_DIR_PATH: string = 'component'; + +const buildConfig: BuildConfig = mockBuildConfig(); +buildConfig.compileFiles = [ + path.resolve(getRootPath(), MOCK_ENTRY_DIR_PATH, COMPONENT_DIR_PATH, 'mix-navigation-nav-destination.ets'), +]; + +const pluginTester = new PluginTester('test mix usage of navigation and navDestination transformation', buildConfig); + +const parsedTransform: Plugins = { + name: 'parsedTrans', + parsed: uiTransform().parsed +}; + +const expectedCheckedScript: string = ` +import { IStateDecoratedVariable as IStateDecoratedVariable } from "arkui.stateManagement.decorator"; + +import { STATE_MGMT_FACTORY as STATE_MGMT_FACTORY } from "arkui.stateManagement.decorator"; + +import { LinkSourceType as LinkSourceType } from "arkui.stateManagement.decorator"; + +import { ILinkDecoratedVariable as ILinkDecoratedVariable } from "arkui.stateManagement.decorator"; + +import { NavDestinationAttribute as NavDestinationAttribute } from "arkui.component.navDestination"; + +import { NavigationAttribute as NavigationAttribute } from "arkui.component.navigation"; + +import { NavigationImpl as NavigationImpl } from "arkui.component.navigation"; + +import { NavDestinationImpl as NavDestinationImpl } from "arkui.component.navDestination"; + +import { MemoSkip as MemoSkip } from "arkui.stateManagement.runtime"; + +import { memo as memo } from "arkui.stateManagement.runtime"; + +import { CustomComponent as CustomComponent } from "arkui.component.customComponent"; + +import { Text as Text, BuilderParam as BuilderParam, Builder as Builder, Component as Component, NavPathStack as NavPathStack, Navigation as Navigation, NavPathInfo as NavPathInfo, NavDestination as NavDestination, NavigationMode as NavigationMode } from "@ohos.arkui.component"; + +import { State as State, Observed as Observed, ObjectLink as ObjectLink, Link as Link } from "@ohos.arkui.stateManagement"; + +function main() {} + + +@Component() export final struct SubNavigation extends CustomComponent { + public __initializeStruct(initializers: (__Options_SubNavigation | undefined), @memo() content: ((()=> void) | undefined)): void { + if (({let gensym___214357609 = initializers; + (((gensym___214357609) == (null)) ? undefined : gensym___214357609.__backing_isPortrait)})) { + this.__backing_isPortrait = STATE_MGMT_FACTORY.makeLink(this, "isPortrait", initializers!.__backing_isPortrait!); + }; + this.__backing_displayMode = STATE_MGMT_FACTORY.makeState(this, "displayMode", ((({let gensym___197056380 = initializers; + (((gensym___197056380) == (null)) ? undefined : gensym___197056380.displayMode)})) ?? (0))); + this.__backing_navDestination = ((((({let gensym___165803261 = initializers; + (((gensym___165803261) == (null)) ? undefined : gensym___165803261.navDestination)})) ?? (content))) ?? (undefined)) + this.__backing_primaryWidth = STATE_MGMT_FACTORY.makeState<(number | string)>(this, "primaryWidth", ((({let gensym___158861871 = initializers; + (((gensym___158861871) == (null)) ? undefined : gensym___158861871.primaryWidth)})) ?? ("50%"))); + this.__backing_onNavigationModeChange = ((({let gensym___114390532 = initializers; + (((gensym___114390532) == (null)) ? undefined : gensym___114390532.onNavigationModeChange)})) ?? (((mode: NavigationMode) => {}))); + this.__backing_primaryStack = STATE_MGMT_FACTORY.makeState(this, "primaryStack", ((({let gensym___224425708 = initializers; + (((gensym___224425708) == (null)) ? undefined : gensym___224425708.primaryStack)})) ?? (new MyNavPathStack()))); + this.__backing_secondaryStack = STATE_MGMT_FACTORY.makeState(this, "secondaryStack", ((({let gensym___57045085 = initializers; + (((gensym___57045085) == (null)) ? undefined : gensym___57045085.secondaryStack)})) ?? (new MyNavPathStack()))); + } + + public __updateStruct(initializers: (__Options_SubNavigation | undefined)): void {} + + private __backing_isPortrait?: ILinkDecoratedVariable; + + public get isPortrait(): boolean { + return this.__backing_isPortrait!.get(); + } + + public set isPortrait(value: boolean) { + this.__backing_isPortrait!.set(value); + } + + private __backing_displayMode?: IStateDecoratedVariable; + + public get displayMode(): number { + return this.__backing_displayMode!.get(); + } + + public set displayMode(value: number) { + this.__backing_displayMode!.set(value); + } + + private __backing_navDestination?: (((name: String, param: (Object | undefined))=> void) | undefined); + + public get navDestination(): (@memo() ((name: String, param: (Object | undefined))=> void) | undefined) { + return this.__backing_navDestination!; + } + + public set navDestination(value: (@memo() ((name: String, param: (Object | undefined))=> void) | undefined)) { + this.__backing_navDestination = value; + } + + private __backing_primaryWidth?: IStateDecoratedVariable<(number | string)>; + + public get primaryWidth(): (number | string) { + return this.__backing_primaryWidth!.get(); + } + + public set primaryWidth(value: (number | string)) { + this.__backing_primaryWidth!.set(value); + } + + private __backing_onNavigationModeChange?: (OnNavigationModeChangeCallback | undefined); + + public get onNavigationModeChange(): (OnNavigationModeChangeCallback | undefined) { + return (this.__backing_onNavigationModeChange as (OnNavigationModeChangeCallback | undefined)); + } + + public set onNavigationModeChange(value: (OnNavigationModeChangeCallback | undefined)) { + this.__backing_onNavigationModeChange = value; + } + + private __backing_primaryStack?: IStateDecoratedVariable; + + public get primaryStack(): MyNavPathStack { + return this.__backing_primaryStack!.get(); + } + + public set primaryStack(value: MyNavPathStack) { + this.__backing_primaryStack!.set(value); + } + + private __backing_secondaryStack?: IStateDecoratedVariable; + + public get secondaryStack(): MyNavPathStack { + return this.__backing_secondaryStack!.get(); + } + + public set secondaryStack(value: MyNavPathStack) { + this.__backing_secondaryStack!.set(value); + } + + @memo() public SubNavDestination(@MemoSkip() name: string, @MemoSkip() param?: object) { + this.navDestination!(name, (param as Object)); + } + + @memo() public build() { + NavDestinationImpl(@memo() ((instance: NavDestinationAttribute): void => { + instance.setNavDestinationOptions({ + moduleName: "entry", + pagePath: "mock/component/mix-navigation-nav-destination", + }).applyAttributesFinish(); + return; + }), @memo() (() => { + NavigationImpl(@memo() ((instance: NavigationAttribute): void => { + instance.setNavigationOptions(this.secondaryStack, { + moduleName: "entry", + pagePath: "mock/component/mix-navigation-nav-destination", + isUserCreateStack: true, + }).onNavigationModeChange(({let gensym%%_44 = this; + (((gensym%%_44) == (null)) ? undefined : gensym%%_44.onNavigationModeChange)})).hideBackButton(true).hideTitleBar(true, true).navDestination(this.SubNavDestination).navBarWidth(this.primaryWidth).applyAttributesFinish(); + return; + }), @memo() (() => { + NavigationImpl(@memo() ((instance: NavigationAttribute): void => { + instance.setNavigationOptions(this.primaryStack, { + moduleName: "entry", + pagePath: "mock/component/mix-navigation-nav-destination", + isUserCreateStack: true, + }).hideNavBar(true).mode(NavigationMode.Stack).navDestination(this.SubNavDestination).hideTitleBar(true, true).hideToolBar(true, true).hideBackButton(true).applyAttributesFinish(); + return; + }), @memo() (() => {})); + })); + })); + } + + public constructor() {} + + public static _buildCompatibleNode(options: __Options_SubNavigation): void { + return; + } + +} + +export final class SplitPolicy extends BaseEnum { + private readonly #ordinal: int; + + private static () {} + + public constructor(ordinal: int, value: int) { + super(value); + this.#ordinal = ordinal; + } + + public static readonly HOME_PAGE: SplitPolicy = new SplitPolicy(0, 0); + + public static readonly DETAIL_PAGE: SplitPolicy = new SplitPolicy(1, 1); + + public static readonly FULL_PAGE: SplitPolicy = new SplitPolicy(2, 2); + + public static readonly PlACE_HOLDER_PAGE: SplitPolicy = new SplitPolicy(3, 3); + + private static readonly #NamesArray: String[] = ["HOME_PAGE", "DETAIL_PAGE", "FULL_PAGE", "PlACE_HOLDER_PAGE"]; + + private static readonly #ValuesArray: int[] = [0, 1, 2, 3]; + + private static readonly #StringValuesArray: String[] = ["0", "1", "2", "3"]; + + private static readonly #ItemsArray: SplitPolicy[] = [SplitPolicy.HOME_PAGE, SplitPolicy.DETAIL_PAGE, SplitPolicy.FULL_PAGE, SplitPolicy.PlACE_HOLDER_PAGE]; + + public getName(): String { + return SplitPolicy.#NamesArray[this.#ordinal]; + } + + public static getValueOf(name: String): SplitPolicy { + for (let i = 0;((i) < (SplitPolicy.#NamesArray.length));(++i)) { + if (((name) == (SplitPolicy.#NamesArray[i]))) { + return SplitPolicy.#ItemsArray[i]; + } + } + throw new Error((("No enum constant SplitPolicy.") + (name))); + } + + public static fromValue(value: int): SplitPolicy { + for (let i = 0;((i) < (SplitPolicy.#ValuesArray.length));(++i)) { + if (((value) == (SplitPolicy.#ValuesArray[i]))) { + return SplitPolicy.#ItemsArray[i]; + } + } + throw new Error((("No enum SplitPolicy with value ") + (value))); + } + + public valueOf(): int { + return SplitPolicy.#ValuesArray[this.#ordinal]; + } + + public toString(): String { + return SplitPolicy.#StringValuesArray[this.#ordinal]; + } + + public static values(): SplitPolicy[] { + return SplitPolicy.#ItemsArray; + } + + public getOrdinal(): int { + return this.#ordinal; + } + + public static $_get(e: SplitPolicy): String { + return e.getName(); + } + +} + +class MultiNavPolicyInfo { + public policy: SplitPolicy = SplitPolicy.DETAIL_PAGE; + + public navInfo: (NavPathInfo | undefined) = undefined; + + public isFullScreen: (boolean | undefined) = undefined; + + public constructor(policy: SplitPolicy, navInfo: NavPathInfo) { + this.policy = policy; + this.navInfo = navInfo; + } + +} + +export class MyNavPathStack extends NavPathStack { + public operates: Array = []; + + public type: string = "NavPathStack"; + + public policyInfoList: Array = []; + + public registerStackOperateCallback(operate: NavPathStackOperate) { + let index = this.operates.findIndex(((item) => { + return ((item) === (operate)); + })); + if (((index) === (-1))) { + this.operates.push(operate); + } + } + + public unregisterStackOperateCallback(operate: NavPathStackOperate) { + let index = this.operates.findIndex(((item) => { + return ((item) === (operate)); + })); + if (((index) !== (-1))) { + this.operates.splice(index, 1); + } + } + + public popInner(animated?: boolean): (NavPathInfo | undefined) { + console.log("MyNavPathStack pop from inner:"); + return super.pop({}, animated); + } + + public pop(animated?: boolean): (NavPathInfo | undefined) { + console.log("MyNavPathStack pop from system:"); + animated = ((((typeof animated)) === ("undefined")) ? true : animated); + let ret: (NavPathInfo | undefined) = undefined; + ret = super.pop({}, animated); + this.policyInfoList.pop(); + this.operates.forEach(((item) => { + item.onSystemPop(); + })); + return ret; + } + + public constructor() {} + +} + +interface NavPathStackOperate { + set onSystemPop(onSystemPop: (()=> void)) + + get onSystemPop(): (()=> void) + +} + +interface MultiNavPathStackOperate { + set onPrimaryPop(onPrimaryPop: (()=> void)) + + get onPrimaryPop(): (()=> void) + set onSecondaryPop(onSecondaryPop: (()=> void)) + + get onSecondaryPop(): (()=> void) + +} + +type OnNavigationModeChangeCallback = ((mode: NavigationMode)=> void); + +type OnHomeShowOnTopCallback = ((name: string)=> void); + +@Retention({policy:"SOURCE"}) @interface __Link_intrinsic {} + +@Component() export interface __Options_SubNavigation { + @__Link_intrinsic() set isPortrait(isPortrait: (boolean | undefined)) + + @__Link_intrinsic() get isPortrait(): (boolean | undefined) + set __backing_isPortrait(__backing_isPortrait: (LinkSourceType | undefined)) + + get __backing_isPortrait(): (LinkSourceType | undefined) + set displayMode(displayMode: (number | undefined)) + + get displayMode(): (number | undefined) + set __backing_displayMode(__backing_displayMode: (IStateDecoratedVariable | undefined)) + + get __backing_displayMode(): (IStateDecoratedVariable | undefined) + set navDestination(navDestination: ((((name: String, param: (Object | undefined))=> void) | undefined) | undefined)) + + get navDestination(): ((((name: String, param: (Object | undefined))=> void) | undefined) | undefined) + set primaryWidth(primaryWidth: ((number | string) | undefined)) + + get primaryWidth(): ((number | string) | undefined) + set __backing_primaryWidth(__backing_primaryWidth: (IStateDecoratedVariable<(number | string)> | undefined)) + + get __backing_primaryWidth(): (IStateDecoratedVariable<(number | string)> | undefined) + set onNavigationModeChange(onNavigationModeChange: ((OnNavigationModeChangeCallback | undefined) | undefined)) + + get onNavigationModeChange(): ((OnNavigationModeChangeCallback | undefined) | undefined) + set primaryStack(primaryStack: (MyNavPathStack | undefined)) + + get primaryStack(): (MyNavPathStack | undefined) + set __backing_primaryStack(__backing_primaryStack: (IStateDecoratedVariable | undefined)) + + get __backing_primaryStack(): (IStateDecoratedVariable | undefined) + set secondaryStack(secondaryStack: (MyNavPathStack | undefined)) + + get secondaryStack(): (MyNavPathStack | undefined) + set __backing_secondaryStack(__backing_secondaryStack: (IStateDecoratedVariable | undefined)) + + get __backing_secondaryStack(): (IStateDecoratedVariable | undefined) + +} +`; + +function testCheckedTransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedCheckedScript)); +} + +pluginTester.run( + 'test mix usage of navigation and navDestination transformation', + [parsedTransform, uiNoRecheck, recheck], + { + 'checked:ui-no-recheck': [testCheckedTransformer], + }, + { + stopAfter: 'checked', + } +); diff --git a/arkui-plugins/ui-plugins/builder-lambda-translators/cache/componentAttributeCache.ts b/arkui-plugins/ui-plugins/builder-lambda-translators/cache/componentAttributeCache.ts new file mode 100644 index 0000000000000000000000000000000000000000..db4b82f671f162f3a78e7795b5c35c8f9615c056 --- /dev/null +++ b/arkui-plugins/ui-plugins/builder-lambda-translators/cache/componentAttributeCache.ts @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { checkIsTrailingLambdaInLastParam, isNavigationOrNavDestination } from '../utils'; +import { + collectTypeRecordFromParameter, + collectTypeRecordFromTypeParameterDeclaration, + collectTypeRecordFromTypeParameterInstatiation, + ParameterRecord, + TypeParameterTypeRecord, + TypeRecord, +} from '../../../collectors/utils/collect-types'; +import { factory as builderLambdaFactory } from '../factory'; + +export interface ComponentRecord { + name: string; + attributeRecords: ParameterRecord[]; + typeParams?: TypeParameterTypeRecord[]; + hasRestParameter?: boolean; + hasReceiver?: boolean; + hasLastTrailingLambda?: boolean; +} + +export class ComponentAttributeCache { + private _cache: Map; + private _attributeName: string | undefined; + private _attributeTypeParams: TypeRecord[] | undefined; + private _isCollected: boolean = false; + private static instance: ComponentAttributeCache; + + private constructor() { + this._cache = new Map(); + } + + static getInstance(): ComponentAttributeCache { + if (!this.instance) { + this.instance = new ComponentAttributeCache(); + } + return this.instance; + } + + private collectAttributeName(type: arkts.TypeNode | undefined): string | undefined { + if ( + !type || + !arkts.isETSTypeReference(type) || + !type.part || + !type.part.name || + !arkts.isIdentifier(type.part.name) + ) { + return; + } + this._attributeName = type.part.name.name; + if (type.part.typeParams) { + this._attributeTypeParams = collectTypeRecordFromTypeParameterInstatiation(type.part.typeParams); + } + } + + get attributeName(): string | undefined { + return this._attributeName; + } + + get attributeTypeParams(): TypeRecord[] | undefined { + return this._attributeTypeParams; + } + + reset(): void { + this._cache.clear(); + this._attributeName = undefined; + this._attributeTypeParams = undefined; + this._isCollected = false; + } + + isCollected(): boolean { + return this._isCollected; + } + + collect(node: arkts.MethodDefinition): void { + this.collectAttributeName(node.scriptFunction.returnTypeAnnotation); + if (!this._attributeName) { + return; + } + const name: string = node.name.name; + const hasRestParameter = node.scriptFunction.hasRestParameter; + const hasReceiver = node.scriptFunction.hasReceiver; + const typeParams = collectTypeRecordFromTypeParameterDeclaration(node.scriptFunction.typeParams); + const params = node.scriptFunction.params as arkts.ETSParameterExpression[]; + const attributeRecords: ParameterRecord[] = []; + const hasLastTrailingLambda = checkIsTrailingLambdaInLastParam(params); + params.forEach((p, index) => { + if (isNavigationOrNavDestination(name) && index === params.length - 1) { + const record = collectTypeRecordFromParameter(builderLambdaFactory.createModuleInfoParam()); + attributeRecords.push(record); + } + if (index === params.length - 1 && hasLastTrailingLambda) { + return; + } + const record = collectTypeRecordFromParameter(p); + attributeRecords.push(record); + }); + const componentRecord: ComponentRecord = { + name, + attributeRecords, + typeParams, + hasRestParameter, + hasReceiver, + hasLastTrailingLambda, + }; + this._cache.set(name, componentRecord); + this._isCollected = true; + } + + getComponentRecord(name: string): ComponentRecord | undefined { + return this._cache.get(name); + } + + getAllComponentRecords(): ComponentRecord[] { + return Array.from(this._cache.values()); + } +} diff --git a/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts b/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts index 51d85a1a2a65a1605a917ce8fea3b64ed10ae892..7be851f68df271222b4009fe7f58c904b1ceb7bb 100644 --- a/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts +++ b/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts @@ -15,6 +15,7 @@ import * as arkts from '@koalaui/libarkts'; import { BuilderLambdaNames, inferTypeFromValue } from '../utils'; +import path from 'path'; import { backingField, filterDefined, @@ -49,10 +50,9 @@ import { BuilderLambdaStyleBodyInfo, getDeclaredSetAttribtueMethodName, checkIsTrailingLambdaInLastParam, - ComponentAttributeCache, - ComponentRecord, getTransformedComponentName, flatObjectExpressionToEntries, + isNavigationOrNavDestination, } from './utils'; import { hasDecorator, isDecoratorIntrinsicAnnotation } from '../property-translators/utils'; import { BuilderFactory } from './builder-factory'; @@ -69,6 +69,9 @@ import { DecoratorNames, StateManagementTypes, TypeNames, + InnerComponentNames, + ModuleType, + NavigationNames, } from '../../common/predefines'; import { ImportCollector } from '../../common/import-collector'; import { GenSymGenerator } from '../../common/gensym-generator'; @@ -80,6 +83,8 @@ import { import { TypeRecord } from '../../collectors/utils/collect-types'; import { StyleInternalsVisitor } from './style-internals-visitor'; import { ConditionBreakCache } from './cache/conditionBreakCache'; +import { MetaDataCollector } from '../../common/metadata-collector'; +import { ComponentAttributeCache, ComponentRecord } from './cache/componentAttributeCache'; export class factory { /** @@ -94,6 +99,9 @@ export class factory { const func: arkts.ScriptFunction = node.scriptFunction; const isFunctionCall: boolean = node.name.name !== BuilderLambdaNames.ORIGIN_METHOD_NAME; const newParams: arkts.Expression[] = [...prefixArgs, ...func.params]; + if (isNavigationOrNavDestination(newName ?? node.name.name)) { + newParams.splice(newParams.length - 1, 0, factory.createModuleInfoParam()); + } const updateFunc = arkts.factory .updateScriptFunction( func, @@ -119,6 +127,18 @@ export class factory { ); } + static createModuleInfoParam(): arkts.ETSParameterExpression { + return arkts.factory + .createParameterDeclaration( + arkts.factory.createIdentifier( + NavigationNames.MODULE_INFO, + UIFactory.createTypeReferenceFromString(NavigationNames.NAVIGATION_MODULE_INFO) + ), + undefined + ) + .setOptional(true); + } + /* * transform arguments in style node. */ @@ -511,6 +531,11 @@ export class factory { }, { isTrailingCall } ); + if (isNavigationOrNavDestination(type?.name, moduleName)) { + const isUserCreateStack: boolean | undefined = + type?.name === InnerComponentNames.NAVIGATION ? leaf.arguments.length > 1 : undefined; + modifiedArgs.push(factory.createModuleInfoArg(isUserCreateStack)); + } const lambdaBody = this.addOptionsArgsToLambdaBodyInStyleArg( lambdaBodyInfo, modifiedArgs, @@ -548,6 +573,37 @@ export class factory { return this.addApplyAttributesFinishToLambdaBodyEnd(newLambdaBody, shouldApplyAttribute); } + static createModuleInfoArg(isUserCreateStack: boolean | undefined): arkts.ObjectExpression { + const projectConfig = MetaDataCollector.getInstance().projectConfig; + const fileAbsName = MetaDataCollector.getInstance().fileAbsName; + const moduleName = projectConfig?.moduleName ?? ''; + const filePath = path.relative(projectConfig?.projectRootPath ?? '', fileAbsName ?? '').replace(/\.ets$/, ''); + const pagePath = projectConfig?.moduleType === ModuleType.HAR ? '' : filePath; + const properties: arkts.Property[] = [ + arkts.factory.createProperty( + arkts.factory.createIdentifier(NavigationNames.MODULE_NAME), + arkts.factory.createStringLiteral(moduleName) + ), + arkts.factory.createProperty( + arkts.factory.createIdentifier(NavigationNames.PAGE_PATH), + arkts.factory.createStringLiteral(pagePath) + ), + ]; + if (isUserCreateStack !== undefined) { + properties.push( + arkts.factory.createProperty( + arkts.factory.createIdentifier(NavigationNames.IS_USER_CREATE_STACK), + arkts.factory.createBooleanLiteral(isUserCreateStack) + ) + ); + } + return arkts.factory.createObjectExpression( + arkts.Es2pandaAstNodeType.AST_NODE_TYPE_OBJECT_EXPRESSION, + properties, + true + ); + } + /** * add `.applyAttributesFinish()` at the end of style argument body. */ diff --git a/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts b/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts index a0bfca783819b728a3a9e397305d2a8892c0b500..95626e0cb205664ad66e69905d08a6ab69d1f59c 100644 --- a/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts +++ b/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts @@ -19,22 +19,18 @@ import { BuilderLambdaNames, expectNameInTypeReference, isCustomComponentAnnotat import { DeclarationCollector } from '../../common/declaration-collector'; import { ARKUI_IMPORT_PREFIX_NAMES, - BindableDecl, Dollars, InnerComponentAttributes, + ARKUI_NAV_DESTINATION_SOURCE_NAME, + ARKUI_NAVIGATION_SOURCE_NAME, + InnerComponentNames, StructDecoratorNames, } from '../../common/predefines'; import { ImportCollector } from '../../common/import-collector'; -import { - collectTypeRecordFromParameter, - collectTypeRecordFromTypeParameterDeclaration, - collectTypeRecordFromTypeParameterInstatiation, - ParameterRecord, - TypeParameterTypeRecord, - TypeRecord, -} from '../../collectors/utils/collect-types'; import { hasMemoAnnotation } from '../../collectors/memo-collectors/utils'; -import { AstNodePointer } from 'common/safe-types'; +import { AstNodePointer } from '../../common/safe-types'; +import { ComponentAttributeCache } from './cache/componentAttributeCache'; +import { MetaDataCollector } from '../../common/metadata-collector'; export type BuilderLambdaDeclInfo = { name: string; @@ -423,10 +419,7 @@ export function isBuilderLambdaFunctionCall(nameNode: arkts.Identifier | undefin return false; } const name = nameNode.name; - return ( - name !== BuilderLambdaNames.ORIGIN_METHOD_NAME && - name !== BuilderLambdaNames.TRANSFORM_METHOD_NAME - ); + return name !== BuilderLambdaNames.ORIGIN_METHOD_NAME && name !== BuilderLambdaNames.TRANSFORM_METHOD_NAME; } export function callIsGoodForBuilderLambda(leaf: arkts.CallExpression): boolean { @@ -669,6 +662,14 @@ export function flatObjectExpressionToEntries( return entries; } +export function isNavigationOrNavDestination(name: string | undefined, sourceName?: string): boolean { + const externalSourceName = sourceName ?? MetaDataCollector.getInstance().externalSourceName; + return ( + (name === InnerComponentNames.NAVIGATION && externalSourceName === ARKUI_NAVIGATION_SOURCE_NAME) || + (name === InnerComponentNames.NAV_DESTINATION && externalSourceName === ARKUI_NAV_DESTINATION_SOURCE_NAME) + ); +} + /** * check whether the last parameter is trailing lambda in components. */ @@ -717,105 +718,3 @@ export function getTransformedComponentName(componentName: string): string { return `${componentName}Impl`; } -// CACHE -export interface ComponentRecord { - name: string; - attributeRecords: ParameterRecord[]; - typeParams?: TypeParameterTypeRecord[]; - hasRestParameter?: boolean; - hasReceiver?: boolean; - hasLastTrailingLambda?: boolean; -} - -export class ComponentAttributeCache { - private _cache: Map; - private _attributeName: string | undefined; - private _attributeTypeParams: TypeRecord[] | undefined; - private _isCollected: boolean = false; - private static instance: ComponentAttributeCache; - - private constructor() { - this._cache = new Map(); - } - - static getInstance(): ComponentAttributeCache { - if (!this.instance) { - this.instance = new ComponentAttributeCache(); - } - return this.instance; - } - - private collectAttributeName(type: arkts.TypeNode | undefined): string | undefined { - if ( - !type || - !arkts.isETSTypeReference(type) || - !type.part || - !type.part.name || - !arkts.isIdentifier(type.part.name) - ) { - return; - } - this._attributeName = type.part.name.name; - if (type.part.typeParams) { - this._attributeTypeParams = collectTypeRecordFromTypeParameterInstatiation(type.part.typeParams); - } - } - - get attributeName(): string | undefined { - return this._attributeName; - } - - get attributeTypeParams(): TypeRecord[] | undefined { - return this._attributeTypeParams; - } - - reset(): void { - this._cache.clear(); - this._attributeName = undefined; - this._attributeTypeParams = undefined; - this._isCollected = false; - } - - isCollected(): boolean { - return this._isCollected; - } - - collect(node: arkts.MethodDefinition): void { - this.collectAttributeName(node.scriptFunction.returnTypeAnnotation); - if (!this._attributeName) { - return; - } - const name: string = node.name.name; - const hasRestParameter = node.scriptFunction.hasRestParameter; - const hasReceiver = node.scriptFunction.hasReceiver; - const typeParams = collectTypeRecordFromTypeParameterDeclaration(node.scriptFunction.typeParams); - const params = node.scriptFunction.params as arkts.ETSParameterExpression[]; - const attributeRecords: ParameterRecord[] = []; - const hasLastTrailingLambda = checkIsTrailingLambdaInLastParam(params); - params.forEach((p, index) => { - if (index === params.length - 1 && hasLastTrailingLambda) { - return; - } - const record = collectTypeRecordFromParameter(p); - attributeRecords.push(record); - }); - const componentRecord: ComponentRecord = { - name, - attributeRecords, - typeParams, - hasRestParameter, - hasReceiver, - hasLastTrailingLambda, - }; - this._cache.set(name, componentRecord); - this._isCollected = true; - } - - getComponentRecord(name: string): ComponentRecord | undefined { - return this._cache.get(name); - } - - getAllComponentRecords(): ComponentRecord[] { - return Array.from(this._cache.values()); - } -} \ No newline at end of file diff --git a/arkui-plugins/ui-plugins/checked-transformer.ts b/arkui-plugins/ui-plugins/checked-transformer.ts index aaa17782067466fea49acc04fd25a8079a1667cd..73807f4f8425cd80e59a5c64392b1215be82254d 100644 --- a/arkui-plugins/ui-plugins/checked-transformer.ts +++ b/arkui-plugins/ui-plugins/checked-transformer.ts @@ -19,11 +19,7 @@ import { factory as structFactory } from './struct-translators/factory'; import { factory as builderLambdaFactory } from './builder-lambda-translators/factory'; import { factory as entryFactory } from './entry-translators/factory'; import { AbstractVisitor } from '../common/abstract-visitor'; -import { - ComponentAttributeCache, - isBuilderLambda, - isBuilderLambdaMethodDecl, -} from './builder-lambda-translators/utils'; +import { isBuilderLambda, isBuilderLambdaMethodDecl } from './builder-lambda-translators/utils'; import { isEntryWrapperClass } from './entry-translators/utils'; import { ImportCollector } from '../common/import-collector'; import { DeclarationCollector } from '../common/declaration-collector'; @@ -37,6 +33,8 @@ import { ResourceInfo, ScopeInfoCollection, isForEachDecl, + RouterInfo, + initRouterInfo, } from './struct-translators/utils'; import { collectCustomComponentScopeInfo, @@ -50,6 +48,8 @@ import { InteroperAbilityNames } from './interop/predefines'; import { generateBuilderCompatible } from './interop/builder-interop'; import { builderRewriteByType } from './builder-lambda-translators/builder-factory'; import { MonitorCache } from './property-translators/cache/monitorCache'; +import { MetaDataCollector } from '../common/metadata-collector'; +import { ComponentAttributeCache } from './builder-lambda-translators/cache/componentAttributeCache'; export class CheckedTransformer extends AbstractVisitor { private scope: ScopeInfoCollection; @@ -57,6 +57,7 @@ export class CheckedTransformer extends AbstractVisitor { projectConfig: ProjectConfig | undefined; aceBuildJson: LoaderJson; resourceInfo: ResourceInfo; + routerInfo: Map; constructor(projectConfig: ProjectConfig | undefined) { super(); @@ -64,8 +65,8 @@ export class CheckedTransformer extends AbstractVisitor { this.scope = { customComponents: [] }; this.aceBuildJson = loadBuildJson(this.projectConfig); this.resourceInfo = initResourceInfo(this.projectConfig, this.aceBuildJson); + this.routerInfo = initRouterInfo(this.aceBuildJson); this.legacyBuilderSet = new Set(); - this.initBuilderMap(); } initBuilderMap(): void { @@ -85,6 +86,14 @@ export class CheckedTransformer extends AbstractVisitor { } } + init(): void { + MetaDataCollector.getInstance() + .setProjectConfig(this.projectConfig) + .setAbsName(this.program?.absName) + .setExternalSourceName(this.externalSourceName) + .setRouterInfo(this.routerInfo); + } + reset(): void { super.reset(); this.scope = { customComponents: [] }; diff --git a/arkui-plugins/ui-plugins/component-transformer.ts b/arkui-plugins/ui-plugins/component-transformer.ts index 37db0024f5b3ea6ce69facc144e4fa580015e668..9b05cefff13b4f15954b23884a12227da2521dd5 100644 --- a/arkui-plugins/ui-plugins/component-transformer.ts +++ b/arkui-plugins/ui-plugins/component-transformer.ts @@ -53,6 +53,8 @@ import { ENTRY_POINT_IMPORT_SOURCE_NAME, NavigationNames, EntryWrapperNames, + ARKUI_NAVIGATION_SOURCE_NAME, + ARKUI_NAV_DESTINATION_SOURCE_NAME, } from '../common/predefines'; import { generateInstantiateInterop } from './interop/interop'; @@ -208,6 +210,12 @@ export class ComponentTransformer extends AbstractVisitor { navInterface.modifiers = arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_EXPORT; return arkts.factory.updateEtsScript(node, [...node.statements, navInterface]); } + if ( + this.externalSourceName === ARKUI_NAVIGATION_SOURCE_NAME || + this.externalSourceName === ARKUI_NAV_DESTINATION_SOURCE_NAME + ) { + return arkts.factory.updateEtsScript(node, [...node.statements, entryFactory.createNavigationModuleInfo()]); + } if (this.isExternal && this.componentInterfaceCollection.length === 0 && this.entryNames.length === 0) { return node; } @@ -277,7 +285,11 @@ export class ComponentTransformer extends AbstractVisitor { node.definition.implements, undefined, node.definition.super, - [entryFactory.generateRegisterNamedRouter(), ...node.definition.body], + [ + entryFactory.generateRegisterNamedRouter(), + entryFactory.generateNavigationBuilderRegister(), + ...node.definition.body, + ], node.definition.modifiers, arkts.classDefinitionFlags(node.definition) ) @@ -510,13 +522,15 @@ export class ComponentTransformer extends AbstractVisitor { } const className = ident.name; const trailingBlock = node.trailingBlock; - const content = trailingBlock ? arkts.factory.createArrowFunction( - factory.createScriptFunction({ - body: trailingBlock, - flags: arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_ARROW, - modifiers: arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_NONE, - }) - ) : undefined; + const content = trailingBlock + ? arkts.factory.createArrowFunction( + factory.createScriptFunction({ + body: trailingBlock, + flags: arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_ARROW, + modifiers: arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_NONE, + }) + ) + : undefined; if (this.legacyCallMap.has(className)) { const path = this.legacyCallMap.get(className)!; const args = node.arguments; @@ -524,7 +538,7 @@ export class ComponentTransformer extends AbstractVisitor { className: className, path: path, arguments: args && args.length === 1 && args[0] instanceof arkts.ObjectExpression ? args[0] : undefined, - content: content + content: content, }; return generateInstantiateInterop(context); } diff --git a/arkui-plugins/ui-plugins/entry-translators/factory.ts b/arkui-plugins/ui-plugins/entry-translators/factory.ts index e4dd29d224a73bb929913285ac9d5fb2cb602a91..8f02731a02d2d6212435b096cda34cccf39d6e19 100644 --- a/arkui-plugins/ui-plugins/entry-translators/factory.ts +++ b/arkui-plugins/ui-plugins/entry-translators/factory.ts @@ -16,7 +16,13 @@ import * as arkts from '@koalaui/libarkts'; import * as path from 'path'; import { annotation, createAndInsertImportDeclaration } from '../../common/arkts-utils'; -import { ENTRY_POINT_IMPORT_SOURCE_NAME, EntryWrapperNames, NavigationNames } from '../../common/predefines'; +import { + ENTRY_POINT_IMPORT_SOURCE_NAME, + EntryWrapperNames, + BuilderNames, + NavigationNames, + TypeNames, +} from '../../common/predefines'; import { ProjectConfig } from '../../common/plugin-context'; import { factory as uiFactory } from '../ui-factory'; import { getRelativePagePath } from './utils'; @@ -356,6 +362,27 @@ export class factory { ); } + static createNavigationModuleInfo(): arkts.TSInterfaceDeclaration { + return arkts.factory.createInterfaceDeclaration( + [], + arkts.factory.createIdentifier(NavigationNames.NAVIGATION_MODULE_INFO), + undefined, + arkts.factory.createInterfaceBody([ + this.createClassProp(NavigationNames.MODULE_NAME), + this.createClassProp(NavigationNames.PAGE_PATH), + arkts.classPropertySetOptional( + this.createClassProp( + NavigationNames.IS_USER_CREATE_STACK, + arkts.factory.createPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_BOOLEAN) + ), + true + ), + ]), + false, + false + ); + } + /** * helper for navInterfaceArg to generate class properties, e.g. buneleName: '...' */ @@ -423,11 +450,11 @@ export class factory { /** * helper for createNavInterface to generate class properties */ - static createClassProp(propName: string): arkts.ClassProperty { + static createClassProp(propName: string, type?: arkts.TypeNode): arkts.ClassProperty { return arkts.factory.createClassProperty( arkts.factory.createIdentifier(propName), undefined, - uiFactory.createTypeReferenceFromString('string'), + type ?? uiFactory.createTypeReferenceFromString('string'), arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, false ); @@ -464,4 +491,37 @@ export class factory { modifiers: arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC, }); } + + /** + * generate `NavigationBuilderRegister` method in header arkui.UserView. + */ + static generateNavigationBuilderRegister(): arkts.MethodDefinition { + const params = [ + uiFactory.createParameterDeclaration(NavigationNames.NAME, TypeNames.STRING), + arkts.factory.createParameterDeclaration( + arkts.factory.createIdentifier( + BuilderNames.BUILDER, + uiFactory.createComplexTypeFromStringAndTypeParameter(BuilderNames.WRAPPED_BUILDER, [ + uiFactory.createTypeReferenceFromString(TypeNames.TYPE_T), + ]) + ), + undefined + ), + ]; + return uiFactory.createMethodDefinition({ + key: arkts.factory.createIdentifier(EntryWrapperNames.NAVIGATION_BUILDER_REGISTER), + kind: arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_METHOD, + function: { + body: arkts.factory.createBlock([]), + typeParams: arkts.factory.createTypeParameterDeclaration( + [arkts.factory.createTypeParameter(arkts.factory.createIdentifier(TypeNames.TYPE_T))], + 0 + ), + params: params, + flags: arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD, + modifiers: arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC, + }, + modifiers: arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC, + }); + } } diff --git a/arkui-plugins/ui-plugins/index.ts b/arkui-plugins/ui-plugins/index.ts index c89dd6eff217958eabd44595a2ff42893c4b9b1d..599b0ad366ac7e4492ef304088fe569105a684b6 100644 --- a/arkui-plugins/ui-plugins/index.ts +++ b/arkui-plugins/ui-plugins/index.ts @@ -20,6 +20,7 @@ import { Plugins, PluginContext, ProjectConfig } from '../common/plugin-context' import { ProgramVisitor } from '../common/program-visitor'; import { EXTERNAL_SOURCE_PREFIX_NAMES } from '../common/predefines'; import { debugLog } from '../common/debug'; +import { MetaDataCollector } from '../common/metadata-collector'; export function uiTransform(): Plugins { return { @@ -134,6 +135,7 @@ function checkedProgramVisit( pluginContext: context, }); program = programVisitor.programVisitor(program); + MetaDataCollector.getInstance().reset(); } return program; } diff --git a/arkui-plugins/ui-plugins/struct-translators/factory.ts b/arkui-plugins/ui-plugins/struct-translators/factory.ts index 13a120dc40a2bdfb6a5df38951de0d33b4e085bf..207d9a28900f116127cce7313935a96dce1538d7 100644 --- a/arkui-plugins/ui-plugins/struct-translators/factory.ts +++ b/arkui-plugins/ui-plugins/struct-translators/factory.ts @@ -59,9 +59,10 @@ import { ObservedAnnoInfo, getNoTransformationMembersInClass, isComputedMethod, + RouterInfo, } from './utils'; import { collectStateManagementTypeImport, generateThisBacking, hasDecorator } from '../property-translators/utils'; -import { ComponentAttributeCache, isComponentAttributeInterface } from '../builder-lambda-translators/utils'; +import { isComponentAttributeInterface } from '../builder-lambda-translators/utils'; import { ProjectConfig } from '../../common/plugin-context'; import { ImportCollector } from '../../common/import-collector'; import { @@ -74,6 +75,9 @@ import { RESOURCE_TYPE, ARKUI_BUILDER_SOURCE_NAME, TypeNames, + EntryWrapperNames, + BuilderNames, + ENTRY_POINT_IMPORT_SOURCE_NAME, } from '../../common/predefines'; import { ObservedTranslator } from '../property-translators/index'; import { @@ -86,6 +90,8 @@ import { GenSymGenerator } from '../../common/gensym-generator'; import { MethodTranslator } from 'ui-plugins/property-translators/base'; import { MonitorCache } from '../property-translators/cache/monitorCache'; import { PropertyCache } from '../property-translators/cache/propertyCache'; +import { ComponentAttributeCache } from '../builder-lambda-translators/cache/componentAttributeCache'; +import { MetaDataCollector } from '../../common/metadata-collector'; export class factory { /** @@ -836,6 +842,66 @@ export class factory { return node; } + static createNavigationBuilderRegister(routerInfo: RouterInfo): arkts.ExpressionStatement { + ImportCollector.getInstance().collectSource(BuilderNames.WRAP_BUILDER, ARKUI_BUILDER_SOURCE_NAME); + ImportCollector.getInstance().collectImport(BuilderNames.WRAP_BUILDER); + ImportCollector.getInstance().collectSource( + EntryWrapperNames.ENTRY_POINT_CLASS_NAME, + ENTRY_POINT_IMPORT_SOURCE_NAME + ); + ImportCollector.getInstance().collectImport(EntryWrapperNames.ENTRY_POINT_CLASS_NAME); + return arkts.factory.createExpressionStatement( + arkts.factory.createCallExpression( + UIFactory.generateMemberExpression( + arkts.factory.createIdentifier(EntryWrapperNames.ENTRY_POINT_CLASS_NAME), + EntryWrapperNames.NAVIGATION_BUILDER_REGISTER + ), + undefined, + [ + arkts.factory.createStringLiteral(routerInfo.name), + arkts.factory.createCallExpression( + arkts.factory.createIdentifier(BuilderNames.WRAP_BUILDER), + undefined, + [arkts.factory.createIdentifier(routerInfo.buildFunction)] + ), + ] + ) + ); + } + + static addNavigationBuilderRegister(): arkts.ExpressionStatement[] { + const routerInfo: Map = MetaDataCollector.getInstance().routerInfo; + const filePath: string | undefined = MetaDataCollector.getInstance().fileAbsName; + if (!filePath || !routerInfo.has(filePath)) { + return []; + } + let navigationBuilders: arkts.ExpressionStatement[] = []; + routerInfo.get(filePath)!.forEach((info: RouterInfo) => { + navigationBuilders.push(factory.createNavigationBuilderRegister(info)); + }); + return navigationBuilders; + } + + static insertMembersInClassStaticBlock(node: arkts.ClassStaticBlock): arkts.ClassStaticBlock { + if (!node.value || !node.function) { + return node; + } + const func = node.function; + const body: arkts.AstNode | undefined = func.body; + if (!body || !arkts.isBlockStatement(body)) { + return node; + } + return arkts.factory.updateClassStaticBlock( + node, + arkts.factory.updateFunctionExpression( + node.value as arkts.FunctionExpression, + UIFactory.updateScriptFunction(func, { + body: arkts.factory.createBlock([...body.statements, ...factory.addNavigationBuilderRegister()]), + }) + ) + ); + } + static transformETSGlobalClass(node: arkts.ClassDeclaration, externalSourceName?: string): arkts.ClassDeclaration { if (!node.definition) { return node; @@ -851,6 +917,8 @@ export class factory { member.modifiers, false ); + } else if (arkts.isClassStaticBlock(member)) { + return factory.insertMembersInClassStaticBlock(member); } return member; }); diff --git a/arkui-plugins/ui-plugins/struct-translators/utils.ts b/arkui-plugins/ui-plugins/struct-translators/utils.ts index 1e32a07868894082c9a6d61ab206778c521dba9f..4ef7ac3cd666c77ba8f4d9912c4ee8b803caea32 100644 --- a/arkui-plugins/ui-plugins/struct-translators/utils.ts +++ b/arkui-plugins/ui-plugins/struct-translators/utils.ts @@ -57,6 +57,7 @@ export interface ResourceInfo { export interface LoaderJson { hspResourcesMap: Record; + routerMap: RouterMap; } export interface ResourceParameter { @@ -73,6 +74,16 @@ export interface ObservedAnnoInfo { classHasTrack: boolean; } +export type RouterMap = RouterInfo & { + ohmurl: string; + pageSourceFile: string; +}; + +export type RouterInfo = { + name: string; + buildFunction: string; +}; + export type ClassScopeInfo = ObservedAnnoInfo & { getters: arkts.MethodDefinition[]; }; @@ -150,6 +161,37 @@ export function loadBuildJson(projectConfig: ProjectConfig | undefined): any { return {}; } +/** + * Initializes routerInfo which maps absolute file paths to corresponding build functions + * + * @param aceBuildJson content of the file 'loader.json'. + */ +export function initRouterInfo(aceBuildJson: Partial): Map { + const routerInfo: Map = new Map(); + if (!aceBuildJson || !aceBuildJson.routerMap || !Array.isArray(aceBuildJson.routerMap)) { + return routerInfo; + } + aceBuildJson.routerMap.forEach((item) => { + if (item.pageSourceFile && item.name && item.buildFunction) { + setRouterInfo(routerInfo, item); + } + }); + return routerInfo; +} + +/** + * set router info based on the information of router map. + */ +function setRouterInfo(routerInfo: Map, routerMapItem: RouterMap): void { + const { pageSourceFile, name, buildFunction } = routerMapItem; + const filePath = path.resolve(pageSourceFile); + if (routerInfo.has(filePath)) { + routerInfo.get(filePath)!.push({ name: name, buildFunction: buildFunction }); + } else { + routerInfo.set(filePath, [{ name: name, buildFunction: buildFunction }]); + } +} + /** * Initialize all resources information, including app resources, system resources, dependent hap resources and rawfile resources. *