diff --git a/.vscode/settings.json b/.vscode/settings.json index fb2ff68..d460c5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,21 +45,15 @@ "pattern": "." } ], - "editor.gotoLocation.alternativeDeclarationCommand": "editor.action.revealDefinition", - "editor.gotoLocation.alternativeDefinitionCommand": "editor.action.revealDefinition", - "editor.gotoLocation.alternativeTypeDefinitionCommand": "editor.action.revealDefinition", - "editor.selectionHighlight": false, - "files.autoSave": "onFocusChange", - "editor.suggest.snippetsPreventQuickSuggestions": false, - "editor.quickSuggestions": { - "other": "on", - "comments": "off", - "strings": "on" - }, - "oxc.enable": true, + "files.readonlyInclude": { "index.js": true, "index.d.ts": true, + "project-detector.wasi-browser.js": true, + "project-detector.wasi.cjs": true, + "browser.js": true, + "wasi-worker-browser.mjs": true, + "wasi-worker.mjs": true, "**/dist/**/*": true, "**/node_modules/**/*": true } diff --git a/build.rs b/build.rs index 4b2ed17..1f866b6 100755 --- a/build.rs +++ b/build.rs @@ -1,19 +1,5 @@ extern crate napi_build; -use std::env; - fn main() { napi_build::setup(); - - // 仅当显式开启时才在 Linux GNU 的 arm64/armv7 目标上链接 libbsd,避免交叉工具链找不到 -lbsd - // 通过设置环境变量 LINK_LIBBSD=1 开启 - if env::var("LINK_LIBBSD").map(|v| v == "1").unwrap_or(false) { - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); - let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); - - if target_os == "linux" && target_env == "gnu" && (target_arch == "aarch64" || target_arch == "arm") { - println!("cargo:rustc-link-lib=bsd"); - } - } } diff --git a/index.d.ts b/index.d.ts index 718f076..42fc927 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,14 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ +export declare class AppScope { + static from(project: Project): AppScope | null + getUri(): Uri + getConfigUri(): Uri + getConfigContent(): string + getParsedConfigContent(): any + reload(): void +} + export declare class ElementDirectory { static from(resourceDirectory: ResourceDirectory): ElementDirectory | null getUri(): Uri @@ -91,53 +100,72 @@ export declare class QualifierUtils { /** * Check if the mcc is a valid MCC code with value. */ static isMcc(mcc: number): boolean - /** * Check if the mcc is a valid MCC code with string `mcc`. + /** + * Check if the mcc is a valid MCC code with string `mcc`. + * * For example: "mcc310" => true, "mcc3100" => false */ - static isMccCode(mcc: string): boolean - /** * Check if the language code is a valid language code. + static isMccCode(mcc: `mcc${number}` | (string & {})): boolean + /** + * Check if the language code is a valid language code. + * * For example: "en" => true, "en-US" => false */ static isLanguageCode(languageCode: string): boolean - /** * Check if the device type is a valid device type. - * - phone - * - tablet - * - tv - * - car - * - wearable - * - 2in1 - */ - static isDeviceType(deviceType: string): boolean - /** * Check if the color mode is a valid color mode. - * - dark - * - light - */ - static isColorMode(colorMode: string): boolean - /** * Check if the mnc is a valid MNC code with value. + /** + * Check if the device type is a valid device type. + * + * - `phone` + * - `tablet` + * - `tv` + * - `car` + * - `wearable` + * - `2in1` + */ + static isDeviceType(deviceType: 'phone' | 'tablet' | 'tv' | 'car' | 'wearable' | '2in1' | (string & {})): boolean + /** + * Check if the color mode is a valid color mode. + * + * - `dark` + * - `light` + */ + static isColorMode(colorMode: 'dark' | 'light' | (string & {})): boolean + /** + * Check if the mnc is a valid MNC code with value. + * + * For example: 00 => true, 000 => false */ static isMnc(mnc: number, mcc: number): boolean - /** * Check if the mnc is a valid MNC code with string `mnc` and `mcc`. + /** + * Check if the mnc is a valid MNC code with string `mnc` and `mcc`. + * * For example: "mnc00" => true, "mnc000" => false */ static isMncCode(mnc: string, mcc: number): boolean - /** * Check if the region code is a valid region code. + /** + * Check if the region code is a valid region code. + * * For example: "CN" => true, "US" => true, "AAA" => false */ static isRegionCode(regionCode: string): boolean - /** * Check if the orientation is a valid orientation. - * - vertical - * - horizontal - */ - static isOrientation(orientation: string): boolean - /** * Check if the screen density is a valid screen density. - * - sdpi - * - mdpi - * - ldpi - * - xldpi - * - xxldpi - * - xxxldpi - */ - static isScreenDensity(screenDensity: string): boolean + /** + * Check if the orientation is a valid orientation. + * + * - `vertical` + * - `horizontal` + */ + static isOrientation(orientation: 'vertical' | 'horizontal' | (string & {})): boolean + /** + * Check if the screen density is a valid screen density. + * + * - `sdpi` + * - `mdpi` + * - `ldpi` + * - `xldpi` + * - `xxldpi` + * - `xxxldpi` + */ + static isScreenDensity(screenDensity: 'sdpi' | 'mdpi' | 'ldpi' | 'xldpi' | 'xxldpi' | 'xxxldpi' | (string & {})): boolean /** * Analyze the qualifier and return the qualifier list. * * 限定词目录由一个或多个表征应用场景或设备特征的限定词组合而成,限定词包括移动国家码和移动网络码、语言、文字、国家或地区、横竖屏、设备类型、颜色模式和屏幕密度,限定词之间通过下划线(_)或者中划线(-)连接。开发者在创建限定词目录时,需要遵守如下限定词目录命名规则。 @@ -166,9 +194,28 @@ export declare class ResfileDirectory { export declare class Resource { static findAll(product: Product): Array + static fromAppScope(appScope: AppScope): Resource | null static create(product: Product, resourceUri: string): Resource | null + /** + * If the resource created by {@linkcode Product}, this method will return the product. + * + * @throw If the resource is not created by {@linkcode Product}, this method will throw an error. + */ getProduct(): Product + /** + * If the resource created by {@linkcode AppScope}, this method will return the app scope. + * + * @throw If the resource is not created by {@linkcode AppScope}, this method will throw an error. + */ + getAppScope(): AppScope getUri(): Uri + /** + * Get the type of current resource. + * + * - If created by {@link Product}, return {@link ResourceType.Product}. + * - If created by {@link AppScope}, return {@link ResourceType.AppScope}. + */ + get resourceType(): ResourceType } export declare class ResourceDirectory { @@ -216,3 +263,8 @@ export declare const enum QualifierType { LanguageCode = 'LanguageCode', DeviceType = 'DeviceType' } + +export declare const enum ResourceType { + Product = 0, + AppScope = 1 +} diff --git a/index.js b/index.js index b9db31f..bffb9e5 100644 --- a/index.js +++ b/index.js @@ -3,9 +3,8 @@ // @ts-nocheck /* auto-generated by NAPI-RS */ -import { createRequire } from 'node:module' -const require = createRequire(import.meta.url) -const __dirname = new URL('.', import.meta.url).pathname +const { createRequire } = require('node:module') +require = createRequire(__filename) const { readFileSync } = require('node:fs') let nativeBinding = null @@ -557,20 +556,22 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { ElementDirectory, ElementJsonFile, ElementJsonFileReference, MediaDirectory, Module, Product, ProfileDirectory, Project, ProjectDetector, QualifierUtils, RawfileDirectory, ResfileDirectory, Resource, ResourceDirectory, Uri, QualifierType } = nativeBinding -export { ElementDirectory } -export { ElementJsonFile } -export { ElementJsonFileReference } -export { MediaDirectory } -export { Module } -export { Product } -export { ProfileDirectory } -export { Project } -export { ProjectDetector } -export { QualifierUtils } -export { RawfileDirectory } -export { ResfileDirectory } -export { Resource } -export { ResourceDirectory } -export { Uri } -export { QualifierType } +module.exports = nativeBinding +module.exports.AppScope = nativeBinding.AppScope +module.exports.ElementDirectory = nativeBinding.ElementDirectory +module.exports.ElementJsonFile = nativeBinding.ElementJsonFile +module.exports.ElementJsonFileReference = nativeBinding.ElementJsonFileReference +module.exports.MediaDirectory = nativeBinding.MediaDirectory +module.exports.Module = nativeBinding.Module +module.exports.Product = nativeBinding.Product +module.exports.ProfileDirectory = nativeBinding.ProfileDirectory +module.exports.Project = nativeBinding.Project +module.exports.ProjectDetector = nativeBinding.ProjectDetector +module.exports.QualifierUtils = nativeBinding.QualifierUtils +module.exports.RawfileDirectory = nativeBinding.RawfileDirectory +module.exports.ResfileDirectory = nativeBinding.ResfileDirectory +module.exports.Resource = nativeBinding.Resource +module.exports.ResourceDirectory = nativeBinding.ResourceDirectory +module.exports.Uri = nativeBinding.Uri +module.exports.QualifierType = nativeBinding.QualifierType +module.exports.ResourceType = nativeBinding.ResourceType diff --git a/mock/harmony-project-1/AppScope/app.json5 b/mock/harmony-project-1/AppScope/app.json5 new file mode 100644 index 0000000..80febb0 --- /dev/null +++ b/mock/harmony-project-1/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "cc.naily.demo1", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} \ No newline at end of file diff --git a/mock/harmony-project-1/AppScope/resources/base/element/string.json b/mock/harmony-project-1/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..c6c0108 --- /dev/null +++ b/mock/harmony-project-1/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "YesPlayNext" + } + ] +} diff --git a/mock/harmony-project-1/AppScope/resources/base/media/background.png b/mock/harmony-project-1/AppScope/resources/base/media/background.png new file mode 100755 index 0000000..923f2b3 Binary files /dev/null and b/mock/harmony-project-1/AppScope/resources/base/media/background.png differ diff --git a/mock/harmony-project-1/AppScope/resources/base/media/foreground.png b/mock/harmony-project-1/AppScope/resources/base/media/foreground.png new file mode 100755 index 0000000..eb94275 Binary files /dev/null and b/mock/harmony-project-1/AppScope/resources/base/media/foreground.png differ diff --git a/mock/harmony-project-1/AppScope/resources/base/media/layered_image.json b/mock/harmony-project-1/AppScope/resources/base/media/layered_image.json new file mode 100755 index 0000000..fb49920 --- /dev/null +++ b/mock/harmony-project-1/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/mock/harmony-project-2/AppScope/app.json5 b/mock/harmony-project-2/AppScope/app.json5 new file mode 100644 index 0000000..cf95950 --- /dev/null +++ b/mock/harmony-project-2/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "cc.naily.demo2", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} \ No newline at end of file diff --git a/mock/harmony-project-2/AppScope/resources/base/element/string.json b/mock/harmony-project-2/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..c6c0108 --- /dev/null +++ b/mock/harmony-project-2/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "YesPlayNext" + } + ] +} diff --git a/mock/harmony-project-2/AppScope/resources/base/media/background.png b/mock/harmony-project-2/AppScope/resources/base/media/background.png new file mode 100755 index 0000000..923f2b3 Binary files /dev/null and b/mock/harmony-project-2/AppScope/resources/base/media/background.png differ diff --git a/mock/harmony-project-2/AppScope/resources/base/media/foreground.png b/mock/harmony-project-2/AppScope/resources/base/media/foreground.png new file mode 100755 index 0000000..eb94275 Binary files /dev/null and b/mock/harmony-project-2/AppScope/resources/base/media/foreground.png differ diff --git a/mock/harmony-project-2/AppScope/resources/base/media/layered_image.json b/mock/harmony-project-2/AppScope/resources/base/media/layered_image.json new file mode 100755 index 0000000..fb49920 --- /dev/null +++ b/mock/harmony-project-2/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/package.json b/package.json index e65cb65..b4d7bf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "@arkts/project-detector", - "type": "module", "version": "1.1.3-alpha.29", "packageManager": "pnpm@10.18.0", "description": "A native Rust-based project detector for HarmonyOS/OpenHarmony projects", @@ -78,7 +77,7 @@ "scripts": { "artifacts": "napi artifacts", "bench": "node --import @oxc-node/core/register benchmark/bench.ts", - "build": "napi build --esm --platform --release", + "build": "napi build --platform --release", "postbuild": "tsdown --config-loader unconfig", "build:debug": "napi build --platform", "format:toml": "taplo format", diff --git a/project-detector.wasi-browser.js b/project-detector.wasi-browser.js index 236b5e6..27be72d 100644 --- a/project-detector.wasi-browser.js +++ b/project-detector.wasi-browser.js @@ -56,6 +56,7 @@ const { }, }) export default __napiModule.exports +export const AppScope = __napiModule.exports.AppScope export const ElementDirectory = __napiModule.exports.ElementDirectory export const ElementJsonFile = __napiModule.exports.ElementJsonFile export const ElementJsonFileReference = __napiModule.exports.ElementJsonFileReference @@ -72,3 +73,4 @@ export const Resource = __napiModule.exports.Resource export const ResourceDirectory = __napiModule.exports.ResourceDirectory export const Uri = __napiModule.exports.Uri export const QualifierType = __napiModule.exports.QualifierType +export const ResourceType = __napiModule.exports.ResourceType diff --git a/project-detector.wasi.cjs b/project-detector.wasi.cjs index e6107e7..f9514bf 100644 --- a/project-detector.wasi.cjs +++ b/project-detector.wasi.cjs @@ -108,6 +108,7 @@ const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule }, }) module.exports = __napiModule.exports +module.exports.AppScope = __napiModule.exports.AppScope module.exports.ElementDirectory = __napiModule.exports.ElementDirectory module.exports.ElementJsonFile = __napiModule.exports.ElementJsonFile module.exports.ElementJsonFileReference = __napiModule.exports.ElementJsonFileReference @@ -124,3 +125,4 @@ module.exports.Resource = __napiModule.exports.Resource module.exports.ResourceDirectory = __napiModule.exports.ResourceDirectory module.exports.Uri = __napiModule.exports.Uri module.exports.QualifierType = __napiModule.exports.QualifierType +module.exports.ResourceType = __napiModule.exports.ResourceType diff --git a/src/node/app-scope.ts b/src/node/app-scope.ts new file mode 100644 index 0000000..197a4b4 --- /dev/null +++ b/src/node/app-scope.ts @@ -0,0 +1,26 @@ +import type { Uri } from '../../index' +import type { Project } from './project' +import type { ProjectDetector } from './project-detector' +import { signal } from 'alien-signals' +import { AppScope as RustAppScope } from '../../index' +import { DisposableSignal } from './types' + +export interface AppScope extends RustAppScope {} + +export namespace AppScope { + export function from(project: Project): DisposableSignal { + const appScope = signal(RustAppScope.from(project.getUnderlyingProject())) + + const handle = (_event: keyof ProjectDetector.EventMap, uri: Uri) => { + if (project.getBuildProfileUri().isEqual(uri) || project.getUri().isEqual(uri)) { + appScope(RustAppScope.from(project.getUnderlyingProject())) + } + else if (uri.fsPath.endsWith('app.json5')) { + appScope(RustAppScope.from(project.getUnderlyingProject())) + } + } + + project.getProjectDetector().on('*', handle) + return DisposableSignal.fromSignal(appScope, () => project.getProjectDetector().off('*', handle)) + } +} diff --git a/src/node/index.ts b/src/node/index.ts index 433041a..b29af7d 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -1,4 +1,5 @@ export { type Qualifier, QualifierType, QualifierUtils, Uri } from '../../index' +export * from './app-scope' export * from './element-directory' export * from './files/element-json-file' export * from './interfaces' diff --git a/src/node/resource.ts b/src/node/resource.ts index c29a6a2..fce40a0 100644 --- a/src/node/resource.ts +++ b/src/node/resource.ts @@ -14,8 +14,10 @@ export namespace Resource { function fromRustResource(resource: RustResource, product: Product): Resource { return { getProduct: () => product, + getAppScope: () => resource.getAppScope(), getUri: () => resource.getUri(), getUnderlyingResource: () => resource, + resourceType: resource.resourceType, } } diff --git a/src/rust/app_scope.rs b/src/rust/app_scope.rs new file mode 100644 index 0000000..4bb66bb --- /dev/null +++ b/src/rust/app_scope.rs @@ -0,0 +1,73 @@ +use crate::{project::Project, utils::uri::Uri}; +use napi::bindgen_prelude::Reference; +use napi_derive::napi; +use std::{fs, path::Path}; + +#[napi] +pub struct AppScope { + app_scope_uri: Uri, + app_scope_config_uri: Uri, + app_scope_config_content: String, + parsed_app_scope_config: serde_json::Value, +} + +#[napi] +impl AppScope { + #[napi] + pub fn from(project: Reference) -> Option { + let app_scope_uri = Path::new(&project.get_uri().fs_path()).join("AppScope").to_string_lossy().to_string(); + let app_scope_config_uri = Path::new(&app_scope_uri).join("app.json5").to_string_lossy().to_string(); + let app_scope_config_content = match fs::read_to_string(&app_scope_config_uri) { + Ok(app_scope_config_content) => app_scope_config_content, + Err(_) => return None, + }; + let parsed_app_scope_config: serde_json::Value = match serde_json5::from_str(&app_scope_config_content) { + Ok(parsed_app_scope_config) => parsed_app_scope_config, + Err(_) => return None, + }; + + Some(AppScope { + app_scope_uri: Uri::file(app_scope_uri), + app_scope_config_uri: Uri::file(app_scope_config_uri), + app_scope_config_content, + parsed_app_scope_config, + }) + } + + #[napi] + pub fn get_uri(&self) -> Uri { + self.app_scope_uri.clone() + } + + #[napi] + pub fn get_config_uri(&self) -> Uri { + self.app_scope_config_uri.clone() + } + + #[napi] + pub fn get_config_content(&self) -> String { + self.app_scope_config_content.clone() + } + + #[napi] + pub fn get_parsed_config_content(&self) -> serde_json::Value { + self.parsed_app_scope_config.clone() + } + + pub fn update_app_scope_config_content(&mut self, app_scope_config_content: String) { + self.app_scope_config_content = app_scope_config_content; + } + + pub fn update_parsed_app_scope_config(&mut self, parsed_app_scope_config: serde_json::Value) { + self.parsed_app_scope_config = parsed_app_scope_config; + } + + #[napi] + pub fn reload(&mut self) { + let app_scope_config_content = fs::read_to_string(self.app_scope_config_uri.fs_path()).unwrap_or_default(); + let parsed_app_scope_config: serde_json::Value = serde_json5::from_str(&app_scope_config_content).unwrap_or_default(); + + self.update_app_scope_config_content(app_scope_config_content); + self.update_parsed_app_scope_config(parsed_app_scope_config); + } +} diff --git a/src/rust/element_directory.rs b/src/rust/element_directory.rs index 393b3b3..7964507 100644 --- a/src/rust/element_directory.rs +++ b/src/rust/element_directory.rs @@ -31,6 +31,9 @@ impl ElementDirectory { #[napi] pub fn get_resource_directory(&self, env: Env) -> Reference { - self.resource_directory.clone(env).unwrap() + match self.resource_directory.clone(env) { + Ok(cloned_resource_directory) => cloned_resource_directory, + Err(_) => panic!("Failed to get cloned resource directory, please check your resource directory is valid in ElementDirectory.getResourceDirectory()."), + } } } diff --git a/src/rust/lib.rs b/src/rust/lib.rs index e56c2fa..6e4be6e 100755 --- a/src/rust/lib.rs +++ b/src/rust/lib.rs @@ -1,3 +1,4 @@ +pub mod app_scope; pub mod element_directory; pub mod files; pub mod media_directory; diff --git a/src/rust/media_directory.rs b/src/rust/media_directory.rs index 8f81986..956a5fe 100644 --- a/src/rust/media_directory.rs +++ b/src/rust/media_directory.rs @@ -28,7 +28,10 @@ impl MediaDirectory { #[napi] pub fn get_resource_directory(&self, env: Env) -> Reference { - self.resource_directory.clone(env).unwrap() + match self.resource_directory.clone(env) { + Ok(cloned_resource_directory) => cloned_resource_directory, + Err(_) => panic!("Failed to get cloned resource directory, please check your resource directory is valid in MediaDirectory.getResourceDirectory()."), + } } #[napi] diff --git a/src/rust/module.rs b/src/rust/module.rs index 2bee3be..42368b5 100644 --- a/src/rust/module.rs +++ b/src/rust/module.rs @@ -19,6 +19,10 @@ pub struct Module { impl Module { #[napi] pub fn create(project: Reference, module_uri: String, env: Env) -> Option { + let cloned_project = match project.clone(env) { + Ok(cloned_project) => cloned_project, + Err(_) => panic!("Failed to get cloned project, please check your project is valid in Module.create()."), + }; let uri = Uri::file(module_uri); let build_profile_path = Path::new(&uri.fs_path()).join("build-profile.json5"); let build_profile_uri = Uri::file(build_profile_path.to_string_lossy().to_string()); @@ -31,7 +35,7 @@ impl Module { Some(Module { module_name: Self::extract_module_name(&parsed_build_profile), uri, - project: project.clone(env).unwrap(), + project: cloned_project, parsed_build_profile, build_profile_uri, build_profile_content, @@ -49,9 +53,12 @@ impl Module { }; for module_config in modules_array { - let project_ref = project.clone(env).unwrap(); - let uri = Self::build_module_uri(project_ref, module_config); - if let Some(module) = Self::create(project.clone(env).unwrap(), uri.to_string(), env) { + let cloned_project = match project.clone(env) { + Ok(cloned_project) => cloned_project, + Err(_) => panic!("Failed to get cloned project, please check your project is valid in Module.find_all()."), + }; + let uri = Self::build_module_uri(&cloned_project, module_config); + if let Some(module) = Self::create(cloned_project, uri.to_string(), env) { modules.push(module); } } @@ -85,7 +92,10 @@ impl Module { #[napi] pub fn get_project(&self, env: Env) -> Reference { - self.project.clone(env).unwrap() + match self.project.clone(env) { + Ok(cloned_project) => cloned_project, + Err(_) => panic!("Failed to get cloned project, please check your project is valid in Module.getProject()."), + } } #[napi] @@ -117,7 +127,7 @@ impl Module { module_config.get("name").and_then(|name| name.as_str()).unwrap_or("").to_string() } - fn build_module_uri(project: Reference, module_config: &serde_json::Value) -> Uri { + fn build_module_uri(project: &Reference, module_config: &serde_json::Value) -> Uri { let project_path = project.get_uri().fs_path(); let src_path = module_config.get("srcPath").and_then(|path| path.as_str()).unwrap_or(""); let full_path = path_clean::clean(Path::new(&project_path).join(src_path).to_string_lossy().to_string()); diff --git a/src/rust/product.rs b/src/rust/product.rs index c912d58..d64fec0 100644 --- a/src/rust/product.rs +++ b/src/rust/product.rs @@ -3,10 +3,11 @@ use crate::utils::uri::Uri; use napi::{bindgen_prelude::Reference, Env}; use napi_derive::napi; use std::path::Path; +use std::rc::Rc; #[napi] pub struct Product { - module: Reference, + module: Rc>, name: String, } @@ -21,11 +22,16 @@ impl Product { None => return products, }; + let cloned_module = Rc::new(match module.clone(env) { + Ok(cloned_module) => cloned_module, + Err(_) => panic!("Failed to get cloned module, please check your module is valid in Product.findAll()."), + }); + for target_config in targets_array { let target_name = target_config.get("name").and_then(|name| name.as_str()).unwrap_or_default(); products.push(Product { - module: module.clone(env).unwrap(), + module: Rc::clone(&cloned_module), name: target_name.to_string(), }) } @@ -35,7 +41,10 @@ impl Product { #[napi] pub fn get_module(&self, env: Env) -> Reference { - self.module.clone(env).unwrap() + match self.module.as_ref().clone(env) { + Ok(cloned_module) => cloned_module, + Err(_) => panic!("Failed to get cloned module, please check your module is valid in Product.getModule()."), + } } #[napi] @@ -45,7 +54,7 @@ impl Product { #[napi] pub fn get_current_target_config(&self) -> serde_json::Value { - let parsed_build_profile = self.module.get_parsed_build_profile(); + let parsed_build_profile = self.module.as_ref().get_parsed_build_profile(); let targets_array = match parsed_build_profile.get("targets").and_then(|targets| targets.as_array()) { Some(array) => array, None => return serde_json::Value::Null, @@ -61,7 +70,7 @@ impl Product { let mut target_directories = Vec::new(); let current_target_config = self.get_current_target_config(); let name = self.get_name(); - let module_uri = self.module.get_uri(); + let module_uri = self.module.as_ref().get_uri(); if current_target_config.is_null() { return target_directories; @@ -117,7 +126,7 @@ impl Product { let mut target_directories = Vec::new(); let current_target_config = self.get_current_target_config(); let name = self.get_name(); - let module_uri = self.module.get_uri(); + let module_uri = self.module.as_ref().get_uri(); if current_target_config.is_null() { return target_directories; diff --git a/src/rust/profile_directory.rs b/src/rust/profile_directory.rs index 1937366..120b22b 100644 --- a/src/rust/profile_directory.rs +++ b/src/rust/profile_directory.rs @@ -28,7 +28,10 @@ impl ProfileDirectory { #[napi] pub fn get_resource_directory(&self, env: Env) -> Reference { - self.resource_directory.clone(env).unwrap() + match self.resource_directory.clone(env) { + Ok(cloned_resource_directory) => cloned_resource_directory, + Err(_) => panic!("Failed to get cloned resource directory, please check your resource directory is valid in ProfileDirectory.getResourceDirectory()."), + } } #[napi] diff --git a/src/rust/project.rs b/src/rust/project.rs index 4267a11..c577545 100644 --- a/src/rust/project.rs +++ b/src/rust/project.rs @@ -4,16 +4,16 @@ use crate::utils::uri::Uri; use napi::{bindgen_prelude::Reference, Env}; use napi_derive::napi; #[cfg(not(test))] -use std::fs; -#[cfg(not(test))] use std::path::Path; #[cfg(not(test))] +use std::{fs, rc::Rc}; +#[cfg(not(test))] use walkdir::WalkDir; #[napi] pub struct Project { #[cfg(not(test))] - project_detector: Reference, + project_detector: Rc>, #[cfg(test)] project_detector: ProjectDetector, uri: Uri, @@ -27,7 +27,10 @@ impl Project { #[napi] #[cfg(not(test))] pub fn get_project_detector(&self, env: Env) -> Reference { - self.project_detector.clone(env).unwrap() + match self.project_detector.as_ref().clone(env) { + Ok(cloned_project_detector) => cloned_project_detector, + Err(_) => panic!("Failed to get cloned project detector, please check your project detector is valid in Project.getProjectDetector()."), + } } #[cfg(test)] @@ -35,7 +38,8 @@ impl Project { self.project_detector.clone() } - pub fn is_in_exclude_dirs(entry: &walkdir::DirEntry) -> bool { + #[cfg(not(test))] + fn is_in_exclude_dirs(entry: &walkdir::DirEntry) -> bool { entry.path().iter().any(|component| { if let Some(component_str) = component.to_str() { component_str == "node_modules" || component_str == "oh_modules" || component_str.starts_with('.') @@ -48,8 +52,12 @@ impl Project { #[napi] #[cfg(not(test))] pub fn find_all(project_detector: Reference, env: Env) -> Vec { + let cloned_project_detector = Rc::new(match project_detector.clone(env) { + Ok(cloned_project_detector) => cloned_project_detector, + Err(_) => panic!("Failed to get cloned project detector, please check your project detector is valid in Project.findAll()."), + }); let mut projects = Vec::new(); - let workspace_folder = project_detector.get_workspace_folder().fs_path(); + let workspace_folder = cloned_project_detector.get_workspace_folder().fs_path(); let entries: Vec<_> = WalkDir::new(workspace_folder) .into_iter() .filter_entry(|entry| !Self::is_in_exclude_dirs(entry)) @@ -60,7 +68,7 @@ impl Project { let path = entry.path(); if path.file_name().and_then(|name| name.to_str()).is_some_and(|name| name == "build-profile.json5") && entry.file_type().is_file() { let project_dir = path.parent().unwrap_or(Path::new("")).to_string_lossy().to_string(); - if let Some(project) = Self::create(project_detector.clone(env).unwrap(), env, project_dir) { + if let Some(project) = Self::create(Rc::clone(&cloned_project_detector), project_dir) { projects.push(project); } } @@ -71,7 +79,7 @@ impl Project { #[napi] #[cfg(not(test))] - pub fn create(project_detector: Reference, env: Env, project_uri: String) -> Option { + pub fn create(project_detector: Rc>, project_uri: String) -> Option { let uri = Uri::file(project_uri); if !fs::metadata(uri.fs_path()).map(|m| m.is_dir()).unwrap_or(false) { return None; @@ -87,7 +95,7 @@ impl Project { .is_some_and(|app| app.is_object() && parsed_build_profile.get("modules").and_then(|modules| modules.as_array()).is_some()) { Some(Project { - project_detector: project_detector.clone(env).unwrap(), + project_detector: Rc::clone(&project_detector), uri, parsed_build_profile, build_profile_uri, diff --git a/src/rust/rawfile_directory.rs b/src/rust/rawfile_directory.rs index 062b1f3..f5b2c3a 100644 --- a/src/rust/rawfile_directory.rs +++ b/src/rust/rawfile_directory.rs @@ -29,7 +29,10 @@ impl RawfileDirectory { #[napi] pub fn get_resource(&self, env: Env) -> Reference { - self.resource.clone(env).unwrap() + match self.resource.clone(env) { + Ok(cloned_resource) => cloned_resource, + Err(_) => panic!("Failed to get cloned resource, please check your resource is valid in RawfileDirectory.getResource()."), + } } #[napi] diff --git a/src/rust/references/element_json_file_reference.rs b/src/rust/references/element_json_file_reference.rs index 317f72f..b74f5f6 100644 --- a/src/rust/references/element_json_file_reference.rs +++ b/src/rust/references/element_json_file_reference.rs @@ -68,7 +68,13 @@ impl ElementJsonFileReference { let mut reference = Vec::new(); let parser = element_json_file.get_parser(); let source_code = element_json_file.get_content(); - let tree = parser.lock().unwrap().parse(&source_code, None).unwrap(); + let tree = match parser.lock() { + Ok(mut parser) => match parser.parse(&source_code, None) { + Some(tree) => tree, + None => panic!("Failed to parse source code, please check your source code is valid in ElementJsonFileReference.findAll()."), + }, + Err(_) => panic!("Failed to lock parser, please check your parser is valid in ElementJsonFileReference.findAll()."), + }; for child in tree.root_node().children(&mut tree.root_node().walk()) { if child.kind() != "object" { @@ -170,7 +176,13 @@ impl ElementJsonFileReference { let mut reference = Vec::new(); let parser = element_json_file.get_parser(); let source_code = element_json_file.get_content(); - let tree = parser.lock().unwrap().parse(&source_code, None).unwrap(); + let tree = match parser.lock() { + Ok(mut parser) => match parser.parse(&source_code, None) { + Some(tree) => tree, + None => panic!("Failed to parse source code, please check your source code is valid in ElementJsonFileReference.findAll()."), + }, + Err(_) => panic!("Failed to lock parser, please check your parser is valid in ElementJsonFileReference.findAll()."), + }; for child in tree.root_node().children(&mut tree.root_node().walk()) { if child.kind() != "object" { diff --git a/src/rust/resource.rs b/src/rust/resource.rs index bd57e38..35d66e8 100644 --- a/src/rust/resource.rs +++ b/src/rust/resource.rs @@ -1,20 +1,32 @@ -use std::{fs, path::Path}; - -use crate::product::Product; use crate::utils::uri::Uri; +use crate::{app_scope::AppScope, product::Product}; use napi::{bindgen_prelude::Reference, Env}; use napi_derive::napi; +use std::{fs, path::Path, rc::Rc}; + +#[napi] +#[derive(Clone)] +pub enum ResourceType { + Product, + AppScope, +} #[napi] pub struct Resource { - product: Reference, + product: Option>>, + app_scope: Option>, uri: Uri, + resource_type: ResourceType, } #[napi] impl Resource { #[napi] pub fn find_all(product: Reference, env: Env) -> Vec { + let cloned_product = Rc::new(match product.clone(env) { + Ok(cloned_product) => cloned_product, + Err(_) => panic!("Failed to get cloned product, please check your product is valid in Resource.findAll()."), + }); let mut resources = Vec::new(); let current_target_config = product.get_current_target_config(); let name = product.get_name(); @@ -35,7 +47,7 @@ impl Resource { if !resource_roots.is_empty() { for resource_root in resource_roots { let resource_root_path = path_clean::clean(Path::new(&module_uri.fs_path()).join(resource_root.as_str().unwrap_or_default())); - if let Some(resource) = Self::create(product.clone(env).unwrap(), resource_root_path.to_string_lossy().to_string(), env) { + if let Some(resource) = Self::create(Rc::clone(&cloned_product), resource_root_path.to_string_lossy().to_string()) { resources.push(resource); } } @@ -44,33 +56,82 @@ impl Resource { } resources.push(Resource { - product: product.clone(env).unwrap(), + product: Some(cloned_product), + app_scope: None, uri: Uri::file(default_resource_root.to_string_lossy().to_string()), + resource_type: ResourceType::Product, }); resources } #[napi] - pub fn create(product: Reference, resource_uri: String, env: Env) -> Option { + pub fn from_app_scope(app_scope: Reference) -> Option { + let app_scope_config = app_scope.get_uri(); + let app_scope_resource_uri = Path::new(&app_scope_config.fs_path()).join("resources").to_string_lossy().to_string(); + + Some(Resource { + product: None, + app_scope: Some(app_scope), + uri: Uri::file(app_scope_resource_uri), + resource_type: ResourceType::AppScope, + }) + } + + #[napi] + pub fn create(product: Rc>, resource_uri: String) -> Option { let uri = Uri::file(resource_uri); if fs::metadata(uri.fs_path()).map(|metadata| metadata.is_dir()).unwrap_or(false) { Some(Resource { - product: product.clone(env).unwrap(), + product: Some(Rc::clone(&product)), + app_scope: None, uri, + resource_type: ResourceType::Product, }) } else { None } } + /// If the resource created by {@linkcode Product}, this method will return the product. + /// + /// @throw If the resource is not created by {@linkcode Product}, this method will throw an error. #[napi] pub fn get_product(&self, env: Env) -> Reference { - self.product.clone(env).unwrap() + match &self.product { + Some(product) => match product.as_ref().clone(env) { + Ok(cloned_product) => cloned_product, + Err(_) => panic!("Failed to get cloned product, please check your product is valid in Resource.getProduct()."), + }, + None => panic!("Resource is not associated with a product, please check your resource is valid in Resource.getProduct()."), + } + } + + /// If the resource created by {@linkcode AppScope}, this method will return the app scope. + /// + /// @throw If the resource is not created by {@linkcode AppScope}, this method will throw an error. + #[napi] + pub fn get_app_scope(&self, env: Env) -> Reference { + match &self.app_scope { + Some(app_scope) => match app_scope.clone(env) { + Ok(cloned_app_scope) => cloned_app_scope, + Err(_) => panic!("Failed to get cloned app scope, please check your app scope is valid in Resource.getAppScope()."), + }, + None => panic!("Resource is not associated with an app scope, please check your resource is valid in Resource.getAppScope()."), + } } #[napi] pub fn get_uri(&self) -> Uri { self.uri.clone() } + + /// Get the type of current resource. + /// + /// - If created by {@link Product}, return {@link ResourceType.Product}. + /// - If created by {@link AppScope}, return {@link ResourceType.AppScope}. + #[napi(getter)] + pub fn resource_type(&self) -> &ResourceType { + &self.resource_type + } } diff --git a/src/rust/resource_directory.rs b/src/rust/resource_directory.rs index da9550f..b0ce4b9 100644 --- a/src/rust/resource_directory.rs +++ b/src/rust/resource_directory.rs @@ -8,17 +8,22 @@ use napi_derive::napi; use serde_json::Value; use std::fs; use std::path::Path; +use std::rc::Rc; #[napi] pub struct ResourceDirectory { uri: Uri, - resource: Reference, + resource: Rc>, } #[napi] impl ResourceDirectory { #[napi] pub fn find_all(resource: Reference, env: Env) -> Vec { + let cloned_resource = Rc::new(match resource.clone(env) { + Ok(cloned_resource) => cloned_resource, + Err(_) => panic!("Failed to get cloned resource, please check your resource is valid in ResourceDirectory.findAll()."), + }); let mut resource_directories = Vec::new(); let resource_directory = resource.get_uri(); @@ -35,7 +40,7 @@ impl ResourceDirectory { } resource_directories.push(ResourceDirectory { uri: Uri::file(dir.path().to_string_lossy().to_string()), - resource: resource.clone(env).unwrap(), + resource: Rc::clone(&cloned_resource), }) } } @@ -51,7 +56,7 @@ impl ResourceDirectory { if dir_name != "base" && dir_name != "rawfile" && dir_name != "resfile" && QualifierUtils::analyze_qualifier(dir_name).is_empty() { return None; } - Some(ResourceDirectory { uri, resource }) + Some(ResourceDirectory { uri, resource: Rc::new(resource) }) } else { None } @@ -64,7 +69,10 @@ impl ResourceDirectory { #[napi] pub fn get_resource(&self, env: Env) -> Reference { - self.resource.clone(env).unwrap() + match self.resource.as_ref().clone(env) { + Ok(cloned_resource) => cloned_resource, + Err(_) => panic!("Failed to get cloned resource, please check your resource is valid in ResourceDirectory.getResource()."), + } } #[napi(ts_return_type = "Array | 'base' | 'rawfile' | 'resfile'")] diff --git a/src/rust/utils/qualifier/utils_impl.rs b/src/rust/utils/qualifier/utils_impl.rs index e3a3927..6928515 100755 --- a/src/rust/utils/qualifier/utils_impl.rs +++ b/src/rust/utils/qualifier/utils_impl.rs @@ -58,94 +58,86 @@ impl QualifierUtils { MCC::is(mcc) } - /** - * Check if the mcc is a valid MCC code with string `mcc`. - * For example: "mcc310" => true, "mcc3100" => false - */ - #[napi] + /// Check if the mcc is a valid MCC code with string `mcc`. + /// + /// For example: "mcc310" => true, "mcc3100" => false + #[napi(ts_args_type = "mcc: `mcc${number}` | (string & {})")] pub fn is_mcc_code(mcc: String) -> bool { MCC::is_code(mcc) } - /** - * Check if the language code is a valid language code. - * For example: "en" => true, "en-US" => false - */ + /// Check if the language code is a valid language code. + /// + /// For example: "en" => true, "en-US" => false #[napi] pub fn is_language_code(language_code: String) -> bool { LanguageCode::is(language_code) } - /** - * Check if the device type is a valid device type. - * - phone - * - tablet - * - tv - * - car - * - wearable - * - 2in1 - */ - #[napi] + /// Check if the device type is a valid device type. + /// + /// - `phone` + /// - `tablet` + /// - `tv` + /// - `car` + /// - `wearable` + /// - `2in1` + #[napi(ts_args_type = "deviceType: 'phone' | 'tablet' | 'tv' | 'car' | 'wearable' | '2in1' | (string & {})")] pub fn is_device_type(device_type: String) -> bool { DeviceType::is(device_type) } - /** - * Check if the color mode is a valid color mode. - * - dark - * - light - */ - #[napi] + /// Check if the color mode is a valid color mode. + /// + /// - `dark` + /// - `light` + #[napi(ts_args_type = "colorMode: 'dark' | 'light' | (string & {})")] pub fn is_color_mode(color_mode: String) -> bool { ColorMode::is(color_mode) } - /** - * Check if the mnc is a valid MNC code with value. - */ + /// Check if the mnc is a valid MNC code with value. + /// + /// For example: 00 => true, 000 => false #[napi] pub fn is_mnc(mnc: u32, mcc: u32) -> bool { MNC::is(mcc, mnc) } - /** - * Check if the mnc is a valid MNC code with string `mnc` and `mcc`. - * For example: "mnc00" => true, "mnc000" => false - */ + /// Check if the mnc is a valid MNC code with string `mnc` and `mcc`. + /// + /// For example: "mnc00" => true, "mnc000" => false #[napi] pub fn is_mnc_code(mnc: String, mcc: u32) -> bool { MNC::is_code(mnc, mcc) } - /** - * Check if the region code is a valid region code. - * For example: "CN" => true, "US" => true, "AAA" => false - */ + /// Check if the region code is a valid region code. + /// + /// For example: "CN" => true, "US" => true, "AAA" => false #[napi] pub fn is_region_code(region_code: String) -> bool { RegionCode::is(region_code) } - /** - * Check if the orientation is a valid orientation. - * - vertical - * - horizontal - */ - #[napi] + /// Check if the orientation is a valid orientation. + /// + /// - `vertical` + /// - `horizontal` + #[napi(ts_args_type = "orientation: 'vertical' | 'horizontal' | (string & {})")] pub fn is_orientation(orientation: String) -> bool { Orientation::is(orientation) } - /** - * Check if the screen density is a valid screen density. - * - sdpi - * - mdpi - * - ldpi - * - xldpi - * - xxldpi - * - xxxldpi - */ - #[napi] + /// Check if the screen density is a valid screen density. + /// + /// - `sdpi` + /// - `mdpi` + /// - `ldpi` + /// - `xldpi` + /// - `xxldpi` + /// - `xxxldpi` + #[napi(ts_args_type = "screenDensity: 'sdpi' | 'mdpi' | 'ldpi' | 'xldpi' | 'xxldpi' | 'xxxldpi' | (string & {})")] pub fn is_screen_density(screen_density: String) -> bool { ScreenDensity::is(screen_density) } diff --git a/test/node.spec.ts b/test/node.spec.ts index 2715f5c..9d55d7c 100644 --- a/test/node.spec.ts +++ b/test/node.spec.ts @@ -3,7 +3,7 @@ import fs from 'node:fs' import path from 'node:path' import { afterAll, describe, expect } from 'vitest' import { Uri } from '../index' -import { ElementDirectory, ElementJsonFile, ElementJsonFileReference, Module, Product, Project, ProjectDetector, Resource, ResourceDirectory, Watcher } from '../src/node' +import { AppScope, ElementDirectory, ElementJsonFile, ElementJsonFileReference, Module, Product, Project, ProjectDetector, Resource, ResourceDirectory, Watcher } from '../src/node' describe.sequential('projectDetector', (it) => { const mockPath = path.resolve(__dirname, '..', 'mock') @@ -45,6 +45,24 @@ describe.sequential('projectDetector', (it) => { expect(projects().length).toBe(2) }) + it.sequential('AppScope.from', async () => { + const appScope = AppScope.from(harmonyProject1) + expect(appScope()).toBeDefined() + + // test the file event listener: + // when the app.json5 file is deleted, the app scope should be removed + const appScopePath = appScope()!.getUri().fsPath + const appJson5Path = appScope()!.getConfigUri().fsPath + fs.renameSync(appJson5Path, path.resolve(appScopePath, 'app.json5.bak')) + await new Promise(resolve => setTimeout(resolve, 2000)) + expect(appScope()).toBeNull() + + // when the app.json5 file is created, the app scope should be added + fs.renameSync(path.resolve(appScopePath, 'app.json5.bak'), appJson5Path) + await new Promise(resolve => setTimeout(resolve, 2000)) + expect(appScope()).toBeDefined() + }) + let harmonyProject1Module: Module it.sequential('module.findAll', async () => {