From b6f70dd489afd8b8e26d3af2b6b684bec2cdc9f2 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Tue, 6 Jan 2026 18:57:33 -0800 Subject: [PATCH 1/2] fix(vite): dynamic and static import mixed usage --- packages/vite/helpers/main-entry.ts | 63 +++++++++++++++++------------ 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/packages/vite/helpers/main-entry.ts b/packages/vite/helpers/main-entry.ts index 7e5833ffa3..de6f986ea7 100644 --- a/packages/vite/helpers/main-entry.ts +++ b/packages/vite/helpers/main-entry.ts @@ -106,6 +106,8 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' } // ---- Platform-specific always-needed modules ---- + // Track if we need to defer Android activity import (non-HMR only) + let needsAndroidActivityDefer = false; if (opts.platform === 'android') { if (opts.hmrActive) { /** @@ -142,26 +144,10 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' /** * Non-HMR: Defer activity lifecycle wiring until native Application is ready * to avoid "application is null" errors at production boot. + * We set a flag here and emit the actual code after the static Application import + * to avoid mixing dynamic and static imports of @nativescript/core. */ - imports += ` - (function __nsDeferAndroidActivityImport(){ - const load = () => { try { import('@nativescript/core/ui/frame/activity.android.js?ns-keep'); } catch (e) { console.error('[ns-entry] failed to import android activity module', e); } }; - try { - import('@nativescript/core').then(({ Application: __NS_Application }) => { - try { - const hasApp = !!(__NS_Application && __NS_Application.android && __NS_Application.android.nativeApp); - if (hasApp) { - ${opts.verbose ? "console.info('[ns-entry] android activity import: nativeApp present, loading now');" : ''} - load(); - } else { - ${opts.verbose ? "console.info('[ns-entry] android activity import: deferring until launch/nativeApp');" : ''} - try { __NS_Application.on && __NS_Application.on(__NS_Application.launchEvent, load); } catch {} - try { setTimeout(load, 0); } catch {} - } - } catch { try { setTimeout(load, 0); } catch {} } - }).catch(() => { try { setTimeout(load, 0); } catch {} }); - } catch { try { setTimeout(load, 0); } catch {} } - })();\n`; + needsAndroidActivityDefer = true; } } @@ -189,16 +175,43 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' // ---- Global CSS injection (always-needed if file exists) ---- const appCssPath = path.resolve(projectRoot, getProjectAppRelativePath('app.css')); - if (fs.existsSync(appCssPath)) { - imports += `// Import and apply global CSS before app bootstrap\n`; - imports += `import appCssContent from './${appRootDir}/app.css?inline';\n`; + const hasAppCss = fs.existsSync(appCssPath); + + // Import Application statically if needed for CSS or Android activity defer + if (hasAppCss || needsAndroidActivityDefer) { + if (hasAppCss) { + imports += `// Import and apply global CSS before app bootstrap\n`; + imports += `import appCssContent from './${appRootDir}/app.css?inline';\n`; + } imports += `import { Application } from '@nativescript/core';\n`; - imports += `if (appCssContent) { try { Application.addCss(appCssContent); } catch (error) { console.error('Error applying CSS:', error); } }\n`; - if (opts.verbose) { - imports += `console.info('[ns-entry] app.css applied');\n`; + if (hasAppCss) { + imports += `if (appCssContent) { try { Application.addCss(appCssContent); } catch (error) { console.error('Error applying CSS:', error); } }\n`; + if (opts.verbose) { + imports += `console.info('[ns-entry] app.css applied');\n`; + } } } + // ---- Deferred Android activity import (non-HMR only) ---- + // Uses the statically imported Application to avoid mixing dynamic and static imports + if (needsAndroidActivityDefer) { + imports += ` + (function __nsDeferAndroidActivityImport(){ + const load = () => { try { import('@nativescript/core/ui/frame/activity.android.js?ns-keep'); } catch (e) { console.error('[ns-entry] failed to import android activity module', e); } }; + try { + const hasApp = !!(Application && Application.android && Application.android.nativeApp); + if (hasApp) { + ${opts.verbose ? "console.info('[ns-entry] android activity import: nativeApp present, loading now');" : ''} + load(); + } else { + ${opts.verbose ? "console.info('[ns-entry] android activity import: deferring until launch/nativeApp');" : ''} + try { Application.on && Application.on(Application.launchEvent, load); } catch {} + try { setTimeout(load, 0); } catch {} + } + } catch { try { setTimeout(load, 0); } catch {} } + })();\n`; + } + // ---- Application main entry ---- if (opts.hmrActive) { // HTTP-only dev boot: try to import the entire app over HTTP; if not reachable, keep retrying. From eeb91ff025beda22b1e7da83e6b2def9358c702b Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Tue, 6 Jan 2026 23:14:57 -0800 Subject: [PATCH 2/2] feat(vite): support custom android activites and application classes --- packages/vite/configuration/base.ts | 12 +- packages/vite/helpers/app-components.ts | 327 ++++++++++++++++++ packages/vite/helpers/main-entry.ts | 19 + .../vite/helpers/nativeclass-transform.ts | 92 ++++- packages/vite/index.ts | 3 + 5 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 packages/vite/helpers/app-components.ts diff --git a/packages/vite/configuration/base.ts b/packages/vite/configuration/base.ts index 7905256e6d..d96ada0664 100644 --- a/packages/vite/configuration/base.ts +++ b/packages/vite/configuration/base.ts @@ -34,6 +34,7 @@ import { createNativeClassTransformerPlugin } from '../helpers/nativeclass-trans import { getThemeCoreGenericAliases, createEnsureHoistedThemeLinkPlugin, createThemeCoreCssFallbackPlugin } from '../helpers/theme-core-plugins.js'; import { createPostCssConfig } from '../helpers/postcss-platform-config.js'; import { getProjectAppPath, getProjectAppRelativePath } from '../helpers/utils.js'; +import { appComponentsPlugin } from '../helpers/app-components.js'; // Load HMR plugins lazily to avoid compiling dev-only sources during library build // This prevents TypeScript from traversing the heavy HMR implementation graph when not needed // function getHMRPluginsSafe(opts: { @@ -342,6 +343,8 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }): NativeScriptPlugin({ platform }), // Ensure globals and Android activity are included early via virtual entry mainEntryPlugin({ platform, isDevMode, verbose, hmrActive }), + // Handle custom Android Activity/Application components (auto-detected or configured) + appComponentsPlugin({ platform, verbose }), dynamicImportPlugin(), // Transform Vite worker URLs to NativeScript format AFTER bundling workerUrlPlugin(), @@ -439,7 +442,14 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }): // Preserve side effects for NativeScript core so classes/functions // aren't tree-shaken out inadvertently. This does NOT cause cross‑chunk duplication; // it only prevents Rollup from dropping modules it considers side‑effect free. - moduleSideEffects: (id) => /node_modules[\\\/]\@nativescript[\\\/]core[\\\/]/.test(id) || null, + // Also preserve side effects for .android and .ios files which may contain + // other decorated classes that register with the native runtime + moduleSideEffects: (id) => { + if (/node_modules[\\\/]\@nativescript[\\\/]core[\\\/]/.test(id)) return true; + // Activity and Application files have side effects (class registration) + if (/\.(android|ios)\.(ts|js)$/.test(id)) return true; + return null; + }, }, input: 'virtual:entry-with-polyfills', output: { diff --git a/packages/vite/helpers/app-components.ts b/packages/vite/helpers/app-components.ts new file mode 100644 index 0000000000..7f7ccdf5e1 --- /dev/null +++ b/packages/vite/helpers/app-components.ts @@ -0,0 +1,327 @@ +import path from 'path'; +import fs from 'fs'; +import type { Plugin, ResolvedConfig } from 'vite'; +import { getProjectRootPath } from './project.js'; +import { getProjectAppRelativePath } from './utils.js'; + +const projectRoot = getProjectRootPath(); + +export interface AppComponentsOptions { + /** + * List of app component paths (relative to project root). + * These are typically custom Android Activity or Application classes. + * Example: ['./app/custom-activity.android.ts', './app/custom-application.android.ts'] + */ + appComponents?: string[]; + platform: 'android' | 'ios' | 'visionos'; + verbose?: boolean; +} + +/** + * Get app components from environment variable or nativescript.config.ts + * Format: comma-separated paths, e.g., "./app/custom-activity.android,./app/custom-application.android" + */ +function getAppComponentsFromEnv(): string[] { + const envValue = process.env.NS_APP_COMPONENTS; + if (!envValue) return []; + return envValue + .split(',') + .map((p) => p.trim()) + .filter(Boolean); +} + +/** + * Resolve an app component path to an absolute path + */ +function resolveComponentPath(componentPath: string): string | null { + // If already absolute, check if exists + if (path.isAbsolute(componentPath)) { + return fs.existsSync(componentPath) ? componentPath : null; + } + + // Remove leading ./ if present for consistency + const cleanPath = componentPath.replace(/^\.\//, ''); + + // Try with and without extensions + const extensions = ['', '.ts', '.js', '.android.ts', '.android.js']; + + for (const ext of extensions) { + const fullPath = path.resolve(projectRoot, cleanPath + ext); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + // Also try in the app directory + const appDir = getProjectAppRelativePath(''); + for (const ext of extensions) { + const fullPath = path.resolve(projectRoot, appDir, cleanPath + ext); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + return null; +} + +/** + * Extract the output name for an app component + * e.g., "./app/custom-activity.android.ts" -> "custom-activity" + * e.g., "./app/custom-application.android.ts" -> "custom-application" + */ +function getComponentOutputName(componentPath: string): string { + const basename = path.basename(componentPath); + // Remove .android.ts, .android.js, .ts, .js extensions + return basename.replace(/\.(android\.)?(ts|js)$/, ''); +} + +/** + * Plugin to handle NativeScript app components (custom Activity/Application classes) + * + * These components need to be bundled as separate entry points because: + * 1. Custom Android Activity classes are loaded by the Android runtime before the main bundle + * 2. Custom Android Application classes are loaded even earlier in the app lifecycle + * + * Usage in vite.config.ts: + * ```ts + * import { defineConfig } from 'vite'; + * import { typescriptConfig, appComponentsPlugin } from '@nativescript/vite'; + * + * export default defineConfig(({ mode }) => { + * const config = typescriptConfig({ mode }); + * config.plugins.push( + * appComponentsPlugin({ + * appComponents: ['./app/custom-activity.android.ts'], + * platform: 'android' + * }) + * ); + * return config; + * }); + * ``` + * + * Or via environment variable: + * NS_APP_COMPONENTS="./app/custom-activity.android,./app/custom-application.android" ns run android + */ +export function appComponentsPlugin(options: AppComponentsOptions): Plugin { + const { platform, verbose = false } = options; + + // Collect app components from all sources + let appComponents: string[] = [...(options.appComponents || []), ...getAppComponentsFromEnv()]; + + // Remove duplicates + appComponents = [...new Set(appComponents)]; + + // Resolve all component paths + const resolvedComponents: Map = new Map(); + + for (const component of appComponents) { + const absolutePath = resolveComponentPath(component); + if (absolutePath) { + const outputName = getComponentOutputName(absolutePath); + resolvedComponents.set(component, { absolutePath, outputName }); + if (verbose) { + console.log(`[app-components] Found: ${component} -> ${outputName}.mjs`); + } + } else if (verbose) { + console.warn(`[app-components] Could not resolve: ${component}`); + } + } + + // Skip if no components found + if (resolvedComponents.size === 0) { + return { + name: 'nativescript-app-components', + apply: 'build', + }; + } + + // Track component output names for entryFileNames + const componentOutputNames = new Set(); + for (const [, { outputName }] of resolvedComponents) { + componentOutputNames.add(outputName); + } + + // Set environment variable so main-entry.ts can inject imports for these components + // This allows the virtual module to know which app components are configured + const componentPaths = Array.from(resolvedComponents.values()).map((c) => c.absolutePath); + process.env.NS_APP_COMPONENTS = componentPaths.join(','); + + // Create a set of output names for quick lookup in resolveId + const outputMjsFiles = new Set(); + const absoluteMjsPaths = new Set(); + for (const [, { outputName }] of resolvedComponents) { + outputMjsFiles.add(`~/${outputName}.mjs`); + outputMjsFiles.add(`./${outputName}.mjs`); + // Also track absolute paths that Vite might resolve ~/foo.mjs to + const appDir = getProjectAppRelativePath(''); + const absoluteMjsPath = path.resolve(projectRoot, appDir, `${outputName}.mjs`); + absoluteMjsPaths.add(absoluteMjsPath); + } + + let config: ResolvedConfig; + + return { + name: 'nativescript-app-components', + apply: 'build', + + configResolved(resolvedConfig) { + config = resolvedConfig; + }, + + // Mark app component output files as external during build + // These are generated as separate entry points and will exist at runtime + resolveId(id) { + // Handle ~/foo.mjs or ./foo.mjs patterns + if (outputMjsFiles.has(id)) { + // Return the id with external flag - this tells Rollup to keep the import as-is + return { id: `./${id.replace(/^~\//, '')}`, external: true }; + } + // Handle absolute paths that Vite resolves ~/foo.mjs to (e.g., /path/to/app/foo.mjs) + if (absoluteMjsPaths.has(id)) { + const basename = path.basename(id); + return { id: `./${basename}`, external: true }; + } + return null; + }, + + // Modify the Vite config to support multiple entry points + config(userConfig) { + if (resolvedComponents.size === 0) return null; + + // We need to modify the output.entryFileNames to handle multiple entries + return { + build: { + rollupOptions: { + output: { + // Use a function to determine entry file names + entryFileNames: (chunkInfo: { name: string }) => { + // App components should output as .mjs files + // This is required because SBG (Static Binding Generator) only parses + // .mjs files as ES modules. If we output as .js, SBG will try to parse + // it as CommonJS and fail on import statements. + if (componentOutputNames.has(chunkInfo.name)) { + return `${chunkInfo.name}.mjs`; + } + // Default: main bundle + return 'bundle.mjs'; + }, + }, + }, + }, + }; + }, + + // Modify rollup options to add additional entry points + options(inputOptions) { + if (resolvedComponents.size === 0) return null; + + // Get current input + const currentInput = inputOptions.input; + const newInput: Record = {}; + + // Preserve existing inputs + if (typeof currentInput === 'string') { + newInput['bundle'] = currentInput; + } else if (Array.isArray(currentInput)) { + currentInput.forEach((input, i) => { + newInput[`entry${i}`] = input; + }); + } else if (currentInput && typeof currentInput === 'object') { + Object.assign(newInput, currentInput); + } + + // Add app component entries - use the actual file path directly + for (const [, { absolutePath, outputName }] of resolvedComponents) { + newInput[outputName] = absolutePath; + } + + if (verbose) { + console.log('[app-components] Build inputs:', newInput); + } + + return { ...inputOptions, input: newInput }; + }, + + // Adjust output file names for app components (fallback in case entryFileNames doesn't work) + generateBundle(options, bundle) { + for (const [fileName, chunk] of Object.entries(bundle)) { + if (chunk.type !== 'chunk') continue; + + // Check if this is an app component entry + if (componentOutputNames.has(chunk.name)) { + // Rename to .mjs (SBG requires .mjs for ES module parsing) + const newFileName = `${chunk.name}.mjs`; + if (fileName !== newFileName) { + chunk.fileName = newFileName; + delete bundle[fileName]; + bundle[newFileName] = chunk; + } + } + } + }, + + // Post-process app component chunks to fix Rollup's internal variable renaming. + // SBG (Static Binding Generator) needs the __extends and __decorate calls to use + // the same class name as the outer variable assignment. + renderChunk(code, chunk) { + // Only process app component chunks + if (!componentOutputNames.has(chunk.name)) { + return null; + } + + // Look for patterns where Rollup renamed the internal class variable + // Pattern: var ClassName = ... __extends(ClassName2, _super); ... return ClassName2; ... + // We need: var ClassName = ... __extends(ClassName, _super); ... return ClassName; ... + + // Use a simpler regex that matches across the various output formats + // This finds: var SomeName = ... __extends(SomeName2, ...) + const varAssignRegex = /var\s+(\w+)\s*=[\s\S]*?__extends\s*\(\s*(\w+)\s*,/g; + + let match; + let modifiedCode = code; + + while ((match = varAssignRegex.exec(code)) !== null) { + const outerName = match[1]; // e.g., "CustomActivity" + const innerName = match[2]; // e.g., "CustomActivity2" + + if (outerName !== innerName && innerName === outerName + '2') { + // Rollup renamed it - fix by replacing all occurrences of the inner name + // Only within this chunk, replace innerName with outerName + // Be careful to only replace as a complete identifier + const innerNameRegex = new RegExp(`\\b${innerName}\\b`, 'g'); + modifiedCode = modifiedCode.replace(innerNameRegex, outerName); + + if (verbose) { + console.log(`[app-components] Fixed Rollup rename: ${innerName} -> ${outerName} in ${chunk.fileName}`); + } + } + } + + if (modifiedCode !== code) { + return { code: modifiedCode, map: null }; + } + + return null; + }, + }; +} + +/** + * Get resolved app components with their output file names + * Used by main-entry.ts to inject imports for custom activities/applications + */ +export function getResolvedAppComponents(platform: string): Array<{ absolutePath: string; outputName: string }> { + // Get components from environment variable (set by appComponentsPlugin during build) + const components = getAppComponentsFromEnv(); + const resolved: Array<{ absolutePath: string; outputName: string }> = []; + + for (const component of components) { + const absolutePath = resolveComponentPath(component); + if (absolutePath) { + const outputName = getComponentOutputName(absolutePath); + resolved.push({ absolutePath, outputName }); + } + } + + return resolved; +} diff --git a/packages/vite/helpers/main-entry.ts b/packages/vite/helpers/main-entry.ts index de6f986ea7..c7c293176c 100644 --- a/packages/vite/helpers/main-entry.ts +++ b/packages/vite/helpers/main-entry.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import path from 'path'; import { getProjectFlavor } from './flavor.js'; import { getProjectAppPath, getProjectAppRelativePath, getProjectAppVirtualPath } from './utils.js'; +import { getResolvedAppComponents } from './app-components.js'; // Switched to runtime modules to avoid fragile string injection and enable TS checks const projectRoot = getProjectRootPath(); const appRootDir = getProjectAppPath(); @@ -105,6 +106,24 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' imports += "import 'virtual:ns-bundler-context';\n"; } + // ---- Custom App Components (Activity/Application) ---- + // These must be loaded early so the JS class is registered before Android instantiates them + if (opts.platform === 'android') { + try { + const appComponents = getResolvedAppComponents('android'); + for (const component of appComponents) { + // The appComponentsPlugin bundles these as separate .mjs entry points + // We must import the output file, not the source, since it's a separate entry + imports += `import "~/${component.outputName}.mjs";\n`; + if (opts.verbose) { + imports += `console.info('[ns-entry] app component loaded: ${component.outputName}');\n`; + } + } + } catch (err) { + console.error('[main-entry] Error resolving app components:', err); + } + } + // ---- Platform-specific always-needed modules ---- // Track if we need to defer Android activity import (non-HMR only) let needsAndroidActivityDefer = false; diff --git a/packages/vite/helpers/nativeclass-transform.ts b/packages/vite/helpers/nativeclass-transform.ts index 371ff9addd..46212c8475 100644 --- a/packages/vite/helpers/nativeclass-transform.ts +++ b/packages/vite/helpers/nativeclass-transform.ts @@ -78,6 +78,16 @@ export function transformNativeClassSource(code: string, fileName: string) { return `${prefix || '\n'}${stacked || ''}/*__NativeClass__*/`; }); + // Also handle cases where @NativeClass() is on its own line with parentheses + // Pattern: @NativeClass() followed by other decorators or class + if (!working.includes('/*__NativeClass__*/') && code.includes('@NativeClass')) { + // Simpler replacement: just replace @NativeClass() or @NativeClass with marker + working = code.replace(/@NativeClass\s*\(\s*\)\s*\n/g, '/*__NativeClass__*/\n'); + if (!working.includes('/*__NativeClass__*/')) { + working = code.replace(/@NativeClass\s*\n/g, '/*__NativeClass__*/\n'); + } + } + // If neither original nor marker is present, skip transform early. if (!working.includes('@NativeClass') && !working.includes('/*__NativeClass__*/')) return null; try { @@ -92,6 +102,8 @@ export function transformNativeClassSource(code: string, fileName: string) { const original = working.slice(fullStart, node.end); const stripped = original.replace(/\/\*__NativeClass__\*\/\s*/g, '').replace(/^\s*@NativeClass(?:\([\s\S]*?\))?\s*$/gm, ''); const hadExport = /^\s*export\s+class\b/.test(stripped); + const className = (node as ts.ClassDeclaration).name?.text; + const down = ts .transpileModule(stripped, { compilerOptions: { @@ -110,11 +122,76 @@ export function transformNativeClassSource(code: string, fileName: string) { return `Object.defineProperty(${obj}, ${key}, {${body}})`; }); let cleaned = down.replace(/export \{\};?\s*$/m, ''); + + // Debug: log the transpileModule output + const debugNativeClass = process.env.NS_DEBUG_NATIVECLASS; + if (debugNativeClass) { + console.log('[NativeClass] fileName:', fileName); + console.log('[NativeClass] className:', className); + console.log('[NativeClass] stripped input (first 300 chars):', stripped.slice(0, 300)); + console.log('[NativeClass] transpileModule output (first 300 chars):', cleaned.slice(0, 300)); + } + + // SBG (Static Binding Generator) expects the __decorate call to be INSIDE the IIFE. + // TypeScript's transpileModule already puts it inside correctly. + // The pattern should be: + // var ClassName = (function(_super) { + // __extends(ClassName, _super); + // function ClassName() { ... } + // // prototype methods... + // ClassName = __decorate([...], ClassName); <-- INSIDE the IIFE + // return ClassName; + // })(BaseClass); + // We do NOT move it outside - the IIFE pattern with __decorate inside is correct. + + // Fix: If the transpiled output is just an IIFE expression (not assigned), + // we need to assign it to a variable so the class is properly registered. + // This happens when TypeScript transpiles a class declaration without assignment context. + if (className) { + // Check if the output is a bare IIFE without assignment + // The transpiled output can look like: + // (/** @class */ (function(_super){...})(Base)); + // or: (function(_super){...})(Base); + // We need to assign it to a variable: var ClassName = ... + const trimmed = cleaned.trim(); + // Pattern: starts with ( and contains IIFE extending a class + // Look for pattern that indicates an unassigned class expression + const startsWithParen = trimmed.startsWith('('); + const hasExtends = /__extends\s*\(/.test(trimmed) || /function\s*\(\s*_super\s*\)/.test(trimmed); + const notAssigned = !/^\s*var\s+\w+\s*=/.test(trimmed) && !/^\s*let\s+\w+\s*=/.test(trimmed) && !/^\s*const\s+\w+\s*=/.test(trimmed); + + if (startsWithParen && hasExtends && notAssigned) { + // The output is a bare class expression, wrap it in assignment + // Simply prepend `var ClassName = ` and remove the trailing semicolon if present + // to avoid `var X = (...);` becoming `var X = (...);` (keep it clean) + let unwrapped = trimmed; + + // Remove trailing semicolon for cleaner output, we'll add it back + if (unwrapped.endsWith(';')) { + unwrapped = unwrapped.slice(0, -1).trimEnd(); + } + + // If wrapped in outer parens that are just for grouping, we can keep them + // The result will be: var CustomActivity = (/** @class */ (function...)); + cleaned = `var ${className} = ${unwrapped};`; + } + } + if (hadExport) { - const name = (node as ts.ClassDeclaration).name?.text; - if (name && !new RegExp(`export\s*{\s*${name}\s*}`, 'm').test(cleaned)) { - cleaned += `\nexport { ${name} };\n`; + if (className && !new RegExp(`export\\s*{\\s*${className}\\s*}`, 'm').test(cleaned)) { + cleaned += `\nexport { ${className} };\n`; } + } else if (className) { + // For non-exported @NativeClass classes (like custom activities), + // we need to ensure they're not tree-shaken by Rollup. + // Register on global to create an unoptimizable side effect. + // Use a pattern that prevents inlining by Rollup. + cleaned += `\n;(function(c) { global.__nativeClasses = global.__nativeClasses || {}; global.__nativeClasses["${className}"] = c; })(${className});\n`; + } + // Ensure the transpiled output starts with a newline to properly separate + // from any preceding code (like import statements) + if (!cleaned.startsWith('\n')) { + cleaned = '\n' + cleaned; } edits.push({ start: fullStart, end: node.end, text: cleaned }); } @@ -148,6 +225,15 @@ export function transformNativeClassSource(code: string, fileName: string) { return true; }) .join('\n'); + + // Debug: Final output + if (process.env.NS_DEBUG_NATIVECLASS === '1') { + console.log('[NativeClass] ===== FINAL OUTPUT ====='); + console.log('[NativeClass] File:', fileName); + console.log('[NativeClass] Output:\n', output); + console.log('[NativeClass] ===== END FINAL OUTPUT ====='); + } + return { code: output, map: null }; } catch { return null; diff --git a/packages/vite/index.ts b/packages/vite/index.ts index 9d116fce27..b72bdc86c6 100644 --- a/packages/vite/index.ts +++ b/packages/vite/index.ts @@ -6,6 +6,9 @@ export * from './configuration/vue.js'; export * from './configuration/javascript.js'; export * from './configuration/typescript.js'; +// App components plugin for custom Android Activity/Application classes +export { appComponentsPlugin, type AppComponentsOptions } from './helpers/app-components.js'; + // Simple CLI entry to support `npx @nativescript/vite init` // This keeps the library export surface intact while allowing a // lightweight command for project bootstrapping.