Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/vite/configuration/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { resolveRelativeToImportMeta } from '../helpers/import-meta-path.js';
import { normalizeModuleId } from '../helpers/normalize-id.js';
import { getHmrWatchIgnoreGlobs } from '../helpers/hmr-scope.js';
import { NS_OPTIMIZE_DEPS_EXCLUDE } from '../helpers/optimize-deps.js';
import { sbgClassNamesPlugin } from '../helpers/sbg-class-names.js';
const require = createRequire(import.meta.url);
// Optional PostCSS import plugin (only used if available)
// Try to load postcss-import to control CSS @import resolution simply
Expand Down Expand Up @@ -327,6 +328,11 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }):
},
};

// The Android static binding generator finds a decorated class only from its own
// `X = __decorate([JavaProxy('name')], X)` statement; compressing statements into comma
// sequences hides those classes from it.
const sbgMinifyOutput = platform === 'android' && !debug ? { minify: { compress: { sequences: false }, mangle: true, codegen: true } } : {};

const baseViteConfig = {
// Suppress logs during HMR development if desired:
// ...(hmrActive ? { logLevel: "warn" as const } : {}),
Expand Down Expand Up @@ -416,6 +422,7 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }):
...(!hmrActive ? [angularWorkerUrlPreservePlugin({ verbose })] : []),
// Transform Vite worker URLs to NativeScript format AFTER bundling
workerUrlPlugin(),
...(platform === 'android' ? [sbgClassNamesPlugin()] : []),
// Dev/HMR only: safety-net TypeScript transform for `.ts`/`.tsx` files
// that escape the Angular plugin's fileEmitter (e.g. worker entry files
// loaded via `new Worker(new URL('./foo.worker', import.meta.url))`,
Expand Down Expand Up @@ -518,6 +525,7 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }):
// Don't externalize anything - bundle everything into the worker
external: [],
output: {
...sbgMinifyOutput,
// Inline all dynamic imports into a single worker bundle.
// `codeSplitting: false` is the modern Rolldown spelling of the
// deprecated `inlineDynamicImports: true`. See:
Expand Down Expand Up @@ -577,6 +585,7 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }):
input: 'virtual:entry-with-polyfills',
output: {
format: 'es', // Emit ES modules (.mjs)
...sbgMinifyOutput,
entryFileNames: 'bundle.mjs',
// Prepend a tiny polyfill prelude to every emitted chunk so timer
// globals (`setTimeout`, `setInterval`, `clearTimeout`,
Expand Down
26 changes: 26 additions & 0 deletions packages/vite/helpers/sbg-class-names.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { quoteBindingNames } from './sbg-class-names.js';

describe('quoteBindingNames', () => {
it('requotes the class name of a minified extend call', () => {
expect(quoteBindingNames('androidx.appcompat.app.AppCompatActivity.extend(`com.tns.NativeScriptActivity`,{init(){}})')).toBe('androidx.appcompat.app.AppCompatActivity.extend("com.tns.NativeScriptActivity",{init(){}})');
});

it('requotes a JavaProxy name', () => {
expect(quoteBindingNames('e=[JavaProxy(`org.nativescript.NativeScriptLifecycleCallbacks`)]')).toBe('e=[JavaProxy("org.nativescript.NativeScriptLifecycleCallbacks")]');
});

it('keeps the length, so source maps stay valid', () => {
const code = 't.extend(`com.tns.FragmentClass`,{})';
expect(quoteBindingNames(code)).toHaveLength(code.length);
});

it('accepts a bare dollar in a nested class name', () => {
expect(quoteBindingNames('t.extend(`com.example.Outer$Inner`,{})')).toBe('t.extend("com.example.Outer$Inner",{})');
});

it('leaves templates with substitutions, other strings and anonymous extends alone', () => {
const code = 'a.extend(`com.${n}.X`,{});b.extend({run(){}});c(`com.tns.Other`);d.extend("com.tns.Quoted",{})';
expect(quoteBindingNames(code)).toBe(code);
});
});
30 changes: 30 additions & 0 deletions packages/vite/helpers/sbg-class-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Plugin } from 'vite';

/**
* The Android static binding generator reads the class name given to `extend('name', {...})` or
* `JavaProxy('name')` only from a quoted string literal, while the minifier prints every string
* as a template literal. A name it cannot read is left for the runtime to generate at launch,
* which only works where dex injection into the app class loader succeeds.
*/
const NAMED_BINDING = /\b(extend|JavaProxy)\(`((?:[^`\\"$]|\$(?!\{))*)`/g;

/** Requotes binding names as double-quoted strings; same length, so source maps still line up. */
export function quoteBindingNames(code: string): string {
return code.replace(NAMED_BINDING, '$1("$2"');
}

/** Runs on the finished chunks, after minification has printed them. */
export function sbgClassNamesPlugin(): Plugin {
return {
name: 'ns-sbg-class-names',
apply: 'build',
enforce: 'post',
generateBundle(_options, bundle) {
for (const output of Object.values(bundle)) {
if (output.type === 'chunk') {
output.code = quoteBindingNames(output.code);
}
}
},
};
}
4 changes: 4 additions & 0 deletions packages/vite/helpers/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { packagePlatformResolverPlugin } from './package-platform-aliases.js';
import { nativescriptPackageResolver } from './nativescript-package-resolver.js';
import { findMonorepoWorkspaceRoot, getProjectRootPath } from './project.js';
import { normalizeModuleId } from './normalize-id.js';
import { sbgClassNamesPlugin } from './sbg-class-names.js';

export interface WorkerPluginsOptions {
platform: string;
Expand Down Expand Up @@ -161,6 +162,9 @@ export function getWorkerPlugins(platformOrOpts: string | WorkerPluginsOptions)
plugins.push(packagePlatformResolverPlugin({ tsConfig, platform, verbose }));
plugins.push(nativescriptPackageResolver(platform));
plugins.push(nativescriptWorkerLoaderStubPlugin());
if (platform === 'android') {
plugins.push(sbgClassNamesPlugin());
}

return plugins;
}
Expand Down
Loading