diff --git a/packages/vite/README.md b/packages/vite/README.md index c950c7aa20..5d9e8b34e1 100644 --- a/packages/vite/README.md +++ b/packages/vite/README.md @@ -111,6 +111,9 @@ it standalone for diagnostics, but do **not** run it alongside `ns run`/`ns debu for the same platform — both would try to bind the same port. CLI-managed is the supported default. +To leave `@NativeClass` for the runtime instead of downleveling it at build time, +pass `--env.disableNativeClassTransformer` or set `NS_DISABLE_NATIVE_CLASS_TRANSFORMER=1`. + ## Usage 1) Create `vite.config.mts` (the `.mts` extension keeps the config ESM without setting `"type": "module"` in the app's package.json, avoiding Vite's `configLoader: 'native'` warning): diff --git a/packages/vite/helpers/nativeclass-esbuild-plugin.ts b/packages/vite/helpers/nativeclass-esbuild-plugin.ts index a4e73d0d14..e849b0ba3d 100644 --- a/packages/vite/helpers/nativeclass-esbuild-plugin.ts +++ b/packages/vite/helpers/nativeclass-esbuild-plugin.ts @@ -1,5 +1,5 @@ import type { Plugin as EsbuildPlugin } from 'esbuild'; -import { isNativeESClassesEnabled, transformNativeClassSource } from './nativeclass-transform.js'; +import { shouldSkipNativeClassTransform, transformNativeClassSource } from './nativeclass-transform.js'; import type { Platform } from './platform-types.js'; /** @@ -15,7 +15,7 @@ export function createNativeClassEsbuildPlugin(platform: Platform): EsbuildPlugi setup(build) { // Native ES class mode (Apple targets only): the runtime handles ES classes and // the NativeClass decorator directly, so vendor bundles need no rewriting either. - if (isNativeESClassesEnabled(platform)) { + if (shouldSkipNativeClassTransform(platform)) { return; } // We need to use onLoad to transform the file contents diff --git a/packages/vite/helpers/nativeclass-transform.ts b/packages/vite/helpers/nativeclass-transform.ts index 9e3cfbb84b..f1de4579b3 100644 --- a/packages/vite/helpers/nativeclass-transform.ts +++ b/packages/vite/helpers/nativeclass-transform.ts @@ -5,6 +5,32 @@ import ts from 'typescript'; import { getCliFlags } from './cli-flags.js'; import type { Platform } from './platform-types.js'; +function isTruthyFlagValue(value: unknown): boolean { + if (value === true) return true; + if (typeof value !== 'string') return false; + const normalized = value.toLowerCase(); + return normalized !== '' && normalized !== '0' && normalized !== 'false'; +} + +/** + * Skip the NativeClass ES5 downlevel on every platform so the runtime can consume + * `@NativeClass` directly. Enabled via `--env.disableNativeClassTransformer` + * (alias: `--env.disableNativeTransformer`) or `NS_DISABLE_NATIVE_CLASS_TRANSFORMER` + * (set to `0`/`false` to force-disable). + */ +export function isNativeClassTransformerDisabled(): boolean { + const envValue = process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER; + if (envValue !== undefined) { + return isTruthyFlagValue(envValue); + } + try { + const flags = getCliFlags(); + return isTruthyFlagValue(flags.disableNativeClassTransformer) || isTruthyFlagValue(flags.disableNativeTransformer); + } catch (e) { + return false; + } +} + /** * Opt-in: skip the NativeClass ES5 downlevel entirely and let the iOS runtime handle plain * ES `class X extends NativeBase {}` declarations natively (the runtime registers the @@ -20,16 +46,24 @@ export function isNativeESClassesEnabled(platform?: Platform): boolean { if (platform === 'android') return false; const envValue = process.env.NS_NATIVE_ES_CLASSES; if (envValue !== undefined) { - return envValue !== '0' && envValue.toLowerCase() !== 'false'; + return isTruthyFlagValue(envValue); } try { const flags = getCliFlags(); - return !!flags.nativeESClasses; + return isTruthyFlagValue(flags.nativeESClasses); } catch (e) { return false; } } +/** + * Returns true when NativeClass sources should be left untouched. + * @param platform Optional build platform used by the Apple-only native ES class mode. + */ +export function shouldSkipNativeClassTransform(platform?: Platform): boolean { + return isNativeClassTransformerDisabled() || isNativeESClassesEnabled(platform); +} + /** * Apply the NativeClass transformer to a source string. Returns null if no change performed. */ @@ -46,9 +80,8 @@ export function transformNativeClassSource(code: string, fileName: string) { // If cli flags cannot be read for any reason, fall back to original behavior. } - // Native ES class mode (Apple targets only): leave sources untouched - the runtime - // understands ES classes extending native types and the NativeClass decorator itself. - if (isNativeESClassesEnabled(platform)) return null; + // Skip downlevel when the runtime should handle NativeClass. + if (shouldSkipNativeClassTransform(platform)) return null; // If this is JS and we see a __decorate* call that references NativeClass, strip it safely. const isJS = /\.(js|mjs|cjs)$/.test(fileName); diff --git a/packages/vite/helpers/nativeclass-transformer-plugin.ts b/packages/vite/helpers/nativeclass-transformer-plugin.ts index 6045d2577d..8be8635f3e 100644 --- a/packages/vite/helpers/nativeclass-transformer-plugin.ts +++ b/packages/vite/helpers/nativeclass-transformer-plugin.ts @@ -1,6 +1,6 @@ import type { Plugin } from 'vite'; import ts from 'typescript'; -import { isNativeESClassesEnabled, transformNativeClassSource } from './nativeclass-transform.js'; +import { isNativeClassTransformerDisabled, shouldSkipNativeClassTransform, transformNativeClassSource } from './nativeclass-transform.js'; import { resolvePlatform } from './cli-flags.js'; /** @@ -277,6 +277,10 @@ export function postCleanupNativeClass(code: string, bareId: string, verbose = f * Wraps NativeClass TS transformer into a Vite plugin. */ export function createNativeClassTransformerPlugin(): Plugin[] { + if (isNativeClassTransformerDisabled()) { + return []; + } + const verbose = !!process.env.NS_DEBUG_NATIVECLASS; return [ @@ -335,7 +339,7 @@ export function createNativeClassTransformerPlugin(): Plugin[] { transform(code: string, id: string) { // Native ES class mode (Apple targets only): the runtime handles ES classes and // the NativeClass decorator directly, so no post-phase strip/downlevel either. - if (isNativeESClassesEnabled(resolvePlatform())) return null; + if (shouldSkipNativeClassTransform(resolvePlatform())) return null; const bareId = id.split('?')[0]; if (!/\.(ts|tsx|js|mjs)$/.test(bareId)) return null; return postCleanupNativeClass(code, bareId, verbose); diff --git a/packages/vite/helpers/nativeclass-transformer.spec.ts b/packages/vite/helpers/nativeclass-transformer.spec.ts index fdc84049f9..4581a16ac6 100644 --- a/packages/vite/helpers/nativeclass-transformer.spec.ts +++ b/packages/vite/helpers/nativeclass-transformer.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect, afterEach } from 'vitest'; import ts from 'typescript'; -import { isNativeESClassesEnabled, transformNativeClassSource } from '../helpers/nativeclass-transform.js'; -import { postCleanupNativeClass } from '../helpers/nativeclass-transformer-plugin.js'; +import { isNativeClassTransformerDisabled, isNativeESClassesEnabled, transformNativeClassSource } from '../helpers/nativeclass-transform.js'; +import { createNativeClassTransformerPlugin, postCleanupNativeClass } from '../helpers/nativeclass-transformer-plugin.js'; const SAMPLE_TS = ` @@ -348,6 +348,37 @@ export class FooImpl extends NSObject { expect(hasTopLevelReturn).toBe(false); }); + describe('disable NativeClass transformer (NS_DISABLE_NATIVE_CLASS_TRANSFORMER)', () => { + afterEach(() => { + delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER; + }); + + it('is off by default so the transformer still runs', () => { + expect(isNativeClassTransformerDisabled()).toBe(false); + const res = transformNativeClassSource(SAMPLE_TS, '/app/src/sample.ts'); + expect(res).not.toBeNull(); + expect(res?.code).not.toContain('@NativeClass'); + }); + + it('skips the transform on every platform when enabled', () => { + process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '1'; + expect(isNativeClassTransformerDisabled()).toBe(true); + expect(transformNativeClassSource(SAMPLE_TS, '/app/src/sample.ts')).toBeNull(); + }); + + it('can be force-disabled with 0/false', () => { + process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '0'; + expect(isNativeClassTransformerDisabled()).toBe(false); + process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = 'false'; + expect(isNativeClassTransformerDisabled()).toBe(false); + }); + + it('does not register the Vite transformer plugins when disabled', () => { + process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '1'; + expect(createNativeClassTransformerPlugin()).toEqual([]); + }); + }); + describe('native ES class mode (NS_NATIVE_ES_CLASSES)', () => { afterEach(() => { delete process.env.NS_NATIVE_ES_CLASSES; diff --git a/packages/webpack5/README.md b/packages/webpack5/README.md index 7324ff08d1..b472160b76 100644 --- a/packages/webpack5/README.md +++ b/packages/webpack5/README.md @@ -90,6 +90,7 @@ Useful globally available variables in your app: | `--env.report` | Generate bundle analysis report | | `--env.verbose` | Print verbose logs and internal config | | `--env.e2e` | Enable E2E mode (enables `testID` property) | +| `--env.disableNativeClassTransformer` | Leave `@NativeClass` for the runtime (skip the bundler downlevel). Alias: `--env.disableNativeTransformer` | ## 📖 Documentation diff --git a/packages/webpack5/__tests__/configuration/angular.spec.ts b/packages/webpack5/__tests__/configuration/angular.spec.ts index 727ec43fc0..e50ac9d36b 100644 --- a/packages/webpack5/__tests__/configuration/angular.spec.ts +++ b/packages/webpack5/__tests__/configuration/angular.spec.ts @@ -83,6 +83,19 @@ describe('angular configuration', () => { }); } + it('skips NativeClass platformTransformers when disableNativeClassTransformer is set', () => { + init({ + ios: true, + disableNativeClassTransformer: true, + }); + const config = angular(new Config()); + + config.plugin('AngularCompilerPlugin').tap((args) => { + expect(args[0].platformTransformers).toEqual([]); + return args; + }); + }); + it(`loads polyfills.ts into the bundle entry if it exists `, () => { polyfillsPath = `__jest__/src/polyfills.ts`; diff --git a/packages/webpack5/__tests__/configuration/base.spec.ts b/packages/webpack5/__tests__/configuration/base.spec.ts index c3608705a5..0db9c4e46c 100644 --- a/packages/webpack5/__tests__/configuration/base.spec.ts +++ b/packages/webpack5/__tests__/configuration/base.spec.ts @@ -202,6 +202,39 @@ describe('base configuration', () => { }); }); + it('skips NativeClass transformer and loaders when disableNativeClassTransformer is set', () => { + init({ + ios: true, + disableNativeClassTransformer: true, + }); + const config = base(new Config()); + const tsRule = config.module.rule('ts'); + + expect(tsRule.uses.has('native-class-downlevel-loader')).toBe(false); + expect(tsRule.uses.has('native-class-strip-loader')).toBe(false); + + tsRule.use('ts-loader').tap((options) => { + expect(options.getCustomTransformers()).toEqual({ before: [] }); + return options; + }); + }); + + it('keeps NativeClass transformer and loaders enabled by default', () => { + init({ + ios: true, + }); + const config = base(new Config()); + const tsRule = config.module.rule('ts'); + + expect(tsRule.uses.has('native-class-downlevel-loader')).toBe(true); + expect(tsRule.uses.has('native-class-strip-loader')).toBe(true); + + tsRule.use('ts-loader').tap((options) => { + expect(options.getCustomTransformers().before.length).toBe(1); + return options; + }); + }); + it('supports --env.profile', () => { init({ platform: 'ios', diff --git a/packages/webpack5/__tests__/helpers/nativeClassTransformer.spec.ts b/packages/webpack5/__tests__/helpers/nativeClassTransformer.spec.ts new file mode 100644 index 0000000000..0d73caa9dd --- /dev/null +++ b/packages/webpack5/__tests__/helpers/nativeClassTransformer.spec.ts @@ -0,0 +1,48 @@ +import { isNativeClassTransformerDisabled } from '../../src/helpers/nativeClassTransformer'; + +describe('isNativeClassTransformerDisabled', () => { + const originalEnv = process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER; + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER; + } else { + process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = originalEnv; + } + }); + + it('is off by default', () => { + delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER; + expect(isNativeClassTransformerDisabled({})).toBe(false); + }); + + it('honors --env.disableNativeClassTransformer', () => { + delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER; + expect( + isNativeClassTransformerDisabled({ + disableNativeClassTransformer: true, + }), + ).toBe(true); + expect( + isNativeClassTransformerDisabled({ + disableNativeClassTransformer: false, + }), + ).toBe(false); + }); + + it('honors --env.disableNativeTransformer as an alias', () => { + delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER; + expect( + isNativeClassTransformerDisabled({ + disableNativeTransformer: true, + }), + ).toBe(true); + }); + + it('honors NS_DISABLE_NATIVE_CLASS_TRANSFORMER', () => { + process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '1'; + expect(isNativeClassTransformerDisabled({})).toBe(true); + process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '0'; + expect(isNativeClassTransformerDisabled({})).toBe(false); + }); +}); diff --git a/packages/webpack5/src/configuration/angular.ts b/packages/webpack5/src/configuration/angular.ts index d17fd1feac..c9b204eca5 100644 --- a/packages/webpack5/src/configuration/angular.ts +++ b/packages/webpack5/src/configuration/angular.ts @@ -7,6 +7,7 @@ import { getTypescript, readTsConfig } from '../helpers/typescript'; import { getDependencyVersion } from '../helpers/dependencies'; import { getProjectTSConfigPath } from '../helpers/project'; import { env as _env, IWebpackEnv } from '../index'; +import { isNativeClassTransformerDisabled } from '../helpers/nativeClassTransformer'; import { warnOnce } from '../helpers/log'; import { getEntryDirPath, @@ -32,6 +33,10 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { const platform = getPlatformName(); const tsConfigPath = getProjectTSConfigPath(); const disableAOT = !!env.disableAOT; + const skipNativeClassTransformer = isNativeClassTransformerDisabled(env); + const nativeClassTransformers = skipNativeClassTransformer + ? [] + : [require('../transformers/NativeClass').default]; // remove default ts rule config.module.rules.delete('ts'); @@ -140,7 +145,7 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { // just return the original path otherwise return path; }, - platformTransformers: [require('../transformers/NativeClass').default], + platformTransformers: nativeClassTransformers, }, ]); } @@ -150,27 +155,29 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { // angular no longer supports transformers. // so we patch their method until they do // https://github.com/angular/angular-cli/pull/21046 - const originalCreateFileEmitter = - angularWebpackPlugin.prototype.createFileEmitter; - angularWebpackPlugin.prototype.createFileEmitter = function ( - ...args: any[] - ) { - let transformers = args[1] || {}; - if (!transformers.before) { - transformers.before = []; - } - if (this.pluginOptions.jitMode) { - transformers.before.unshift( - require('../transformers/NativeClass').default, - ); - } else { - transformers.before.push( - require('../transformers/NativeClass').default, - ); - } - args[1] = transformers; - return originalCreateFileEmitter.apply(this, args); - }; + if (!skipNativeClassTransformer) { + const originalCreateFileEmitter = + angularWebpackPlugin.prototype.createFileEmitter; + angularWebpackPlugin.prototype.createFileEmitter = function ( + ...args: any[] + ) { + let transformers = args[1] || {}; + if (!transformers.before) { + transformers.before = []; + } + if (this.pluginOptions.jitMode) { + transformers.before.unshift( + require('../transformers/NativeClass').default, + ); + } else { + transformers.before.push( + require('../transformers/NativeClass').default, + ); + } + args[1] = transformers; + return originalCreateFileEmitter.apply(this, args); + }; + } config.plugin('AngularWebpackPlugin').use(angularWebpackPlugin, [ { tsconfig: tsConfigPath, diff --git a/packages/webpack5/src/configuration/base.ts b/packages/webpack5/src/configuration/base.ts index 6d21e44469..cf70645fc5 100644 --- a/packages/webpack5/src/configuration/base.ts +++ b/packages/webpack5/src/configuration/base.ts @@ -22,6 +22,7 @@ import { WatchStatePlugin } from '../plugins/WatchStatePlugin'; import { CompatDefinePlugin } from '../plugins/CompatDefinePlugin'; import { applyDotEnvPlugin } from '../helpers/dotEnv'; import { env as _env, IWebpackEnv } from '../index'; +import { isNativeClassTransformerDisabled } from '../helpers/nativeClassTransformer'; import { getValue } from '../helpers/config'; import { getIPS } from '../helpers/host'; import { @@ -139,15 +140,12 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { }, alias: { // Mock mdn-data modules that css-tree tries to load - 'mdn-data/css/properties.json': require.resolve( - '../polyfills/mdn-data-properties', - ), - 'mdn-data/css/syntaxes.json': require.resolve( - '../polyfills/mdn-data-syntaxes', - ), - 'mdn-data/css/at-rules.json': require.resolve( - '../polyfills/mdn-data-at-rules', - ), + 'mdn-data/css/properties.json': + require.resolve('../polyfills/mdn-data-properties'), + 'mdn-data/css/syntaxes.json': + require.resolve('../polyfills/mdn-data-syntaxes'), + 'mdn-data/css/at-rules.json': + require.resolve('../polyfills/mdn-data-at-rules'), // Ensure imports of the Node 'module' builtin resolve to our polyfill module: require.resolve('../polyfills/module'), }, @@ -446,10 +444,12 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { } : undefined; + const skipNativeClassTransformer = isNativeClassTransformerDisabled(env); + // set up ts support - config.module - .rule('ts') - .test([/\.ts$/]) + const tsRule = config.module.rule('ts').test([/\.ts$/]); + + tsRule .use('ts-loader') .loader('ts-loader') .options({ @@ -464,19 +464,24 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { }, getCustomTransformers() { return { - before: [require('../transformers/NativeClass').default], + before: skipNativeClassTransformer + ? [] + : [require('../transformers/NativeClass').default], }; }, - }) - .end() + }); + + if (!skipNativeClassTransformer) { // Ensure pre-loaders run BEFORE ts-loader (loaders execute right-to-left): // order: [ts-loader, native-class-downlevel-loader, native-class-strip-loader] // execution: strip -> downlevel -> ts-loader - .use('native-class-downlevel-loader') - .loader('native-class-downlevel-loader') - .end() - .use('native-class-strip-loader') - .loader('native-class-strip-loader'); + tsRule + .use('native-class-downlevel-loader') + .loader('native-class-downlevel-loader') + .end() + .use('native-class-strip-loader') + .loader('native-class-strip-loader'); + } // Use Fork TS Checker to do type checking in a separate non-blocking process config.when(hasDependency('typescript'), (config) => { diff --git a/packages/webpack5/src/helpers/nativeClassTransformer.ts b/packages/webpack5/src/helpers/nativeClassTransformer.ts new file mode 100644 index 0000000000..e3e89e6149 --- /dev/null +++ b/packages/webpack5/src/helpers/nativeClassTransformer.ts @@ -0,0 +1,23 @@ +import type { IWebpackEnv } from '..'; + +function isTruthyProcessEnvFlag(value: string | undefined): boolean { + if (value === undefined) { + return false; + } + + return value !== '0' && value.toLowerCase() !== 'false'; +} + +export function isNativeClassTransformerDisabled(env: IWebpackEnv): boolean { + if (env.disableNativeClassTransformer !== undefined) { + return !!env.disableNativeClassTransformer; + } + + if (env.disableNativeTransformer !== undefined) { + return !!env.disableNativeTransformer; + } + + return isTruthyProcessEnvFlag( + process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER, + ); +} diff --git a/packages/webpack5/src/index.ts b/packages/webpack5/src/index.ts index 8adc3bfaa8..58ffbc90ff 100644 --- a/packages/webpack5/src/index.ts +++ b/packages/webpack5/src/index.ts @@ -53,6 +53,9 @@ export interface IWebpackEnv { // enable commonjs modules (default: ES modules, esm) commonjs?: boolean; + // skip NativeClass decorator downlevel so the runtime can consume @NativeClass + disableNativeClassTransformer?: boolean; + // misc replace?: string[] | string; watchNodeModules?: boolean;