chore: working with builder xml require vs dynamic import wip

This commit is contained in:
Nathan Walker
2025-07-22 14:37:11 -07:00
parent b4579d1d2f
commit 0ba0ab0d59
23 changed files with 141 additions and 121 deletions

View File

@@ -23,6 +23,7 @@ function runTests() {
export function onNavigatedTo(args) {
args.object.off(Page.loadedEvent, onNavigatedTo);
console.log('onNavigatedTo called', executeTests);
if (executeTests) {
executeTests = false;
runTests();

View File

@@ -455,6 +455,7 @@ function getAllProperties(obj: any) {
let testsSelector: string;
export function runAll(testSelector?: string) {
testsSelector = testSelector;
console.log('runAll called with:', testSelector);
if (running) {
// TODO: We may schedule pending run requests
return;

View File

@@ -359,6 +359,7 @@ export function waitUntilReady(isReady: () => boolean, timeoutSec: number = 5, s
const currentRunLoop = NSRunLoop.currentRunLoop;
currentRunLoop.limitDateForMode(currentRunLoop.currentMode);
if (isReady()) {
console.log('waitUntilReady: isReady() returned true');
break;
}

View File

@@ -203,11 +203,13 @@ export function waitUntilNavigatedFrom(action: Function, topFrame?: Frame) {
const currentPage = topFrame ? topFrame.currentPage : Frame.topmost().currentPage;
let completed = false;
function navigatedFrom(args) {
console.log('navigatedFrom called');
args.object.page.off('navigatedFrom', navigatedFrom);
completed = true;
}
currentPage.on('navigatedFrom', navigatedFrom);
console.log('calling action!');
action();
TKUnit.waitUntilReady(() => completed);
}

View File

@@ -13,7 +13,7 @@
},
"devDependencies": {
"@nativescript/android": "~8.9.0",
"@nativescript/ios": "esm",
"@nativescript/ios": "~8.9.0",
"@nativescript/visionos": "~8.9.0",
"@nativescript/webpack": "file:../../dist/packages/webpack5",
"typescript": "~5.8.0"

View File

@@ -30,7 +30,12 @@
"forDevice": false,
"prepare": false
},
"configurations": {},
"defaultConfiguration": "main",
"configurations": {
"main": {
"flags": "--env.env=main --env.target=main --no-hmr --env.commonjs"
}
},
"dependsOn": ["^build"]
},
"prepare": {

View File

@@ -2,6 +2,7 @@ import { EventData, Page, Utils } from '@nativescript/core';
import { HelloWorldModel } from './main-view-model';
export function navigatingTo(args: EventData) {
console.log('toolbox navigatingTo called');
const page = <Page>args.object;
page.bindingContext = new HelloWorldModel();

View File

@@ -2,6 +2,7 @@ import { Observable, Frame, StackLayout } from '@nativescript/core';
export class HelloWorldModel extends Observable {
viewDemo(args) {
console.log('Navigating to view demo:', args.object.text);
Frame.topmost().navigate({
moduleName: `pages/${args.object.text}`,
});

View File

@@ -410,6 +410,7 @@ export class Observable {
this._globalNotify(eventClass, 'First', dataWithObject);
const observers = this._observers[data.eventName];
console.log('notify called for event:', data.eventName, 'with observers:', observers);
if (observers) {
Observable._handleEvent(observers, dataWithObject);
}

View File

@@ -92,8 +92,34 @@ const defaultExtensionMap: ExtensionMap = {
'.xml': '.xml',
};
// ESM-compatible module resolver
function esmModuleResolver(name: string): any {
// First try to resolve from registered modules
if (modules.has(name)) {
return modules.get(name).loader(name);
}
// For ESM builds, we can't use require() synchronously for dynamic imports
// Instead, we need to rely on pre-registered modules or throw an error
// indicating the module needs to be pre-registered
if (!__COMMONJS__) {
console.warn(`Module '${name}' not found in registered modules. In ESM builds, modules must be pre-registered as follows:`);
console.warn(`import * as myModule from '${name}';`);
console.warn(`global.registerModule('${name}', () => myModule);`);
return null;
}
// Fallback to require for CommonJS builds
try {
return global.require ? global.require(name) : null;
} catch (e) {
console.warn(`Failed to require module '${name}':`, e);
return null;
}
}
// Cast to <any> because moduleResolvers is read-only in definitions
global.moduleResolvers = [global.require];
global.moduleResolvers = [esmModuleResolver];
global.registerModule = function (name: string, loader: ModuleLoader): void {
modules.set(name, { loader, moduleId: name });
@@ -195,7 +221,16 @@ global.System = {
import(path) {
return new Promise((resolve, reject) => {
try {
resolve(global.require(path));
if (__COMMONJS__ && global.require) {
resolve(global.require(path));
} else {
// Use dynamic import for ESM
import(path)
.then((module) => {
resolve(module.default || module);
})
.catch(reject);
}
} catch (e) {
reject(e);
}
@@ -252,8 +287,8 @@ global.loadModule = function loadModule(name: string): any {
const moduleInfo = modules.get(name);
if (moduleInfo) {
const result = moduleInfo.loader(name);
if (result.enableAutoAccept) {
console.log(`@@@ loadModule resolved to`, result);
if (result?.enableAutoAccept) {
result.enableAutoAccept();
}
@@ -263,11 +298,16 @@ global.loadModule = function loadModule(name: string): any {
for (const resolver of global.moduleResolvers) {
const result = resolver(name);
if (result) {
console.log(`@@@ loadModule SET SET resolver: ${name} resolved to`, result);
modules.set(name, { moduleId: name, loader: () => result });
return result;
}
}
// If no resolver found the module, return null
console.warn(`Module '${name}' could not be loaded by any resolver.`);
return null;
};
function registerOnGlobalContext(moduleName: string, exportName: string): void {
Object.defineProperty(global, exportName, {
@@ -301,6 +341,7 @@ export function installPolyfills(moduleName: string, exportNames: string[]) {
if (!global.NativeScriptHasPolyfilled) {
global.NativeScriptHasPolyfilled = true;
console.log('Installing polyfills...');
// DOM api polyfills
if (__COMMONJS__) {
global.registerModule('timer', () => timer);

View File

@@ -1,6 +1,6 @@
{
"name": "@nativescript/core",
"version": "9.0.0-alpha.6",
"version": "9.0.0-alpha.5",
"description": "A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.",
"type": "module",
"main": "index",

View File

@@ -1,16 +0,0 @@
//@private
export module bindingConstants {
export const sourceProperty: string;
export const targetProperty: string;
export const expression: string;
export const twoWay: string;
export const source: string;
export const bindingValueKey: string;
export const parentValueKey: string;
export const parentsValueKey: string;
export const newPropertyValueKey: string;
}
export function getBindingOptions(name: string, value: string): any;
export const parentsRegex: RegExp;

View File

@@ -72,7 +72,7 @@ const createComponentInstance = profile('createComponentInstance', (elementName:
// console.log('CUSTOM namespace:', namespace)
resolvedModuleName = resolveModuleName(namespace, '');
}
instanceModule = global.loadModule(resolvedModuleName, true);
instanceModule = global.loadModule(resolvedModuleName);
} else {
// load module from @nativescript/core/ui or mapped paths
// resolvedModuleName =
@@ -84,7 +84,7 @@ const createComponentInstance = profile('createComponentInstance', (elementName:
// .join('-')
// .toLowerCase();
instanceModule = global.loadModule(CORE_UI_BARREL, false);
instanceModule = global.loadModule(CORE_UI_BARREL);
// don't register core modules for HMR self-accept
// instanceModule = global.loadModule(resolvedModuleName, false);
}
@@ -107,7 +107,7 @@ const getComponentModuleExports = profile('getComponentModuleExports', (instance
if (codeFileAttribute) {
const resolvedCodeFileModule = resolveModuleName(sanitizeModuleName(codeFileAttribute), '');
if (resolvedCodeFileModule) {
moduleExports = global.loadModule(resolvedCodeFileModule, true);
moduleExports = global.loadModule(resolvedCodeFileModule);
(<any>instance).exports = moduleExports;
} else {
throw new Error(`Code file with path "${codeFileAttribute}" cannot be found! Looking for webpack module with name "${resolvedCodeFileModule}"`);
@@ -155,6 +155,10 @@ const applyComponentAttributes = profile('applyComponentAttributes', (instance:
}
}
console.log('applyComponentAttributes called for attr:', attr, 'with value:', attrValue);
if (attr === 'navigatingTo') {
console.log('@@@ navigatingTo moduleExports:', moduleExports);
}
if (attr.indexOf('.') !== -1) {
let subObj = instance;
const properties = attr.split('.');
@@ -185,6 +189,9 @@ export function getComponentModule(elementName: string, namespace: string, attri
const { instance, instanceModule } = createComponentInstance(elementName, namespace, null);
moduleExports = getComponentModuleExports(instance, <any>moduleExports, attributes);
console.log('getComponentModule called for element:', elementName, 'with namespace:', namespace, 'and attributes:', attributes);
console.log('moduleExports ---');
console.log(moduleExports);
if (isRootComponent) {
applyComponentCss(instance, moduleNamePath, attributes);
}
@@ -202,6 +209,7 @@ export function getComponentModule(elementName: string, namespace: string, attri
export function setPropertyValue(instance: View, instanceModule: Object, exports: Object, propertyName: string, propertyValue: any) {
// Note: instanceModule can be null if we are loading custom component with no code-behind.
if (isBinding(propertyValue) && instance.bind) {
console.log('1 setPropertyValue, Binding detected for property:', propertyName, 'with value:', propertyValue);
const bindOptions = getBindingOptions(propertyName, getBindingExpressionFromAttribute(propertyValue));
instance.bind(
{
@@ -213,6 +221,8 @@ export function setPropertyValue(instance: View, instanceModule: Object, exports
bindOptions[bindingConstants.source],
);
} else if (isEventOrGesture(propertyName, instance)) {
console.log('2 setPropertyValue, Binding detected for property:', propertyName, 'with value:', propertyValue);
console.log('setPropertyValue, exports:', exports);
// Get the event handler from page module exports.
const handler = exports && exports[propertyValue];
@@ -221,8 +231,10 @@ export function setPropertyValue(instance: View, instanceModule: Object, exports
instance.on(propertyName, handler);
}
} else if (isKnownFunction(propertyName, instance) && exports && typeof exports[propertyValue] === 'function') {
console.log('3 setPropertyValue, Binding detected for property:', propertyName, 'with value:', propertyValue);
instance[propertyName] = exports[propertyValue];
} else {
console.log('4 setPropertyValue, Binding detected for property:', propertyName, 'with value:', propertyValue);
instance[propertyName] = propertyValue;
}
}
@@ -233,11 +245,11 @@ function getBindingExpressionFromAttribute(value: string): string {
function isBinding(value: any): boolean {
let isBinding;
if (typeof value === 'string') {
const str = value.trim();
isBinding = str.indexOf('{{') === 0 && str.lastIndexOf('}}') === str.length - 2;
}
console.log('isBinding called with value:', value, ' isBinding:', isBinding);
return isBinding;
}

View File

@@ -1,73 +0,0 @@
import { View, Template, KeyedTemplate } from '../core/view';
import { Page } from '../page';
import { NavigationEntry } from '../frame';
export interface LoadOptions {
path: string;
name: string;
attributes?: any;
exports?: any;
page?: Page;
}
/**
* @deprecated Use Builder.createViewFromEntry() instead.
*/
export function createViewFromEntry(entry: NavigationEntry): View;
/**
* @deprecated Use Builder.parse() instead.
*/
export function parse(value: string | Template, exports?: any): View;
/**
* @deprecated Use Builder.parseMultipleTemplates() instead.
*/
export function parseMultipleTemplates(value: string, exports?: any): Array<KeyedTemplate>;
/**
* @deprecated Use Builder.load() instead.
*/
export function load(fileName: string, exports?: any): View;
/**
* @deprecated Use Builder.load() instead.
*/
export function load(options: LoadOptions): View;
export class Builder {
/**
* UI plugin developers can add to these to define their own custom types if needed
*/
static knownTemplates: Set<string>;
static knownMultiTemplates: Set<string>;
static knownCollections: Set<string>;
/**
* Creates view from navigation entry
* @param entry NavigationEntry
*/
static createViewFromEntry(entry: NavigationEntry): View;
static parse(value: string | Template, exports?: any): View;
/**
* Creates an array of KeyedTemplates from string
* @param value The xml of the template to be parsed
* @param exports Current context of the template
*/
static parseMultipleTemplates(value: string, exports?: any): Array<KeyedTemplate>;
/**
* Loads component from module with context
* @param moduleName the module name
* @param exports the context of the component to be loaded
*/
static load(moduleName: string, exports?: any): View;
/**
* Loads component from options
* @param options Load options
*/
static load(options: LoadOptions): View;
}

View File

@@ -52,11 +52,17 @@ export interface LoadOptions {
}
export class Builder {
// ui plugin developers can add to these to define their own custom types if needed
/**
* 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']);
/**
* Creates view from navigation entry
* @param entry NavigationEntry
*/
static createViewFromEntry(entry: ViewEntry): View {
if (entry.create) {
const view = entry.create();
@@ -68,8 +74,8 @@ export class Builder {
} else if (entry.moduleName) {
const moduleName = sanitizeModuleName(entry.moduleName);
const resolvedCodeModuleName = resolveModuleName(moduleName, ''); //`${moduleName}.xml`;
const moduleExports = resolvedCodeModuleName ? global.loadModule(resolvedCodeModuleName, true) : null;
const moduleExports = resolvedCodeModuleName ? global.loadModule(resolvedCodeModuleName) : null;
console.log('Resolved code module name:', resolvedCodeModuleName, ' exports:', moduleExports);
if (moduleExports && moduleExports.createPage) {
// Exports has a createPage() method
const view = moduleExports.createPage();
@@ -86,7 +92,7 @@ export class Builder {
componentView = componentModule && componentModule.component;
} else {
const resolvedXmlModuleName = resolveModuleName(moduleName, 'xml');
const componentModule = resolvedXmlModuleName ? global.loadModule(resolvedXmlModuleName, true) : null;
const componentModule = resolvedXmlModuleName ? global.loadModule(resolvedXmlModuleName) : null;
if (componentModule?.default) {
componentView = new componentModule.default();
} else {
@@ -111,9 +117,13 @@ export class Builder {
}
}
/**
* Loads component from module with context
* @param moduleName the module name
* @param exports the context of the component to be loaded
*/
static load(pathOrOptions: string | LoadOptions, context?: any): View {
let componentModule: ComponentModule;
if (typeof pathOrOptions === 'string') {
const moduleName = sanitizeModuleName(pathOrOptions);
componentModule = loadInternal(moduleName, context);
@@ -124,6 +134,11 @@ export class Builder {
return componentModule && componentModule.component;
}
/**
* Creates an array of KeyedTemplates from string
* @param value The xml of the template to be parsed
* @param exports Current context of the template
*/
static parseMultipleTemplates(value: string, context: any): Array<KeyedTemplate> {
const dummyComponent = `<ListView><ListView.itemTemplates>${value}</ListView.itemTemplates></ListView>`;
@@ -131,24 +146,36 @@ export class Builder {
}
}
/**
* @deprecated Use Builder.parse() instead.
*/
export function parse(value: string | Template, context?: any): View {
console.log('parse() is deprecated. Use Builder.parse() instead.');
return Builder.parse(value, context);
}
/**
* @deprecated Use Builder.parseMultipleTemplates() instead.
*/
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 {
/**
* @deprecated Use Builder.load() instead.
*/
export async function load(pathOrOptions: string | LoadOptions, context?: any): Promise<View> {
console.log('load() is deprecated. Use Builder.load() instead.');
return Builder.load(pathOrOptions, context);
return await Builder.load(pathOrOptions, context);
}
/**
* @deprecated Use Builder.createViewFromEntry() instead.
*/
export function createViewFromEntry(entry: ViewEntry): View {
console.log('createViewFromEntry() is deprecated. Use Builder.createViewFromEntry() instead.');
@@ -157,11 +184,11 @@ export function createViewFromEntry(entry: ViewEntry): View {
function loadInternal(moduleName: string, moduleExports: any): ComponentModule {
let componentModule: ComponentModule;
console.log('loadInternal called for moduleName:', moduleName, 'with moduleExports:', moduleExports);
const resolvedXmlModule = resolveModuleName(moduleName, 'xml');
if (resolvedXmlModule) {
const text = global.loadModule(resolvedXmlModule, true);
const text = global.loadModule(resolvedXmlModule);
componentModule = parseInternal(text, moduleExports, resolvedXmlModule, moduleName);
}
@@ -202,7 +229,7 @@ export function loadCustomComponent(componentNamespace: string, componentName?:
let subExports = context;
if (resolvedCodeModuleName) {
// Component has registered code module.
subExports = global.loadModule(resolvedCodeModuleName, true);
subExports = global.loadModule(resolvedCodeModuleName);
}
// Pass the parent page down the chain in case of custom components nested on many levels. Use the context for piggybacking.
@@ -258,6 +285,7 @@ export function getExports(instance: ViewBase): any {
}
function parseInternal(value: string, context: any, xmlModule?: string, moduleName?: string): ComponentModule {
console.log('parseInternal called with value:', value, 'context:', context, 'xmlModule:', xmlModule, 'moduleName:', moduleName);
if (__UI_USE_XML_PARSER__) {
let start: xml2ui.XmlStringParser;
let ui: xml2ui.ComponentParser;
@@ -483,7 +511,6 @@ export namespace xml2ui {
constructor(parent: XmlStateConsumer, templateProperty: TemplateProperty, setTemplateProperty = true) {
this.parent = parent;
this._context = templateProperty.context;
this._recordedXmlStream = new Array<xml.ParserEvent>();
this._templateProperty = templateProperty;
@@ -641,6 +668,7 @@ export namespace xml2ui {
@profile
private buildComponent(args: xml.ParserEvent): ComponentModule {
console.log('ComponentParser.buildComponent called for element:', args.elementName, 'with namespace:', args.namespace, 'and attributes:', args.attributes, 'context:', this.context);
if (args.prefix && args.namespace) {
// Custom components
return loadCustomComponent(args.namespace, args.elementName, args.attributes, this.context, this.currentRootView, !this.currentRootView, this.moduleName);

View File

@@ -226,6 +226,7 @@ export class FrameBase extends CustomLayoutView {
}
public navigate(param: any) {
console.log('framebase navigate() called with:', param);
if (Trace.isEnabled()) {
Trace.write(`NAVIGATE`, Trace.categories.Navigation);
}

View File

@@ -23,11 +23,18 @@ export { unsetValue } from './core/properties/property-shared';
export { addWeakEventListener, removeWeakEventListener } from './core/weak-event-listener';
export { DatePicker } from './date-picker';
import { installPolyfills } from '../globals';
// import { installPolyfills } from '../globals';
// No need to export dialogs, they are already export exported globally
import * as uiDialogs from '../ui/dialogs';
global.registerModule('ui-dialogs', () => uiDialogs);
installPolyfills('ui-dialogs', ['alert', 'confirm', 'prompt', 'login', 'action']);
global.alert = uiDialogs.alert;
// @ts-ignore
global.confirm = uiDialogs.confirm;
// @ts-ignore
global.prompt = uiDialogs.prompt;
global.login = uiDialogs.login;
global.action = uiDialogs.action;
// global.registerModule('ui-dialogs', () => uiDialogs);
// installPolyfills('ui-dialogs', ['alert', 'confirm', 'prompt', 'login', 'action']);
export { DialogStrings, action, alert, confirm, login, prompt, getCurrentPage, Dialogs, inputType, capitalizationType } from './dialogs';
export type { DialogOptions, CancelableOptions, AlertOptions, PromptResult, PromptOptions, ActionOptions, ConfirmOptions, LoginResult, LoginOptions } from './dialogs';

View File

@@ -135,6 +135,7 @@ export class PageBase extends ContentView {
@profile
public onNavigatedTo(isBackNavigation: boolean): void {
console.log('onNavigatedTo called with isBackNavigation:', isBackNavigation);
this.notify(this.createNavigatedData(PageBase.navigatedToEvent, isBackNavigation));
if (this.accessibilityAnnouncePageEnabled) {

View File

@@ -13,6 +13,7 @@ global.__ANDROID__ = false;
global.__IOS__ = true;
global.__VISIONOS__ = false;
global.__APPLE__ = true;
global.__COMMONJS__ = false;
global.WeakRef.prototype.get = global.WeakRef.prototype.deref;
global.NativeClass = function () {};
global.NSTimer = class NSTimer {};
@@ -98,6 +99,8 @@ global.UIColor = {
},
clearColor: cgColors,
};
global.UITextField = function () {};
global.UITextFieldDelegate = function () {};
global.NSSearchPathDirectory = {
LibraryDirectory: '',
DeveloperDirectory: '',

View File

@@ -1,6 +1,6 @@
{
"name": "@nativescript/webpack",
"version": "5.1.0-alpha.6",
"version": "5.1.0-esm.2",
"private": false,
"main": "dist/index.js",
"files": [

View File

@@ -526,6 +526,7 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
__UI_USE_XML_PARSER__: true,
__UI_USE_EXTERNAL_RENDERER__: false,
__COMMONJS__: !!env.commonjs,
__ANDROID__: platform === 'android',
__IOS__: platform === 'ios',
__VISIONOS__: platform === 'visionos',

View File

@@ -50,6 +50,9 @@ export interface IWebpackEnv {
// print webpack stats (default: true)
stats?: boolean;
// enable commonjs modules (default: ES modules, esm)
commonjs?: boolean;
// misc
replace?: string[] | string;
watchNodeModules?: boolean;
@@ -158,10 +161,9 @@ export function chainWebpack(
* @param mergeFn An object or a function that optionally returns an object (can mutate the object directly and return nothing)
*/
export function mergeWebpack(
mergeFn: ((
config: Partial<webpack.Configuration>,
env: IWebpackEnv,
) => any) | Partial<webpack.Configuration>,
mergeFn:
| ((config: Partial<webpack.Configuration>, env: IWebpackEnv) => any)
| Partial<webpack.Configuration>,
) {
webpackMerges.push(mergeFn);
}

View File

@@ -26,13 +26,13 @@ export default function loader(content: string, map: any) {
const relativePath = relative(
opts.appPath ?? this.rootContext,
this.resourcePath
this.resourcePath,
).replace(/\\/g, '/');
const hmrCode = this.hot
? dedent`
/* NATIVESCRIPT-HOT-LOADER */
if(module.hot && global._isModuleLoadedForUI && global._isModuleLoadedForUI("./${relativePath}")) {
if(module.hot) {
module.hot.accept()
}
`