mirror of
				https://github.com/NativeScript/NativeScript.git
				synced 2025-11-04 12:58:38 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			259 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
// Definitions.
 | 
						|
import { View, Template, KeyedTemplate } from '../core/view';
 | 
						|
import { ViewBase } from '../core/view-base';
 | 
						|
import { ViewEntry } from '../frame';
 | 
						|
import { Page } from '../page';
 | 
						|
 | 
						|
// Types.
 | 
						|
import { debug } from '../../utils/debug';
 | 
						|
import { isDefined } from '../../utils/types';
 | 
						|
import { sanitizeModuleName } from '../../utils/common';
 | 
						|
import { setPropertyValue, getComponentModule } from './component-builder';
 | 
						|
import type { ComponentModule } from './component-builder';
 | 
						|
import { platformNames } from '../../platform';
 | 
						|
import { resolveModuleName } from '../../module-name-resolver';
 | 
						|
import { xml2ui } from './xml2ui';
 | 
						|
 | 
						|
export const ios = platformNames.ios.toLowerCase();
 | 
						|
export const android = platformNames.android.toLowerCase();
 | 
						|
export const visionos = platformNames.visionos.toLowerCase();
 | 
						|
export const apple = platformNames.apple.toLowerCase();
 | 
						|
export const defaultNameSpaceMatcher = /tns\.xsd$/i;
 | 
						|
 | 
						|
export interface LoadOptions {
 | 
						|
	path: string;
 | 
						|
	name: string;
 | 
						|
	attributes?: any;
 | 
						|
	exports?: any;
 | 
						|
	page?: Page;
 | 
						|
}
 | 
						|
 | 
						|
export class Builder {
 | 
						|
	// ui plugin developers can add to these to define their own custom types if needed
 | 
						|
	static knownTemplates: Set<string> = new Set(['itemTemplate']);
 | 
						|
	static knownMultiTemplates: Set<string> = new Set(['itemTemplates']);
 | 
						|
	static knownCollections: Set<string> = new Set(['items', 'spans', 'actionItems']);
 | 
						|
 | 
						|
	static createViewFromEntry(entry: ViewEntry): View {
 | 
						|
		if (entry.create) {
 | 
						|
			const view = entry.create();
 | 
						|
			if (!view) {
 | 
						|
				throw new Error('Failed to create View with entry.create() function.');
 | 
						|
			}
 | 
						|
 | 
						|
			return view;
 | 
						|
		} else if (entry.moduleName) {
 | 
						|
			const moduleName = sanitizeModuleName(entry.moduleName);
 | 
						|
			const resolvedCodeModuleName = resolveModuleName(moduleName, ''); //`${moduleName}.xml`;
 | 
						|
			const moduleExports = resolvedCodeModuleName ? global.loadModule(resolvedCodeModuleName, true) : null;
 | 
						|
 | 
						|
			if (moduleExports && moduleExports.createPage) {
 | 
						|
				// Exports has a createPage() method
 | 
						|
				const view = moduleExports.createPage();
 | 
						|
				const resolvedCssModuleName = resolveModuleName(moduleName, 'css'); //entry.moduleName + ".css";
 | 
						|
				if (resolvedCssModuleName) {
 | 
						|
					view.addCssFile(resolvedCssModuleName);
 | 
						|
				}
 | 
						|
 | 
						|
				return view;
 | 
						|
			} else {
 | 
						|
				let componentView;
 | 
						|
				if (__UI_USE_XML_PARSER__) {
 | 
						|
					const componentModule = loadInternal(moduleName, moduleExports);
 | 
						|
					componentView = componentModule && componentModule.component;
 | 
						|
				} else {
 | 
						|
					const resolvedXmlModuleName = resolveModuleName(moduleName, 'xml');
 | 
						|
					const componentModule = resolvedXmlModuleName ? global.loadModule(resolvedXmlModuleName, true) : null;
 | 
						|
					if (componentModule?.default) {
 | 
						|
						componentView = new componentModule.default();
 | 
						|
					} else {
 | 
						|
						throw new Error('Failed to load component from module: ' + moduleName);
 | 
						|
					}
 | 
						|
				}
 | 
						|
				return componentView;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		throw new Error('Failed to load page XML file for module: ' + entry.moduleName);
 | 
						|
	}
 | 
						|
 | 
						|
	static parse(value: string | Template, context?: any): View {
 | 
						|
		if (typeof value === 'function') {
 | 
						|
			return (<Template>value)();
 | 
						|
		} else {
 | 
						|
			const exports = context ? getExports(context) : undefined;
 | 
						|
			const componentModule = parseInternal(value, exports);
 | 
						|
 | 
						|
			return componentModule && componentModule.component;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	static load(pathOrOptions: string | LoadOptions, context?: any): View {
 | 
						|
		let componentModule: ComponentModule;
 | 
						|
 | 
						|
		if (typeof pathOrOptions === 'string') {
 | 
						|
			const moduleName = sanitizeModuleName(pathOrOptions);
 | 
						|
			componentModule = loadInternal(moduleName, context);
 | 
						|
		} else {
 | 
						|
			componentModule = loadCustomComponent(pathOrOptions.path, pathOrOptions.name, pathOrOptions.attributes, pathOrOptions.exports, pathOrOptions.page, true);
 | 
						|
		}
 | 
						|
 | 
						|
		return componentModule && componentModule.component;
 | 
						|
	}
 | 
						|
 | 
						|
	static parseMultipleTemplates(value: string, context: any): Array<KeyedTemplate> {
 | 
						|
		const dummyComponent = `<ListView><ListView.itemTemplates>${value}</ListView.itemTemplates></ListView>`;
 | 
						|
 | 
						|
		return parseInternal(dummyComponent, context).component['itemTemplates'];
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export function parse(value: string | Template, context?: any): View {
 | 
						|
	console.log('parse() is deprecated. Use Builder.parse() instead.');
 | 
						|
 | 
						|
	return Builder.parse(value, context);
 | 
						|
}
 | 
						|
 | 
						|
export function parseMultipleTemplates(value: string, context: any): Array<KeyedTemplate> {
 | 
						|
	console.log('parseMultipleTemplates() is deprecated. Use Builder.parseMultipleTemplates() instead.');
 | 
						|
 | 
						|
	return Builder.parseMultipleTemplates(value, context);
 | 
						|
}
 | 
						|
 | 
						|
export function load(pathOrOptions: string | LoadOptions, context?: any): View {
 | 
						|
	console.log('load() is deprecated. Use Builder.load() instead.');
 | 
						|
 | 
						|
	return Builder.load(pathOrOptions, context);
 | 
						|
}
 | 
						|
 | 
						|
export function createViewFromEntry(entry: ViewEntry): View {
 | 
						|
	console.log('createViewFromEntry() is deprecated. Use Builder.createViewFromEntry() instead.');
 | 
						|
 | 
						|
	return Builder.createViewFromEntry(entry);
 | 
						|
}
 | 
						|
 | 
						|
function loadInternal(moduleName: string, moduleExports: any): ComponentModule {
 | 
						|
	let componentModule: ComponentModule;
 | 
						|
 | 
						|
	const resolvedXmlModule = resolveModuleName(moduleName, 'xml');
 | 
						|
 | 
						|
	if (resolvedXmlModule) {
 | 
						|
		const text = global.loadModule(resolvedXmlModule, true);
 | 
						|
		componentModule = parseInternal(text, moduleExports, resolvedXmlModule, moduleName);
 | 
						|
	}
 | 
						|
 | 
						|
	const componentView = componentModule && componentModule.component;
 | 
						|
	if (componentView) {
 | 
						|
		// Save exports to root component (will be used for templates).
 | 
						|
		(<any>componentView).exports = moduleExports;
 | 
						|
 | 
						|
		// Save _moduleName - used for livesync
 | 
						|
		componentView._moduleName = moduleName;
 | 
						|
	}
 | 
						|
 | 
						|
	if (!componentModule) {
 | 
						|
		throw new Error('Failed to load component from module: ' + moduleName);
 | 
						|
	}
 | 
						|
 | 
						|
	return componentModule;
 | 
						|
}
 | 
						|
 | 
						|
export function loadCustomComponent(componentNamespace: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: View, isRootComponent = true, moduleNamePath?: string): ComponentModule {
 | 
						|
	if (!parentPage && context) {
 | 
						|
		// Read the parent page that was passed down below
 | 
						|
		// https://github.com/NativeScript/NativeScript/issues/1639
 | 
						|
		parentPage = context['_parentPage'];
 | 
						|
		delete context['_parentPage'];
 | 
						|
	}
 | 
						|
 | 
						|
	let result: ComponentModule;
 | 
						|
 | 
						|
	componentNamespace = sanitizeModuleName(componentNamespace);
 | 
						|
	const moduleName = `${componentNamespace}/${componentName}`;
 | 
						|
	const resolvedCodeModuleName = resolveModuleName(moduleName, '');
 | 
						|
	const resolvedXmlModuleName = resolveModuleName(moduleName, 'xml');
 | 
						|
	let resolvedCssModuleName = resolveModuleName(moduleName, 'css');
 | 
						|
 | 
						|
	if (resolvedXmlModuleName) {
 | 
						|
		// Custom components with XML
 | 
						|
		let subExports = context;
 | 
						|
		if (resolvedCodeModuleName) {
 | 
						|
			// Component has registered code module.
 | 
						|
			subExports = global.loadModule(resolvedCodeModuleName, true);
 | 
						|
		}
 | 
						|
 | 
						|
		// Pass the parent page down the chain in case of custom components nested on many levels. Use the context for piggybacking.
 | 
						|
		// https://github.com/NativeScript/NativeScript/issues/1639
 | 
						|
		if (!subExports) {
 | 
						|
			subExports = {};
 | 
						|
		}
 | 
						|
 | 
						|
		subExports['_parentPage'] = parentPage;
 | 
						|
 | 
						|
		result = loadInternal(moduleName, subExports);
 | 
						|
 | 
						|
		// Attributes will be transferred to the custom component
 | 
						|
		if (isDefined(result) && isDefined(result.component) && isDefined(attributes)) {
 | 
						|
			for (const attr in attributes) {
 | 
						|
				setPropertyValue(result.component, subExports, context, attr, attributes[attr]);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		// Custom components without XML
 | 
						|
		result = getComponentModule(componentName, componentNamespace, attributes, context, moduleNamePath, isRootComponent);
 | 
						|
 | 
						|
		// The namespace is the JS module and the (componentName is the name of the class in the module)
 | 
						|
		// So if there is no componentNamespace/componentName.{qualifiers}.css we should also look for
 | 
						|
		// componentNamespace.{qualifiers}.css
 | 
						|
		if (!resolvedCssModuleName) {
 | 
						|
			resolvedCssModuleName = resolveModuleName(componentNamespace, 'css');
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Add CSS from webpack module if exists.
 | 
						|
	if (parentPage && resolvedCssModuleName) {
 | 
						|
		parentPage.addCssFile(resolvedCssModuleName);
 | 
						|
	}
 | 
						|
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
export function getExports(instance: ViewBase): any {
 | 
						|
	const isView = !!instance._domId;
 | 
						|
	if (!isView) {
 | 
						|
		return (<any>instance).exports || instance;
 | 
						|
	}
 | 
						|
 | 
						|
	let exportObject = (<any>instance).exports;
 | 
						|
	let parent = instance.parent;
 | 
						|
	while (exportObject === undefined && parent) {
 | 
						|
		exportObject = (<any>parent).exports;
 | 
						|
		parent = parent.parent;
 | 
						|
	}
 | 
						|
 | 
						|
	return exportObject;
 | 
						|
}
 | 
						|
 | 
						|
function parseInternal(value: string, context: any, xmlModule?: string, moduleName?: string): ComponentModule {
 | 
						|
	if (__UI_USE_XML_PARSER__) {
 | 
						|
		let start: xml2ui.XmlStringParser;
 | 
						|
		let ui: xml2ui.ComponentParser;
 | 
						|
 | 
						|
		const errorFormat = debug && xmlModule ? xml2ui.SourceErrorFormat(xmlModule) : xml2ui.PositionErrorFormat;
 | 
						|
		const componentSourceTracker =
 | 
						|
			debug && xmlModule
 | 
						|
				? xml2ui.ComponentSourceTracker(xmlModule)
 | 
						|
				: () => {
 | 
						|
						// no-op
 | 
						|
					};
 | 
						|
 | 
						|
		(start = new xml2ui.XmlStringParser(errorFormat)).pipe(new xml2ui.PlatformFilter()).pipe(new xml2ui.XmlStateParser((ui = new xml2ui.ComponentParser(context, errorFormat, componentSourceTracker, moduleName))));
 | 
						|
 | 
						|
		start.parse(value);
 | 
						|
 | 
						|
		return ui.rootComponentModule;
 | 
						|
	} else {
 | 
						|
		return null;
 | 
						|
	}
 | 
						|
}
 |