mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-16 11:42:04 +08:00

Exposes a module registration API, that webpack users can use to bundle "dynamic" modules that are required through some variable. Falls back to the require API if no module is registered. Reworked dynamic `require` code to use module-loader: - navigation: ui/frame. - XML UI build ui/builder/builder and ui/builder/component-builder. - module on-demand loads in global functions: globals.
547 lines
20 KiB
TypeScript
547 lines
20 KiB
TypeScript
import * as trace from "trace";
|
|
import {debug, ScopeError, SourceError, Source} from "utils/debug";
|
|
import * as xml from "xml";
|
|
import {View, Template} from "ui/core/view";
|
|
import {File, path, knownFolders} from "file-system";
|
|
import {isString, isFunction, isDefined} from "utils/types";
|
|
import {ComponentModule, setPropertyValue, getComponentModule} from "ui/builder/component-builder";
|
|
import {platformNames, device} from "platform";
|
|
import {LoadOptions} from "ui/builder";
|
|
import {Page} from "ui/page";
|
|
import {resolveFileName} from "file-system/file-name-resolver";
|
|
|
|
export function parse(value: string | Template, context: any): View {
|
|
if (isString(value)) {
|
|
var viewToReturn: View;
|
|
|
|
if (context instanceof View) {
|
|
context = getExports(context);
|
|
}
|
|
|
|
var componentModule = parseInternal(<string>value, context);
|
|
|
|
if (componentModule) {
|
|
viewToReturn = componentModule.component;
|
|
}
|
|
|
|
return viewToReturn;
|
|
} else if (isFunction(value)) {
|
|
return (<Template>value)();
|
|
}
|
|
}
|
|
|
|
function parseInternal(value: string, context: any, uri?: string): ComponentModule {
|
|
var start: xml2ui.XmlStringParser;
|
|
var ui: xml2ui.ComponentParser;
|
|
|
|
var errorFormat = (debug && uri) ? xml2ui.SourceErrorFormat(uri) : xml2ui.PositionErrorFormat;
|
|
|
|
(start = new xml2ui.XmlStringParser(errorFormat))
|
|
.pipe(new xml2ui.PlatformFilter())
|
|
.pipe(new xml2ui.XmlStateParser(ui = new xml2ui.ComponentParser(context, errorFormat)));
|
|
|
|
start.parse(value);
|
|
|
|
return ui.rootComponentModule;
|
|
}
|
|
|
|
function loadCustomComponent(componentPath: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: Page): ComponentModule {
|
|
var result: ComponentModule;
|
|
componentPath = componentPath.replace("~/", "");
|
|
|
|
var fullComponentPathFilePathWithoutExt = componentPath;
|
|
|
|
if (!File.exists(componentPath) || componentPath === "." || componentPath === "./") {
|
|
fullComponentPathFilePathWithoutExt = path.join(knownFolders.currentApp().path, componentPath, componentName);
|
|
}
|
|
|
|
var xmlFilePath = resolveFileName(fullComponentPathFilePathWithoutExt, "xml");
|
|
|
|
if (xmlFilePath) {
|
|
// Custom components with XML
|
|
var jsFilePath = resolveFileName(fullComponentPathFilePathWithoutExt, "js");
|
|
|
|
var subExports = context;
|
|
if (jsFilePath) {
|
|
// Custom components with XML and code
|
|
subExports = global.loadModule(jsFilePath)
|
|
}
|
|
|
|
result = loadInternal(xmlFilePath, subExports);
|
|
|
|
// Attributes will be transfered to the custom component
|
|
if (isDefined(result) && isDefined(result.component) && isDefined(attributes)) {
|
|
var attr: string;
|
|
for (attr in attributes) {
|
|
setPropertyValue(result.component, subExports, context, attr, attributes[attr]);
|
|
}
|
|
}
|
|
} else {
|
|
// Custom components without XML
|
|
result = getComponentModule(componentName, componentPath, attributes, context);
|
|
}
|
|
|
|
// Add component CSS file if exists.
|
|
var cssFilePath = resolveFileName(fullComponentPathFilePathWithoutExt, "css");
|
|
if (cssFilePath) {
|
|
if (parentPage) {
|
|
parentPage.addCssFile(cssFilePath);
|
|
} else {
|
|
trace.write("CSS file found but no page specified. Please specify page in the options!", trace.categories.Error, trace.messageType.error);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function load(pathOrOptions: string | LoadOptions, context?: any): View {
|
|
var viewToReturn: View;
|
|
var componentModule: ComponentModule;
|
|
|
|
if (!context) {
|
|
if (!isString(pathOrOptions)) {
|
|
let options = <LoadOptions>pathOrOptions;
|
|
componentModule = loadCustomComponent(options.path, options.name, undefined, options.exports, options.page);
|
|
} else {
|
|
let path = <string>pathOrOptions;
|
|
componentModule = loadInternal(path);
|
|
}
|
|
} else {
|
|
let path = <string>pathOrOptions;
|
|
componentModule = loadInternal(path, context);
|
|
}
|
|
|
|
if (componentModule) {
|
|
viewToReturn = componentModule.component;
|
|
}
|
|
|
|
return viewToReturn;
|
|
}
|
|
|
|
function loadInternal(fileName: string, context?: any): ComponentModule {
|
|
var componentModule: ComponentModule;
|
|
|
|
// Check if the XML file exists.
|
|
if (File.exists(fileName)) {
|
|
var file = File.fromPath(fileName);
|
|
var onError = function (error) {
|
|
throw new Error("Error loading file " + fileName + " :" + error.message);
|
|
}
|
|
var text = file.readTextSync(onError);
|
|
componentModule = parseInternal(text, context, fileName);
|
|
}
|
|
|
|
if (componentModule && componentModule.component) {
|
|
// Save exports to root component (will be used for templates).
|
|
(<any>componentModule.component).exports = context;
|
|
}
|
|
|
|
return componentModule;
|
|
}
|
|
|
|
function getExports(instance: View): any {
|
|
var parent = instance.parent;
|
|
|
|
while (parent && (<any>parent).exports === undefined) {
|
|
parent = parent.parent;
|
|
}
|
|
|
|
return parent ? (<any>parent).exports : undefined;
|
|
}
|
|
|
|
namespace xml2ui {
|
|
/**
|
|
* Pipes and filters:
|
|
* https://en.wikipedia.org/wiki/Pipeline_(software)
|
|
*/
|
|
interface XmlProducer {
|
|
pipe<Next extends XmlConsumer>(next: Next): Next;
|
|
}
|
|
|
|
interface XmlConsumer {
|
|
parse(args: xml.ParserEvent);
|
|
}
|
|
|
|
export class XmlProducerBase implements XmlProducer {
|
|
private _next: XmlConsumer;
|
|
public pipe<Next extends XmlConsumer>(next: Next) {
|
|
this._next = next;
|
|
return next;
|
|
}
|
|
protected next(args: xml.ParserEvent) {
|
|
this._next.parse(args);
|
|
}
|
|
}
|
|
|
|
export class XmlStringParser extends XmlProducerBase implements XmlProducer {
|
|
private error: ErrorFormatter;
|
|
|
|
constructor(error?: ErrorFormatter) {
|
|
super();
|
|
this.error = error || PositionErrorFormat;
|
|
}
|
|
|
|
public parse(value: string) {
|
|
var xmlParser = new xml.XmlParser((args: xml.ParserEvent) => {
|
|
try {
|
|
this.next(args);
|
|
} catch(e) {
|
|
throw this.error(e, args.position);
|
|
}
|
|
}, (e, p) => {
|
|
throw this.error(e, p);
|
|
}, true);
|
|
|
|
if (isString(value)) {
|
|
value = value.replace(/xmlns=("|')http:\/\/((www)|(schemas))\.nativescript\.org\/tns\.xsd\1/, "");
|
|
xmlParser.parse(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
interface ErrorFormatter {
|
|
(e: Error, p: xml.Position): Error;
|
|
}
|
|
|
|
export function PositionErrorFormat(e: Error, p: xml.Position): Error {
|
|
return new ScopeError(e, "Parsing XML at " + p.line + ":" + p.column);
|
|
}
|
|
|
|
export function SourceErrorFormat(uri): ErrorFormatter {
|
|
return (e: Error, p: xml.Position) => {
|
|
var source = new Source(uri, p.line, p.column);
|
|
e = new SourceError(e, source, "Building UI from XML.");
|
|
return e;
|
|
}
|
|
}
|
|
|
|
export class PlatformFilter extends XmlProducerBase implements XmlProducer, XmlConsumer {
|
|
private currentPlatformContext: string;
|
|
|
|
public parse(args: xml.ParserEvent) {
|
|
if (args.eventType === xml.ParserEventType.StartElement) {
|
|
if (PlatformFilter.isPlatform(args.elementName)) {
|
|
|
|
if (this.currentPlatformContext) {
|
|
throw new Error("Already in '" + this.currentPlatformContext + "' platform context and cannot switch to '" + args.elementName + "' platform! Platform tags cannot be nested.");
|
|
}
|
|
|
|
this.currentPlatformContext = args.elementName;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (args.eventType === xml.ParserEventType.EndElement) {
|
|
if (PlatformFilter.isPlatform(args.elementName)) {
|
|
this.currentPlatformContext = undefined;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.currentPlatformContext && !PlatformFilter.isCurentPlatform(this.currentPlatformContext)) {
|
|
return;
|
|
}
|
|
|
|
this.next(args);
|
|
}
|
|
|
|
private static isPlatform(value: string): boolean {
|
|
return value && (value.toLowerCase() === platformNames.android.toLowerCase()
|
|
|| value.toLowerCase() === platformNames.ios.toLowerCase());
|
|
}
|
|
|
|
private static isCurentPlatform(value: string): boolean {
|
|
return value && value.toLowerCase() === device.os.toLowerCase();
|
|
}
|
|
}
|
|
|
|
export class XmlArgsReplay extends XmlProducerBase implements XmlProducer {
|
|
private error: ErrorFormatter;
|
|
private args: xml.ParserEvent[];
|
|
|
|
constructor(args: xml.ParserEvent[], errorFormat: ErrorFormatter) {
|
|
super();
|
|
this.args = args;
|
|
this.error = errorFormat;
|
|
}
|
|
|
|
public replay() {
|
|
this.args.forEach((args: xml.ParserEvent) => {
|
|
try {
|
|
this.next(args);
|
|
} catch(e) {
|
|
throw this.error(e, args.position);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
interface TemplateProperty {
|
|
context?: any;
|
|
parent: ComponentModule;
|
|
name: string;
|
|
elementName: string;
|
|
templateItems: Array<string>;
|
|
errorFormat: ErrorFormatter;
|
|
}
|
|
|
|
/**
|
|
* It is a state pattern
|
|
* https://en.wikipedia.org/wiki/State_pattern
|
|
*/
|
|
export class XmlStateParser implements XmlConsumer {
|
|
private state: XmlStateConsumer;
|
|
|
|
constructor(state: XmlStateConsumer) {
|
|
this.state = state;
|
|
}
|
|
|
|
parse(args: xml.ParserEvent) {
|
|
this.state = this.state.parse(args);
|
|
}
|
|
}
|
|
|
|
interface XmlStateConsumer extends XmlConsumer {
|
|
parse(args: xml.ParserEvent): XmlStateConsumer;
|
|
}
|
|
|
|
export class TemplateParser implements XmlStateConsumer {
|
|
|
|
private _context: any;
|
|
private _recordedXmlStream: Array<xml.ParserEvent>;
|
|
private _templateProperty: TemplateProperty;
|
|
private _nestingLevel: number;
|
|
private _state: TemplateParser.State;
|
|
|
|
private parent: XmlStateConsumer;
|
|
|
|
constructor(parent: XmlStateConsumer, templateProperty: TemplateProperty) {
|
|
this.parent = parent;
|
|
|
|
this._context = templateProperty.context;
|
|
this._recordedXmlStream = new Array<xml.ParserEvent>();
|
|
this._templateProperty = templateProperty;
|
|
this._nestingLevel = 0;
|
|
this._state = TemplateParser.State.EXPECTING_START;
|
|
}
|
|
|
|
public parse(args: xml.ParserEvent): XmlStateConsumer {
|
|
if (args.eventType === xml.ParserEventType.StartElement) {
|
|
this.parseStartElement(args.prefix, args.namespace, args.elementName, args.attributes);
|
|
} else if (args.eventType === xml.ParserEventType.EndElement) {
|
|
this.parseEndElement(args.prefix, args.elementName);
|
|
}
|
|
|
|
this._recordedXmlStream.push(args);
|
|
|
|
return this._state === TemplateParser.State.FINISHED ? this.parent : this;
|
|
}
|
|
|
|
public get elementName(): string {
|
|
return this._templateProperty.elementName;
|
|
}
|
|
|
|
private parseStartElement(prefix: string, namespace: string, elementName: string, attributes: Object) {
|
|
if (this._state === TemplateParser.State.EXPECTING_START) {
|
|
this._state = TemplateParser.State.PARSING;
|
|
} else if (this._state === TemplateParser.State.FINISHED) {
|
|
throw new Error("Template must have exactly one root element but multiple elements were found.");
|
|
}
|
|
|
|
this._nestingLevel++;
|
|
}
|
|
|
|
private parseEndElement(prefix: string, elementName: string) {
|
|
if (this._state === TemplateParser.State.EXPECTING_START) {
|
|
throw new Error("Template must have exactly one root element but none was found.");
|
|
} else if (this._state === TemplateParser.State.FINISHED) {
|
|
throw new Error("No more closing elements expected for this template.");
|
|
}
|
|
|
|
this._nestingLevel--;
|
|
|
|
if (this._nestingLevel === 0) {
|
|
this._state = TemplateParser.State.FINISHED;
|
|
this.build();
|
|
}
|
|
}
|
|
|
|
private build() {
|
|
if (this._templateProperty.name in this._templateProperty.parent.component) {
|
|
var context = this._context;
|
|
var errorFormat = this._templateProperty.errorFormat;
|
|
var template: Template = () => {
|
|
var start: xml2ui.XmlArgsReplay;
|
|
var ui: xml2ui.ComponentParser;
|
|
|
|
(start = new xml2ui.XmlArgsReplay(this._recordedXmlStream, errorFormat))
|
|
// No platform filter, it has been filtered allready
|
|
.pipe(new XmlStateParser(ui = new ComponentParser(context, errorFormat)));
|
|
|
|
start.replay();
|
|
|
|
return ui.rootComponentModule.component;
|
|
}
|
|
this._templateProperty.parent.component[this._templateProperty.name] = template;
|
|
}
|
|
}
|
|
}
|
|
|
|
export namespace TemplateParser {
|
|
export const enum State {
|
|
EXPECTING_START,
|
|
PARSING,
|
|
FINISHED
|
|
}
|
|
}
|
|
|
|
export class ComponentParser implements XmlStateConsumer {
|
|
|
|
private static KNOWNCOLLECTIONS = "knownCollections";
|
|
private static KNOWNTEMPLATES = "knownTemplates";
|
|
|
|
public rootComponentModule: ComponentModule;
|
|
|
|
private context: any;
|
|
|
|
private currentPage: Page;
|
|
private parents = new Array<ComponentModule>();
|
|
private complexProperties = new Array<ComponentParser.ComplexProperty>();
|
|
|
|
private error;
|
|
|
|
constructor(context: any, errorFormat: ErrorFormatter) {
|
|
this.context = context;
|
|
this.error = errorFormat;
|
|
}
|
|
|
|
public parse(args: xml.ParserEvent): XmlStateConsumer {
|
|
|
|
// Get the current parent.
|
|
var parent = this.parents[this.parents.length - 1];
|
|
var complexProperty = this.complexProperties[this.complexProperties.length - 1];
|
|
|
|
// Create component instance from every element declaration.
|
|
if (args.eventType === xml.ParserEventType.StartElement) {
|
|
if (ComponentParser.isComplexProperty(args.elementName)) {
|
|
|
|
var name = ComponentParser.getComplexPropertyName(args.elementName);
|
|
|
|
this.complexProperties.push({
|
|
parent: parent,
|
|
name: name,
|
|
items: [],
|
|
});
|
|
|
|
if (ComponentParser.isKnownTemplate(name, parent.exports)) {
|
|
return new TemplateParser(this, {
|
|
context: (parent ? getExports(parent.component) : null) || this.context, // Passing 'context' won't work if you set "codeFile" on the page
|
|
parent: parent,
|
|
name: name,
|
|
elementName: args.elementName,
|
|
templateItems: [],
|
|
errorFormat: this.error
|
|
});
|
|
}
|
|
|
|
} else {
|
|
|
|
var componentModule: ComponentModule;
|
|
|
|
if (args.prefix && args.namespace) {
|
|
// Custom components
|
|
componentModule = loadCustomComponent(args.namespace, args.elementName, args.attributes, this.context, this.currentPage);
|
|
} else {
|
|
// Default components
|
|
componentModule = getComponentModule(args.elementName, args.namespace, args.attributes, this.context);
|
|
}
|
|
|
|
if (componentModule) {
|
|
if (parent) {
|
|
if (complexProperty) {
|
|
// Add component to complex property of parent component.
|
|
ComponentParser.addToComplexProperty(parent, complexProperty, componentModule);
|
|
} else if ((<any>parent.component)._addChildFromBuilder) {
|
|
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
|
|
}
|
|
} else if (this.parents.length === 0) {
|
|
// Set root component.
|
|
this.rootComponentModule = componentModule;
|
|
|
|
if (this.rootComponentModule && this.rootComponentModule.component instanceof Page) {
|
|
this.currentPage = <Page>this.rootComponentModule.component;
|
|
}
|
|
}
|
|
|
|
// Add the component instance to the parents scope collection.
|
|
this.parents.push(componentModule);
|
|
}
|
|
}
|
|
|
|
} else if (args.eventType === xml.ParserEventType.EndElement) {
|
|
if (ComponentParser.isComplexProperty(args.elementName)) {
|
|
if (complexProperty) {
|
|
if (parent && (<any>parent.component)._addArrayFromBuilder) {
|
|
// If parent is AddArrayFromBuilder call the interface method to populate the array property.
|
|
(<any>parent.component)._addArrayFromBuilder(complexProperty.name, complexProperty.items);
|
|
complexProperty.items = [];
|
|
}
|
|
}
|
|
// Remove the last complexProperty from the complexProperties collection (move to the previous complexProperty scope).
|
|
this.complexProperties.pop();
|
|
|
|
} else {
|
|
// Remove the last parent from the parents collection (move to the previous parent scope).
|
|
this.parents.pop();
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
private static isComplexProperty(name: string): boolean {
|
|
return isString(name) && name.indexOf(".") !== -1;
|
|
}
|
|
|
|
private static getComplexPropertyName(fullName: string): string {
|
|
var name: string;
|
|
|
|
if (isString(fullName)) {
|
|
var names = fullName.split(".");
|
|
name = names[names.length - 1];
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
private static isKnownTemplate(name: string, exports: any): boolean {
|
|
return ComponentParser.KNOWNTEMPLATES in exports && exports[ComponentParser.KNOWNTEMPLATES] && name in exports[ComponentParser.KNOWNTEMPLATES];
|
|
}
|
|
|
|
private static addToComplexProperty(parent: ComponentModule, complexProperty: ComponentParser.ComplexProperty, elementModule: ComponentModule) {
|
|
// If property name is known collection we populate array with elements.
|
|
var parentComponent = <any>parent.component;
|
|
if (ComponentParser.isKnownCollection(complexProperty.name, parent.exports)) {
|
|
complexProperty.items.push(elementModule.component);
|
|
} else if (parentComponent._addChildFromBuilder) {
|
|
parentComponent._addChildFromBuilder(complexProperty.name, elementModule.component);
|
|
} else {
|
|
// Or simply assign the value;
|
|
parentComponent[complexProperty.name] = elementModule.component;
|
|
}
|
|
}
|
|
|
|
private static isKnownCollection(name: string, context: any): boolean {
|
|
return ComponentParser.KNOWNCOLLECTIONS in context && context[ComponentParser.KNOWNCOLLECTIONS] && name in context[ComponentParser.KNOWNCOLLECTIONS];
|
|
}
|
|
}
|
|
|
|
export namespace ComponentParser {
|
|
export interface ComplexProperty {
|
|
parent: ComponentModule;
|
|
name: string;
|
|
items?: Array<any>;
|
|
}
|
|
}
|
|
}
|